mirror of
https://github.com/pretix/pretix.git
synced 2025-12-11 01:22:28 +00:00
Compare commits
676 Commits
fix-seatin
...
language-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
118c206bb1 | ||
|
|
a56c6ae1e0 | ||
|
|
1efd952a19 | ||
|
|
ddd0db3d98 | ||
|
|
dde724d0be | ||
|
|
9f55187690 | ||
|
|
e572bfb752 | ||
|
|
beccdf8dad | ||
|
|
b141ea4ed5 | ||
|
|
32dd125b65 | ||
|
|
b5794780da | ||
|
|
835c08f1ca | ||
|
|
a79d5fddda | ||
|
|
beb03e07e0 | ||
|
|
dd2e5d09f9 | ||
|
|
5c2456e92e | ||
|
|
3579a7f298 | ||
|
|
5dd20de745 | ||
|
|
5708099fc9 | ||
|
|
2c2e8e7d21 | ||
|
|
5993482f6c | ||
|
|
c905659dfb | ||
|
|
196c131ac9 | ||
|
|
a554433fad | ||
|
|
c552dd876c | ||
|
|
105ae8592d | ||
|
|
ca4540eeb7 | ||
|
|
381366a248 | ||
|
|
816a3ec994 | ||
|
|
88d3d12dbc | ||
|
|
71caa17879 | ||
|
|
2ecdfde756 | ||
|
|
1f753a57c5 | ||
|
|
595c042624 | ||
|
|
5a5a551c21 | ||
|
|
e74793994a | ||
|
|
f1bdd3b7af | ||
|
|
13c40f9bb7 | ||
|
|
3e15e2a887 | ||
|
|
7525ee853b | ||
|
|
f02b1be659 | ||
|
|
20f171b790 | ||
|
|
22906dfa77 | ||
|
|
5f74e661b3 | ||
|
|
5b99788354 | ||
|
|
b45d58b60e | ||
|
|
2a58e958b0 | ||
|
|
e44695dfcf | ||
|
|
1d289088f4 | ||
|
|
a9be6337bc | ||
|
|
df29d4e8c4 | ||
|
|
bf2cabf7b6 | ||
|
|
db614d36e6 | ||
|
|
13452b5d8c | ||
|
|
6422cd7858 | ||
|
|
2e87cb5691 | ||
|
|
b53ee938bf | ||
|
|
ec3bdd4a57 | ||
|
|
7b607594d8 | ||
|
|
53f129d5d3 | ||
|
|
a4385c8b6e | ||
|
|
3acae96021 | ||
|
|
b9add5ff6f | ||
|
|
4ca9813a1d | ||
|
|
347748896d | ||
|
|
0f590caa18 | ||
|
|
18801f2d1c | ||
|
|
e5f29bd592 | ||
|
|
1f904d482b | ||
|
|
b8ad276f53 | ||
|
|
e109c37738 | ||
|
|
4d597d5be3 | ||
|
|
ae8ec42905 | ||
|
|
e5b89e9b08 | ||
|
|
da91f5f117 | ||
|
|
ae29240e58 | ||
|
|
74edf10b04 | ||
|
|
e2e0eca872 | ||
|
|
6132e4a2c4 | ||
|
|
7df7d28518 | ||
|
|
11ab5c5eeb | ||
|
|
20211d2097 | ||
|
|
d760ad38bf | ||
|
|
69af2cee93 | ||
|
|
6b199a2b9c | ||
|
|
94a64ba53a | ||
|
|
70f06a8f40 | ||
|
|
a747ab154a | ||
|
|
6317233150 | ||
|
|
4d94158ff0 | ||
|
|
8f92eb2d2d | ||
|
|
f29896b267 | ||
|
|
2dc625cf31 | ||
|
|
855226d37c | ||
|
|
648c0da9fe | ||
|
|
59e3494fa2 | ||
|
|
c4ff57c07a | ||
|
|
cc4fbfe4c7 | ||
|
|
e99ee91573 | ||
|
|
e2753686ee | ||
|
|
33f8b9851e | ||
|
|
e3d8cf07af | ||
|
|
0279ca7d94 | ||
|
|
d1989c3cd3 | ||
|
|
61cb2e15cf | ||
|
|
f2ee1d00b3 | ||
|
|
e8e9698a31 | ||
|
|
a1bf7be244 | ||
|
|
f4ca9a5681 | ||
|
|
e6d984538f | ||
|
|
9f1ee9157f | ||
|
|
242e5af4b5 | ||
|
|
7d6e98e6da | ||
|
|
27f964f3ae | ||
|
|
84b3060c0f | ||
|
|
25dcb72f92 | ||
|
|
4b078867c6 | ||
|
|
c595a59d4a | ||
|
|
f164daeaee | ||
|
|
c6b6dd8d49 | ||
|
|
8038c87963 | ||
|
|
c45a970d32 | ||
|
|
a34517233d | ||
|
|
8fb2e5383c | ||
|
|
86a00f3338 | ||
|
|
c8c0d3e7f5 | ||
|
|
7dd455ce15 | ||
|
|
391eda25da | ||
|
|
fcff5a522d | ||
|
|
7e93d38a01 | ||
|
|
6469381899 | ||
|
|
761706c60c | ||
|
|
f91315c88e | ||
|
|
bc05afeab9 | ||
|
|
02d495d287 | ||
|
|
894878d9da | ||
|
|
5896ca0197 | ||
|
|
fe6fc8df32 | ||
|
|
9de8f3a775 | ||
|
|
c92bb9cb8b | ||
|
|
76ecec8b98 | ||
|
|
4b8416df8f | ||
|
|
a601c75923 | ||
|
|
f94227f00f | ||
|
|
a0c1e5369c | ||
|
|
633bfcf73a | ||
|
|
0d3b5b82c1 | ||
|
|
ab95f33546 | ||
|
|
5034b366c5 | ||
|
|
03d3c389da | ||
|
|
3e934acfa0 | ||
|
|
d2a364e848 | ||
|
|
2824b40299 | ||
|
|
c6c2c90908 | ||
|
|
d4ae7df2ec | ||
|
|
79dd7fb596 | ||
|
|
5ed87cd019 | ||
|
|
ccdcbe0cc5 | ||
|
|
4f8607a9db | ||
|
|
57ecaa2676 | ||
|
|
96fd2b1a95 | ||
|
|
5cf24fb6a6 | ||
|
|
1d2ea35a39 | ||
|
|
ac98ae7941 | ||
|
|
a0d055e202 | ||
|
|
27ec5ca006 | ||
|
|
9d2edc405d | ||
|
|
fb95fe7cf6 | ||
|
|
5b5360ef8b | ||
|
|
129d10ca35 | ||
|
|
093a705ff9 | ||
|
|
6130ae4630 | ||
|
|
11a8ed6c7a | ||
|
|
f6392592c5 | ||
|
|
ecb9ad28ea | ||
|
|
45a506fd37 | ||
|
|
3b16e6356b | ||
|
|
9583a50c4e | ||
|
|
6e6d6b2746 | ||
|
|
7266d90c6b | ||
|
|
5e4e88c91d | ||
|
|
e74d12e8b8 | ||
|
|
a5c39271dd | ||
|
|
3170744c56 | ||
|
|
9ec161561b | ||
|
|
aff4f4b8f8 | ||
|
|
75addfe9f4 | ||
|
|
4b05ce5835 | ||
|
|
34c247f423 | ||
|
|
3aad6852cb | ||
|
|
5cdb07bce6 | ||
|
|
6cb2d68948 | ||
|
|
4a7a6273c6 | ||
|
|
ebe343458a | ||
|
|
f9a93b765c | ||
|
|
5aba1f9a23 | ||
|
|
a4eed87396 | ||
|
|
08879d0d55 | ||
|
|
c276a19bcc | ||
|
|
1e3c6e0b68 | ||
|
|
4e283eb560 | ||
|
|
52a1983630 | ||
|
|
3d85d9d865 | ||
|
|
4ca9a43890 | ||
|
|
d8bac7db65 | ||
|
|
91de0f93e6 | ||
|
|
901565203b | ||
|
|
14c6c9c0d7 | ||
|
|
6de6cf6c08 | ||
|
|
29306b3a4d | ||
|
|
ca69996611 | ||
|
|
16419b6ae4 | ||
|
|
d6258b9b54 | ||
|
|
6f75608196 | ||
|
|
6ef88e009b | ||
|
|
957100a195 | ||
|
|
112ef0908f | ||
|
|
91aaff7359 | ||
|
|
8ab61e2c38 | ||
|
|
c8ba5cc427 | ||
|
|
5ebad31b7d | ||
|
|
0429377f7d | ||
|
|
76e4b797a1 | ||
|
|
5f0009c996 | ||
|
|
de63a4be01 | ||
|
|
f3432139cb | ||
|
|
0b82ac9115 | ||
|
|
eb685b5141 | ||
|
|
5f7f0bd8f1 | ||
|
|
9fcef2dcaa | ||
|
|
fc3b186b93 | ||
|
|
a406884575 | ||
|
|
57ccd5f289 | ||
|
|
f4ac7e7f65 | ||
|
|
81d7045b31 | ||
|
|
f9502a3212 | ||
|
|
a31f624417 | ||
|
|
3f99e0bece | ||
|
|
7e64f2b38a | ||
|
|
ee2bc93608 | ||
|
|
fb4bed9d0d | ||
|
|
aec75e4d0c | ||
|
|
e7e41470fb | ||
|
|
0aa9dda90a | ||
|
|
d97c983b6f | ||
|
|
6c957f31ca | ||
|
|
8e6b4b3ec7 | ||
|
|
b24de62f73 | ||
|
|
cdbd220a12 | ||
|
|
2f11aee512 | ||
|
|
8ea475ce39 | ||
|
|
b29bc9db96 | ||
|
|
6bd6694132 | ||
|
|
110e6e248e | ||
|
|
985f4d969d | ||
|
|
826bd07b01 | ||
|
|
3e4e86742a | ||
|
|
ef5fcde5d9 | ||
|
|
8f1d53d016 | ||
|
|
9ca1573fcf | ||
|
|
5795aa6492 | ||
|
|
6e0613a2af | ||
|
|
b43ed38483 | ||
|
|
f0fedf0001 | ||
|
|
19373b8f91 | ||
|
|
45fd13786a | ||
|
|
ae5111ee7e | ||
|
|
d8bf3065d0 | ||
|
|
54f077665c | ||
|
|
482a66c546 | ||
|
|
e4cef6e46b | ||
|
|
cbee1b71fe | ||
|
|
0cd1290624 | ||
|
|
565f5e2ea7 | ||
|
|
b46c0eba0c | ||
|
|
39c3aef7bc | ||
|
|
cf3087453c | ||
|
|
7a870ee521 | ||
|
|
3922290633 | ||
|
|
8aa13d7e3e | ||
|
|
22e9a6eb92 | ||
|
|
2b6f82502e | ||
|
|
a10bf2a939 | ||
|
|
a80b7087d9 | ||
|
|
4b143e98eb | ||
|
|
bdb8b597d0 | ||
|
|
b1c9f40bc8 | ||
|
|
a3b6a008b5 | ||
|
|
9ce05e5cb9 | ||
|
|
f306527981 | ||
|
|
3e17ff9faa | ||
|
|
2a16cd4655 | ||
|
|
d1078da5bf | ||
|
|
483e7bc4ad | ||
|
|
401218b0a3 | ||
|
|
19175258fd | ||
|
|
22c36b89da | ||
|
|
2697ed0c5d | ||
|
|
f81d820a02 | ||
|
|
f8df66e621 | ||
|
|
2d9bfc80dc | ||
|
|
17b2e95569 | ||
|
|
e49f938eb3 | ||
|
|
8d63906341 | ||
|
|
cfefa1aad0 | ||
|
|
1d16049dc5 | ||
|
|
8452899edd | ||
|
|
d67ebc0f80 | ||
|
|
0e87f03e1e | ||
|
|
868408ea55 | ||
|
|
fc75cd35f8 | ||
|
|
a3e2540331 | ||
|
|
99ce7effde | ||
|
|
0d645fc4c5 | ||
|
|
359df1f51e | ||
|
|
7607cc5d2f | ||
|
|
40c8d014df | ||
|
|
c10efc692d | ||
|
|
8f0a277c7b | ||
|
|
9dc38e42d8 | ||
|
|
bfd88d1496 | ||
|
|
be6bd501bd | ||
|
|
d160c9fd67 | ||
|
|
221f14cc21 | ||
|
|
1dda2eb4fb | ||
|
|
30f2e99020 | ||
|
|
8efe276ed0 | ||
|
|
61b25acdd2 | ||
|
|
6cc9529d9a | ||
|
|
cdc5401dc2 | ||
|
|
1334a570e4 | ||
|
|
7a66aea2cb | ||
|
|
ee77a5e447 | ||
|
|
827e127568 | ||
|
|
ce0e0d7fd1 | ||
|
|
152a956dc5 | ||
|
|
68e2c355e6 | ||
|
|
171615558f | ||
|
|
a1765910ea | ||
|
|
417277958b | ||
|
|
0d50494e89 | ||
|
|
c6f634ce72 | ||
|
|
adc78c14ab | ||
|
|
b4ca2bdbb4 | ||
|
|
9a7ff592af | ||
|
|
548b54cca6 | ||
|
|
e736791446 | ||
|
|
7bd945b2e6 | ||
|
|
a07d5aaf05 | ||
|
|
0cf1a32902 | ||
|
|
be6aae8577 | ||
|
|
fe80f5fb78 | ||
|
|
a2c15ad89e | ||
|
|
cab0f37830 | ||
|
|
0423980058 | ||
|
|
63983b1b68 | ||
|
|
61241c2a1e | ||
|
|
4069c61054 | ||
|
|
9bf4fb2d0f | ||
|
|
ff910f293f | ||
|
|
74f7bec617 | ||
|
|
467a35e353 | ||
|
|
770c13a4f0 | ||
|
|
5373d4d8ba | ||
|
|
42e673b5f6 | ||
|
|
7af2f2a87b | ||
|
|
e408521769 | ||
|
|
8ed0d36346 | ||
|
|
14cbe99667 | ||
|
|
b059995eff | ||
|
|
100e8d0a4b | ||
|
|
eb92e4d8e6 | ||
|
|
32d6ded003 | ||
|
|
aa07533693 | ||
|
|
e7d01f91a6 | ||
|
|
9616369f07 | ||
|
|
af606090ba | ||
|
|
931f3eca1b | ||
|
|
36f306120e | ||
|
|
a3ba0c97e9 | ||
|
|
484d24b66c | ||
|
|
2d39d3cc8e | ||
|
|
78b1adf423 | ||
|
|
c3eedcc396 | ||
|
|
682c328390 | ||
|
|
5230827f5e | ||
|
|
dad9915435 | ||
|
|
a9d2c1eb34 | ||
|
|
66fe45a478 | ||
|
|
24e2b1b9ab | ||
|
|
eebdce80cd | ||
|
|
09af95ec20 | ||
|
|
1ade674beb | ||
|
|
76ff59f9c2 | ||
|
|
0986522c2f | ||
|
|
91f4e731da | ||
|
|
98709286c6 | ||
|
|
667c2555b2 | ||
|
|
6f5acb1ca7 | ||
|
|
65ec3e3fd6 | ||
|
|
1a8d0a973d | ||
|
|
3c94631405 | ||
|
|
1dda7732a5 | ||
|
|
33accf5f99 | ||
|
|
be2efd9df2 | ||
|
|
fe69137a4e | ||
|
|
7ccfb3a27a | ||
|
|
b7205622dc | ||
|
|
44da5b81b1 | ||
|
|
5a058342a6 | ||
|
|
2d15dc7ce5 | ||
|
|
dd4ccc864e | ||
|
|
b812f0affe | ||
|
|
2af4183ce6 | ||
|
|
8ac0b93ca5 | ||
|
|
51a1193f32 | ||
|
|
002da2c9b7 | ||
|
|
9a2ebe4e95 | ||
|
|
bc6da2512a | ||
|
|
6378dc69b8 | ||
|
|
2b53d04a19 | ||
|
|
7efe7b5ff7 | ||
|
|
ae5464d486 | ||
|
|
67fec8d1f6 | ||
|
|
95a081676b | ||
|
|
7228a6304d | ||
|
|
04b9134e36 | ||
|
|
2e0769bc41 | ||
|
|
4d2f854710 | ||
|
|
b9ac9496d2 | ||
|
|
a975f5dc50 | ||
|
|
4ea1f6284a | ||
|
|
a01d105829 | ||
|
|
b1bfa1acee | ||
|
|
0b4e99c2d8 | ||
|
|
0cdce7a9cd | ||
|
|
464f625301 | ||
|
|
0c1072503c | ||
|
|
9ead82839a | ||
|
|
c346e3a7f4 | ||
|
|
a26f219faf | ||
|
|
74fb8e7d0c | ||
|
|
b9dbeef1ef | ||
|
|
54079797d2 | ||
|
|
02a4ed4be2 | ||
|
|
7f7c95aedb | ||
|
|
47af20d417 | ||
|
|
91e69f793d | ||
|
|
43e24ff88c | ||
|
|
fa3f6def82 | ||
|
|
34469bc222 | ||
|
|
d0364300b5 | ||
|
|
55bc55cc53 | ||
|
|
0ee5511cca | ||
|
|
192699a2c2 | ||
|
|
b8255bc7a0 | ||
|
|
d7f0c14fdc | ||
|
|
3f9ba2f223 | ||
|
|
3f811cc020 | ||
|
|
03f3203a82 | ||
|
|
59901603c6 | ||
|
|
aefb38cdd7 | ||
|
|
aed3ccd2dd | ||
|
|
893d115948 | ||
|
|
8e87cf67c7 | ||
|
|
8972715252 | ||
|
|
1879e440a7 | ||
|
|
f819f0c316 | ||
|
|
a1db13b75e | ||
|
|
6087665775 | ||
|
|
a6f93b6cf0 | ||
|
|
b96374fcf6 | ||
|
|
eb2ad48089 | ||
|
|
64dac504ca | ||
|
|
cf15a08712 | ||
|
|
9197274528 | ||
|
|
d19176ab41 | ||
|
|
8d8abbd941 | ||
|
|
5142c62e6e | ||
|
|
7f7223fcdc | ||
|
|
cdde688964 | ||
|
|
233bcaf00e | ||
|
|
0a5f3e6dd5 | ||
|
|
446d24553e | ||
|
|
45c32bcb05 | ||
|
|
5a5090604a | ||
|
|
2b370bde6d | ||
|
|
024a223ec7 | ||
|
|
022f44ad00 | ||
|
|
a682eab18e | ||
|
|
6721762a3f | ||
|
|
ad443d0eb6 | ||
|
|
ececd3e572 | ||
|
|
ffc4a76b11 | ||
|
|
4beb0c2e30 | ||
|
|
48e161d2d4 | ||
|
|
dc1973f4ff | ||
|
|
a0b046d204 | ||
|
|
0032f83d93 | ||
|
|
f312200881 | ||
|
|
9946da57c2 | ||
|
|
11e04ea3f2 | ||
|
|
9cef63d641 | ||
|
|
cb833cc6da | ||
|
|
5320a69c27 | ||
|
|
510ca67107 | ||
|
|
13720e731e | ||
|
|
78cfbd6460 | ||
|
|
a65f94fa85 | ||
|
|
288f73b735 | ||
|
|
ad33785f4c | ||
|
|
bbc175d3d6 | ||
|
|
2876ff5549 | ||
|
|
ed9caa04fc | ||
|
|
83a8fcaa47 | ||
|
|
858a448db5 | ||
|
|
58b803539b | ||
|
|
6c92c5bacf | ||
|
|
f0089f20fb | ||
|
|
cb2d056afd | ||
|
|
afb115c9a2 | ||
|
|
bb92ffe4eb | ||
|
|
8da8e2f43d | ||
|
|
cab360bdb6 | ||
|
|
c6a2ae3783 | ||
|
|
26ec9dcf6c | ||
|
|
c0832098ef | ||
|
|
fa3ac69b8e | ||
|
|
17f1d571b0 | ||
|
|
a692940397 | ||
|
|
7f2ec51c64 | ||
|
|
aba59a391c | ||
|
|
a819b8bb71 | ||
|
|
8a3b18fbd2 | ||
|
|
dd444299f0 | ||
|
|
3ee5e9cfbc | ||
|
|
f660f35766 | ||
|
|
42e26738e5 | ||
|
|
7c43f115b2 | ||
|
|
f055a598ce | ||
|
|
9138464896 | ||
|
|
479f51a84c | ||
|
|
a3ac54d419 | ||
|
|
b2841e5c61 | ||
|
|
3009f50d51 | ||
|
|
ff3a49ab2a | ||
|
|
19f3fbc7e8 | ||
|
|
bb9b9ac9aa | ||
|
|
d7f6befb5b | ||
|
|
2287be2009 | ||
|
|
0480b6873d | ||
|
|
711f08c9e8 | ||
|
|
d18914fcca | ||
|
|
2411144262 | ||
|
|
2f02d35a52 | ||
|
|
71e82fda81 | ||
|
|
ca3802da90 | ||
|
|
2c68b9e895 | ||
|
|
01092498f4 | ||
|
|
fd841ed66d | ||
|
|
04cbccb536 | ||
|
|
b8ea93de1e | ||
|
|
c49f42301c | ||
|
|
2ae0a16e67 | ||
|
|
6b06fdf822 | ||
|
|
ea3f4e5f62 | ||
|
|
d71c23f7e0 | ||
|
|
5ed7b0032b | ||
|
|
a77f2d01a7 | ||
|
|
ca4f511cde | ||
|
|
83b1c2ea7e | ||
|
|
c91eb2e20d | ||
|
|
bfb480a288 | ||
|
|
22e2143623 | ||
|
|
9e61f7f978 | ||
|
|
092de9e3c4 | ||
|
|
f0822d3c27 | ||
|
|
6fc47ca3b6 | ||
|
|
3716a686f5 | ||
|
|
9c0c77958e | ||
|
|
6154a44274 | ||
|
|
d5aff10297 | ||
|
|
846e39a652 | ||
|
|
75d37b2a37 | ||
|
|
2a0c3da8c4 | ||
|
|
fb7f4d1160 | ||
|
|
5c8817f0c3 | ||
|
|
7663bf7994 | ||
|
|
ea8f74f8aa | ||
|
|
7b34701449 | ||
|
|
06f4bfea24 | ||
|
|
e2862a98a0 | ||
|
|
82f4feadc3 | ||
|
|
906222c7d3 | ||
|
|
7ce2089ca8 | ||
|
|
2133584ed2 | ||
|
|
c3c50d7205 | ||
|
|
4f7a41a4b2 | ||
|
|
93fd79ab33 | ||
|
|
59595c2f10 | ||
|
|
91c6d09f0b | ||
|
|
570a818129 | ||
|
|
d63e2ebe2d | ||
|
|
884c97d62a | ||
|
|
032e958a00 | ||
|
|
7b16dfefbc | ||
|
|
8a5b13dee9 | ||
|
|
d7dde8c23e | ||
|
|
1e2f93fbc5 | ||
|
|
493fc03686 | ||
|
|
8d6d885f6e | ||
|
|
2aa989c293 | ||
|
|
06b226f40f | ||
|
|
73038b0d97 | ||
|
|
4513e31f0d | ||
|
|
d34175114b | ||
|
|
b2c71b47ce | ||
|
|
a889abc52b | ||
|
|
8c01b2a469 | ||
|
|
720c7fd7bb | ||
|
|
6ae6eba4de | ||
|
|
a173e347ea | ||
|
|
94d13e4cdd | ||
|
|
e618441231 | ||
|
|
cd57f1f024 | ||
|
|
075b9c187f | ||
|
|
d9f46cb817 | ||
|
|
2892d16861 | ||
|
|
9128624d68 | ||
|
|
d2cf8f801d | ||
|
|
682d0f886d | ||
|
|
d2cbd41a19 | ||
|
|
828f4e3168 | ||
|
|
e691afdd34 | ||
|
|
add90b08ec | ||
|
|
4539c6523b | ||
|
|
3453818c16 | ||
|
|
e5725d6d33 | ||
|
|
0cda1aeaaf | ||
|
|
769c451bcb | ||
|
|
5b60928205 | ||
|
|
b05f3a449d | ||
|
|
3262f2ba84 | ||
|
|
fe9d8e58a1 | ||
|
|
6706dbb4db | ||
|
|
87632ff8c5 | ||
|
|
2278dbdc4a | ||
|
|
e4e6cc0fcc | ||
|
|
ea73977c37 | ||
|
|
4fb5c6bef0 | ||
|
|
95511b0330 | ||
|
|
3340599aec | ||
|
|
e9a52d07d1 | ||
|
|
0a00a35ab1 | ||
|
|
24d8dc6c76 | ||
|
|
a9d48eaafe | ||
|
|
bb959fa494 | ||
|
|
6126309429 | ||
|
|
29b49ca82f | ||
|
|
7b7b83cb3e | ||
|
|
6bb2b3425d | ||
|
|
182d30ffb7 | ||
|
|
844c291575 | ||
|
|
ecdb1a8e09 | ||
|
|
6c6e5d5af6 | ||
|
|
c33853173b | ||
|
|
5f90bf80b8 | ||
|
|
0ba246846b | ||
|
|
88ea04551e | ||
|
|
2bfd8e17b0 | ||
|
|
1379e5f723 | ||
|
|
9fcaa3d730 | ||
|
|
22d92025c9 | ||
|
|
2b84c8d7a7 | ||
|
|
25ba2f1145 | ||
|
|
ae8ff60964 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pip-
|
${{ runner.os }}-pip-
|
||||||
- name: Install system dependencies
|
- name: Install system dependencies
|
||||||
run: sudo apt update && sudo apt install gettext unzip
|
run: sudo apt update && sudo apt install -y gettext unzip
|
||||||
- name: Install Python dependencies
|
- name: Install Python dependencies
|
||||||
run: pip3 install -U setuptools build pip check-manifest
|
run: pip3 install -U setuptools build pip check-manifest
|
||||||
- name: Run check-manifest
|
- name: Run check-manifest
|
||||||
|
|||||||
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pip-
|
${{ runner.os }}-pip-
|
||||||
- name: Install system packages
|
- name: Install system packages
|
||||||
run: sudo apt update && sudo apt install enchant-2 hunspell aspell-en
|
run: sudo apt update && sudo apt install -y enchant-2 hunspell aspell-en
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: pip3 install -Ur requirements.txt
|
run: pip3 install -Ur requirements.txt
|
||||||
working-directory: ./doc
|
working-directory: ./doc
|
||||||
|
|||||||
6
.github/workflows/strings.yml
vendored
6
.github/workflows/strings.yml
vendored
@@ -35,9 +35,9 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pip-
|
${{ runner.os }}-pip-
|
||||||
- name: Install system packages
|
- name: Install system packages
|
||||||
run: sudo apt update && sudo apt install gettext
|
run: sudo apt update && sudo apt -y install gettext
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: pip3 install -e ".[dev]"
|
run: pip3 install uv && uv pip install --system -e ".[dev]"
|
||||||
- name: Compile messages
|
- name: Compile messages
|
||||||
run: python manage.py compilemessages
|
run: python manage.py compilemessages
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
- name: Install system packages
|
- name: Install system packages
|
||||||
run: sudo apt update && sudo apt install enchant-2 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
|
- name: Install Dependencies
|
||||||
run: pip3 install -e ".[dev]"
|
run: pip3 install uv && uv pip install --system -e ".[dev]"
|
||||||
- name: Spellcheck translations
|
- name: Spellcheck translations
|
||||||
run: potypo
|
run: potypo
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
|
|||||||
4
.github/workflows/style.yml
vendored
4
.github/workflows/style.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pip-
|
${{ runner.os }}-pip-
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: pip3 install -e ".[dev]" psycopg2-binary
|
run: pip3 install uv && uv pip install --system -e ".[dev]" psycopg2-binary
|
||||||
- name: Run isort
|
- name: Run isort
|
||||||
run: isort -c .
|
run: isort -c .
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pip-
|
${{ runner.os }}-pip-
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: pip3 install -e ".[dev]" psycopg2-binary
|
run: pip3 install uv && uv pip install --system -e ".[dev]" psycopg2-binary
|
||||||
- name: Run flake8
|
- name: Run flake8
|
||||||
run: flake8 .
|
run: flake8 .
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
|
|||||||
33
.github/workflows/tests.yml
vendored
33
.github/workflows/tests.yml
vendored
@@ -5,7 +5,6 @@ on:
|
|||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- 'doc/**'
|
- 'doc/**'
|
||||||
- 'src/pretix/locale/**'
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
@@ -31,15 +30,21 @@ jobs:
|
|||||||
python-version: "3.9"
|
python-version: "3.9"
|
||||||
- database: sqlite
|
- database: sqlite
|
||||||
python-version: "3.10"
|
python-version: "3.10"
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:15
|
||||||
|
env:
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_DB: pretix
|
||||||
|
options: >-
|
||||||
|
--health-cmd "pg_isready -U postgres -d pretix"
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: harmon758/postgresql-action@v1
|
|
||||||
with:
|
|
||||||
postgresql version: '15'
|
|
||||||
postgresql db: 'pretix'
|
|
||||||
postgresql user: 'postgres'
|
|
||||||
postgresql password: 'postgres'
|
|
||||||
if: matrix.database == 'postgres'
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
@@ -51,9 +56,9 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pip-
|
${{ runner.os }}-pip-
|
||||||
- name: Install system dependencies
|
- name: Install system dependencies
|
||||||
run: sudo apt update && sudo apt install gettext
|
run: sudo apt update && sudo apt install -y gettext
|
||||||
- name: Install Python dependencies
|
- name: Install Python dependencies
|
||||||
run: pip3 install --ignore-requires-python -e ".[dev]" psycopg2-binary # We ignore that flake8 needs newer python as we don't run flake8 during tests
|
run: pip3 install uv && uv pip install --system -e ".[dev]" psycopg2-binary
|
||||||
- name: Run checks
|
- name: Run checks
|
||||||
run: python manage.py check
|
run: python manage.py check
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
@@ -65,15 +70,15 @@ jobs:
|
|||||||
run: make all compress
|
run: make all compress
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test -n 3 -p no:sugar --cov=./ --cov-report=xml --reruns 3 tests --maxfail=100
|
run: PRETIX_CONFIG_FILE=tests/ci_${{ matrix.database }}.cfg py.test -n 3 -p no:sugar --cov=./ --cov-report=xml tests --maxfail=100
|
||||||
- name: Run concurrency tests
|
- name: Run concurrency tests
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test tests/concurrency_tests/ --reruns 0 --reuse-db
|
run: PRETIX_CONFIG_FILE=tests/ci_${{ matrix.database }}.cfg py.test tests/concurrency_tests/ --reuse-db
|
||||||
if: matrix.database == 'postgres'
|
if: matrix.database == 'postgres'
|
||||||
- name: Upload coverage
|
- name: Upload coverage
|
||||||
uses: codecov/codecov-action@v1
|
uses: codecov/codecov-action@v4
|
||||||
with:
|
with:
|
||||||
file: src/coverage.xml
|
file: src/coverage.xml
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: false
|
||||||
if: matrix.database == 'postgres' && matrix.python-version == '3.11'
|
if: matrix.database == 'postgres' && matrix.python-version == '3.11'
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ tests:
|
|||||||
- cd src
|
- cd src
|
||||||
- python manage.py check
|
- python manage.py check
|
||||||
- make all compress
|
- make all compress
|
||||||
- PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg py.test --reruns 3 -n 3 tests --maxfail=100
|
- PRETIX_CONFIG_FILE=tests/ci_sqlite.cfg py.test -n 3 tests --maxfail=100
|
||||||
except:
|
except:
|
||||||
- pypi
|
- pypi
|
||||||
pypi:
|
pypi:
|
||||||
|
|||||||
1
.node-version
Normal file
1
.node-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
17
|
||||||
@@ -10,6 +10,8 @@ recursive-include src/pretix/helpers/locale *
|
|||||||
recursive-include src/pretix/base/templates *
|
recursive-include src/pretix/base/templates *
|
||||||
recursive-include src/pretix/control/templates *
|
recursive-include src/pretix/control/templates *
|
||||||
recursive-include src/pretix/presale/templates *
|
recursive-include src/pretix/presale/templates *
|
||||||
|
recursive-include src/pretix/plugins/autocheckin/templates *
|
||||||
|
recursive-include src/pretix/plugins/autocheckin/static *
|
||||||
recursive-include src/pretix/plugins/banktransfer/templates *
|
recursive-include src/pretix/plugins/banktransfer/templates *
|
||||||
recursive-include src/pretix/plugins/banktransfer/static *
|
recursive-include src/pretix/plugins/banktransfer/static *
|
||||||
recursive-include src/pretix/plugins/manualpayment/templates *
|
recursive-include src/pretix/plugins/manualpayment/templates *
|
||||||
|
|||||||
@@ -60,6 +60,14 @@ http {
|
|||||||
deny all;
|
deny all;
|
||||||
return 404;
|
return 404;
|
||||||
}
|
}
|
||||||
|
location /static/staticfiles.json {
|
||||||
|
deny all;
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
location /static/CACHE/manifest.json {
|
||||||
|
deny all;
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
location /static/ {
|
location /static/ {
|
||||||
alias /pretix/src/pretix/static.dist/;
|
alias /pretix/src/pretix/static.dist/;
|
||||||
access_log off;
|
access_log off;
|
||||||
|
|||||||
@@ -288,17 +288,26 @@ Example::
|
|||||||
[django]
|
[django]
|
||||||
secret=j1kjps5a5&4ilpn912s7a1!e2h!duz^i3&idu@_907s$wrz@x-
|
secret=j1kjps5a5&4ilpn912s7a1!e2h!duz^i3&idu@_907s$wrz@x-
|
||||||
debug=off
|
debug=off
|
||||||
|
passwords_argon2=on
|
||||||
|
|
||||||
``secret``
|
``secret``
|
||||||
The secret to be used by Django for signing and verification purposes. If this
|
The secret to be used by Django for signing and verification purposes. If this
|
||||||
setting is not provided, pretix will generate a random secret on the first start
|
setting is not provided, pretix will generate a random secret on the first start
|
||||||
and will store it in the filesystem for later usage.
|
and will store it in the filesystem for later usage.
|
||||||
|
|
||||||
|
``secret_fallback0`` ... ``secret_fallback9``
|
||||||
|
Prior versions of the secret to be used by Django for signing and verification purposes that will still
|
||||||
|
be accepted but no longer be used for new signing.
|
||||||
|
|
||||||
``debug``
|
``debug``
|
||||||
Whether or not to run in debug mode. Default is ``False``.
|
Whether or not to run in debug mode. Default is ``False``.
|
||||||
|
|
||||||
.. WARNING:: Never set this to ``True`` in production!
|
.. WARNING:: Never set this to ``True`` in production!
|
||||||
|
|
||||||
|
``passwords_argon``
|
||||||
|
Use the ``argon2`` algorithm for password hashing. Disable on systems with a small number of CPU cores (currently
|
||||||
|
less than 8).
|
||||||
|
|
||||||
``profile``
|
``profile``
|
||||||
Enable code profiling for a random subset of requests. Disabled by default, see
|
Enable code profiling for a random subset of requests. Disabled by default, see
|
||||||
:ref:`perf-monitoring` for details.
|
:ref:`perf-monitoring` for details.
|
||||||
|
|||||||
@@ -231,11 +231,10 @@ The following snippet is an example on how to configure a nginx proxy for pretix
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
server {
|
server {
|
||||||
listen 443 default_server;
|
listen 443 ssl default_server;
|
||||||
listen [::]:443 ipv6only=on default_server;
|
listen [::]:443 ipv6only=on ssl default_server;
|
||||||
server_name pretix.mydomain.com;
|
server_name pretix.mydomain.com;
|
||||||
|
|
||||||
ssl on;
|
|
||||||
ssl_certificate /path/to/cert.chain.pem;
|
ssl_certificate /path/to/cert.chain.pem;
|
||||||
ssl_certificate_key /path/to/key.pem;
|
ssl_certificate_key /path/to/key.pem;
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ Package dependencies
|
|||||||
To build and run pretix, you will need the following debian packages::
|
To build and run pretix, you will need the following debian packages::
|
||||||
|
|
||||||
# apt-get install git build-essential python3-dev python3-venv python3 python3-pip \
|
# apt-get install git build-essential python3-dev python3-venv python3 python3-pip \
|
||||||
python3-dev libxml2-dev libxslt1-dev libffi-dev zlib1g-dev libssl-dev \
|
libxml2-dev libxslt1-dev libffi-dev zlib1g-dev libssl-dev \
|
||||||
gettext libpq-dev libjpeg-dev libopenjp2-7-dev
|
gettext libpq-dev libjpeg-dev libopenjp2-7-dev
|
||||||
|
|
||||||
Config file
|
Config file
|
||||||
@@ -216,11 +216,10 @@ The following snippet is an example on how to configure a nginx proxy for pretix
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
server {
|
server {
|
||||||
listen 443 default_server;
|
listen 443 ssl default_server;
|
||||||
listen [::]:443 ipv6only=on default_server;
|
listen [::]:443 ipv6only=on ssl default_server;
|
||||||
server_name pretix.mydomain.com;
|
server_name pretix.mydomain.com;
|
||||||
|
|
||||||
ssl on;
|
|
||||||
ssl_certificate /path/to/cert.chain.pem;
|
ssl_certificate /path/to/cert.chain.pem;
|
||||||
ssl_certificate_key /path/to/key.pem;
|
ssl_certificate_key /path/to/key.pem;
|
||||||
|
|
||||||
@@ -249,6 +248,14 @@ The following snippet is an example on how to configure a nginx proxy for pretix
|
|||||||
return 404;
|
return 404;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /static/staticfiles.json {
|
||||||
|
deny all;
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
location /static/CACHE/manifest.json {
|
||||||
|
deny all;
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
location /static/ {
|
location /static/ {
|
||||||
alias /var/pretix/venv/lib/python3.11/site-packages/pretix/static.dist/;
|
alias /var/pretix/venv/lib/python3.11/site-packages/pretix/static.dist/;
|
||||||
access_log off;
|
access_log off;
|
||||||
|
|||||||
@@ -73,4 +73,11 @@ This release includes a migration that changes retroactively fills an `organizer
|
|||||||
`pretixbase_logentry`. If you have a large database, the migration step of the upgrade might take significantly
|
`pretixbase_logentry`. If you have a large database, the migration step of the upgrade might take significantly
|
||||||
longer than usual, so plan the update accordingly.
|
longer than usual, so plan the update accordingly.
|
||||||
|
|
||||||
|
Upgrade to 2024.7.0 or newer
|
||||||
|
"""""""""""""""""""""""""""""
|
||||||
|
|
||||||
|
This release includes a migration that changes how sales channels are referred on orders.
|
||||||
|
If you have a large database, the migration step of the upgrade might take significantly longer than usual, so plan
|
||||||
|
the update accordingly.
|
||||||
|
|
||||||
.. _blog: https://pretix.eu/about/en/blog/
|
.. _blog: https://pretix.eu/about/en/blog/
|
||||||
|
|||||||
259
doc/api/resources/auto_checkin_rules.rst
Normal file
259
doc/api/resources/auto_checkin_rules.rst
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
.. _rest-autocheckinrules:
|
||||||
|
|
||||||
|
Auto check-in rules
|
||||||
|
===================
|
||||||
|
|
||||||
|
This feature requires the bundled ``pretix.plugins.autocheckin`` plugin to be active for the event in order to work properly.
|
||||||
|
|
||||||
|
Resource description
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
Auto check-in rules specify that tickets should under specific conditions automatically be considered checked in after
|
||||||
|
they have been purchased.
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
id integer Internal ID of the rule
|
||||||
|
list integer ID of the check-in list to check the ticket in on. If
|
||||||
|
``None``, the system will select all matching check-in lists.
|
||||||
|
mode string ``"placed"`` if the rule should be evaluated right after
|
||||||
|
an order has been created, ``"paid"`` if the rule should
|
||||||
|
be evaluated after the order has been fully paid.
|
||||||
|
all_sales_channels boolean If ``true`` (default), the rule applies to tickets sold on all sales channels.
|
||||||
|
limit_sales_channels list of strings List of sales channel identifiers the rule should apply to
|
||||||
|
if ``all_sales_channels`` is ``false``.
|
||||||
|
all_products boolean If ``true`` (default), the rule affects all products and variations.
|
||||||
|
limit_products list of integers List of item IDs, if ``all_products`` is not set. If the
|
||||||
|
product listed here has variations, all variations will be matched.
|
||||||
|
limit_variations list of integers List of product variation IDs, if ``all_products`` is not set.
|
||||||
|
The parent product does not need to be part of ``limit_products``.
|
||||||
|
all_payment_methods boolean If ``true`` (default), the rule applies to tickets paid with all payment methods.
|
||||||
|
limit_payment_methods list of strings List of payment method identifiers the rule should apply to
|
||||||
|
if ``all_payment_methods`` is ``false``.
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
.. versionadded:: 2024.7
|
||||||
|
|
||||||
|
Endpoints
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/auto_checkin_rules/
|
||||||
|
|
||||||
|
Returns a list of all rules configured for an event.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/auto_checkin_rules/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"list": 12345,
|
||||||
|
"mode": "placed",
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
|
"all_products": false,
|
||||||
|
"limit_products": [2, 3],
|
||||||
|
"limit_variations": [456],
|
||||||
|
"all_payment_methods": true,
|
||||||
|
"limit_payment_methods": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:query page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:param organizer: The ``slug`` field of a valid organizer
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/auto_checkin_rules/(id)/
|
||||||
|
|
||||||
|
Returns information on one rule, identified by its ID.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/auto_checkin_rules/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"list": 12345,
|
||||||
|
"mode": "placed",
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
|
"all_products": false,
|
||||||
|
"limit_products": [2, 3],
|
||||||
|
"limit_variations": [456],
|
||||||
|
"all_payment_methods": true,
|
||||||
|
"limit_payment_methods": []
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:param id: The ``id`` field of the rule to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/auto_checkin_rules/
|
||||||
|
|
||||||
|
Create a new rule.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/auto_checkin_rules/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 166
|
||||||
|
|
||||||
|
{
|
||||||
|
"list": 12345,
|
||||||
|
"mode": "placed",
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
|
"all_products": false,
|
||||||
|
"limit_products": [2, 3],
|
||||||
|
"limit_variations": [456],
|
||||||
|
"all_payment_methods": true,
|
||||||
|
"limit_payment_methods": []
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"list": 12345,
|
||||||
|
"mode": "placed",
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
|
"all_products": false,
|
||||||
|
"limit_products": [2, 3],
|
||||||
|
"limit_variations": [456],
|
||||||
|
"all_payment_methods": true,
|
||||||
|
"limit_payment_methods": []
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to create a rule for
|
||||||
|
:param event: The ``slug`` field of the event to create a rule for
|
||||||
|
:statuscode 201: no error
|
||||||
|
:statuscode 400: The rule could not be created due to invalid submitted data.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create rules.
|
||||||
|
|
||||||
|
|
||||||
|
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/auto_checkin_rules/(id)/
|
||||||
|
|
||||||
|
Update a rule. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||||
|
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||||
|
want to change.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PATCH /api/v1/organizers/bigevents/events/sampleconf/auto_checkin_rules/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 34
|
||||||
|
|
||||||
|
{
|
||||||
|
"mode": "paid",
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"list": 12345,
|
||||||
|
"mode": "placed",
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
|
"all_products": false,
|
||||||
|
"limit_products": [2, 3],
|
||||||
|
"limit_variations": [456],
|
||||||
|
"all_payment_methods": true,
|
||||||
|
"limit_payment_methods": []
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param event: The ``slug`` field of the event to modify
|
||||||
|
:param id: The ``id`` field of the rule to modify
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The rule could not be modified due to invalid submitted data.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it.
|
||||||
|
|
||||||
|
|
||||||
|
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/auto_checkin_rules/(id)/
|
||||||
|
|
||||||
|
Delete a rule.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
DELETE /api/v1/organizers/bigevents/events/sampleconf/auto_checkin_rules/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
Vary: Accept
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param event: The ``slug`` field of the event to modify
|
||||||
|
:param id: The ``id`` field of the rule to delete
|
||||||
|
:statuscode 204: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it **or** this rule cannot be deleted since it is currently in use.
|
||||||
@@ -40,6 +40,11 @@ answers list of objects Answers to user
|
|||||||
seat objects The assigned seat (or ``null``)
|
seat objects The assigned seat (or ``null``)
|
||||||
├ id integer Internal ID of the seat instance
|
├ id integer Internal ID of the seat instance
|
||||||
├ name string Human-readable seat name
|
├ name string Human-readable seat name
|
||||||
|
├ zone_name string Name of the zone the seat is in
|
||||||
|
├ row_name string Name/number of the row the seat is in
|
||||||
|
├ row_label string Additional label of the row (or ``null``)
|
||||||
|
├ seat_number string Number of the seat within the row
|
||||||
|
├ seat_label string Additional label of the seat (or ``null``)
|
||||||
└ seat_guid string Identifier of the seat within the seating plan
|
└ seat_guid string Identifier of the seat within the seating plan
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,22 @@ position integer An integer, use
|
|||||||
is_addon boolean If ``true``, items within this category are not on sale
|
is_addon boolean If ``true``, items within this category are not on sale
|
||||||
on their own but the category provides a source for
|
on their own but the category provides a source for
|
||||||
defining add-ons for other products.
|
defining add-ons for other products.
|
||||||
|
cross_selling_mode string If ``null``, cross-selling is disabled for this category.
|
||||||
|
If ``"only"``, it is only visible in the cross-selling
|
||||||
|
step.
|
||||||
|
If ``"both"``, it is visible on the normal index page
|
||||||
|
as well.
|
||||||
|
Only available if ``is_addon`` is ``false``.
|
||||||
|
cross_selling_condition string Only relevant if ``cross_selling_mode`` is not ``null``.
|
||||||
|
If ``"always"``, always show in cross-selling step.
|
||||||
|
If ``"products"``, only show if the cart contains one of
|
||||||
|
the products listed in ``cross_selling_match_products``.
|
||||||
|
If ``"discounts"``, only show products that qualify for
|
||||||
|
a discount according to discount rules.
|
||||||
|
cross_selling_match_products list of integer Only relevant if ``cross_selling_condition`` is
|
||||||
|
``"products"``. Internal ID of the items of which at
|
||||||
|
least one needs to be in the cart for this category to
|
||||||
|
be shown.
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
@@ -60,7 +76,10 @@ Endpoints
|
|||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
"description": {"en": "Tickets are what you need to get in."},
|
"description": {"en": "Tickets are what you need to get in."},
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"is_addon": false
|
"is_addon": false,
|
||||||
|
"cross_selling_mode": null,
|
||||||
|
"cross_selling_condition": null,
|
||||||
|
"cross_selling_match_products": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -102,7 +121,10 @@ Endpoints
|
|||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
"description": {"en": "Tickets are what you need to get in."},
|
"description": {"en": "Tickets are what you need to get in."},
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"is_addon": false
|
"is_addon": false,
|
||||||
|
"cross_selling_mode": null,
|
||||||
|
"cross_selling_condition": null,
|
||||||
|
"cross_selling_match_products": []
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -130,7 +152,10 @@ Endpoints
|
|||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
"description": {"en": "Tickets are what you need to get in."},
|
"description": {"en": "Tickets are what you need to get in."},
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"is_addon": false
|
"is_addon": false,
|
||||||
|
"cross_selling_mode": null,
|
||||||
|
"cross_selling_condition": null,
|
||||||
|
"cross_selling_match_products": []
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -147,7 +172,10 @@ Endpoints
|
|||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
"description": {"en": "Tickets are what you need to get in."},
|
"description": {"en": "Tickets are what you need to get in."},
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"is_addon": false
|
"is_addon": false,
|
||||||
|
"cross_selling_mode": null,
|
||||||
|
"cross_selling_condition": null,
|
||||||
|
"cross_selling_match_products": []
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer of the event to create a category for
|
:param organizer: The ``slug`` field of the organizer of the event to create a category for
|
||||||
@@ -193,7 +221,10 @@ Endpoints
|
|||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
"description": {"en": "Tickets are what you need to get in."},
|
"description": {"en": "Tickets are what you need to get in."},
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"is_addon": true
|
"is_addon": true,
|
||||||
|
"cross_selling_mode": null,
|
||||||
|
"cross_selling_condition": null,
|
||||||
|
"cross_selling_match_products": []
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to modify
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ subevent integer ID of the date
|
|||||||
position_count integer Number of tickets that match this list (read-only).
|
position_count integer Number of tickets that match this list (read-only).
|
||||||
checkin_count integer Number of check-ins performed on this list (read-only).
|
checkin_count integer Number of check-ins performed on this list (read-only).
|
||||||
include_pending boolean If ``true``, the check-in list also contains tickets from orders in pending state.
|
include_pending boolean If ``true``, the check-in list also contains tickets from orders in pending state.
|
||||||
auto_checkin_sales_channels list of strings All items on the check-in list will be automatically marked as checked-in when purchased through any of the listed sales channels.
|
|
||||||
allow_multiple_entries boolean If ``true``, subsequent scans of a ticket on this list should not show a warning but instead be stored as an additional check-in.
|
allow_multiple_entries boolean If ``true``, subsequent scans of a ticket on this list should not show a warning but instead be stored as an additional check-in.
|
||||||
allow_entry_after_exit boolean If ``true``, subsequent scans of a ticket on this list are valid if the last scan of the ticket was an exit scan.
|
allow_entry_after_exit boolean If ``true``, subsequent scans of a ticket on this list are valid if the last scan of the ticket was an exit scan.
|
||||||
rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged.
|
rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged.
|
||||||
@@ -90,10 +89,7 @@ Endpoints
|
|||||||
"allow_entry_after_exit": true,
|
"allow_entry_after_exit": true,
|
||||||
"exit_all_at": null,
|
"exit_all_at": null,
|
||||||
"rules": {},
|
"rules": {},
|
||||||
"addon_match": false,
|
"addon_match": false
|
||||||
"auto_checkin_sales_channels": [
|
|
||||||
"pretixpos"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -145,10 +141,7 @@ Endpoints
|
|||||||
"allow_entry_after_exit": true,
|
"allow_entry_after_exit": true,
|
||||||
"exit_all_at": null,
|
"exit_all_at": null,
|
||||||
"rules": {},
|
"rules": {},
|
||||||
"addon_match": false,
|
"addon_match": false
|
||||||
"auto_checkin_sales_channels": [
|
|
||||||
"pretixpos"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -245,10 +238,7 @@ Endpoints
|
|||||||
"subevent": null,
|
"subevent": null,
|
||||||
"allow_multiple_entries": false,
|
"allow_multiple_entries": false,
|
||||||
"allow_entry_after_exit": true,
|
"allow_entry_after_exit": true,
|
||||||
"addon_match": false,
|
"addon_match": false
|
||||||
"auto_checkin_sales_channels": [
|
|
||||||
"pretixpos"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -270,10 +260,7 @@ Endpoints
|
|||||||
"subevent": null,
|
"subevent": null,
|
||||||
"allow_multiple_entries": false,
|
"allow_multiple_entries": false,
|
||||||
"allow_entry_after_exit": true,
|
"allow_entry_after_exit": true,
|
||||||
"addon_match": false,
|
"addon_match": false
|
||||||
"auto_checkin_sales_channels": [
|
|
||||||
"pretixpos"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer of the event/item to create a list for
|
:param organizer: The ``slug`` field of the organizer of the event/item to create a list for
|
||||||
@@ -325,10 +312,7 @@ Endpoints
|
|||||||
"subevent": null,
|
"subevent": null,
|
||||||
"allow_multiple_entries": false,
|
"allow_multiple_entries": false,
|
||||||
"allow_entry_after_exit": true,
|
"allow_entry_after_exit": true,
|
||||||
"addon_match": false,
|
"addon_match": false
|
||||||
"auto_checkin_sales_channels": [
|
|
||||||
"pretixpos"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to modify
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
@@ -341,7 +325,7 @@ Endpoints
|
|||||||
|
|
||||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/checkinlist/(id)/
|
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/checkinlist/(id)/
|
||||||
|
|
||||||
Delete a check-in list. Note that this also deletes the information on all check-ins performed via this list.
|
Delete a check-in list. **Note that this also deletes the information on all check-ins performed via this list.**
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,12 @@ id integer Internal ID
|
|||||||
active boolean The discount will be ignored if this is ``false``
|
active boolean The discount will be ignored if this is ``false``
|
||||||
internal_name string A name for the rule used in the backend
|
internal_name string A name for the rule used in the backend
|
||||||
position integer An integer, used for sorting the rules which are applied in order
|
position integer An integer, used for sorting the rules which are applied in order
|
||||||
sales_channels list of strings Sales channels this discount is available on, such as
|
all_sales_channels boolean If ``true`` (default), the discount is available on all sales channels
|
||||||
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
|
that support discounts.
|
||||||
|
limit_sales_channels list of strings List of sales channel identifiers the discount is available on
|
||||||
|
if ``all_sales_channels`` is ``false``.
|
||||||
|
sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
|
||||||
|
and ``limit_sales_channels`` instead.
|
||||||
available_from datetime The first date time at which this discount can be applied
|
available_from datetime The first date time at which this discount can be applied
|
||||||
(or ``null``).
|
(or ``null``).
|
||||||
available_until datetime The last date time at which this discount can be applied
|
available_until datetime The last date time at which this discount can be applied
|
||||||
@@ -95,6 +99,8 @@ Endpoints
|
|||||||
"active": true,
|
"active": true,
|
||||||
"internal_name": "3 for 2",
|
"internal_name": "3 for 2",
|
||||||
"position": 1,
|
"position": 1,
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
@@ -151,6 +157,8 @@ Endpoints
|
|||||||
"active": true,
|
"active": true,
|
||||||
"internal_name": "3 for 2",
|
"internal_name": "3 for 2",
|
||||||
"position": 1,
|
"position": 1,
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
@@ -193,6 +201,8 @@ Endpoints
|
|||||||
"active": true,
|
"active": true,
|
||||||
"internal_name": "3 for 2",
|
"internal_name": "3 for 2",
|
||||||
"position": 1,
|
"position": 1,
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
@@ -224,6 +234,8 @@ Endpoints
|
|||||||
"active": true,
|
"active": true,
|
||||||
"internal_name": "3 for 2",
|
"internal_name": "3 for 2",
|
||||||
"position": 1,
|
"position": 1,
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
@@ -284,6 +296,8 @@ Endpoints
|
|||||||
"active": false,
|
"active": false,
|
||||||
"internal_name": "3 for 2",
|
"internal_name": "3 for 2",
|
||||||
"position": 1,
|
"position": 1,
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
|||||||
@@ -49,8 +49,11 @@ item_meta_properties object Item-specific m
|
|||||||
valid_keys object Cryptographic keys for non-default signature schemes.
|
valid_keys object Cryptographic keys for non-default signature schemes.
|
||||||
For performance reason, value is omitted in lists and
|
For performance reason, value is omitted in lists and
|
||||||
only contained in detail views. Value can be cached.
|
only contained in detail views. Value can be cached.
|
||||||
sales_channels list A list of sales channels this event is available for
|
all_sales_channels boolean If ``true`` (default), the event is available on all sales channels.
|
||||||
sale on.
|
limit_sales_channels list of strings List of sales channel identifiers the event is available on
|
||||||
|
if ``all_sales_channels`` is ``false``.
|
||||||
|
sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
|
||||||
|
and ``limit_sales_channels`` instead.
|
||||||
public_url string The public, customer-facing URL of the event (read-only).
|
public_url string The public, customer-facing URL of the event (read-only).
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
@@ -131,11 +134,13 @@ Endpoints
|
|||||||
"pretix.plugins.paypal",
|
"pretix.plugins.paypal",
|
||||||
"pretix.plugins.ticketoutputpdf"
|
"pretix.plugins.ticketoutputpdf"
|
||||||
],
|
],
|
||||||
"sales_channels": [
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": [
|
||||||
"web",
|
"web",
|
||||||
"pretixpos",
|
"pretixpos",
|
||||||
"resellers"
|
"resellers"
|
||||||
],
|
],
|
||||||
|
"sales_channels": [],
|
||||||
"public_url": "https://pretix.eu/bigevents/sampleconf/"
|
"public_url": "https://pretix.eu/bigevents/sampleconf/"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -225,6 +230,8 @@ Endpoints
|
|||||||
"LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUNvd0JRWURLMlZ3QXlFQTdBRDcvdkZBMzNFc1k0ejJQSHI3aVpQc1o4bjVkaDBhalA4Z3l6Tm1tSXM9Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo="
|
"LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUNvd0JRWURLMlZ3QXlFQTdBRDcvdkZBMzNFc1k0ejJQSHI3aVpQc1o4bjVkaDBhalA4Z3l6Tm1tSXM9Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo="
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"all_sales_channels": true,
|
||||||
|
"limit_sales_channels": [],
|
||||||
"sales_channels": [
|
"sales_channels": [
|
||||||
"web",
|
"web",
|
||||||
"pretixpos",
|
"pretixpos",
|
||||||
@@ -282,11 +289,8 @@ Endpoints
|
|||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
"pretix.plugins.paypal"
|
"pretix.plugins.paypal"
|
||||||
],
|
],
|
||||||
"sales_channels": [
|
"all_sales_channels": true,
|
||||||
"web",
|
"limit_sales_channels": []
|
||||||
"pretixpos",
|
|
||||||
"resellers"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -322,6 +326,8 @@ Endpoints
|
|||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
"pretix.plugins.paypal"
|
"pretix.plugins.paypal"
|
||||||
],
|
],
|
||||||
|
"all_sales_channels": true,
|
||||||
|
"limit_sales_channels": [],
|
||||||
"sales_channels": [
|
"sales_channels": [
|
||||||
"web",
|
"web",
|
||||||
"pretixpos",
|
"pretixpos",
|
||||||
@@ -387,11 +393,8 @@ Endpoints
|
|||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
"pretix.plugins.paypal"
|
"pretix.plugins.paypal"
|
||||||
],
|
],
|
||||||
"sales_channels": [
|
"all_sales_channels": true,
|
||||||
"web",
|
"limit_sales_channels": []
|
||||||
"pretixpos",
|
|
||||||
"resellers"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -427,6 +430,8 @@ Endpoints
|
|||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
"pretix.plugins.paypal"
|
"pretix.plugins.paypal"
|
||||||
],
|
],
|
||||||
|
"all_sales_channels": true,
|
||||||
|
"limit_sales_channels": [],
|
||||||
"sales_channels": [
|
"sales_channels": [
|
||||||
"web",
|
"web",
|
||||||
"pretixpos",
|
"pretixpos",
|
||||||
@@ -502,6 +507,8 @@ Endpoints
|
|||||||
"pretix.plugins.paypal",
|
"pretix.plugins.paypal",
|
||||||
"pretix.plugins.pretixdroid"
|
"pretix.plugins.pretixdroid"
|
||||||
],
|
],
|
||||||
|
"all_sales_channels": true,
|
||||||
|
"limit_sales_channels": [],
|
||||||
"sales_channels": [
|
"sales_channels": [
|
||||||
"web",
|
"web",
|
||||||
"pretixpos",
|
"pretixpos",
|
||||||
|
|||||||
@@ -96,6 +96,8 @@ Endpoints
|
|||||||
|
|
||||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
:query string secret: Only show gift cards with the given secret.
|
:query string secret: Only show gift cards with the given secret.
|
||||||
|
:query string value: Only show gift cards with the given value.
|
||||||
|
:query boolean expired: Filter for gift cards that are (not) expired.
|
||||||
:query boolean testmode: Filter for gift cards that are (not) in test mode.
|
:query boolean testmode: Filter for gift cards that are (not) in test mode.
|
||||||
:query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer.
|
:query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer.
|
||||||
:query string expand: If you pass ``"owner_ticket"``, the respective field will be shown as a nested value instead of just an ID.
|
:query string expand: If you pass ``"owner_ticket"``, the respective field will be shown as a nested value instead of just an ID.
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ at :ref:`plugin-docs`.
|
|||||||
checkinlists
|
checkinlists
|
||||||
waitinglist
|
waitinglist
|
||||||
customers
|
customers
|
||||||
|
saleschannels
|
||||||
membershiptypes
|
membershiptypes
|
||||||
memberships
|
memberships
|
||||||
giftcards
|
giftcards
|
||||||
@@ -43,5 +44,7 @@ at :ref:`plugin-docs`.
|
|||||||
scheduled_exports
|
scheduled_exports
|
||||||
shredders
|
shredders
|
||||||
sendmail_rules
|
sendmail_rules
|
||||||
|
auto_checkin_rules
|
||||||
billing_invoices
|
billing_invoices
|
||||||
billing_var
|
billing_var
|
||||||
|
seats
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ lines list of objects The actual invo
|
|||||||
├ gross_value money (string) Price including taxes
|
├ gross_value money (string) Price including taxes
|
||||||
├ tax_value money (string) Tax amount included
|
├ tax_value money (string) Tax amount included
|
||||||
├ tax_name string Name of used tax rate (e.g. "VAT")
|
├ tax_name string Name of used tax rate (e.g. "VAT")
|
||||||
|
├ tax_code string Codified reason for tax rate (or ``null``), see :ref:`rest-taxcodes`.
|
||||||
└ tax_rate decimal (string) Used tax rate
|
└ tax_rate decimal (string) Used tax rate
|
||||||
foreign_currency_display string If the invoice should also show the total and tax
|
foreign_currency_display string If the invoice should also show the total and tax
|
||||||
amount in a different currency, this contains the
|
amount in a different currency, this contains the
|
||||||
@@ -126,6 +127,10 @@ internal_reference string Customer's refe
|
|||||||
|
|
||||||
The ``event`` attribute has been added. The organizer-level endpoint has been added.
|
The ``event`` attribute has been added. The organizer-level endpoint has been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 2024.8
|
||||||
|
|
||||||
|
The ``tax_code`` attribute has been added.
|
||||||
|
|
||||||
|
|
||||||
List of all invoices
|
List of all invoices
|
||||||
--------------------
|
--------------------
|
||||||
@@ -203,6 +208,7 @@ List of all invoices
|
|||||||
"gross_value": "23.00",
|
"gross_value": "23.00",
|
||||||
"tax_value": "0.00",
|
"tax_value": "0.00",
|
||||||
"tax_name": "VAT",
|
"tax_name": "VAT",
|
||||||
|
"tax_code": "S/standard",
|
||||||
"tax_rate": "0.00"
|
"tax_rate": "0.00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -217,6 +223,9 @@ List of all invoices
|
|||||||
:query boolean is_cancellation: If set to ``true`` or ``false``, only invoices with this value for the field
|
:query boolean is_cancellation: If set to ``true`` or ``false``, only invoices with this value for the field
|
||||||
``is_cancellation`` will be returned.
|
``is_cancellation`` will be returned.
|
||||||
:query string order: If set, only invoices belonging to the order with the given order code will be returned.
|
:query string order: If set, only invoices belonging to the order with the given order code will be returned.
|
||||||
|
This parameter may be given multiple times. In this case, all invoices matching one of the inputs will be returned.
|
||||||
|
:query string number: If set, only invoices with the given invoice number will be returned.
|
||||||
|
This parameter may be given multiple times. In this case, all invoices matching one of the inputs will be returned.
|
||||||
:query string refers: If set, only invoices referring to the given invoice will be returned.
|
:query string refers: If set, only invoices referring to the given invoice will be returned.
|
||||||
:query string locale: If set, only invoices with the given locale will be returned.
|
:query string locale: If set, only invoices with the given locale will be returned.
|
||||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``date`` and
|
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``date`` and
|
||||||
@@ -339,6 +348,7 @@ Fetching individual invoices
|
|||||||
"gross_value": "23.00",
|
"gross_value": "23.00",
|
||||||
"tax_value": "0.00",
|
"tax_value": "0.00",
|
||||||
"tax_name": "VAT",
|
"tax_name": "VAT",
|
||||||
|
"tax_code": "S/standard",
|
||||||
"tax_rate": "0.00"
|
"tax_rate": "0.00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -349,12 +359,12 @@ Fetching individual invoices
|
|||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:param event: The ``slug`` field of the event to fetch
|
:param event: The ``slug`` field of the event to fetch
|
||||||
:param invoice_no: The ``invoice_no`` field of the invoice to fetch
|
:param number: The ``number`` field of the invoice to fetch
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
: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)/invoices/(invoice_no)/download/
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/invoices/(number)/download/
|
||||||
|
|
||||||
Download an invoice in PDF format.
|
Download an invoice in PDF format.
|
||||||
|
|
||||||
@@ -381,7 +391,7 @@ Fetching individual invoices
|
|||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:param event: The ``slug`` field of the event to fetch
|
:param event: The ``slug`` field of the event to fetch
|
||||||
:param invoice_no: The ``invoice_no`` field of the invoice to fetch
|
:param number: The ``number`` field of the invoice to fetch
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||||
@@ -394,7 +404,7 @@ Modifying invoices
|
|||||||
|
|
||||||
Invoices cannot be edited directly, but the following actions can be triggered:
|
Invoices cannot be edited directly, but the following actions can be triggered:
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(invoice_no)/reissue/
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(number)/reissue/
|
||||||
|
|
||||||
Cancels the invoice and creates a new one.
|
Cancels the invoice and creates a new one.
|
||||||
|
|
||||||
@@ -416,13 +426,13 @@ Invoices cannot be edited directly, but the following actions can be triggered:
|
|||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:param event: The ``slug`` field of the event to fetch
|
:param event: The ``slug`` field of the event to fetch
|
||||||
:param invoice_no: The ``invoice_no`` field of the invoice to reissue
|
:param number: The ``number`` field of the invoice to reissue
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 400: The invoice has already been canceled
|
:statuscode 400: The invoice has already been canceled
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(invoice_no)/regenerate/
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(number)/regenerate/
|
||||||
|
|
||||||
Re-generates the invoice from order data.
|
Re-generates the invoice from order data.
|
||||||
|
|
||||||
@@ -444,7 +454,7 @@ Invoices cannot be edited directly, but the following actions can be triggered:
|
|||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:param event: The ``slug`` field of the event to fetch
|
:param event: The ``slug`` field of the event to fetch
|
||||||
:param invoice_no: The ``invoice_no`` field of the invoice to regenerate
|
:param number: The ``number`` field of the invoice to regenerate
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 400: The invoice has already been canceled
|
:statuscode 400: The invoice has already been canceled
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
|
|||||||
@@ -38,11 +38,14 @@ require_membership boolean If ``true``, bo
|
|||||||
require_membership_hidden boolean If ``true`` and ``require_membership`` is set, this variation will
|
require_membership_hidden boolean If ``true`` and ``require_membership`` is set, this variation will
|
||||||
be hidden from users without a valid membership.
|
be hidden from users without a valid membership.
|
||||||
require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
|
require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
|
||||||
sales_channels list of strings Sales channels this variation is available on, such as
|
all_sales_channels boolean If ``true`` (default), the variation is available on all sales channels.
|
||||||
``"web"`` or ``"resellers"``. Defaults to all existing sales channels.
|
limit_sales_channels list of strings List of sales channel identifiers the variation is available on
|
||||||
|
if ``all_sales_channels`` is ``false``.
|
||||||
The item-level list takes precedence, i.e. a sales
|
The item-level list takes precedence, i.e. a sales
|
||||||
channel needs to be on both lists for the item to be
|
channel needs to be on both lists for the variation to be
|
||||||
available.
|
available (unless ``all_sales_channels`` is used).
|
||||||
|
sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
|
||||||
|
and ``limit_sales_channels`` instead.
|
||||||
available_from datetime The first date time at which this variation can be bought
|
available_from datetime The first date time at which this variation can be bought
|
||||||
(or ``null``).
|
(or ``null``).
|
||||||
available_from_mode string If ``hide`` (the default), this variation is hidden in the shop
|
available_from_mode string If ``hide`` (the default), this variation is hidden in the shop
|
||||||
@@ -111,6 +114,8 @@ Endpoints
|
|||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_hidden": false,
|
"require_membership_hidden": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -139,6 +144,8 @@ Endpoints
|
|||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_hidden": false,
|
"require_membership_hidden": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -157,6 +164,7 @@ Endpoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:query string search: Filter the list by the value of the variation (substring search).
|
||||||
:query boolean active: If set to ``true`` or ``false``, only items with this value for the field ``active`` will be
|
:query boolean active: If set to ``true`` or ``false``, only items with this value for the field ``active`` will be
|
||||||
returned.
|
returned.
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -202,6 +210,8 @@ Endpoints
|
|||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_hidden": false,
|
"require_membership_hidden": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -244,7 +254,8 @@ Endpoints
|
|||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_hidden": false,
|
"require_membership_hidden": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
@@ -277,6 +288,8 @@ Endpoints
|
|||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_hidden": false,
|
"require_membership_hidden": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -341,6 +354,8 @@ Endpoints
|
|||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_hidden": false,
|
"require_membership_hidden": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
|
|||||||
@@ -46,8 +46,11 @@ personalized boolean ``true`` for
|
|||||||
position integer An integer, used for sorting
|
position integer An integer, used for sorting
|
||||||
picture file A product picture to be displayed in the shop
|
picture file A product picture to be displayed in the shop
|
||||||
(can be ``null``).
|
(can be ``null``).
|
||||||
sales_channels list of strings Sales channels this product is available on, such as
|
all_sales_channels boolean If ``true`` (default), the item is available on all sales channels.
|
||||||
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
|
limit_sales_channels list of strings List of sales channel identifiers the item is available on
|
||||||
|
if ``all_sales_channels`` is ``false``.
|
||||||
|
sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
|
||||||
|
and ``limit_sales_channels`` instead.
|
||||||
available_from datetime The first date time at which this item can be bought
|
available_from datetime The first date time at which this item can be bought
|
||||||
(or ``null``).
|
(or ``null``).
|
||||||
available_from_mode string If ``hide`` (the default), this item is hidden in the shop
|
available_from_mode string If ``hide`` (the default), this item is hidden in the shop
|
||||||
@@ -157,11 +160,14 @@ variations list of objects A list with o
|
|||||||
be hidden from users without a valid membership.
|
be hidden from users without a valid membership.
|
||||||
├ require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
|
├ require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
|
||||||
Markdown syntax or can be ``null``.
|
Markdown syntax or can be ``null``.
|
||||||
├ sales_channels list of strings Sales channels this variation is available on, such as
|
├ all_sales_channels boolean If ``true`` (default), the variation is available on all sales channels.
|
||||||
``"web"`` or ``"resellers"``. Defaults to all existing sales channels.
|
├ limit_sales_channels list of strings List of sales channel identifiers the variation is available on
|
||||||
|
if ``all_sales_channels`` is ``false``.
|
||||||
The item-level list takes precedence, i.e. a sales
|
The item-level list takes precedence, i.e. a sales
|
||||||
channel needs to be on both lists for the item to be
|
channel needs to be on both lists for the variation to be
|
||||||
available.
|
available (unless ``all_sales_channels`` is used).
|
||||||
|
├ sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
|
||||||
|
and ``limit_sales_channels`` instead.
|
||||||
├ available_from datetime The first date time at which this variation can be bought
|
├ available_from datetime The first date time at which this variation can be bought
|
||||||
(or ``null``).
|
(or ``null``).
|
||||||
├ available_from_mode string If ``hide`` (the default), this variation is hidden in the shop
|
├ available_from_mode string If ``hide`` (the default), this variation is hidden in the shop
|
||||||
@@ -276,6 +282,8 @@ Endpoints
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Standard ticket"},
|
"name": {"en": "Standard ticket"},
|
||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"default_price": "23.00",
|
"default_price": "23.00",
|
||||||
"original_price": null,
|
"original_price": null,
|
||||||
@@ -340,6 +348,8 @@ Endpoints
|
|||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -362,6 +372,8 @@ Endpoints
|
|||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -380,6 +392,7 @@ Endpoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:query string search: Filter the list by internal name or name of the item (substring search).
|
||||||
:query boolean active: If set to ``true`` or ``false``, only items with this value for the field ``active`` will be
|
:query boolean active: If set to ``true`` or ``false``, only items with this value for the field ``active`` will be
|
||||||
returned.
|
returned.
|
||||||
:query integer category: If set to the ID of a category, only items within that category will be returned.
|
:query integer category: If set to the ID of a category, only items within that category will be returned.
|
||||||
@@ -420,6 +433,8 @@ Endpoints
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Standard ticket"},
|
"name": {"en": "Standard ticket"},
|
||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"default_price": "23.00",
|
"default_price": "23.00",
|
||||||
"original_price": null,
|
"original_price": null,
|
||||||
@@ -485,6 +500,8 @@ Endpoints
|
|||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"description": null,
|
"description": null,
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -506,6 +523,8 @@ Endpoints
|
|||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -545,7 +564,8 @@ Endpoints
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Standard ticket"},
|
"name": {"en": "Standard ticket"},
|
||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
"sales_channels": ["web"],
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"default_price": "23.00",
|
"default_price": "23.00",
|
||||||
"original_price": null,
|
"original_price": null,
|
||||||
"category": null,
|
"category": null,
|
||||||
@@ -608,7 +628,8 @@ Endpoints
|
|||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
@@ -630,7 +651,8 @@ Endpoints
|
|||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
@@ -657,6 +679,8 @@ Endpoints
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Standard ticket"},
|
"name": {"en": "Standard ticket"},
|
||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"default_price": "23.00",
|
"default_price": "23.00",
|
||||||
"original_price": null,
|
"original_price": null,
|
||||||
@@ -721,6 +745,8 @@ Endpoints
|
|||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -743,6 +769,8 @@ Endpoints
|
|||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -801,6 +829,8 @@ Endpoints
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Ticket"},
|
"name": {"en": "Ticket"},
|
||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"default_price": "25.00",
|
"default_price": "25.00",
|
||||||
"original_price": null,
|
"original_price": null,
|
||||||
@@ -865,6 +895,8 @@ Endpoints
|
|||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -887,6 +919,8 @@ Endpoints
|
|||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ payment_date date **DEPRECATED AN
|
|||||||
payment_provider string **DEPRECATED AND INACCURATE** Payment provider used for this order
|
payment_provider string **DEPRECATED AND INACCURATE** Payment provider used for this order
|
||||||
total money (string) Total value of this order
|
total money (string) Total value of this order
|
||||||
comment string Internal comment on this order
|
comment string Internal comment on this order
|
||||||
|
api_meta object Meta data for that order. Only available through API, no guarantees
|
||||||
|
on the content structure. You can use this to save references to your system.
|
||||||
custom_followup_at date Internal date for a custom follow-up action
|
custom_followup_at date Internal date for a custom follow-up action
|
||||||
checkin_attention boolean If ``true``, the check-in app should show a warning
|
checkin_attention boolean If ``true``, the check-in app should show a warning
|
||||||
that this ticket requires special attention if a ticket
|
that this ticket requires special attention if a ticket
|
||||||
@@ -82,6 +84,7 @@ fees list of objects List of fees in
|
|||||||
├ tax_rate decimal (string) VAT rate applied for this fee
|
├ tax_rate decimal (string) VAT rate applied for this fee
|
||||||
├ tax_value money (string) VAT included in this fee
|
├ tax_value money (string) VAT included in this fee
|
||||||
├ tax_rule integer The ID of the used tax rule (or ``null``)
|
├ tax_rule integer The ID of the used tax rule (or ``null``)
|
||||||
|
├ tax_code string Codified reason for tax rate (or ``null``), see :ref:`rest-taxcodes`.
|
||||||
└ canceled boolean Whether or not this fee has been canceled.
|
└ canceled boolean Whether or not this fee has been canceled.
|
||||||
downloads list of objects List of ticket download options for order-wise ticket
|
downloads list of objects List of ticket download options for order-wise ticket
|
||||||
downloading. This might be a multi-page PDF or a ZIP
|
downloading. This might be a multi-page PDF or a ZIP
|
||||||
@@ -102,6 +105,10 @@ url string The full URL to
|
|||||||
payments list of objects List of payment processes (see below)
|
payments list of objects List of payment processes (see below)
|
||||||
refunds list of objects List of refund processes (see below)
|
refunds list of objects List of refund processes (see below)
|
||||||
last_modified datetime Last modification of this object
|
last_modified datetime Last modification of this object
|
||||||
|
cancellation_date datetime Time of order cancellation (or ``null``). **Note**:
|
||||||
|
Will not be set for partial cancellations and is not
|
||||||
|
reliable for orders that have been cancelled,
|
||||||
|
reactivated and cancelled again.
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
@@ -149,6 +156,13 @@ last_modified datetime Last modificati
|
|||||||
|
|
||||||
The ``expires`` attribute can now be passed during order creation.
|
The ``expires`` attribute can now be passed during order creation.
|
||||||
|
|
||||||
|
.. versionchanged:: 2024.11
|
||||||
|
|
||||||
|
The ``cancellation_date`` attribute has been added and can also be used as an ordering key.
|
||||||
|
|
||||||
|
.. versionchanged:: 2025.1
|
||||||
|
|
||||||
|
The ``tax_code`` attribute has been added.
|
||||||
|
|
||||||
.. _order-position-resource:
|
.. _order-position-resource:
|
||||||
|
|
||||||
@@ -186,6 +200,7 @@ voucher_budget_use money (string) Amount of money
|
|||||||
are changed *after* the order was created. Can be ``null``.
|
are changed *after* the order was created. Can be ``null``.
|
||||||
tax_rate decimal (string) VAT rate applied for this position
|
tax_rate decimal (string) VAT rate applied for this position
|
||||||
tax_value money (string) VAT included in this position
|
tax_value money (string) VAT included in this position
|
||||||
|
tax_code string Codified reason for tax rate (or ``null``), see :ref:`rest-taxcodes`.
|
||||||
tax_rule integer The ID of the used tax rule (or ``null``)
|
tax_rule integer The ID of the used tax rule (or ``null``)
|
||||||
secret string Secret code printed on the tickets for validation
|
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``)
|
addon_to integer Internal ID of the position this position is an add-on for (or ``null``)
|
||||||
@@ -201,8 +216,20 @@ checkins list of objects List of **succe
|
|||||||
├ datetime datetime Time of check-in
|
├ datetime datetime Time of check-in
|
||||||
├ type string Type of scan (defaults to ``entry``)
|
├ type string Type of scan (defaults to ``entry``)
|
||||||
├ gate integer Internal ID of the gate. Can be ``null``.
|
├ gate integer Internal ID of the gate. Can be ``null``.
|
||||||
├ device integer Internal ID of the device. Can be ``null``.
|
├ device integer Internal ID of the device. Can be ``null``. **Deprecated**, since this ID is not otherwise used in the API and is therefore not very useful.
|
||||||
|
├ device_id integer Attribute ``device_id`` of the device. Can be ``null``.
|
||||||
└ auto_checked_in boolean Indicates if this check-in been performed automatically by the system
|
└ auto_checked_in boolean Indicates if this check-in been performed automatically by the system
|
||||||
|
print_logs list of objects List of print jobs recorded e.g. by the pretix apps
|
||||||
|
├ id integer Internal ID of the print job
|
||||||
|
├ successful boolean Whether the print job successfully resulted in a print.
|
||||||
|
This is not expected to be 100 % reliable information (since
|
||||||
|
printer feedback is never perfect) and there is no guarantee
|
||||||
|
that unsuccessful jobs will be logged.
|
||||||
|
├ device_id integer Attribute ``device_id`` of the device that recorded the print. Can be ``null``.
|
||||||
|
├ datetime datetime Time of printing
|
||||||
|
├ source string Source of print job, e.g. name of the app used.
|
||||||
|
├ type string Type of print (currently ``badge``, ``ticket``, ``certificate``, or ``other``)
|
||||||
|
└ info object Additional data with client-dependent structure.
|
||||||
downloads list of objects List of ticket download options
|
downloads list of objects List of ticket download options
|
||||||
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
|
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
|
||||||
└ url string Download URL
|
└ url string Download URL
|
||||||
@@ -215,6 +242,11 @@ answers list of objects Answers to user
|
|||||||
seat objects The assigned seat. Can be ``null``.
|
seat objects The assigned seat. Can be ``null``.
|
||||||
├ id integer Internal ID of the seat instance
|
├ id integer Internal ID of the seat instance
|
||||||
├ name string Human-readable seat name
|
├ name string Human-readable seat name
|
||||||
|
├ zone_name string Name of the zone the seat is in
|
||||||
|
├ row_name string Name/number of the row the seat is in
|
||||||
|
├ row_label string Additional label of the row (or ``null``)
|
||||||
|
├ seat_number string Number of the seat within the row
|
||||||
|
├ seat_label string Additional label of the seat (or ``null``)
|
||||||
└ seat_guid string Identifier of the seat within the seating plan
|
└ seat_guid string Identifier of the seat within the seating plan
|
||||||
pdf_data object Data object required for ticket PDF generation. By default,
|
pdf_data object Data object required for ticket PDF generation. By default,
|
||||||
this field is missing. It will be added only if you add the
|
this field is missing. It will be added only if you add the
|
||||||
@@ -225,6 +257,14 @@ pdf_data object Data object req
|
|||||||
|
|
||||||
The attributes ``blocked``, ``valid_from`` and ``valid_until`` have been added.
|
The attributes ``blocked``, ``valid_from`` and ``valid_until`` have been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 2024.9
|
||||||
|
|
||||||
|
The attribute ``print_logs`` has been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 2025.1
|
||||||
|
|
||||||
|
The ``tax_code`` attribute has been added.
|
||||||
|
|
||||||
.. _order-payment-resource:
|
.. _order-payment-resource:
|
||||||
|
|
||||||
Order payment resource
|
Order payment resource
|
||||||
@@ -376,6 +416,7 @@ List of all orders
|
|||||||
"tax_rate": "0.00",
|
"tax_rate": "0.00",
|
||||||
"tax_value": "0.00",
|
"tax_value": "0.00",
|
||||||
"tax_rule": null,
|
"tax_rule": null,
|
||||||
|
"tax_code": null,
|
||||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||||
"addon_to": null,
|
"addon_to": null,
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
@@ -391,10 +432,21 @@ List of all orders
|
|||||||
"type": "entry",
|
"type": "entry",
|
||||||
"gate": null,
|
"gate": null,
|
||||||
"device": 2,
|
"device": 2,
|
||||||
|
"device_id": 1,
|
||||||
"datetime": "2017-12-25T12:45:23Z",
|
"datetime": "2017-12-25T12:45:23Z",
|
||||||
"auto_checked_in": false
|
"auto_checked_in": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"print_logs": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "badge",
|
||||||
|
"datetime": "2017-12-25T12:45:23Z",
|
||||||
|
"device_id": 1,
|
||||||
|
"source": "pretixSCAN",
|
||||||
|
"info": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
"answers": [
|
"answers": [
|
||||||
{
|
{
|
||||||
"question": 12,
|
"question": 12,
|
||||||
@@ -430,14 +482,15 @@ List of all orders
|
|||||||
"provider": "banktransfer"
|
"provider": "banktransfer"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"refunds": []
|
"refunds": [],
|
||||||
|
"cancellation_date": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``datetime``, ``code``,
|
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``datetime``, ``code``,
|
||||||
``last_modified``, and ``status``. Default: ``datetime``
|
``last_modified``, ``status`` and ``cancellation_date``. Default: ``datetime``
|
||||||
:query string code: Only return orders that match the given order code
|
: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 status: Only return orders in the given order status (see above)
|
||||||
:query string search: Only return orders matching a given search query (matching for names, email addresses, and company names)
|
:query string search: Only return orders matching a given search query (matching for names, email addresses, and company names)
|
||||||
@@ -455,10 +508,13 @@ List of all orders
|
|||||||
:query datetime modified_since: Only return orders that have changed since the given date. Be careful: We only
|
:query datetime modified_since: Only return orders that have changed since the given date. Be careful: We only
|
||||||
recommend using this in combination with ``testmode=false``, since test mode orders can vanish at any time and
|
recommend using this in combination with ``testmode=false``, since test mode orders can vanish at any time and
|
||||||
you will not notice it using this method.
|
you will not notice it using this method.
|
||||||
:query datetime created_since: Only return orders that have been created since the given date.
|
:query datetime created_since: Only return orders that have been created since the given date (inclusive).
|
||||||
|
:query datetime created_before: Only return orders that have been created before the given date (exclusive).
|
||||||
:query integer subevent: Only return orders with a position that contains this subevent ID. *Warning:* Result will also include orders if they contain mixed subevents, and it will even return orders where the subevent is only contained in a canceled position.
|
:query integer subevent: Only return orders with a position that contains this subevent ID. *Warning:* Result will also include orders if they contain mixed subevents, and it will even return orders where the subevent is only contained in a canceled position.
|
||||||
:query datetime subevent_after: Only return orders that contain a ticket for a subevent taking place after the given date. This is an exclusive after, and it considers the **end** of the subevent (or its start, if the end is not set).
|
:query datetime subevent_after: Only return orders that contain a ticket for a subevent taking place after the given date. This is an exclusive after, and it considers the **end** of the subevent (or its start, if the end is not set).
|
||||||
:query datetime subevent_before: Only return orders that contain a ticket for a subevent taking place after the given date. This is an exclusive before, and it considers the **start** of the subevent.
|
:query datetime subevent_before: Only return orders that contain a ticket for a subevent taking place after the given date. This is an exclusive before, and it considers the **start** of the subevent.
|
||||||
|
:query string sales_channel: Only return orders with the given sales channel identifier (e.g. ``"web"``).
|
||||||
|
:query string payment_provider: Only return orders that contain a payment using the given payment provider. Note that this also searches for partial incomplete, or failed payments within the order and is not useful to get a sum of payment amounts without further processing.
|
||||||
:query string exclude: Exclude a field from the output, e.g. ``fees`` or ``positions.downloads``. Can be used as a performance optimization. Can be passed multiple times.
|
:query string exclude: Exclude a field from the output, e.g. ``fees`` or ``positions.downloads``. Can be used as a performance optimization. Can be passed multiple times.
|
||||||
:query string include: Include only the given field in the output, e.g. ``fees`` or ``positions.downloads``. Can be used as a performance optimization. Can be passed multiple times. ``include`` is applied before ``exclude``, so ``exclude`` takes precedence.
|
:query string include: Include only the given field in the output, e.g. ``fees`` or ``positions.downloads``. Can be used as a performance optimization. Can be passed multiple times. ``include`` is applied before ``exclude``, so ``exclude`` takes precedence.
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -554,6 +610,7 @@ Fetching individual orders
|
|||||||
"fees": [],
|
"fees": [],
|
||||||
"total": "23.00",
|
"total": "23.00",
|
||||||
"comment": "",
|
"comment": "",
|
||||||
|
"api_meta": {},
|
||||||
"custom_followup_at": null,
|
"custom_followup_at": null,
|
||||||
"checkin_attention": false,
|
"checkin_attention": false,
|
||||||
"checkin_text": null,
|
"checkin_text": null,
|
||||||
@@ -599,6 +656,7 @@ Fetching individual orders
|
|||||||
"tax_rate": "0.00",
|
"tax_rate": "0.00",
|
||||||
"tax_rule": null,
|
"tax_rule": null,
|
||||||
"tax_value": "0.00",
|
"tax_value": "0.00",
|
||||||
|
"tax_code": null,
|
||||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||||
"addon_to": null,
|
"addon_to": null,
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
@@ -614,10 +672,22 @@ Fetching individual orders
|
|||||||
"type": "entry",
|
"type": "entry",
|
||||||
"gate": null,
|
"gate": null,
|
||||||
"device": 2,
|
"device": 2,
|
||||||
|
"device_id": 1,
|
||||||
"datetime": "2017-12-25T12:45:23Z",
|
"datetime": "2017-12-25T12:45:23Z",
|
||||||
"auto_checked_in": false
|
"auto_checked_in": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"print_logs": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "badge",
|
||||||
|
"successful": true,
|
||||||
|
"datetime": "2017-12-25T12:45:23Z",
|
||||||
|
"device_id": 1,
|
||||||
|
"source": "pretixSCAN",
|
||||||
|
"info": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
"answers": [
|
"answers": [
|
||||||
{
|
{
|
||||||
"question": 12,
|
"question": 12,
|
||||||
@@ -653,7 +723,8 @@ Fetching individual orders
|
|||||||
"provider": "banktransfer"
|
"provider": "banktransfer"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"refunds": []
|
"refunds": [],
|
||||||
|
"cancellation_date": null
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -734,6 +805,8 @@ Updating order fields
|
|||||||
|
|
||||||
* ``comment``
|
* ``comment``
|
||||||
|
|
||||||
|
* ``api_meta``
|
||||||
|
|
||||||
* ``custom_followup_at``
|
* ``custom_followup_at``
|
||||||
|
|
||||||
* ``invoice_address`` (you always need to supply the full object, or ``null`` to delete the current address)
|
* ``invoice_address`` (you always need to supply the full object, or ``null`` to delete the current address)
|
||||||
@@ -782,7 +855,7 @@ Generating new secrets
|
|||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/regenerate_secrets/
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/regenerate_secrets/
|
||||||
|
|
||||||
Triggers generation of new ``secret`` attributes for both the order and all order positions.
|
Triggers generation of new ``secret`` and ``ẁeb_secret`` attributes for both the order and all order positions.
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
@@ -813,7 +886,7 @@ Generating new secrets
|
|||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/regenerate_secrets/
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/regenerate_secrets/
|
||||||
|
|
||||||
Triggers generation of a new ``secret`` attribute for a single order position.
|
Triggers generation of a new ``secret`` and ``web_secret`` attribute for a single order position.
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
@@ -963,8 +1036,8 @@ Creating orders
|
|||||||
* ``internal_reference``
|
* ``internal_reference``
|
||||||
* ``vat_id``
|
* ``vat_id``
|
||||||
* ``vat_id_validated`` (optional) – If you need support for reverse charge (rarely the case), you need to check
|
* ``vat_id_validated`` (optional) – If you need support for reverse charge (rarely the case), you need to check
|
||||||
yourself if the passed VAT ID is a valid EU VAT ID. In that case, set this to ``true``. Only valid VAT IDs will
|
yourself if the passed VAT ID is a valid EU VAT ID. In that case, set this to ``true``. Only valid VAT IDs will
|
||||||
trigger reverse charge taxation. Don't forget to set ``is_business`` as well!
|
trigger reverse charge taxation. Don't forget to set ``is_business`` as well!
|
||||||
|
|
||||||
* ``positions``
|
* ``positions``
|
||||||
|
|
||||||
@@ -1552,6 +1625,7 @@ List of all order positions
|
|||||||
"tax_rate": "0.00",
|
"tax_rate": "0.00",
|
||||||
"tax_rule": null,
|
"tax_rule": null,
|
||||||
"tax_value": "0.00",
|
"tax_value": "0.00",
|
||||||
|
"tax_code": null,
|
||||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||||
"discount": null,
|
"discount": null,
|
||||||
"pseudonymization_id": "MQLJvANO3B",
|
"pseudonymization_id": "MQLJvANO3B",
|
||||||
@@ -1567,10 +1641,22 @@ List of all order positions
|
|||||||
"type": "entry",
|
"type": "entry",
|
||||||
"gate": null,
|
"gate": null,
|
||||||
"device": 2,
|
"device": 2,
|
||||||
|
"device_id": 1,
|
||||||
"datetime": "2017-12-25T12:45:23Z",
|
"datetime": "2017-12-25T12:45:23Z",
|
||||||
"auto_checked_in": false
|
"auto_checked_in": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"print_logs": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "badge",
|
||||||
|
"successful": true,
|
||||||
|
"datetime": "2017-12-25T12:45:23Z",
|
||||||
|
"device_id": 1,
|
||||||
|
"source": "pretixSCAN",
|
||||||
|
"info": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
"answers": [
|
"answers": [
|
||||||
{
|
{
|
||||||
"question": 12,
|
"question": 12,
|
||||||
@@ -1666,6 +1752,7 @@ Fetching individual positions
|
|||||||
"tax_rate": "0.00",
|
"tax_rate": "0.00",
|
||||||
"tax_rule": null,
|
"tax_rule": null,
|
||||||
"tax_value": "0.00",
|
"tax_value": "0.00",
|
||||||
|
"tax_code": null,
|
||||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||||
"addon_to": null,
|
"addon_to": null,
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
@@ -1681,10 +1768,22 @@ Fetching individual positions
|
|||||||
"type": "entry",
|
"type": "entry",
|
||||||
"gate": null,
|
"gate": null,
|
||||||
"device": 2,
|
"device": 2,
|
||||||
|
"device_id": 1,
|
||||||
"datetime": "2017-12-25T12:45:23Z",
|
"datetime": "2017-12-25T12:45:23Z",
|
||||||
"auto_checked_in": false
|
"auto_checked_in": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"print_logs": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "badge",
|
||||||
|
"successful": true,
|
||||||
|
"datetime": "2017-12-25T12:45:23Z",
|
||||||
|
"device_id": 1,
|
||||||
|
"source": "pretixSCAN",
|
||||||
|
"info": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
"answers": [
|
"answers": [
|
||||||
{
|
{
|
||||||
"question": 12,
|
"question": 12,
|
||||||
@@ -1781,6 +1880,10 @@ Manipulating individual positions
|
|||||||
|
|
||||||
The endpoints to manage blocks have been added.
|
The endpoints to manage blocks have been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 2024.9
|
||||||
|
|
||||||
|
The API now supports logging ticket and badge prints.
|
||||||
|
|
||||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/
|
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/
|
||||||
|
|
||||||
Updates specific fields on an order position. Currently, only the following fields are supported:
|
Updates specific fields on an order position. Currently, only the following fields are supported:
|
||||||
@@ -2040,6 +2143,59 @@ Manipulating individual positions
|
|||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order position.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order position.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/printlog/
|
||||||
|
|
||||||
|
Creates a print log, stating that this ticket has been printed.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/printlog/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"datetime": "2024-09-19T13:37:00+02:00",
|
||||||
|
"source": "pretixPOS",
|
||||||
|
"type": "badge",
|
||||||
|
"info": {
|
||||||
|
"cashier": 1234
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/pdf
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1234,
|
||||||
|
"device_id": null,
|
||||||
|
"datetime": "2024-09-19T13:37:00+02:00",
|
||||||
|
"source": "pretixPOS",
|
||||||
|
"type": "badge",
|
||||||
|
"info": {
|
||||||
|
"cashier": 1234
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to create a log for
|
||||||
|
:param event: The ``slug`` field of the event to create a log for
|
||||||
|
:param id: The ``id`` field of the order position to create a log for
|
||||||
|
:statuscode 201: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource
|
||||||
|
**or** downloads are not available for this order position at this time. The response content will
|
||||||
|
contain more details.
|
||||||
|
:statuscode 404: The requested order position or download provider does not exist.
|
||||||
|
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
|
||||||
|
seconds.
|
||||||
|
|
||||||
Changing order contents
|
Changing order contents
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
|
|||||||
219
doc/api/resources/saleschannels.rst
Normal file
219
doc/api/resources/saleschannels.rst
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
Sales channels
|
||||||
|
==============
|
||||||
|
|
||||||
|
Resource description
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
The sales channel resource contains the following public fields:
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
identifier string Internal ID of the sales channel. For sales channel types
|
||||||
|
that allow only one instance, this is the same as ``type``.
|
||||||
|
For sales channel types that allow multiple instances, this
|
||||||
|
is always prefixed with ``type.``.
|
||||||
|
label multi-lingual string Human-readable name of the sales channel
|
||||||
|
type string Type of the sales channel. Only channels with type ``api``
|
||||||
|
can currently be created through the API.
|
||||||
|
position integer Position for sorting lists of sales channels
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
Endpoints
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/saleschannels/
|
||||||
|
|
||||||
|
Returns a list of all sales channels within a given organizer.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/saleschannels/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"identifier": "web",
|
||||||
|
"label": {
|
||||||
|
"en": "Online shop"
|
||||||
|
},
|
||||||
|
"type": "web",
|
||||||
|
"position": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/saleschannels/(identifier)/
|
||||||
|
|
||||||
|
Returns information on one sales channel, identified by its identifier.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/saleschannels/web/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"identifier": "web",
|
||||||
|
"label": {
|
||||||
|
"en": "Online shop"
|
||||||
|
},
|
||||||
|
"type": "web",
|
||||||
|
"position": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param identifier: The ``identifier`` field of the sales channel to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/saleschannels/
|
||||||
|
|
||||||
|
Creates a sales channel
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/saleschannels/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"identifier": "api.custom",
|
||||||
|
"label": {
|
||||||
|
"en": "Custom integration"
|
||||||
|
},
|
||||||
|
"type": "api",
|
||||||
|
"position": 2
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"identifier": "api.custom",
|
||||||
|
"label": {
|
||||||
|
"en": "Custom integration"
|
||||||
|
},
|
||||||
|
"type": "api",
|
||||||
|
"position": 2
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to create a sales channel for
|
||||||
|
:statuscode 201: no error
|
||||||
|
:statuscode 400: The sales channel could not be created due to invalid submitted data.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
||||||
|
|
||||||
|
.. http:patch:: /api/v1/organizers/(organizer)/saleschannels/(identifier)/
|
||||||
|
|
||||||
|
Update a sales channel. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||||
|
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||||
|
want to change.
|
||||||
|
|
||||||
|
You can change all fields of the resource except the ``identifier`` and ``type`` fields.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PATCH /api/v1/organizers/bigevents/saleschannels/web/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 94
|
||||||
|
|
||||||
|
{
|
||||||
|
"position": 5
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"identifier": "web",
|
||||||
|
"label": {
|
||||||
|
"en": "Online shop"
|
||||||
|
},
|
||||||
|
"type": "web",
|
||||||
|
"position": 5
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param identifier: The ``identifier`` field of the sales channel to modify
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The sales channel could not be modified due to invalid submitted data
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
|
||||||
|
|
||||||
|
.. http:delete:: /api/v1/organizers/(organizer)/saleschannels/(identifier)/
|
||||||
|
|
||||||
|
Delete a sales channel. You can not delete sales channels which have already been used or which are integral parts
|
||||||
|
of the system.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
DELETE /api/v1/organizers/bigevents/saleschannels/api.custom/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
Vary: Accept
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param identifier: The ``identifier`` field of the sales channel to delete
|
||||||
|
:statuscode 204: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource **or** the sales channel is currently in use.
|
||||||
@@ -313,7 +313,7 @@ Endpoints for event exports
|
|||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
|
||||||
|
|
||||||
Endpoints for organizer exports
|
Endpoints for organizer exports
|
||||||
---------------------------
|
-------------------------------
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/scheduled_exports/
|
.. http:get:: /api/v1/organizers/(organizer)/scheduled_exports/
|
||||||
|
|
||||||
@@ -553,4 +553,4 @@ Endpoints for organizer exports
|
|||||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource.
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource.
|
||||||
|
|
||||||
|
|
||||||
.. _RFC 5545: https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.5.3
|
.. _RFC 5545: https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.5.3
|
||||||
|
|||||||
373
doc/api/resources/seats.rst
Normal file
373
doc/api/resources/seats.rst
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
.. _`rest-seats`:
|
||||||
|
|
||||||
|
Seats
|
||||||
|
=====
|
||||||
|
|
||||||
|
The seat resource represents the seats in a seating plan in a specific event or subevent.
|
||||||
|
|
||||||
|
Resource description
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
The seat resource contains the following public fields:
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
id integer Internal ID of this seat
|
||||||
|
subevent integer Internal ID of the subevent this seat belongs to
|
||||||
|
zone_name string Name of the zone the seat is in
|
||||||
|
row_name string Name/number of the row the seat is in
|
||||||
|
row_label string Additional label of the row (or ``null``)
|
||||||
|
seat_number string Number of the seat within the row
|
||||||
|
seat_label string Additional label of the seat (or ``null``)
|
||||||
|
seat_guid string Identifier of the seat within the seating plan
|
||||||
|
product integer Internal ID of the product that is mapped to this seat
|
||||||
|
blocked boolean Whether this seat is blocked manually.
|
||||||
|
orderposition integer / object Internal ID of an order position reserving this seat.
|
||||||
|
cartposition integer / object Internal ID of a cart position reserving this seat.
|
||||||
|
voucher integer / object Internal ID of a voucher reserving this seat.
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
Endpoints
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/seats/
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/(subevent_id)/seats/
|
||||||
|
|
||||||
|
Returns a list of all seats in the specified event or subevent. Depending on whether the event has subevents, the
|
||||||
|
according endpoint has to be used.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/seats/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"count": 500,
|
||||||
|
"next": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/seats/?page=2",
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1633,
|
||||||
|
"subevent": null,
|
||||||
|
"zone_name": "Ground floor",
|
||||||
|
"row_name": "1",
|
||||||
|
"row_label": null,
|
||||||
|
"seat_number": "1",
|
||||||
|
"seat_label": null,
|
||||||
|
"seat_guid": "b9746230-6f31-4f41-bbc9-d6b60bdb3342",
|
||||||
|
"product": 104,
|
||||||
|
"blocked": false,
|
||||||
|
"orderposition": null,
|
||||||
|
"cartposition": null,
|
||||||
|
"voucher": 51
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1634,
|
||||||
|
"subevent": null,
|
||||||
|
"zone_name": "Ground floor",
|
||||||
|
"row_name": "1",
|
||||||
|
"row_label": null,
|
||||||
|
"seat_number": "2",
|
||||||
|
"seat_label": null,
|
||||||
|
"seat_guid": "1d29fe20-8e1e-4984-b0ee-2773b0d07e07",
|
||||||
|
"product": 104,
|
||||||
|
"blocked": true,
|
||||||
|
"orderposition": 4321,
|
||||||
|
"cartposition": null,
|
||||||
|
"voucher": null
|
||||||
|
},
|
||||||
|
// ...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:query integer page: The page number in case of a multi-page result set, default is 1.
|
||||||
|
:query string zone_name: Only show seats with the given zone_name.
|
||||||
|
:query string row_name: Only show seats with the given row_name.
|
||||||
|
:query string row_label: Only show seats with the given row_label.
|
||||||
|
:query string seat_number: Only show seats with the given seat_number.
|
||||||
|
:query string seat_label: Only show seats with the given seat_label.
|
||||||
|
:query string seat_guid: Only show seats with the given seat_guid.
|
||||||
|
:query boolean blocked: Only show seats with the given blocked status.
|
||||||
|
:query boolean is_available: Only show seats that are (not) currently available.
|
||||||
|
:query string expand: If you pass ``"orderposition"``, ``"cartposition"``, or ``"voucher"``, the respective field will be
|
||||||
|
shown as a nested value instead of just an ID. This requires permission to access that object.
|
||||||
|
The nested objects are identical to the respective resources, except that order positions
|
||||||
|
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
|
||||||
|
matching easier, and won't include the `seat` attribute, as that would be redundant.
|
||||||
|
The parameter can be given multiple times.
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:param subevent_id: The ``id`` field of the subevent to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||||
|
:statuscode 404: Endpoint without subevent id was used for event with subevents, or vice versa.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/seats/(id)/
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/(subevent_id)/seats/(id)/
|
||||||
|
|
||||||
|
Returns information on one seat, identified by its ID.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/seats/1634/?expand=orderposition HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1634,
|
||||||
|
"subevent": null,
|
||||||
|
"zone_name": "Ground floor",
|
||||||
|
"row_name": "1",
|
||||||
|
"row_label": null,
|
||||||
|
"seat_number": "2",
|
||||||
|
"seat_label": null,
|
||||||
|
"seat_guid": "1d29fe20-8e1e-4984-b0ee-2773b0d07e07",
|
||||||
|
"product": 104,
|
||||||
|
"blocked": true,
|
||||||
|
"orderposition": {
|
||||||
|
"id": 134,
|
||||||
|
"order": {
|
||||||
|
"code": "U0HW7",
|
||||||
|
"event": "sampleconf"
|
||||||
|
},
|
||||||
|
"positionid": 1,
|
||||||
|
"item": 104,
|
||||||
|
"variation": 59,
|
||||||
|
"price": "60.00",
|
||||||
|
"attendee_name": "",
|
||||||
|
"attendee_name_parts": {
|
||||||
|
"_scheme": "given_family"
|
||||||
|
},
|
||||||
|
"company": null,
|
||||||
|
"street": null,
|
||||||
|
"zipcode": null,
|
||||||
|
"city": null,
|
||||||
|
"country": null,
|
||||||
|
"state": null,
|
||||||
|
"discount": null,
|
||||||
|
"attendee_email": null,
|
||||||
|
"voucher": null,
|
||||||
|
"tax_rate": "0.00",
|
||||||
|
"tax_value": "0.00",
|
||||||
|
"secret": "4rfgp263jduratnsvwvy6cc6r6wnptbj",
|
||||||
|
"addon_to": null,
|
||||||
|
"subevent": null,
|
||||||
|
"checkins": [],
|
||||||
|
"downloads": [],
|
||||||
|
"answers": [],
|
||||||
|
"tax_rule": null,
|
||||||
|
"pseudonymization_id": "ZSNYSG3URZ",
|
||||||
|
"canceled": false,
|
||||||
|
"valid_from": null,
|
||||||
|
"valid_until": null,
|
||||||
|
"blocked": null,
|
||||||
|
"voucher_budget_use": null
|
||||||
|
},
|
||||||
|
"cartposition": null,
|
||||||
|
"voucher": null
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:param subevent_id: The ``id`` field of the subevent to fetch
|
||||||
|
:param id: The ``id`` field of the seat to fetch
|
||||||
|
:query string expand: If you pass ``"orderposition"``, ``"cartposition"``, or ``"voucher"``, the respective field will be
|
||||||
|
shown as a nested value instead of just an ID. This requires permission to access that object.
|
||||||
|
The nested objects are identical to the respective resources, except that order positions
|
||||||
|
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
|
||||||
|
matching easier, and won't include the `seat` attribute, as that would be redundant.
|
||||||
|
The parameter can be given multiple times.
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||||
|
:statuscode 404: Seat does not exist; or the endpoint without subevent id was used for event with subevents, or vice versa.
|
||||||
|
|
||||||
|
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/seats/(id)/
|
||||||
|
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/seats/(id)/
|
||||||
|
|
||||||
|
Update a seat.
|
||||||
|
|
||||||
|
You can only change the ``blocked`` field.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PATCH /api/v1/organizers/bigevents/events/sampleconf/seats/1636/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"blocked": true
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1636,
|
||||||
|
"subevent": null,
|
||||||
|
"zone_name": "Ground floor",
|
||||||
|
"row_name": "1",
|
||||||
|
"row_label": null,
|
||||||
|
"seat_number": "4",
|
||||||
|
"seat_label": null,
|
||||||
|
"seat_guid": "6c0e29e5-05d6-421f-99f3-afd01478ecad",
|
||||||
|
"product": 104,
|
||||||
|
"blocked": true,
|
||||||
|
"orderposition": null,
|
||||||
|
"cartposition": null,
|
||||||
|
"voucher": null
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param event: The ``slug`` field of the event to modify
|
||||||
|
:param subevent_id: The ``id`` field of the subevent to modify
|
||||||
|
:param id: The ``id`` field of the seat to modify
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The seat could not be modified due to invalid submitted data
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to change this resource.
|
||||||
|
:statuscode 404: Seat does not exist; or the endpoint without subevent id was used for event with subevents, or vice versa.
|
||||||
|
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/seats/bulk_block/
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/seats/bulk_block/
|
||||||
|
|
||||||
|
Set the ``blocked`` attribute to ``true`` for a large number of seats at once.
|
||||||
|
You can pass either a list of ``id`` values or a list of ``seat_guid`` values.
|
||||||
|
You can pass up to 10,000 seats in one request.
|
||||||
|
|
||||||
|
The endpoint will return an error if you pass a seat ID that does not exist.
|
||||||
|
However, it will not return an error if one of the passed seats is already blocked or sold.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PATCH /api/v1/organizers/bigevents/events/sampleconf/seats/bulk_block/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"ids": [12, 45, 56]
|
||||||
|
}
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PATCH /api/v1/organizers/bigevents/events/sampleconf/seats/bulk_block/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"seat_guids": ["6c0e29e5-05d6-421f-99f3-afd01478ecad", "c2899340-e2e7-4d05-8100-000a4b6d7cf4"]
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param event: The ``slug`` field of the event to modify
|
||||||
|
:param subevent_id: The ``id`` field of the subevent to modify
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The seat could not be modified due to invalid submitted data
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to change this resource.
|
||||||
|
:statuscode 404: Seat does not exist; or the endpoint without subevent id was used for event with subevents, or vice versa.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/seats/bulk_unblock/
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/seats/bulk_unblock/
|
||||||
|
|
||||||
|
Set the ``blocked`` attribute to ``false`` for a large number of seats at once.
|
||||||
|
You can pass either a list of ``id`` values or a list of ``seat_guid`` values.
|
||||||
|
You can pass up to 10,000 seats in one request.
|
||||||
|
|
||||||
|
The endpoint will return an error if you pass a seat ID that does not exist.
|
||||||
|
However, it will not return an error if one of the passed seat is already unblocked or is sold.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PATCH /api/v1/organizers/bigevents/events/sampleconf/seats/bulk_unblock/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"ids": [12, 45, 56]
|
||||||
|
}
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PATCH /api/v1/organizers/bigevents/events/sampleconf/seats/bulk_unblock/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"seat_guids": ["6c0e29e5-05d6-421f-99f3-afd01478ecad", "c2899340-e2e7-4d05-8100-000a4b6d7cf4"]
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param event: The ``slug`` field of the event to modify
|
||||||
|
:param subevent_id: The ``id`` field of the subevent to modify
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The seat could not be modified due to invalid submitted data
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to change this resource.
|
||||||
|
:statuscode 404: Seat does not exist; or the endpoint without subevent id was used for event with subevents, or vice versa.
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
Scheduled email rules
|
Scheduled email rules
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
|
This feature requires the bundled ``pretix.plugins.sendmail`` plugin to be active for the event in order to work properly.
|
||||||
|
|
||||||
Resource description
|
Resource description
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
@@ -48,6 +50,7 @@ send_to string Can be ``"order
|
|||||||
or ``"both"``.
|
or ``"both"``.
|
||||||
date. Otherwise it is relative to the event start date.
|
date. Otherwise it is relative to the event start date.
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
.. versionchanged:: 2023.7
|
.. versionchanged:: 2023.7
|
||||||
|
|
||||||
The ``include_pending`` field has been deprecated.
|
The ``include_pending`` field has been deprecated.
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ Endpoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
:query page: The page number in case of a multi-page result set, default is 1
|
:query page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:query is_public: If set to ``true``/``false``, only subevents with a matching value of ``is_public`` are returned.
|
||||||
:query active: If set to ``true``/``false``, only events with a matching value of ``active`` are returned.
|
:query active: If set to ``true``/``false``, only events with a matching value of ``active`` are returned.
|
||||||
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
|
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
|
||||||
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned.
|
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned.
|
||||||
@@ -467,6 +468,7 @@ Endpoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
:query page: The page number in case of a multi-page result set, default is 1
|
:query page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:query is_public: If set to ``true``/``false``, only subevents with a matching value of ``is_public`` are returned.
|
||||||
:query active: If set to ``true``/``false``, only events with a matching value of ``active`` are returned.
|
:query active: If set to ``true``/``false``, only events with a matching value of ``active`` are returned.
|
||||||
:query event__live: If set to ``true``/``false``, only events with a matching value of ``live`` on the parent event are returned.
|
:query event__live: If set to ``true``/``false``, only events with a matching value of ``live`` on the parent event are returned.
|
||||||
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
|
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
.. spelling:word-list::
|
||||||
|
|
||||||
|
EN16931
|
||||||
|
DSFinV-K
|
||||||
|
|
||||||
.. _rest-taxrules:
|
.. _rest-taxrules:
|
||||||
|
|
||||||
Tax rules
|
Tax rules
|
||||||
@@ -18,10 +23,12 @@ id integer Internal ID of
|
|||||||
name multi-lingual string The tax rules' name
|
name multi-lingual string The tax rules' name
|
||||||
internal_name string An optional name that is only used in the backend
|
internal_name string An optional name that is only used in the backend
|
||||||
rate decimal (string) Tax rate in percent
|
rate decimal (string) Tax rate in percent
|
||||||
|
code string Codified reason for tax rate (or ``null``), see :ref:`rest-taxcodes`.
|
||||||
price_includes_tax boolean If ``true`` (default), tax is assumed to be included in
|
price_includes_tax boolean If ``true`` (default), tax is assumed to be included in
|
||||||
the specified product price
|
the specified product price
|
||||||
eu_reverse_charge boolean If ``true``, EU reverse charge rules are applied. Will
|
eu_reverse_charge boolean **DEPRECATED**. If ``true``, EU reverse charge rules
|
||||||
be ignored if custom rules are set.
|
are applied. Will be ignored if custom rules are set.
|
||||||
|
Use custom rules instead.
|
||||||
home_country string Merchant country (required for reverse charge), can be
|
home_country string Merchant country (required for reverse charge), can be
|
||||||
``null`` or empty string
|
``null`` or empty string
|
||||||
keep_gross_if_rate_changes boolean If ``true``, changes of the tax rate based on custom
|
keep_gross_if_rate_changes boolean If ``true``, changes of the tax rate based on custom
|
||||||
@@ -41,6 +48,42 @@ custom_rules object Dynamic rules s
|
|||||||
|
|
||||||
The ``custom_rules`` attribute has been added.
|
The ``custom_rules`` attribute has been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 2023.8
|
||||||
|
|
||||||
|
The ``code`` attribute has been added.
|
||||||
|
|
||||||
|
.. _rest-taxcodes:
|
||||||
|
|
||||||
|
Tax codes
|
||||||
|
---------
|
||||||
|
|
||||||
|
For integration with external systems, such as electronic invoicing or bookkeeping systems, the tax rate itself is often
|
||||||
|
not sufficient information. For example, there could be many different reasons why a sale has a tax rate of 0 %, but the
|
||||||
|
external handling of the transaction depends on which reason applies. Therefore, pretix allows to supply a codified
|
||||||
|
reason that allows us to understand what the specific legal situation is. These tax codes are modeled after a combination
|
||||||
|
of the code lists from the European standard EN16931 and the German standard DSFinV-K.
|
||||||
|
|
||||||
|
The following codes are supported:
|
||||||
|
|
||||||
|
- ``S/standard`` -- Standard VAT rate in the merchant country
|
||||||
|
- ``S/reduced`` -- Reduced VAT rate in the merchant country
|
||||||
|
- ``S/averaged`` -- Averaged VAT rate in the merchant country (known use case: agricultural businesses in Germany)
|
||||||
|
- ``AE`` -- Reverse charge
|
||||||
|
- ``O`` -- Services outside of scope of tax
|
||||||
|
- ``E`` -- Exempt from tax (no reason given)
|
||||||
|
- ``E/<reason>`` -- Exempt from tax, where ``<reason>`` is one of the codes listed in the `VATEX code list`_ version 5.0.
|
||||||
|
- ``Z`` -- Zero-rated goods
|
||||||
|
- ``G`` -- Free export item, VAT not charged
|
||||||
|
- ``K`` -- VAT exempt for EEA intra-community supply of goods and services
|
||||||
|
- ``L`` -- Canary Islands general indirect tax
|
||||||
|
- ``M`` -- Tax for production, services and importation in Ceuta and Melilla
|
||||||
|
- ``B`` -- Transferred (VAT), only in Italy
|
||||||
|
|
||||||
|
The code set in the ``code`` attribute of the tax rule is used by default. When ``eu_reverse_charge`` is active, the
|
||||||
|
code is replaced by ``AE`` for reverse charge sales and by ``O`` for non-EU sales. When configuring custom rules, you
|
||||||
|
should actively set a ``"code"`` key on each rule. Only for ``"action": "reverse"`` we automatically apply the code
|
||||||
|
``AE``, in all other cases the default ``code`` of the tax rule is selected.
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
|
|
||||||
@@ -73,6 +116,7 @@ Endpoints
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "VAT"},
|
"name": {"en": "VAT"},
|
||||||
"internal_name": "VAT",
|
"internal_name": "VAT",
|
||||||
|
"code": "S/standard",
|
||||||
"rate": "19.00",
|
"rate": "19.00",
|
||||||
"price_includes_tax": true,
|
"price_includes_tax": true,
|
||||||
"eu_reverse_charge": false,
|
"eu_reverse_charge": false,
|
||||||
@@ -114,6 +158,7 @@ Endpoints
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "VAT"},
|
"name": {"en": "VAT"},
|
||||||
"internal_name": "VAT",
|
"internal_name": "VAT",
|
||||||
|
"code": "S/standard",
|
||||||
"rate": "19.00",
|
"rate": "19.00",
|
||||||
"price_includes_tax": true,
|
"price_includes_tax": true,
|
||||||
"eu_reverse_charge": false,
|
"eu_reverse_charge": false,
|
||||||
@@ -163,6 +208,7 @@ Endpoints
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "VAT"},
|
"name": {"en": "VAT"},
|
||||||
"internal_name": "VAT",
|
"internal_name": "VAT",
|
||||||
|
"code": "S/standard",
|
||||||
"rate": "19.00",
|
"rate": "19.00",
|
||||||
"price_includes_tax": true,
|
"price_includes_tax": true,
|
||||||
"eu_reverse_charge": false,
|
"eu_reverse_charge": false,
|
||||||
@@ -211,6 +257,7 @@ Endpoints
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "VAT"},
|
"name": {"en": "VAT"},
|
||||||
"internal_name": "VAT",
|
"internal_name": "VAT",
|
||||||
|
"code": "S/standard",
|
||||||
"rate": "20.00",
|
"rate": "20.00",
|
||||||
"price_includes_tax": true,
|
"price_includes_tax": true,
|
||||||
"eu_reverse_charge": false,
|
"eu_reverse_charge": false,
|
||||||
@@ -257,3 +304,4 @@ Endpoints
|
|||||||
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it **or** this tax rule cannot be deleted since it is currently in use.
|
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it **or** this tax rule cannot be deleted since it is currently in use.
|
||||||
|
|
||||||
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/schema/tax-rules-custom.schema.json
|
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/schema/tax-rules-custom.schema.json
|
||||||
|
.. _VATEX code list: https://ec.europa.eu/digital-building-blocks/sites/display/DIGITAL/Registry+of+supporting+artefacts+to+implement+EN16931#RegistryofsupportingartefactstoimplementEN16931-Codelists
|
||||||
@@ -41,6 +41,7 @@ The following values for ``action_types`` are valid with pretix core:
|
|||||||
* ``pretix.event.order.modified``
|
* ``pretix.event.order.modified``
|
||||||
* ``pretix.event.order.contact.changed``
|
* ``pretix.event.order.contact.changed``
|
||||||
* ``pretix.event.order.changed.*``
|
* ``pretix.event.order.changed.*``
|
||||||
|
* ``pretix.event.order.deleted`` (can only occur for test mode orders)
|
||||||
* ``pretix.event.order.refund.created``
|
* ``pretix.event.order.refund.created``
|
||||||
* ``pretix.event.order.refund.created.externally``
|
* ``pretix.event.order.refund.created.externally``
|
||||||
* ``pretix.event.order.refund.requested``
|
* ``pretix.event.order.refund.requested``
|
||||||
@@ -115,6 +116,7 @@ Endpoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:query boolean enabled: Only show webhooks that are or are not enabled
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ First, you need to declare that you are using non-essential cookies by respondin
|
|||||||
signal:
|
signal:
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
|
:no-index:
|
||||||
:members: register_cookie_providers
|
:members: register_cookie_providers
|
||||||
|
|
||||||
You are expected to return a list of ``CookieProvider`` objects instantiated from the following class:
|
You are expected to return a list of ``CookieProvider`` objects instantiated from the following class:
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ Core
|
|||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification,
|
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification,
|
||||||
item_copy_data, register_sales_channels, register_global_settings, quota_availability, global_email_filter,
|
item_copy_data, register_sales_channel_types, register_global_settings, quota_availability, global_email_filter,
|
||||||
register_ticket_secret_generators, gift_card_transaction_display,
|
register_ticket_secret_generators, gift_card_transaction_display,
|
||||||
register_text_placeholders, register_mail_placeholders
|
register_text_placeholders, register_mail_placeholders, device_info_updated
|
||||||
|
|
||||||
Order events
|
Order events
|
||||||
""""""""""""
|
""""""""""""
|
||||||
@@ -22,12 +22,14 @@ Order events
|
|||||||
There are multiple signals that will be sent out in the ordering cycle:
|
There are multiple signals that will be sent out in the ordering cycle:
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
|
:no-index:
|
||||||
:members: validate_cart, validate_cart_addons, validate_order, order_valid_if_pending, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
|
:members: validate_cart, validate_cart_addons, validate_order, order_valid_if_pending, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
|
||||||
|
|
||||||
Check-ins
|
Check-ins
|
||||||
"""""""""
|
"""""""""
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
|
:no-index:
|
||||||
:members: checkin_created
|
:members: checkin_created
|
||||||
|
|
||||||
|
|
||||||
@@ -35,22 +37,25 @@ Frontend
|
|||||||
--------
|
--------
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
:members: html_head, html_footer, footer_link, global_footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header
|
:members: html_head, html_footer, footer_link, global_footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header, seatingframe_html_head
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
:members: order_info, order_info_top, order_meta_from_request
|
:no-index:
|
||||||
|
:members: order_info, order_info_top, order_meta_from_request, order_api_meta_from_request
|
||||||
|
|
||||||
Request flow
|
Request flow
|
||||||
""""""""""""
|
""""""""""""
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
|
:no-index:
|
||||||
:members: process_request, process_response
|
:members: process_request, process_response
|
||||||
|
|
||||||
Vouchers
|
Vouchers
|
||||||
""""""""
|
""""""""
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
|
:no-index:
|
||||||
:members: voucher_redeem_info
|
:members: voucher_redeem_info
|
||||||
|
|
||||||
Backend
|
Backend
|
||||||
@@ -62,24 +67,28 @@ Backend
|
|||||||
item_formsets, order_search_filter_q, order_search_forms
|
item_formsets, order_search_filter_q, order_search_forms
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
|
:no-index:
|
||||||
:members: logentry_display, logentry_object_link, requiredaction_display, timeline_events, orderposition_blocked_display, customer_created, customer_signed_in
|
:members: logentry_display, logentry_object_link, requiredaction_display, timeline_events, orderposition_blocked_display, customer_created, customer_signed_in
|
||||||
|
|
||||||
Vouchers
|
Vouchers
|
||||||
""""""""
|
""""""""
|
||||||
|
|
||||||
.. automodule:: pretix.control.signals
|
.. automodule:: pretix.control.signals
|
||||||
|
:no-index:
|
||||||
:members: item_forms, voucher_form_class, voucher_form_html, voucher_form_validation
|
:members: item_forms, voucher_form_class, voucher_form_html, voucher_form_validation
|
||||||
|
|
||||||
Dashboards
|
Dashboards
|
||||||
""""""""""
|
""""""""""
|
||||||
|
|
||||||
.. automodule:: pretix.control.signals
|
.. automodule:: pretix.control.signals
|
||||||
|
:no-index:
|
||||||
:members: event_dashboard_widgets, user_dashboard_widgets, event_dashboard_top
|
:members: event_dashboard_widgets, user_dashboard_widgets, event_dashboard_top
|
||||||
|
|
||||||
Ticket designs
|
Ticket designs
|
||||||
""""""""""""""
|
""""""""""""""
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
|
:no-index:
|
||||||
:members: layout_text_variables, layout_image_variables
|
:members: layout_text_variables, layout_image_variables
|
||||||
|
|
||||||
.. automodule:: pretix.plugins.ticketoutputpdf.signals
|
.. automodule:: pretix.plugins.ticketoutputpdf.signals
|
||||||
@@ -89,4 +98,9 @@ API
|
|||||||
---
|
---
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
|
:no-index:
|
||||||
:members: validate_event_settings, api_event_settings_fields
|
:members: validate_event_settings, api_event_settings_fields
|
||||||
|
|
||||||
|
.. automodule:: pretix.api.signals
|
||||||
|
:no-index:
|
||||||
|
:members: register_device_security_profile
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ that we'll provide in this plugin:
|
|||||||
Similar signals exist for other objects:
|
Similar signals exist for other objects:
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
|
:no-index:
|
||||||
:members: voucher_import_columns
|
:members: voucher_import_columns
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -84,8 +84,6 @@ convenient to you:
|
|||||||
|
|
||||||
.. automethod:: _register_fonts
|
.. automethod:: _register_fonts
|
||||||
|
|
||||||
.. automethod:: _register_event_fonts
|
|
||||||
|
|
||||||
.. automethod:: _on_first_page
|
.. automethod:: _on_first_page
|
||||||
|
|
||||||
.. automethod:: _on_other_page
|
.. automethod:: _on_other_page
|
||||||
|
|||||||
@@ -86,7 +86,10 @@ Signals
|
|||||||
-------
|
-------
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
|
:no-index:
|
||||||
:members: register_text_placeholders
|
:members: register_text_placeholders
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
|
:no-index:
|
||||||
:members: register_mail_placeholders
|
:members: register_mail_placeholders
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ The project pretix is split into several components. The main components are:
|
|||||||
create and manage their events, items, orders and tickets.
|
create and manage their events, items, orders and tickets.
|
||||||
|
|
||||||
**presale**
|
**presale**
|
||||||
This is the ticket-shop itself, containing all of the parts visible to the
|
This is the ticket shop itself, containing all of the parts visible to the
|
||||||
end user. Also called "frontend" in parts of this documentation.
|
end user. Also called "frontend" in parts of this documentation.
|
||||||
|
|
||||||
**api**
|
**api**
|
||||||
|
|||||||
@@ -136,9 +136,7 @@ It is a good idea to put this command into your git hook ``.git/hooks/pre-commit
|
|||||||
for example, to check for any errors in any staged files when committing::
|
for example, to check for any errors in any staged files when committing::
|
||||||
|
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
cd $GIT_DIR/../src
|
|
||||||
export GIT_WORK_TREE=../
|
|
||||||
export GIT_DIR=../.git
|
|
||||||
source ../env/bin/activate # Adjust to however you activate your virtual environment
|
source ../env/bin/activate # Adjust to however you activate your virtual environment
|
||||||
for file in $(git diff --cached --name-only | grep -E '\.py$' | grep -Ev "migrations|mt940\.py|pretix/settings\.py|make_testdata\.py|testutils/settings\.py|tests/settings\.py|pretix/base/models/__init__\.py|.*_pb2\.py")
|
for file in $(git diff --cached --name-only | grep -E '\.py$' | grep -Ev "migrations|mt940\.py|pretix/settings\.py|make_testdata\.py|testutils/settings\.py|tests/settings\.py|pretix/base/models/__init__\.py|.*_pb2\.py")
|
||||||
do
|
do
|
||||||
|
|||||||
105
doc/plugins/getyourguide.rst
Normal file
105
doc/plugins/getyourguide.rst
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
GetYourGuide
|
||||||
|
============
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The GetYourGuide integration is currently in Beta. Please contact support@pretix.eu to enable the integration
|
||||||
|
for your pretix.eu organizer account.
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
------------
|
||||||
|
Using third party aggregators, such als GetYourGuide, event organizers can sell tickets to their events not only on
|
||||||
|
their own ticket-shop but also on the aggregator's portal. While this service is not for free, it allows event
|
||||||
|
organizers to reacher a larger audience that would otherwise not have found their way into the organizers webshop.
|
||||||
|
|
||||||
|
Using pretix' integration with GetYourGuide, event organizers can profit from an additional sales and revenue channel,
|
||||||
|
while keeping the effort for setting up and maintaining multiple ticket shops to a minimum.
|
||||||
|
|
||||||
|
Preparing your organizer account
|
||||||
|
--------------------------------
|
||||||
|
The first step in enabling the GetYourGuide integration, is to setup a corresponding Sales Channel, which will be used
|
||||||
|
to properly attribute the sales generated. This needs to be done only once per organizer account.
|
||||||
|
|
||||||
|
To do so, log into the pretix backend, select ``Organizers`` from the navigation and then the organizer in question.
|
||||||
|
Extending the ``Settings``-menu, find the ``Sales channels`` configuration and click the ``Add a new channel`` button.
|
||||||
|
|
||||||
|
On the following page, you will be able to select ``GetYourGuide`` as the sales channel type and give it a custom name.
|
||||||
|
|
||||||
|
Preparing your event
|
||||||
|
--------------------
|
||||||
|
In order to now sell your events on GetYourGuide, you will need to configure each event in question.
|
||||||
|
|
||||||
|
1. Enabling the plugin
|
||||||
|
Within your event, extend the ``Settings`` menu and navigate to ``Plugins``. Activate the plugin in the
|
||||||
|
``Integrations`` tab.
|
||||||
|
|
||||||
|
2. Sell the event on the sales channel
|
||||||
|
Pick the sales channel or channels, on which you would like to sell your event by navigating to the event's general
|
||||||
|
settings page using the ``Sell on all sales channels`` or ``Restrict to specific sales channels`` checkboxes.
|
||||||
|
|
||||||
|
3. Configure one or more products to be sold on GetYourGuide
|
||||||
|
Either create a new or edit an existing product, that you would like to sell on GetYourGuide. To do so, you will
|
||||||
|
need to have checked the ``Sell on all sales channels`` or appropriate ``Restrict to specific sales channels``
|
||||||
|
checkbox of the product within it's ``Availability`` tab.
|
||||||
|
In addition, you will also need to set the GetYourGuide equivalent ticket category in the product's accordingly
|
||||||
|
named settings tab. Within your event, there can be only one product per ticket category. Depending on your further
|
||||||
|
configuration, you must at least select one product to be in the ``Adult`` or ``Group`` category.
|
||||||
|
|
||||||
|
4. Configuring the GetYourGuide-plugin
|
||||||
|
Once you have configured one or more products to be eligible to be sold on GetYourGuide, you'll need to configure a
|
||||||
|
few basic settings within the event (``Settings`` --> ``GetYourGuide``). The most important settings can be found
|
||||||
|
the in the ``Configuration`` tab, such as the location of the event on sale.
|
||||||
|
|
||||||
|
Ticket Categories
|
||||||
|
-----------------
|
||||||
|
While pretix only uses the ticket category term loosely to group together multiple products for nicer display,
|
||||||
|
GetYourGuide is relying on the ticket categories to price the tickets.
|
||||||
|
|
||||||
|
First of all, you need to make the decision on how you are planning on selling your tickets on GetYourGuide - in most
|
||||||
|
cases, this will reflect your current sales strategy within your pretix shop.
|
||||||
|
|
||||||
|
- Individual tickets
|
||||||
|
Every single person attending will need to purchase their own ticket. A family of two adults and two
|
||||||
|
children will have to purchase and pay for a total of 4 tickets.
|
||||||
|
In this case, you will need to offer *at least* a ticket of the ``Adult`` type, but may offer any other ticket
|
||||||
|
category type (Child, Youth, Senior, ...) in addition. But you cannot offer a ``Group`` ticket.
|
||||||
|
|
||||||
|
- Group tickets
|
||||||
|
Two groups, consisting of 10 and 20 participants respectively, won't need to purchase a total of 30 tickets, but
|
||||||
|
rather two group tickets. It is up to you to configure the group size limits within the GetYourGuide-settings of your
|
||||||
|
product.
|
||||||
|
Choosing this option, you cannot offer any other ticket categories besides ``Group``.
|
||||||
|
|
||||||
|
Setting up event dates and quotas
|
||||||
|
---------------------------------
|
||||||
|
Of course, in addition to creating products, you will also need to add them to a quota for them to be available for
|
||||||
|
sale. The process for doing this is the very same as for any regular event or event series.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
When selling individual tickets through GetYourGuide, you will not be able to offer differing quantities for
|
||||||
|
individual ticket categories.
|
||||||
|
|
||||||
|
For this reason, we recommend to place all GetYourGuide-eligible products into the same quota. Should you however opt
|
||||||
|
to create multiple quotas which create an imbalance, pretix will report only the available number of tickets for the
|
||||||
|
lowest relevant quota.
|
||||||
|
|
||||||
|
Connecting your event to GetYourGuide
|
||||||
|
-------------------------------------
|
||||||
|
Once you have set up your event and products and performed all necessary configuration, you may want to use the
|
||||||
|
Analyzer-feature of our GetYourGuide-plugin (``Settings`` -> ``GetYourGuide`` -> tab ``Analyzer``).
|
||||||
|
|
||||||
|
The Analyzer should not display any blocking error messages and at least one event date that is ready for publishing on
|
||||||
|
the GetYourGuide platform.
|
||||||
|
|
||||||
|
At this point, you will need to setup your event (called ``product`` in the GetYourGuide universe) on their
|
||||||
|
`Supplier Portal`_ and connect it with your pretix shop. To do so, please follow the
|
||||||
|
`Connecting a new product to your Reservation System`_ on the GetYourGuide Supply Partner Help Center.
|
||||||
|
|
||||||
|
Select ``pretix.eu`` as your reservation system; the required ``product ID`` can be found in the ``Configuration`` tab
|
||||||
|
of the GetYourGuide plugin settings page.
|
||||||
|
|
||||||
|
From this point on, GetYourGuide will automatically import the availabilities and products and offer them for sale.
|
||||||
|
|
||||||
|
.. _Supplier Portal: https://suppliers.getyourguide.com/
|
||||||
|
.. _Connecting a new product to your Reservation System: https://supply.getyourguide.support/hc/en-us/articles/18008029689373-Connecting-a-new-product-to-your-Reservation-system
|
||||||
@@ -25,3 +25,4 @@ If you want to **create** a plugin, please go to the
|
|||||||
webinar
|
webinar
|
||||||
presale-saml
|
presale-saml
|
||||||
kulturpass
|
kulturpass
|
||||||
|
getyourguide
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
KulturPass
|
KulturPass
|
||||||
=========
|
==========
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ expects and - more importantly - supports.
|
|||||||
for a sample configuration in an academic context.
|
for a sample configuration in an academic context.
|
||||||
|
|
||||||
Note, that you can have multiple attributes with the same ``friendlyName``
|
Note, that you can have multiple attributes with the same ``friendlyName``
|
||||||
but different ``name``s. This is often used in systems, where the same
|
but different ``name`` value. This is often used in systems, where the same
|
||||||
information (for example a persons name) is saved in different fields -
|
information (for example a persons name) is saved in different fields -
|
||||||
for example because one institution is returning SAML 1.0 and other
|
for example because one institution is returning SAML 1.0 and other
|
||||||
institutions are returning SAML 2.0 style attributes. Typically, this only
|
institutions are returning SAML 2.0 style attributes. Typically, this only
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ item_assignments list of objects Products this l
|
|||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
Endpoints
|
Layout endpoints
|
||||||
---------
|
----------------
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/
|
||||||
|
|
||||||
@@ -268,5 +268,75 @@ Endpoints
|
|||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
|
||||||
|
|
||||||
|
Ticket rendering endpoint
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/ticketpdfrenderer/render_batch/
|
||||||
|
|
||||||
|
With this API call, you can instruct the system to render a set of tickets into one combined PDF file. To specify
|
||||||
|
which tickets to render, you need to submit a list of "parts". For every part, the following fields are supported:
|
||||||
|
|
||||||
|
* ``orderposition`` (``integer``, required): The ID of the order position to render.
|
||||||
|
* ``override_channel`` (``string``, optional): The sales channel ID to be used for layout selection instead of the
|
||||||
|
original channel of the order.
|
||||||
|
* ``override_layout`` (``integer``, optional): The ticket layout ID to be used instead of the auto-selected one.
|
||||||
|
|
||||||
|
If your input parameters validate correctly, a ``202 Accepted`` status code is returned.
|
||||||
|
The body points you to the download URL of the result. Running a ``GET`` request on that result URL will
|
||||||
|
yield one of the following status codes:
|
||||||
|
|
||||||
|
* ``200 OK`` – The export succeeded. The body will be your resulting file. Might be large!
|
||||||
|
* ``409 Conflict`` – Your export is still running. The body will be JSON with the structure ``{"status": "running"}``. ``status`` can be ``waiting`` before the task is actually being processed. Please retry, but wait at least one second before you do.
|
||||||
|
* ``410 Gone`` – Running the export has failed permanently. The body will be JSON with the structure ``{"status": "failed", "message": "Error message"}``
|
||||||
|
* ``404 Not Found`` – The export does not exist / is expired.
|
||||||
|
|
||||||
|
.. warning:: This endpoint is considered **experimental**. It might change at any time without prior notice.
|
||||||
|
|
||||||
|
.. note:: To avoid performance issues, a maximum number of 1000 parts is currently allowed.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/ticketpdfrenderer/render_batch/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"orderposition": 55412
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"orderposition": 55412,
|
||||||
|
"override_channel": "web"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"orderposition": 55412,
|
||||||
|
"override_layout": 56
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"download": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/ticketpdfrenderer/download/29891ede-196f-4942-9e26-d055a36e98b8/3f279f13-c198-4137-b49b-9b360ce9fcce/"
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:statuscode 202: no error
|
||||||
|
:statuscode 400: Invalid input options
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||||
|
|
||||||
|
|
||||||
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/schema/pdf-layout.schema.json
|
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/schema/pdf-layout.schema.json
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
sphinx==7.3.*
|
sphinx==7.4.*
|
||||||
jinja2==3.1.*
|
jinja2==3.1.*
|
||||||
sphinx-rtd-theme
|
sphinx-rtd-theme
|
||||||
sphinxcontrib-httpdomain
|
sphinxcontrib-httpdomain
|
||||||
@@ -6,5 +6,4 @@ sphinxcontrib-images
|
|||||||
sphinxcontrib-jquery
|
sphinxcontrib-jquery
|
||||||
sphinxcontrib-spelling==8.*
|
sphinxcontrib-spelling==8.*
|
||||||
sphinxemoji
|
sphinxemoji
|
||||||
pygments-markdown-lexer
|
|
||||||
pyenchant==3.2.*
|
pyenchant==3.2.*
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
-e ../
|
-e ../
|
||||||
sphinx==7.3.*
|
sphinx==7.4.*
|
||||||
jinja2==3.1.*
|
jinja2==3.1.*
|
||||||
sphinx-rtd-theme
|
sphinx-rtd-theme
|
||||||
sphinxcontrib-httpdomain
|
sphinxcontrib-httpdomain
|
||||||
@@ -7,5 +7,4 @@ sphinxcontrib-images
|
|||||||
sphinxcontrib-jquery
|
sphinxcontrib-jquery
|
||||||
sphinxcontrib-spelling==8.*
|
sphinxcontrib-spelling==8.*
|
||||||
sphinxemoji
|
sphinxemoji
|
||||||
pygments-markdown-lexer
|
|
||||||
pyenchant==3.2.*
|
pyenchant==3.2.*
|
||||||
|
|||||||
@@ -31,8 +31,7 @@ Android 9 Support planned until at least 12/2025.
|
|||||||
Android 8 Support planned until at least 12/2025.
|
Android 8 Support planned until at least 12/2025.
|
||||||
Android 7 Support planned until at least 06/2025.
|
Android 7 Support planned until at least 06/2025.
|
||||||
Android 6 Support planned until at least 06/2025.
|
Android 6 Support planned until at least 06/2025.
|
||||||
Android 5 | Support planned until at least 06/2025.
|
Android 5 Support planned until at least 06/2025.
|
||||||
| No support for COVID certificate verification.
|
|
||||||
Android 4 Support dropped.
|
Android 4 Support dropped.
|
||||||
=========================== ==========================================================
|
=========================== ==========================================================
|
||||||
|
|
||||||
@@ -57,16 +56,17 @@ Android 8 | Support planned until at least 12/2025.
|
|||||||
Android 7 | Support planned until at least 12/2024.
|
Android 7 | Support planned until at least 12/2024.
|
||||||
| Support for Stripe Terminal to be dropped 05/2024.
|
| Support for Stripe Terminal to be dropped 05/2024.
|
||||||
| No support for Cryptovision TSE.
|
| No support for Cryptovision TSE.
|
||||||
|
| No support for SumUp.
|
||||||
Android 6 | Support planned until at least 12/2024.
|
Android 6 | Support planned until at least 12/2024.
|
||||||
| No support for Cryptovision TSE.
|
| No support for Cryptovision TSE.
|
||||||
| No support for Fiskal Cloud.
|
| No support for Fiskal Cloud.
|
||||||
| No support for Stripe Terminal.
|
| No support for Stripe Terminal.
|
||||||
|
| No support for SumUp.
|
||||||
Android 5 | Support planned until at least 12/2024.
|
Android 5 | Support planned until at least 12/2024.
|
||||||
| No support for Cryptovision TSE.
|
| No support for Cryptovision TSE.
|
||||||
| No support for Fiskal Cloud.
|
| No support for Fiskal Cloud.
|
||||||
| No support for Stripe Terminal.
|
| No support for Stripe Terminal.
|
||||||
| No support for SumUp.
|
| No support for SumUp.
|
||||||
| No support for COVID certificate verification.
|
|
||||||
Android 4 Support dropped.
|
Android 4 Support dropped.
|
||||||
=========================== ==========================================================
|
=========================== ==========================================================
|
||||||
|
|
||||||
@@ -87,9 +87,6 @@ Android 7 Support planned until at least 06/2025.
|
|||||||
Android 6 Support planned until at least 06/2025.
|
Android 6 Support planned until at least 06/2025.
|
||||||
Android 5 | Support planned until at least 06/2025.
|
Android 5 | Support planned until at least 06/2025.
|
||||||
| No support for Evolis printers on some devices.
|
| No support for Evolis printers on some devices.
|
||||||
Android 4.4 | Support planned until at least 06/2024.
|
|
||||||
| No support for USB printers.
|
|
||||||
| No support for Evolis printers.
|
|
||||||
Android 4 Support dropped.
|
Android 4 Support dropped.
|
||||||
=========================== ==========================================================
|
=========================== ==========================================================
|
||||||
|
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ without any special behavior.
|
|||||||
Connecting SSO providers (pretix as the SSO client)
|
Connecting SSO providers (pretix as the SSO client)
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
To connect an external application as a SSO client, go to "Customer accounts" → "SSO providers" → "Create a new SSO provider"
|
To connect an external application as a SSO provider, go to "Customer accounts" → "SSO providers" → "Create a new SSO provider"
|
||||||
in your organizer account.
|
in your organizer account.
|
||||||
|
|
||||||
.. thumbnail:: ../../screens/organizer/customer_ssoprovider_add.png
|
.. thumbnail:: ../../screens/organizer/customer_ssoprovider_add.png
|
||||||
|
|||||||
@@ -449,6 +449,29 @@ Further reading:
|
|||||||
|
|
||||||
* `Stripe Payment Method Domain registration`_
|
* `Stripe Payment Method Domain registration`_
|
||||||
|
|
||||||
|
|
||||||
|
Content Security Policy
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
When using a Content Security Policy (CSP) on your website, you may need to make some adjustments. If your pretix
|
||||||
|
shop is running under a custom domain, you need to add the following rules:
|
||||||
|
|
||||||
|
* ``script-src``: ``'unsafe-eval' https://pretix.eu`` (adjust to your domain for self-hosted pretix)
|
||||||
|
* ``style-src``: ``https://pretix.eu`` (adjust to your domain for self-hosted pretix **and** for custom domain on pretix Hosted)
|
||||||
|
* ``connect-src``: ``https://pretix.eu`` (adjust to your domain for self-hosted pretix **and** for custom domain on pretix Hosted)
|
||||||
|
* ``frame-src``: ``https://pretix.eu`` (adjust to your domain for self-hosted pretix **and** for custom domain on pretix Hosted)
|
||||||
|
* ``img-src``: ``https://pretix.eu`` (adjust to your domain for self-hosted pretix **and** for custom domain on pretix Hosted) and for pretix Hosted additionally add ``https://cdn.pretix.space``
|
||||||
|
|
||||||
|
|
||||||
|
External payment providers and Cross-Origin-Opener-Policy
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
If you use a payment provider that opens a new window during checkout (such as PayPal), be aware that setting
|
||||||
|
``Cross-Origin-Opener-Policy: same-origin`` results in an empty popup-window being opened in the foreground. This is
|
||||||
|
due to JavaScript not having access to the opened window. To mitigate this, you either need to always open the widget’s
|
||||||
|
checkout in a new tab (see :ref:`Always open a new tab`) or set ``Cross-Origin-Opener-Policy: same-origin-allow-popups``
|
||||||
|
|
||||||
|
|
||||||
Working with Cross-Origin-Embedder-Policy
|
Working with Cross-Origin-Embedder-Policy
|
||||||
-----------------------------------------
|
-----------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -22,30 +22,29 @@ classifiers = [
|
|||||||
"Programming Language :: Python :: 3.9",
|
"Programming Language :: Python :: 3.9",
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Framework :: Django :: 4.1",
|
"Framework :: Django :: 4.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arabic-reshaper==3.0.0", # Support for Arabic in reportlab
|
"arabic-reshaper==3.0.0", # Support for Arabic in reportlab
|
||||||
"babel",
|
"babel",
|
||||||
"BeautifulSoup4==4.12.*",
|
"BeautifulSoup4==4.12.*",
|
||||||
"bleach==5.0.*",
|
"bleach==6.2.*",
|
||||||
"celery==5.4.*",
|
"celery==5.4.*",
|
||||||
"chardet==5.2.*",
|
"chardet==5.2.*",
|
||||||
"cryptography>=3.4.2",
|
"cryptography>=44.0.0",
|
||||||
"css-inline==0.14.*",
|
"css-inline==0.14.*",
|
||||||
"defusedcsv>=1.1.0",
|
"defusedcsv>=1.1.0",
|
||||||
"dj-static",
|
"Django[argon2]==4.2.*,>=4.2.15",
|
||||||
"Django[argon2]==4.2.*",
|
"django-bootstrap3==24.3",
|
||||||
"django-bootstrap3==24.2",
|
"django-compressor==4.5.1",
|
||||||
"django-compressor==4.5",
|
|
||||||
"django-countries==7.6.*",
|
"django-countries==7.6.*",
|
||||||
"django-filter==24.2",
|
"django-filter==24.3",
|
||||||
"django-formset-js-improved==0.5.0.3",
|
"django-formset-js-improved==0.5.0.3",
|
||||||
"django-formtools==2.5.1",
|
"django-formtools==2.5.1",
|
||||||
"django-hierarkey==1.2.*",
|
"django-hierarkey==1.2.*",
|
||||||
"django-hijack==3.5.*",
|
"django-hijack==3.7.*",
|
||||||
"django-i18nfield==1.9.*,>=1.9.4",
|
"django-i18nfield==1.10.*",
|
||||||
"django-libsass==0.9",
|
"django-libsass==0.9",
|
||||||
"django-localflavor==4.0",
|
"django-localflavor==4.0",
|
||||||
"django-markup",
|
"django-markup",
|
||||||
@@ -54,18 +53,18 @@ dependencies = [
|
|||||||
"django-phonenumber-field==7.3.*",
|
"django-phonenumber-field==7.3.*",
|
||||||
"django-redis==5.4.*",
|
"django-redis==5.4.*",
|
||||||
"django-scopes==2.0.*",
|
"django-scopes==2.0.*",
|
||||||
"django-statici18n==2.5.*",
|
"django-statici18n==2.6.*",
|
||||||
"djangorestframework==3.15.*",
|
"djangorestframework==3.15.*",
|
||||||
"dnspython==2.6.*",
|
"dnspython==2.7.*",
|
||||||
"drf_ujson2==1.7.*",
|
"drf_ujson2==1.7.*",
|
||||||
"geoip2==4.*",
|
"geoip2==4.*",
|
||||||
"importlib_metadata==8.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
"importlib_metadata==8.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
||||||
"isoweek",
|
"isoweek",
|
||||||
"jsonschema",
|
"jsonschema",
|
||||||
"kombu==5.3.*",
|
"kombu==5.4.*",
|
||||||
"libsass==0.23.*",
|
"libsass==0.23.*",
|
||||||
"lxml",
|
"lxml",
|
||||||
"markdown==3.6", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
"markdown==3.7", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
||||||
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
|
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
|
||||||
"mt-940==4.30.*",
|
"mt-940==4.30.*",
|
||||||
"oauthlib==3.2.*",
|
"oauthlib==3.2.*",
|
||||||
@@ -73,61 +72,58 @@ dependencies = [
|
|||||||
"packaging",
|
"packaging",
|
||||||
"paypalrestsdk==1.13.*",
|
"paypalrestsdk==1.13.*",
|
||||||
"paypal-checkout-serversdk==1.0.*",
|
"paypal-checkout-serversdk==1.0.*",
|
||||||
"PyJWT==2.8.*",
|
"PyJWT==2.9.*",
|
||||||
"phonenumberslite==8.13.*",
|
"phonenumberslite==8.13.*",
|
||||||
"Pillow==10.3.*",
|
"Pillow==11.1.*",
|
||||||
"pretix-plugin-build",
|
"pretix-plugin-build",
|
||||||
"protobuf==5.27.*",
|
"protobuf==5.29.*",
|
||||||
"psycopg2-binary",
|
"psycopg2-binary",
|
||||||
"pycountry",
|
"pycountry",
|
||||||
"pycparser==2.22",
|
"pycparser==2.22",
|
||||||
"pycryptodome==3.20.*",
|
"pycryptodome==3.21.*",
|
||||||
"pypdf==4.2.*",
|
"pypdf==5.1.*",
|
||||||
"python-bidi==0.4.*", # Support for Arabic in reportlab
|
"python-bidi==0.6.*", # Support for Arabic in reportlab
|
||||||
"python-dateutil==2.9.*",
|
"python-dateutil==2.9.*",
|
||||||
"pytz",
|
"pytz",
|
||||||
"pytz-deprecation-shim==0.1.*",
|
"pytz-deprecation-shim==0.1.*",
|
||||||
"pyuca",
|
"pyuca",
|
||||||
"qrcode==7.4.*",
|
"qrcode==8.0",
|
||||||
"redis==5.0.*",
|
"redis==5.2.*",
|
||||||
"reportlab==4.2.*",
|
"reportlab==4.2.*",
|
||||||
"requests==2.31.*",
|
"requests==2.31.*",
|
||||||
"sentry-sdk==2.5.*",
|
"sentry-sdk==2.18.*",
|
||||||
"sepaxml==2.6.*",
|
"sepaxml==2.6.*",
|
||||||
"slimit",
|
|
||||||
"static3==0.7.*",
|
|
||||||
"stripe==7.9.*",
|
"stripe==7.9.*",
|
||||||
"text-unidecode==1.*",
|
"text-unidecode==1.*",
|
||||||
"tlds>=2020041600",
|
"tlds>=2020041600",
|
||||||
"tqdm==4.*",
|
"tqdm==4.*",
|
||||||
"ua-parser==0.18.*",
|
"ua-parser==1.0.*",
|
||||||
"vat_moss_forked==2020.3.20.0.11.0",
|
"vat_moss_forked==2020.3.20.0.11.0",
|
||||||
"vobject==0.9.*",
|
"vobject==0.9.*",
|
||||||
"webauthn==2.2.*",
|
"webauthn==2.4.*",
|
||||||
"zeep==4.2.*"
|
"zeep==4.3.*"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
memcached = ["pylibmc"]
|
memcached = ["pylibmc"]
|
||||||
dev = [
|
dev = [
|
||||||
"aiohttp==3.9.*",
|
"aiohttp==3.11.*",
|
||||||
"coverage",
|
"coverage",
|
||||||
"coveralls",
|
"coveralls",
|
||||||
"fakeredis==2.23.*",
|
"fakeredis==2.26.*",
|
||||||
"flake8==7.1.*",
|
"flake8==7.1.*",
|
||||||
"freezegun",
|
"freezegun",
|
||||||
"isort==5.13.*",
|
"isort==5.13.*",
|
||||||
"pep8-naming==0.14.*",
|
"pep8-naming==0.14.*",
|
||||||
"potypo",
|
"potypo",
|
||||||
"pytest-asyncio",
|
"pytest-asyncio>=0.24",
|
||||||
"pytest-cache",
|
"pytest-cache",
|
||||||
"pytest-cov",
|
"pytest-cov",
|
||||||
"pytest-django==4.*",
|
"pytest-django==4.*",
|
||||||
"pytest-mock==3.14.*",
|
"pytest-mock==3.14.*",
|
||||||
"pytest-rerunfailures==14.*",
|
|
||||||
"pytest-sugar",
|
"pytest-sugar",
|
||||||
"pytest-xdist==3.6.*",
|
"pytest-xdist==3.6.*",
|
||||||
"pytest==8.2.*",
|
"pytest==8.3.*",
|
||||||
"responses",
|
"responses",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -19,4 +19,4 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
__version__ = "2024.7.0.dev0"
|
__version__ = "2024.12.0.dev0"
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ INSTALLED_APPS = [
|
|||||||
'pretix.plugins.badges',
|
'pretix.plugins.badges',
|
||||||
'pretix.plugins.manualpayment',
|
'pretix.plugins.manualpayment',
|
||||||
'pretix.plugins.returnurl',
|
'pretix.plugins.returnurl',
|
||||||
|
'pretix.plugins.autocheckin',
|
||||||
'pretix.plugins.webcheckin',
|
'pretix.plugins.webcheckin',
|
||||||
'django_countries',
|
'django_countries',
|
||||||
'oauth2_provider',
|
'oauth2_provider',
|
||||||
@@ -79,6 +80,7 @@ ALL_LANGUAGES = [
|
|||||||
('de', _('German')),
|
('de', _('German')),
|
||||||
('de-informal', _('German (informal)')),
|
('de-informal', _('German (informal)')),
|
||||||
('ar', _('Arabic')),
|
('ar', _('Arabic')),
|
||||||
|
('eu', _('Basque')),
|
||||||
('ca', _('Catalan')),
|
('ca', _('Catalan')),
|
||||||
('zh-hans', _('Chinese (simplified)')),
|
('zh-hans', _('Chinese (simplified)')),
|
||||||
('zh-hant', _('Chinese (traditional)')),
|
('zh-hant', _('Chinese (traditional)')),
|
||||||
@@ -100,6 +102,7 @@ ALL_LANGUAGES = [
|
|||||||
('ro', _('Romanian')),
|
('ro', _('Romanian')),
|
||||||
('ru', _('Russian')),
|
('ru', _('Russian')),
|
||||||
('sk', _('Slovak')),
|
('sk', _('Slovak')),
|
||||||
|
('sv', _('Swedish')),
|
||||||
('es', _('Spanish')),
|
('es', _('Spanish')),
|
||||||
('tr', _('Turkish')),
|
('tr', _('Turkish')),
|
||||||
('uk', _('Ukrainian')),
|
('uk', _('Ukrainian')),
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from rest_framework import exceptions
|
|||||||
from rest_framework.authentication import TokenAuthentication
|
from rest_framework.authentication import TokenAuthentication
|
||||||
|
|
||||||
from pretix.api.auth.devicesecurity import (
|
from pretix.api.auth.devicesecurity import (
|
||||||
DEVICE_SECURITY_PROFILES, FullAccessSecurityProfile,
|
FullAccessSecurityProfile, get_all_security_profiles,
|
||||||
)
|
)
|
||||||
from pretix.base.models import Device
|
from pretix.base.models import Device
|
||||||
|
|
||||||
@@ -58,7 +58,8 @@ class DeviceTokenAuthentication(TokenAuthentication):
|
|||||||
def authenticate(self, request):
|
def authenticate(self, request):
|
||||||
r = super().authenticate(request)
|
r = super().authenticate(request)
|
||||||
if r and isinstance(r[1], Device):
|
if r and isinstance(r[1], Device):
|
||||||
profile = DEVICE_SECURITY_PROFILES.get(r[1].security_profile, FullAccessSecurityProfile)
|
profiles = get_all_security_profiles()
|
||||||
|
profile = profiles.get(r[1].security_profile, FullAccessSecurityProfile())
|
||||||
if not profile.is_allowed(request):
|
if not profile.is_allowed(request):
|
||||||
raise exceptions.PermissionDenied('Request denied by device security profile.')
|
raise exceptions.PermissionDenied('Request denied by device security profile.')
|
||||||
return r
|
return r
|
||||||
|
|||||||
@@ -20,13 +20,40 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import logging
|
import logging
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from pretix.api.signals import register_device_security_profile
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
_ALL_PROFILES = None
|
||||||
|
|
||||||
|
|
||||||
class FullAccessSecurityProfile:
|
class BaseSecurityProfile:
|
||||||
|
@property
|
||||||
|
def identifier(self) -> str:
|
||||||
|
"""
|
||||||
|
Unique identifier for this profile.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def verbose_name(self) -> str:
|
||||||
|
"""
|
||||||
|
Human-readable name (can be a ``gettext_lazy`` object).
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def is_allowed(self, request) -> bool:
|
||||||
|
"""
|
||||||
|
Return whether a given request should be allowed.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class FullAccessSecurityProfile(BaseSecurityProfile):
|
||||||
identifier = 'full'
|
identifier = 'full'
|
||||||
verbose_name = _('Full device access (reading and changing orders and gift cards, reading of products and settings)')
|
verbose_name = _('Full device access (reading and changing orders and gift cards, reading of products and settings)')
|
||||||
|
|
||||||
@@ -34,7 +61,7 @@ class FullAccessSecurityProfile:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class AllowListSecurityProfile:
|
class AllowListSecurityProfile(BaseSecurityProfile):
|
||||||
allowlist = ()
|
allowlist = ()
|
||||||
|
|
||||||
def is_allowed(self, request):
|
def is_allowed(self, request):
|
||||||
@@ -77,6 +104,7 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
|
|||||||
('GET', 'api-v1:blockedsecrets-list'),
|
('GET', 'api-v1:blockedsecrets-list'),
|
||||||
('GET', 'api-v1:order-list'),
|
('GET', 'api-v1:order-list'),
|
||||||
('GET', 'api-v1:orderposition-pdf_image'),
|
('GET', 'api-v1:orderposition-pdf_image'),
|
||||||
|
('POST', 'api-v1:orderposition-printlog'),
|
||||||
('GET', 'api-v1:event.settings'),
|
('GET', 'api-v1:event.settings'),
|
||||||
('POST', 'api-v1:upload'),
|
('POST', 'api-v1:upload'),
|
||||||
('POST', 'api-v1:checkinrpc.redeem'),
|
('POST', 'api-v1:checkinrpc.redeem'),
|
||||||
@@ -112,6 +140,7 @@ class PretixScanNoSyncNoSearchSecurityProfile(AllowListSecurityProfile):
|
|||||||
('GET', 'api-v1:revokedsecrets-list'),
|
('GET', 'api-v1:revokedsecrets-list'),
|
||||||
('GET', 'api-v1:blockedsecrets-list'),
|
('GET', 'api-v1:blockedsecrets-list'),
|
||||||
('GET', 'api-v1:orderposition-pdf_image'),
|
('GET', 'api-v1:orderposition-pdf_image'),
|
||||||
|
('POST', 'api-v1:orderposition-printlog'),
|
||||||
('GET', 'api-v1:event.settings'),
|
('GET', 'api-v1:event.settings'),
|
||||||
('POST', 'api-v1:upload'),
|
('POST', 'api-v1:upload'),
|
||||||
('POST', 'api-v1:checkinrpc.redeem'),
|
('POST', 'api-v1:checkinrpc.redeem'),
|
||||||
@@ -147,6 +176,7 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
|
|||||||
('GET', 'api-v1:revokedsecrets-list'),
|
('GET', 'api-v1:revokedsecrets-list'),
|
||||||
('GET', 'api-v1:blockedsecrets-list'),
|
('GET', 'api-v1:blockedsecrets-list'),
|
||||||
('GET', 'api-v1:orderposition-pdf_image'),
|
('GET', 'api-v1:orderposition-pdf_image'),
|
||||||
|
('POST', 'api-v1:orderposition-printlog'),
|
||||||
('GET', 'api-v1:event.settings'),
|
('GET', 'api-v1:event.settings'),
|
||||||
('POST', 'api-v1:upload'),
|
('POST', 'api-v1:upload'),
|
||||||
('POST', 'api-v1:checkinrpc.redeem'),
|
('POST', 'api-v1:checkinrpc.redeem'),
|
||||||
@@ -154,87 +184,28 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PretixPosSecurityProfile(AllowListSecurityProfile):
|
def get_all_security_profiles():
|
||||||
identifier = 'pretixpos'
|
global _ALL_PROFILES
|
||||||
verbose_name = _('pretixPOS')
|
|
||||||
allowlist = (
|
if _ALL_PROFILES:
|
||||||
('GET', 'api-v1:version'),
|
return _ALL_PROFILES
|
||||||
('GET', 'api-v1:device.eventselection'),
|
|
||||||
('GET', 'api-v1:idempotency.query'),
|
types = OrderedDict()
|
||||||
('GET', 'api-v1:device.info'),
|
for recv, ret in register_device_security_profile.send(None):
|
||||||
('POST', 'api-v1:device.update'),
|
if isinstance(ret, (list, tuple)):
|
||||||
('POST', 'api-v1:device.revoke'),
|
for r in ret:
|
||||||
('POST', 'api-v1:device.roll'),
|
types[r.identifier] = r
|
||||||
('GET', 'api-v1:event-list'),
|
else:
|
||||||
('GET', 'api-v1:event-detail'),
|
types[ret.identifier] = ret
|
||||||
('GET', 'api-v1:subevent-list'),
|
_ALL_PROFILES = types
|
||||||
('GET', 'api-v1:subevent-detail'),
|
return types
|
||||||
('GET', 'api-v1:itemcategory-list'),
|
|
||||||
('GET', 'api-v1:item-list'),
|
|
||||||
('GET', 'api-v1:question-list'),
|
|
||||||
('GET', 'api-v1:quota-list'),
|
|
||||||
('GET', 'api-v1:taxrule-list'),
|
|
||||||
('GET', 'api-v1:ticketlayout-list'),
|
|
||||||
('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'),
|
|
||||||
('DELETE', 'api-v1:orderposition-detail'),
|
|
||||||
('PATCH', 'api-v1:orderposition-detail'),
|
|
||||||
('GET', 'api-v1:orderposition-list'),
|
|
||||||
('GET', 'api-v1:orderposition-answer'),
|
|
||||||
('GET', 'api-v1:orderposition-pdf_image'),
|
|
||||||
('POST', 'api-v1:order-mark-canceled'),
|
|
||||||
('POST', 'api-v1:orderpayment-list'),
|
|
||||||
('POST', 'api-v1:orderrefund-list'),
|
|
||||||
('POST', 'api-v1:orderrefund-done'),
|
|
||||||
('POST', 'api-v1:cartposition-list'),
|
|
||||||
('POST', 'api-v1:cartposition-bulk-create'),
|
|
||||||
('GET', 'api-v1:checkinlist-list'),
|
|
||||||
('POST', 'api-v1:checkinlistpos-redeem'),
|
|
||||||
('POST', 'plugins:pretix_posbackend:order.posprintlog'),
|
|
||||||
('POST', 'plugins:pretix_posbackend:order.poslock'),
|
|
||||||
('DELETE', 'plugins:pretix_posbackend:order.poslock'),
|
|
||||||
('DELETE', 'api-v1:cartposition-detail'),
|
|
||||||
('GET', 'api-v1:giftcard-list'),
|
|
||||||
('POST', 'api-v1:giftcard-transact'),
|
|
||||||
('PATCH', 'api-v1:giftcard-detail'),
|
|
||||||
('GET', 'plugins:pretix_posbackend:posclosing-list'),
|
|
||||||
('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:blockedsecrets-list'),
|
|
||||||
('GET', 'api-v1:event.settings'),
|
|
||||||
('GET', 'plugins:pretix_seating:event.event'),
|
|
||||||
('GET', 'plugins:pretix_seating:event.event.subevent'),
|
|
||||||
('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'),
|
|
||||||
('POST', 'api-v1:reusablemedium-lookup'),
|
|
||||||
('GET', 'api-v1:reusablemedium-list'),
|
|
||||||
('POST', 'api-v1:reusablemedium-list'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
DEVICE_SECURITY_PROFILES = {
|
@receiver(register_device_security_profile, dispatch_uid="base_register_default_security_profiles")
|
||||||
k.identifier: k() for k in (
|
def register_default_webhook_events(sender, **kwargs):
|
||||||
FullAccessSecurityProfile,
|
return (
|
||||||
PretixScanSecurityProfile,
|
FullAccessSecurityProfile(),
|
||||||
PretixScanNoSyncSecurityProfile,
|
PretixScanSecurityProfile(),
|
||||||
PretixScanNoSyncNoSearchSecurityProfile,
|
PretixScanNoSyncSecurityProfile(),
|
||||||
PretixPosSecurityProfile,
|
PretixScanNoSyncNoSearchSecurityProfile(),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|||||||
82
src/pretix/api/filters.py
Normal file
82
src/pretix/api/filters.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from django import forms
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.db.models.constants import LOOKUP_SEP
|
||||||
|
from django.forms import MultipleChoiceField
|
||||||
|
from django_filters import Filter
|
||||||
|
from django_filters.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
class MultipleCharField(forms.CharField):
|
||||||
|
widget = forms.MultipleHiddenInput
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
if not value:
|
||||||
|
return []
|
||||||
|
elif not isinstance(value, (list, tuple)):
|
||||||
|
raise ValidationError(
|
||||||
|
MultipleChoiceField.default_error_messages["invalid_list"], code="invalid_list"
|
||||||
|
)
|
||||||
|
return [str(val) for val in value]
|
||||||
|
|
||||||
|
|
||||||
|
class MultipleCharFilter(Filter):
|
||||||
|
"""
|
||||||
|
This filter performs OR(by default) or AND(using conjoined=True) query
|
||||||
|
on the selected inputs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
field_class = MultipleCharField
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.conjoined = kwargs.pop("conjoined", False)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def filter(self, qs, value):
|
||||||
|
if not value:
|
||||||
|
# Even though not a noop, no point filtering if empty.
|
||||||
|
return qs
|
||||||
|
|
||||||
|
if not self.conjoined:
|
||||||
|
q = Q()
|
||||||
|
for v in set(value):
|
||||||
|
predicate = self.get_filter_predicate(v)
|
||||||
|
if self.conjoined:
|
||||||
|
qs = self.get_method(qs)(**predicate)
|
||||||
|
else:
|
||||||
|
q |= Q(**predicate)
|
||||||
|
|
||||||
|
if not self.conjoined:
|
||||||
|
qs = self.get_method(qs)(q)
|
||||||
|
|
||||||
|
return qs.distinct() if self.distinct else qs
|
||||||
|
|
||||||
|
def get_filter_predicate(self, v):
|
||||||
|
name = self.field_name
|
||||||
|
if name and self.lookup_expr != settings.DEFAULT_LOOKUP_EXPR:
|
||||||
|
name = LOOKUP_SEP.join([name, self.lookup_expr])
|
||||||
|
try:
|
||||||
|
return {name: getattr(v, self.field.to_field_name)}
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
return {name: v}
|
||||||
@@ -21,7 +21,9 @@
|
|||||||
#
|
#
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from django.db.models import prefetch_related_objects
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
class AsymmetricField(serializers.Field):
|
class AsymmetricField(serializers.Field):
|
||||||
@@ -61,3 +63,67 @@ class CompatibleJSONField(serializers.JSONField):
|
|||||||
if value:
|
if value:
|
||||||
return json.loads(value)
|
return json.loads(value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class SalesChannelMigrationMixin:
|
||||||
|
"""
|
||||||
|
Translates between the old field "sales_channels" and the new field combo "all_sales_channels"/"limit_sales_channels".
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def organizer(self):
|
||||||
|
if "organizer" in self.context:
|
||||||
|
return self.context["organizer"]
|
||||||
|
elif "event" in self.context:
|
||||||
|
return self.context["event"].organizer
|
||||||
|
else:
|
||||||
|
raise ValueError("organizer not in context")
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
if "sales_channels" in data:
|
||||||
|
prefetch_related_objects([self.organizer], "sales_channels")
|
||||||
|
all_channels = {
|
||||||
|
s.identifier for s in
|
||||||
|
self.organizer.sales_channels.all()
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.get("all_sales_channels") and set(data["sales_channels"]) != all_channels:
|
||||||
|
raise ValidationError({
|
||||||
|
"limit_sales_channels": [
|
||||||
|
"If 'all_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
||||||
|
"the list of all sales channels."
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
if data.get("limit_sales_channels") and set(data["sales_channels"]) != set(data["limit_sales_channels"]):
|
||||||
|
raise ValidationError({
|
||||||
|
"limit_sales_channels": [
|
||||||
|
"If 'limit_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
||||||
|
"the same list."
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
if set(data["sales_channels"]) == all_channels:
|
||||||
|
data["all_sales_channels"] = True
|
||||||
|
data["limit_sales_channels"] = []
|
||||||
|
else:
|
||||||
|
data["all_sales_channels"] = False
|
||||||
|
data["limit_sales_channels"] = data["sales_channels"]
|
||||||
|
del data["sales_channels"]
|
||||||
|
|
||||||
|
if data.get("all_sales_channels"):
|
||||||
|
data["limit_sales_channels"] = []
|
||||||
|
|
||||||
|
return super().to_internal_value(data)
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
value = super().to_representation(value)
|
||||||
|
if value.get("all_sales_channels"):
|
||||||
|
prefetch_related_objects([self.organizer], "sales_channels")
|
||||||
|
value["sales_channels"] = sorted([
|
||||||
|
s.identifier for s in
|
||||||
|
self.organizer.sales_channels.all()
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
value["sales_channels"] = value["limit_sales_channels"]
|
||||||
|
return value
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
|||||||
from pretix.api.serializers.order import (
|
from pretix.api.serializers.order import (
|
||||||
AnswerCreateSerializer, AnswerSerializer, InlineSeatSerializer,
|
AnswerCreateSerializer, AnswerSerializer, InlineSeatSerializer,
|
||||||
)
|
)
|
||||||
from pretix.base.models import Seat, Voucher
|
from pretix.base.models import SalesChannel, Seat, Voucher
|
||||||
from pretix.base.models.orders import CartPosition
|
from pretix.base.models.orders import CartPosition
|
||||||
|
|
||||||
|
|
||||||
@@ -212,7 +212,11 @@ class CartPositionCreateSerializer(BaseCartPositionCreateSerializer):
|
|||||||
addons = BaseCartPositionCreateSerializer(many=True, required=False)
|
addons = BaseCartPositionCreateSerializer(many=True, required=False)
|
||||||
bundled = BaseCartPositionCreateSerializer(many=True, required=False)
|
bundled = BaseCartPositionCreateSerializer(many=True, required=False)
|
||||||
seat = serializers.CharField(required=False, allow_null=True)
|
seat = serializers.CharField(required=False, allow_null=True)
|
||||||
sales_channel = serializers.CharField(required=False, default='sales_channel')
|
sales_channel = serializers.SlugRelatedField(
|
||||||
|
slug_field='identifier',
|
||||||
|
queryset=SalesChannel.objects.none(),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
voucher = serializers.CharField(required=False, allow_null=True)
|
voucher = serializers.CharField(required=False, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -221,13 +225,17 @@ class CartPositionCreateSerializer(BaseCartPositionCreateSerializer):
|
|||||||
'cart_id', 'expires', 'addons', 'bundled', 'seat', 'sales_channel', 'voucher'
|
'cart_id', 'expires', 'addons', 'bundled', 'seat', 'sales_channel', 'voucher'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
||||||
|
|
||||||
def validate_cart_id(self, cid):
|
def validate_cart_id(self, cid):
|
||||||
if cid and not cid.endswith('@api'):
|
if cid and not cid.endswith('@api'):
|
||||||
raise ValidationError('Cart ID should end in @api or be empty.')
|
raise ValidationError('Cart ID should end in @api or be empty.')
|
||||||
return cid
|
return cid
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
validated_data.pop('sales_channel')
|
validated_data.pop('sales_channel', None)
|
||||||
addons_data = validated_data.pop('addons', None)
|
addons_data = validated_data.pop('addons', None)
|
||||||
bundled_data = validated_data.pop('bundled', None)
|
bundled_data = validated_data.pop('bundled', None)
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ from rest_framework.exceptions import ValidationError
|
|||||||
|
|
||||||
from pretix.api.serializers.event import SubEventSerializer
|
from pretix.api.serializers.event import SubEventSerializer
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.base.channels import get_all_sales_channels
|
|
||||||
from pretix.base.media import MEDIA_TYPES
|
from pretix.base.media import MEDIA_TYPES
|
||||||
from pretix.base.models import Checkin, CheckinList
|
from pretix.base.models import Checkin, CheckinList
|
||||||
|
|
||||||
@@ -37,7 +36,7 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = CheckinList
|
model = CheckinList
|
||||||
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
|
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',
|
'include_pending', 'allow_multiple_entries', 'allow_entry_after_exit',
|
||||||
'rules', 'exit_all_at', 'addon_match', 'ignore_in_statistics', 'consider_tickets_used')
|
'rules', 'exit_all_at', 'addon_match', 'ignore_in_statistics', 'consider_tickets_used')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -72,10 +71,6 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
|||||||
if full_data.get('subevent'):
|
if full_data.get('subevent'):
|
||||||
raise ValidationError(_('The subevent does not belong to this event.'))
|
raise ValidationError(_('The subevent does not belong to this event.'))
|
||||||
|
|
||||||
for channel in full_data.get('auto_checkin_sales_channels') or []:
|
|
||||||
if channel not in get_all_sales_channels():
|
|
||||||
raise ValidationError(_('Unknown sales channel.'))
|
|
||||||
|
|
||||||
CheckinList.validate_rules(data.get('rules'))
|
CheckinList.validate_rules(data.get('rules'))
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|||||||
@@ -19,18 +19,27 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from pretix.api.serializers import SalesChannelMigrationMixin
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.base.models import Discount
|
from pretix.base.models import Discount, SalesChannel
|
||||||
|
|
||||||
|
|
||||||
class DiscountSerializer(I18nAwareModelSerializer):
|
class DiscountSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||||
|
limit_sales_channels = serializers.SlugRelatedField(
|
||||||
|
slug_field="identifier",
|
||||||
|
queryset=SalesChannel.objects.none(),
|
||||||
|
required=False,
|
||||||
|
allow_empty=True,
|
||||||
|
many=True,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Discount
|
model = Discount
|
||||||
fields = ('id', 'active', 'internal_name', 'position', 'sales_channels', 'available_from',
|
fields = ('id', 'active', 'internal_name', 'position', 'all_sales_channels', 'limit_sales_channels',
|
||||||
'available_until', 'subevent_mode', 'condition_all_products', 'condition_limit_products',
|
'available_from', 'available_until', 'subevent_mode', 'condition_all_products',
|
||||||
'condition_apply_to_addons', 'condition_min_count', 'condition_min_value',
|
'condition_limit_products', 'condition_apply_to_addons', 'condition_min_count', 'condition_min_value',
|
||||||
'benefit_discount_matching_percent', 'benefit_only_apply_to_cheapest_n_matches',
|
'benefit_discount_matching_percent', 'benefit_only_apply_to_cheapest_n_matches',
|
||||||
'benefit_same_products', 'benefit_limit_products', 'benefit_apply_to_addons',
|
'benefit_same_products', 'benefit_limit_products', 'benefit_apply_to_addons',
|
||||||
'benefit_ignore_voucher_discounted', 'condition_ignore_voucher_discounted')
|
'benefit_ignore_voucher_discounted', 'condition_ignore_voucher_discounted')
|
||||||
@@ -39,6 +48,7 @@ class DiscountSerializer(I18nAwareModelSerializer):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['condition_limit_products'].queryset = self.context['event'].items.all()
|
self.fields['condition_limit_products'].queryset = self.context['event'].items.all()
|
||||||
self.fields['benefit_limit_products'].queryset = self.context['event'].items.all()
|
self.fields['benefit_limit_products'].queryset = self.context['event'].items.all()
|
||||||
|
self.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
@@ -43,13 +43,19 @@ from django.utils.translation import gettext as _
|
|||||||
from django_countries.serializers import CountryFieldMixin
|
from django_countries.serializers import CountryFieldMixin
|
||||||
from pytz import common_timezones
|
from pytz import common_timezones
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.fields import ChoiceField, Field
|
from rest_framework.fields import ChoiceField, Field
|
||||||
from rest_framework.relations import SlugRelatedField
|
from rest_framework.relations import SlugRelatedField
|
||||||
|
|
||||||
from pretix.api.serializers import CompatibleJSONField
|
from pretix.api.serializers import (
|
||||||
|
CompatibleJSONField, SalesChannelMigrationMixin,
|
||||||
|
)
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.api.serializers.settings import SettingsSerializer
|
from pretix.api.serializers.settings import SettingsSerializer
|
||||||
from pretix.base.models import Device, Event, TaxRule, TeamAPIToken
|
from pretix.base.models import (
|
||||||
|
CartPosition, Device, Event, OrderPosition, SalesChannel, Seat, TaxRule,
|
||||||
|
TeamAPIToken, Voucher,
|
||||||
|
)
|
||||||
from pretix.base.models.event import SubEvent
|
from pretix.base.models.event import SubEvent
|
||||||
from pretix.base.models.items import (
|
from pretix.base.models.items import (
|
||||||
ItemMetaProperty, SubEventItem, SubEventItemVariation,
|
ItemMetaProperty, SubEventItem, SubEventItemVariation,
|
||||||
@@ -161,7 +167,7 @@ class ValidKeysField(Field):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class EventSerializer(I18nAwareModelSerializer):
|
class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||||
meta_data = MetaDataField(required=False, source='*')
|
meta_data = MetaDataField(required=False, source='*')
|
||||||
item_meta_properties = MetaPropertyField(required=False, source='*')
|
item_meta_properties = MetaPropertyField(required=False, source='*')
|
||||||
plugins = PluginsField(required=False, source='*')
|
plugins = PluginsField(required=False, source='*')
|
||||||
@@ -170,6 +176,13 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
valid_keys = ValidKeysField(source='*', read_only=True)
|
valid_keys = ValidKeysField(source='*', read_only=True)
|
||||||
best_availability_state = serializers.IntegerField(allow_null=True, read_only=True)
|
best_availability_state = serializers.IntegerField(allow_null=True, read_only=True)
|
||||||
public_url = serializers.SerializerMethodField('get_event_url', read_only=True)
|
public_url = serializers.SerializerMethodField('get_event_url', read_only=True)
|
||||||
|
limit_sales_channels = serializers.SlugRelatedField(
|
||||||
|
slug_field="identifier",
|
||||||
|
queryset=SalesChannel.objects.none(),
|
||||||
|
required=False,
|
||||||
|
allow_empty=True,
|
||||||
|
many=True,
|
||||||
|
)
|
||||||
|
|
||||||
def get_event_url(self, event):
|
def get_event_url(self, event):
|
||||||
return build_absolute_uri(event, 'presale:event.index')
|
return build_absolute_uri(event, 'presale:event.index')
|
||||||
@@ -180,7 +193,7 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
'date_to', 'date_admission', 'is_public', 'presale_start',
|
'date_to', 'date_admission', 'is_public', 'presale_start',
|
||||||
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
|
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
|
||||||
'plugins', 'seat_category_mapping', 'timezone', 'item_meta_properties', 'valid_keys',
|
'plugins', 'seat_category_mapping', 'timezone', 'item_meta_properties', 'valid_keys',
|
||||||
'sales_channels', 'best_availability_state', 'public_url')
|
'all_sales_channels', 'limit_sales_channels', 'best_availability_state', 'public_url')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -188,6 +201,7 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
self.fields.pop('valid_keys')
|
self.fields.pop('valid_keys')
|
||||||
if not self.context.get('request') or 'with_availability_for' not in self.context['request'].GET:
|
if not self.context.get('request') or 'with_availability_for' not in self.context['request'].GET:
|
||||||
self.fields.pop('best_availability_state')
|
self.fields.pop('best_availability_state')
|
||||||
|
self.fields['limit_sales_channels'].child_relation.queryset = self.context['organizer'].sales_channels.all()
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
@@ -269,13 +283,17 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
from pretix.base.plugins import get_all_plugins
|
from pretix.base.plugins import get_all_plugins
|
||||||
|
|
||||||
plugins_available = {
|
plugins_available = {
|
||||||
p.module for p in get_all_plugins(self.instance)
|
p.module: p for p in get_all_plugins(self.instance)
|
||||||
if not p.name.startswith('.') and getattr(p, 'visible', True)
|
if not p.name.startswith('.') and getattr(p, 'visible', True)
|
||||||
}
|
}
|
||||||
|
settings_holder = self.instance if self.instance and self.instance.pk else self.context['organizer']
|
||||||
|
|
||||||
for plugin in value.get('plugins'):
|
for plugin in value.get('plugins'):
|
||||||
if plugin not in plugins_available:
|
if plugin not in plugins_available:
|
||||||
raise ValidationError(_('Unknown plugin: \'{name}\'.').format(name=plugin))
|
raise ValidationError(_('Unknown plugin: \'{name}\'.').format(name=plugin))
|
||||||
|
if getattr(plugins_available[plugin], 'restricted', False):
|
||||||
|
if plugin not in settings_holder.settings.allowed_restricted_plugins:
|
||||||
|
raise ValidationError(_('Restricted plugin: \'{name}\'.').format(name=plugin))
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@@ -419,7 +437,8 @@ class CloneEventSerializer(EventSerializer):
|
|||||||
testmode = validated_data.pop('testmode', None)
|
testmode = validated_data.pop('testmode', None)
|
||||||
has_subevents = validated_data.pop('has_subevents', None)
|
has_subevents = validated_data.pop('has_subevents', None)
|
||||||
tz = validated_data.pop('timezone', None)
|
tz = validated_data.pop('timezone', None)
|
||||||
sales_channels = validated_data.pop('sales_channels', None)
|
all_sales_channels = validated_data.pop('all_sales_channels', None)
|
||||||
|
limit_sales_channels = validated_data.pop('limit_sales_channels', None)
|
||||||
date_admission = validated_data.pop('date_admission', None)
|
date_admission = validated_data.pop('date_admission', None)
|
||||||
new_event = super().create({**validated_data, 'plugins': None})
|
new_event = super().create({**validated_data, 'plugins': None})
|
||||||
|
|
||||||
@@ -432,8 +451,9 @@ class CloneEventSerializer(EventSerializer):
|
|||||||
new_event.is_public = is_public
|
new_event.is_public = is_public
|
||||||
if testmode is not None:
|
if testmode is not None:
|
||||||
new_event.testmode = testmode
|
new_event.testmode = testmode
|
||||||
if sales_channels is not None:
|
if all_sales_channels is not None or limit_sales_channels is not None:
|
||||||
new_event.sales_channels = sales_channels
|
new_event.all_sales_channels = all_sales_channels
|
||||||
|
new_event.limit_sales_channels.set(limit_sales_channels)
|
||||||
if has_subevents is not None:
|
if has_subevents is not None:
|
||||||
new_event.has_subevents = has_subevents
|
new_event.has_subevents = has_subevents
|
||||||
if has_subevents is not None:
|
if has_subevents is not None:
|
||||||
@@ -661,8 +681,8 @@ class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TaxRule
|
model = TaxRule
|
||||||
fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country', 'internal_name',
|
fields = ('id', 'name', 'rate', 'code', 'price_includes_tax', 'eu_reverse_charge', 'home_country',
|
||||||
'keep_gross_if_rate_changes', 'custom_rules')
|
'internal_name', 'keep_gross_if_rate_changes', 'custom_rules')
|
||||||
|
|
||||||
|
|
||||||
class EventSettingsSerializer(SettingsSerializer):
|
class EventSettingsSerializer(SettingsSerializer):
|
||||||
@@ -755,6 +775,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
'invoice_address_company_required',
|
'invoice_address_company_required',
|
||||||
'invoice_address_beneficiary',
|
'invoice_address_beneficiary',
|
||||||
'invoice_address_custom_field',
|
'invoice_address_custom_field',
|
||||||
|
'invoice_address_custom_field_helptext',
|
||||||
'invoice_name_required',
|
'invoice_name_required',
|
||||||
'invoice_address_not_asked_free',
|
'invoice_address_not_asked_free',
|
||||||
'invoice_show_payments',
|
'invoice_show_payments',
|
||||||
@@ -828,6 +849,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
'reusable_media_type_nfc_mf0aes_autocreate_giftcard',
|
'reusable_media_type_nfc_mf0aes_autocreate_giftcard',
|
||||||
'reusable_media_type_nfc_mf0aes_autocreate_giftcard_currency',
|
'reusable_media_type_nfc_mf0aes_autocreate_giftcard_currency',
|
||||||
'reusable_media_type_nfc_mf0aes_random_uid',
|
'reusable_media_type_nfc_mf0aes_random_uid',
|
||||||
|
'seating_allow_blocked_seats_for_channel',
|
||||||
]
|
]
|
||||||
readonly_fields = [
|
readonly_fields = [
|
||||||
# These are read-only since they are currently only settable on organizers, not events
|
# These are read-only since they are currently only settable on organizers, not events
|
||||||
@@ -878,6 +900,7 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
|
|||||||
'locale',
|
'locale',
|
||||||
'last_order_modification_date',
|
'last_order_modification_date',
|
||||||
'show_quota_left',
|
'show_quota_left',
|
||||||
|
'show_dates_on_frontpage',
|
||||||
'max_items_per_order',
|
'max_items_per_order',
|
||||||
'attendee_names_asked',
|
'attendee_names_asked',
|
||||||
'attendee_names_required',
|
'attendee_names_required',
|
||||||
@@ -897,6 +920,7 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
|
|||||||
'invoice_address_company_required',
|
'invoice_address_company_required',
|
||||||
'invoice_address_beneficiary',
|
'invoice_address_beneficiary',
|
||||||
'invoice_address_custom_field',
|
'invoice_address_custom_field',
|
||||||
|
'invoice_address_custom_field_helptext',
|
||||||
'invoice_name_required',
|
'invoice_name_required',
|
||||||
'invoice_address_not_asked_free',
|
'invoice_address_not_asked_free',
|
||||||
'invoice_address_from_name',
|
'invoice_address_from_name',
|
||||||
@@ -953,3 +977,111 @@ class ItemMetaPropertiesSerializer(I18nAwareModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = ItemMetaProperty
|
model = ItemMetaProperty
|
||||||
fields = ('id', 'name', 'default', 'required', 'allowed_values')
|
fields = ('id', 'name', 'default', 'required', 'allowed_values')
|
||||||
|
|
||||||
|
|
||||||
|
def prefetch_by_id(items, qs, id_attr, target_attr):
|
||||||
|
"""
|
||||||
|
Prefetches a related object on each item in the given list of items by searching by id or another
|
||||||
|
unique field. The id value is read from the attribute on item specified in `id_attr`, searched on queryset `qs` by
|
||||||
|
the primary key, and the resulting prefetched model object is stored into `target_attr` on the item.
|
||||||
|
"""
|
||||||
|
ids = [getattr(item, id_attr) for item in items if getattr(item, id_attr)]
|
||||||
|
if ids:
|
||||||
|
result = qs.in_bulk(id_list=ids)
|
||||||
|
for item in items:
|
||||||
|
setattr(item, target_attr, result.get(getattr(item, id_attr)))
|
||||||
|
|
||||||
|
|
||||||
|
class SeatBulkBlockInputSerializer(serializers.Serializer):
|
||||||
|
ids = serializers.ListField(child=serializers.IntegerField(), required=False, allow_empty=True)
|
||||||
|
seat_guids = serializers.ListField(child=serializers.CharField(), required=False, allow_empty=True)
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
data = super().to_internal_value(data)
|
||||||
|
|
||||||
|
if data.get("seat_guids") and data.get("ids"):
|
||||||
|
raise ValidationError("Please pass either seat_guids or ids.")
|
||||||
|
|
||||||
|
if data.get("seat_guids"):
|
||||||
|
seat_ids = data["seat_guids"]
|
||||||
|
if len(seat_ids) > 10000:
|
||||||
|
raise ValidationError({"seat_guids": ["Please do not pass over 10000 seats."]})
|
||||||
|
|
||||||
|
seats = {s.seat_guid: s for s in self.context["queryset"].filter(seat_guid__in=seat_ids)}
|
||||||
|
for s in seat_ids:
|
||||||
|
if s not in seats:
|
||||||
|
raise ValidationError({"seat_guids": [f"The seat '{s}' does not exist."]})
|
||||||
|
elif data.get("ids"):
|
||||||
|
seat_ids = data["ids"]
|
||||||
|
if len(seat_ids) > 10000:
|
||||||
|
raise ValidationError({"ids": ["Please do not pass over 10000 seats."]})
|
||||||
|
|
||||||
|
seats = self.context["queryset"].in_bulk(seat_ids)
|
||||||
|
for s in seat_ids:
|
||||||
|
if s not in seats:
|
||||||
|
raise ValidationError({"ids": [f"The seat '{s}' does not exist."]})
|
||||||
|
else:
|
||||||
|
raise ValidationError("Please pass either seat_guids or ids.")
|
||||||
|
|
||||||
|
return {"seats": seats.values()}
|
||||||
|
|
||||||
|
|
||||||
|
class SeatSerializer(I18nAwareModelSerializer):
|
||||||
|
orderposition = serializers.IntegerField(source='orderposition_id')
|
||||||
|
cartposition = serializers.IntegerField(source='cartposition_id')
|
||||||
|
voucher = serializers.IntegerField(source='voucher_id')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Seat
|
||||||
|
read_only_fields = (
|
||||||
|
'id', 'subevent', 'zone_name', 'row_name', 'row_label',
|
||||||
|
'seat_number', 'seat_label', 'seat_guid', 'product',
|
||||||
|
'orderposition', 'cartposition', 'voucher',
|
||||||
|
)
|
||||||
|
fields = (
|
||||||
|
'id', 'subevent', 'zone_name', 'row_name', 'row_label',
|
||||||
|
'seat_number', 'seat_label', 'seat_guid', 'product', 'blocked',
|
||||||
|
'orderposition', 'cartposition', 'voucher',
|
||||||
|
)
|
||||||
|
|
||||||
|
def prefetch_expanded_data(self, items, request, expand_fields):
|
||||||
|
if 'orderposition' in expand_fields:
|
||||||
|
if 'can_view_orders' not in request.eventpermset:
|
||||||
|
raise PermissionDenied('can_view_orders permission required for expand=orderposition')
|
||||||
|
prefetch_by_id(items, OrderPosition.objects.prefetch_related('order'), 'orderposition_id', 'orderposition')
|
||||||
|
if 'cartposition' in expand_fields:
|
||||||
|
if 'can_view_orders' not in request.eventpermset:
|
||||||
|
raise PermissionDenied('can_view_orders permission required for expand=cartposition')
|
||||||
|
prefetch_by_id(items, CartPosition.objects, 'cartposition_id', 'cartposition')
|
||||||
|
if 'voucher' in expand_fields:
|
||||||
|
if 'can_view_vouchers' not in request.eventpermset:
|
||||||
|
raise PermissionDenied('can_view_vouchers permission required for expand=voucher')
|
||||||
|
prefetch_by_id(items, Voucher.objects, 'voucher_id', 'voucher')
|
||||||
|
|
||||||
|
def __init__(self, instance, *args, **kwargs):
|
||||||
|
if not kwargs.get('data'):
|
||||||
|
self.prefetch_expanded_data(instance if hasattr(instance, '__iter__') else [instance],
|
||||||
|
kwargs['context']['request'],
|
||||||
|
kwargs['context']['expand_fields'])
|
||||||
|
|
||||||
|
super().__init__(instance, *args, **kwargs)
|
||||||
|
|
||||||
|
if 'orderposition' in self.context['expand_fields']:
|
||||||
|
from pretix.api.serializers.media import (
|
||||||
|
NestedOrderPositionSerializer,
|
||||||
|
)
|
||||||
|
self.fields['orderposition'] = NestedOrderPositionSerializer(read_only=True, context=self.context['order_context'])
|
||||||
|
try:
|
||||||
|
del self.fields['orderposition'].fields['seat']
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if 'cartposition' in self.context['expand_fields']:
|
||||||
|
from pretix.api.serializers.cart import CartPositionSerializer
|
||||||
|
self.fields['cartposition'] = CartPositionSerializer(read_only=True)
|
||||||
|
del self.fields['cartposition'].fields['seat']
|
||||||
|
|
||||||
|
if 'voucher' in self.context['expand_fields']:
|
||||||
|
from pretix.api.serializers.voucher import VoucherSerializer
|
||||||
|
self.fields['voucher'] = VoucherSerializer(read_only=True)
|
||||||
|
del self.fields['voucher'].fields['seat']
|
||||||
|
|||||||
@@ -19,57 +19,8 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
from django.conf import settings
|
|
||||||
from django.core.validators import URLValidator
|
from django.core.validators import URLValidator
|
||||||
from i18nfield.fields import I18nCharField, I18nTextField
|
from i18nfield.rest_framework import I18nAwareModelSerializer, I18nField
|
||||||
from i18nfield.strings import LazyI18nString
|
|
||||||
from rest_framework.exceptions import ValidationError
|
|
||||||
from rest_framework.fields import Field
|
|
||||||
from rest_framework.serializers import ModelSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class I18nField(Field):
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
self.allow_blank = kwargs.pop('allow_blank', False)
|
|
||||||
self.trim_whitespace = kwargs.pop('trim_whitespace', True)
|
|
||||||
self.max_length = kwargs.pop('max_length', None)
|
|
||||||
self.min_length = kwargs.pop('min_length', None)
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
|
|
||||||
def to_representation(self, value):
|
|
||||||
if hasattr(value, 'data'):
|
|
||||||
if isinstance(value.data, dict):
|
|
||||||
return value.data
|
|
||||||
elif value.data is None:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return {
|
|
||||||
settings.LANGUAGE_CODE: str(value.data)
|
|
||||||
}
|
|
||||||
elif value is None:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return {
|
|
||||||
settings.LANGUAGE_CODE: str(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
|
||||||
if isinstance(data, str):
|
|
||||||
return LazyI18nString(data)
|
|
||||||
elif isinstance(data, dict):
|
|
||||||
if any([k not in dict(settings.LANGUAGES) for k in data.keys()]):
|
|
||||||
raise ValidationError('Invalid languages included.')
|
|
||||||
return LazyI18nString(data)
|
|
||||||
else:
|
|
||||||
raise ValidationError('Invalid data type.')
|
|
||||||
|
|
||||||
|
|
||||||
class I18nAwareModelSerializer(ModelSerializer):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
I18nAwareModelSerializer.serializer_field_mapping[I18nCharField] = I18nField
|
|
||||||
I18nAwareModelSerializer.serializer_field_mapping[I18nTextField] = I18nField
|
|
||||||
|
|
||||||
|
|
||||||
class I18nURLField(I18nField):
|
class I18nURLField(I18nField):
|
||||||
@@ -84,3 +35,10 @@ class I18nURLField(I18nField):
|
|||||||
else:
|
else:
|
||||||
URLValidator()(value.data)
|
URLValidator()(value.data)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"I18nAwareModelSerializer", # for backwards compatibility
|
||||||
|
"I18nField", # for backwards compatibility
|
||||||
|
"I18nURLField",
|
||||||
|
]
|
||||||
|
|||||||
@@ -42,19 +42,27 @@ from django.utils.functional import cached_property, lazy
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from pretix.api.serializers import SalesChannelMigrationMixin
|
||||||
from pretix.api.serializers.event import MetaDataField
|
from pretix.api.serializers.event import MetaDataField
|
||||||
from pretix.api.serializers.fields import UploadedFileField
|
from pretix.api.serializers.fields import UploadedFileField
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation,
|
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation,
|
||||||
ItemVariationMetaValue, Question, QuestionOption, Quota,
|
ItemVariationMetaValue, Question, QuestionOption, Quota, SalesChannel,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
class InlineItemVariationSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||||
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=13,
|
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=13,
|
||||||
coerce_to_string=True)
|
coerce_to_string=True)
|
||||||
meta_data = MetaDataField(required=False, source='*')
|
meta_data = MetaDataField(required=False, source='*')
|
||||||
|
limit_sales_channels = serializers.SlugRelatedField(
|
||||||
|
slug_field="identifier",
|
||||||
|
queryset=SalesChannel.objects.none(),
|
||||||
|
required=False,
|
||||||
|
allow_empty=True,
|
||||||
|
many=True,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ItemVariation
|
model = ItemVariation
|
||||||
@@ -63,11 +71,14 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
|||||||
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
||||||
'checkin_attention', 'checkin_text',
|
'checkin_attention', 'checkin_text',
|
||||||
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||||
'sales_channels', 'hide_without_voucher', 'meta_data')
|
'all_sales_channels', 'limit_sales_channels', 'hide_without_voucher', 'meta_data')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['require_membership_types'].queryset = lazy(lambda: self.context['event'].organizer.membership_types.all(), QuerySet)
|
self.fields['require_membership_types'].queryset = lazy(lambda: self.context['event'].organizer.membership_types.all(), QuerySet)
|
||||||
|
self.fields['limit_sales_channels'].child_relation.queryset = (
|
||||||
|
self.context['event'].organizer.sales_channels.all() if 'event' in self.context else SalesChannel.objects.none()
|
||||||
|
)
|
||||||
|
|
||||||
def validate_meta_data(self, value):
|
def validate_meta_data(self, value):
|
||||||
for key in value['meta_data'].keys():
|
for key in value['meta_data'].keys():
|
||||||
@@ -76,10 +87,17 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
class ItemVariationSerializer(I18nAwareModelSerializer):
|
class ItemVariationSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||||
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=13,
|
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=13,
|
||||||
coerce_to_string=True)
|
coerce_to_string=True)
|
||||||
meta_data = MetaDataField(required=False, source='*')
|
meta_data = MetaDataField(required=False, source='*')
|
||||||
|
limit_sales_channels = serializers.SlugRelatedField(
|
||||||
|
slug_field="identifier",
|
||||||
|
queryset=SalesChannel.objects.none(),
|
||||||
|
required=False,
|
||||||
|
allow_empty=True,
|
||||||
|
many=True,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ItemVariation
|
model = ItemVariation
|
||||||
@@ -88,21 +106,26 @@ class ItemVariationSerializer(I18nAwareModelSerializer):
|
|||||||
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
||||||
'checkin_attention', 'checkin_text',
|
'checkin_attention', 'checkin_text',
|
||||||
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||||
'sales_channels', 'hide_without_voucher', 'meta_data')
|
'all_sales_channels', 'limit_sales_channels', 'hide_without_voucher', 'meta_data')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
|
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
|
||||||
|
self.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
meta_data = validated_data.pop('meta_data', None)
|
meta_data = validated_data.pop('meta_data', None)
|
||||||
require_membership_types = validated_data.pop('require_membership_types', [])
|
require_membership_types = validated_data.pop('require_membership_types', [])
|
||||||
|
limit_sales_channels = validated_data.pop('limit_sales_channels', [])
|
||||||
variation = ItemVariation.objects.create(**validated_data)
|
variation = ItemVariation.objects.create(**validated_data)
|
||||||
|
|
||||||
if require_membership_types:
|
if require_membership_types:
|
||||||
variation.require_membership_types.add(*require_membership_types)
|
variation.require_membership_types.add(*require_membership_types)
|
||||||
|
|
||||||
|
if limit_sales_channels:
|
||||||
|
variation.limit_sales_channels.add(*limit_sales_channels)
|
||||||
|
|
||||||
# Meta data
|
# Meta data
|
||||||
if meta_data is not None:
|
if meta_data is not None:
|
||||||
for key, value in meta_data.items():
|
for key, value in meta_data.items():
|
||||||
@@ -223,7 +246,7 @@ class ItemTaxRateField(serializers.Field):
|
|||||||
return str(Decimal('0.00'))
|
return str(Decimal('0.00'))
|
||||||
|
|
||||||
|
|
||||||
class ItemSerializer(I18nAwareModelSerializer):
|
class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||||
addons = InlineItemAddOnSerializer(many=True, required=False)
|
addons = InlineItemAddOnSerializer(many=True, required=False)
|
||||||
bundles = InlineItemBundleSerializer(many=True, required=False)
|
bundles = InlineItemBundleSerializer(many=True, required=False)
|
||||||
variations = InlineItemVariationSerializer(many=True, required=False)
|
variations = InlineItemVariationSerializer(many=True, required=False)
|
||||||
@@ -232,11 +255,18 @@ class ItemSerializer(I18nAwareModelSerializer):
|
|||||||
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
|
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
|
||||||
'image/png', 'image/jpeg', 'image/gif'
|
'image/png', 'image/jpeg', 'image/gif'
|
||||||
), max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE)
|
), max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE)
|
||||||
|
limit_sales_channels = serializers.SlugRelatedField(
|
||||||
|
slug_field="identifier",
|
||||||
|
queryset=SalesChannel.objects.none(),
|
||||||
|
required=False,
|
||||||
|
allow_empty=True,
|
||||||
|
many=True,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Item
|
model = Item
|
||||||
fields = ('id', 'category', 'name', 'internal_name', 'active', 'sales_channels', 'description',
|
fields = ('id', 'category', 'name', 'internal_name', 'active', 'all_sales_channels', 'limit_sales_channels',
|
||||||
'default_price', 'free_price', 'free_price_suggestion', 'tax_rate', 'tax_rule', 'admission',
|
'description', 'default_price', 'free_price', 'free_price_suggestion', 'tax_rate', 'tax_rule', 'admission',
|
||||||
'personalized', 'position', 'picture',
|
'personalized', 'position', 'picture',
|
||||||
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||||
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
||||||
@@ -259,6 +289,8 @@ class ItemSerializer(I18nAwareModelSerializer):
|
|||||||
if not self.read_only:
|
if not self.read_only:
|
||||||
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
|
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
|
||||||
self.fields['grant_membership_type'].queryset = self.context['event'].organizer.membership_types.all()
|
self.fields['grant_membership_type'].queryset = self.context['event'].organizer.membership_types.all()
|
||||||
|
self.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||||
|
self.fields['variations'].child.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
@@ -335,7 +367,10 @@ class ItemSerializer(I18nAwareModelSerializer):
|
|||||||
meta_data = validated_data.pop('meta_data', None)
|
meta_data = validated_data.pop('meta_data', None)
|
||||||
picture = validated_data.pop('picture', None)
|
picture = validated_data.pop('picture', None)
|
||||||
require_membership_types = validated_data.pop('require_membership_types', [])
|
require_membership_types = validated_data.pop('require_membership_types', [])
|
||||||
|
limit_sales_channels = validated_data.pop('limit_sales_channels', [])
|
||||||
item = Item.objects.create(**validated_data)
|
item = Item.objects.create(**validated_data)
|
||||||
|
if limit_sales_channels and not validated_data.get('all_sales_channels'):
|
||||||
|
item.limit_sales_channels.add(*limit_sales_channels)
|
||||||
if picture:
|
if picture:
|
||||||
item.picture.save(os.path.basename(picture.name), picture)
|
item.picture.save(os.path.basename(picture.name), picture)
|
||||||
if require_membership_types:
|
if require_membership_types:
|
||||||
@@ -343,10 +378,13 @@ class ItemSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
for variation_data in variations_data:
|
for variation_data in variations_data:
|
||||||
require_membership_types = variation_data.pop('require_membership_types', [])
|
require_membership_types = variation_data.pop('require_membership_types', [])
|
||||||
|
limit_sales_channels = variation_data.pop('limit_sales_channels', [])
|
||||||
var_meta_data = variation_data.pop('meta_data', {})
|
var_meta_data = variation_data.pop('meta_data', {})
|
||||||
v = ItemVariation.objects.create(item=item, **variation_data)
|
v = ItemVariation.objects.create(item=item, **variation_data)
|
||||||
if require_membership_types:
|
if require_membership_types:
|
||||||
v.require_membership_types.add(*require_membership_types)
|
v.require_membership_types.add(*require_membership_types)
|
||||||
|
if limit_sales_channels:
|
||||||
|
v.limit_sales_channels.add(*limit_sales_channels)
|
||||||
|
|
||||||
if var_meta_data is not None:
|
if var_meta_data is not None:
|
||||||
for key, value in var_meta_data.items():
|
for key, value in var_meta_data.items():
|
||||||
@@ -403,7 +441,22 @@ class ItemCategorySerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ItemCategory
|
model = ItemCategory
|
||||||
fields = ('id', 'name', 'internal_name', 'description', 'position', 'is_addon')
|
fields = (
|
||||||
|
'id', 'name', 'internal_name', 'description', 'position',
|
||||||
|
'is_addon', 'cross_selling_mode',
|
||||||
|
'cross_selling_condition', 'cross_selling_match_products'
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
data = super().validate(data)
|
||||||
|
|
||||||
|
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||||
|
full_data.update(data)
|
||||||
|
|
||||||
|
if full_data.get('is_addon') and full_data.get('cross_selling_mode'):
|
||||||
|
raise ValidationError('is_addon and cross_selling_mode are mutually exclusive')
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class QuestionOptionSerializer(I18nAwareModelSerializer):
|
class QuestionOptionSerializer(I18nAwareModelSerializer):
|
||||||
|
|||||||
@@ -46,17 +46,16 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
|||||||
from pretix.api.serializers.item import (
|
from pretix.api.serializers.item import (
|
||||||
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
|
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
|
||||||
)
|
)
|
||||||
from pretix.base.channels import get_all_sales_channels
|
|
||||||
from pretix.base.decimal import round_decimal
|
from pretix.base.decimal import round_decimal
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CachedFile, Checkin, Customer, Invoice, InvoiceAddress, InvoiceLine, Item,
|
CachedFile, Checkin, Customer, Invoice, InvoiceAddress, InvoiceLine, Item,
|
||||||
ItemVariation, Order, OrderPosition, Question, QuestionAnswer,
|
ItemVariation, Order, OrderPosition, Question, QuestionAnswer,
|
||||||
ReusableMedium, Seat, SubEvent, TaxRule, Voucher,
|
ReusableMedium, SalesChannel, Seat, SubEvent, TaxRule, Voucher,
|
||||||
)
|
)
|
||||||
from pretix.base.models.orders import (
|
from pretix.base.models.orders import (
|
||||||
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
|
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
|
||||||
RevokedTicketSecret,
|
PrintLog, RevokedTicketSecret,
|
||||||
)
|
)
|
||||||
from pretix.base.pdf import get_images, get_variables
|
from pretix.base.pdf import get_images, get_variables
|
||||||
from pretix.base.services.cart import error_messages
|
from pretix.base.services.cart import error_messages
|
||||||
@@ -166,7 +165,7 @@ class InlineSeatSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Seat
|
model = Seat
|
||||||
fields = ('id', 'name', 'seat_guid')
|
fields = ('id', 'name', 'seat_guid', 'zone_name', 'row_name', 'row_label', 'seat_label', 'seat_number')
|
||||||
|
|
||||||
|
|
||||||
class AnswerSerializer(I18nAwareModelSerializer):
|
class AnswerSerializer(I18nAwareModelSerializer):
|
||||||
@@ -274,9 +273,35 @@ class AnswerSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class CheckinSerializer(I18nAwareModelSerializer):
|
class CheckinSerializer(I18nAwareModelSerializer):
|
||||||
|
device_id = serializers.SlugRelatedField(
|
||||||
|
source='device',
|
||||||
|
slug_field='device_id',
|
||||||
|
read_only=True,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Checkin
|
model = Checkin
|
||||||
fields = ('id', 'datetime', 'list', 'auto_checked_in', 'gate', 'device', 'type')
|
fields = ('id', 'datetime', 'list', 'auto_checked_in', 'gate', 'device', 'device_id', 'type')
|
||||||
|
|
||||||
|
|
||||||
|
class PrintLogSerializer(serializers.ModelSerializer):
|
||||||
|
device_id = serializers.SlugRelatedField(
|
||||||
|
source='device',
|
||||||
|
slug_field='device_id',
|
||||||
|
read_only=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PrintLog
|
||||||
|
fields = (
|
||||||
|
"id",
|
||||||
|
"successful",
|
||||||
|
"datetime",
|
||||||
|
"source",
|
||||||
|
"type",
|
||||||
|
"device_id",
|
||||||
|
"info",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FailedCheckinSerializer(I18nAwareModelSerializer):
|
class FailedCheckinSerializer(I18nAwareModelSerializer):
|
||||||
@@ -471,6 +496,7 @@ class OrderPositionListSerializer(serializers.ListSerializer):
|
|||||||
|
|
||||||
class OrderPositionSerializer(I18nAwareModelSerializer):
|
class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||||
checkins = CheckinSerializer(many=True, read_only=True)
|
checkins = CheckinSerializer(many=True, read_only=True)
|
||||||
|
print_logs = PrintLogSerializer(many=True, read_only=True)
|
||||||
answers = AnswerSerializer(many=True)
|
answers = AnswerSerializer(many=True)
|
||||||
downloads = PositionDownloadsField(source='*', read_only=True)
|
downloads = PositionDownloadsField(source='*', read_only=True)
|
||||||
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
||||||
@@ -485,12 +511,13 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
|||||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||||
'company', 'street', 'zipcode', 'city', 'country', 'state', 'discount',
|
'company', 'street', 'zipcode', 'city', 'country', 'state', 'discount',
|
||||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled',
|
'print_logs', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled',
|
||||||
'valid_from', 'valid_until', 'blocked', 'voucher_budget_use')
|
'print_logs', 'downloads', 'answers', 'tax_rule', 'tax_code', 'pseudonymization_id', 'pdf_data', 'seat',
|
||||||
|
'canceled', 'valid_from', 'valid_until', 'blocked', 'voucher_budget_use')
|
||||||
read_only_fields = (
|
read_only_fields = (
|
||||||
'id', 'order', 'positionid', 'item', 'variation', 'price', 'voucher', 'tax_rate', 'tax_value', 'secret',
|
'id', 'order', 'positionid', 'item', 'variation', 'price', 'voucher', 'tax_rate', 'tax_value', 'secret',
|
||||||
'addon_to', 'subevent', 'checkins', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data',
|
'addon_to', 'subevent', 'checkins', 'downloads', 'answers', 'tax_rule', 'tax_code', 'pseudonymization_id',
|
||||||
'seat', 'canceled', 'discount', 'valid_from', 'valid_until', 'blocked', 'voucher_budget_use'
|
'pdf_data', 'seat', 'canceled', 'discount', 'valid_from', 'valid_until', 'blocked', 'voucher_budget_use'
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -572,9 +599,9 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
|||||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||||
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'require_attention',
|
'print_logs', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat',
|
||||||
'order__status', 'order__valid_if_pending', 'order__require_approval', 'valid_from', 'valid_until',
|
'require_attention', 'order__status', 'order__valid_if_pending', 'order__require_approval',
|
||||||
'blocked')
|
'valid_from', 'valid_until', 'blocked')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -586,7 +613,7 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
|||||||
self.fields['item'] = ItemSerializer(read_only=True, context=self.context)
|
self.fields['item'] = ItemSerializer(read_only=True, context=self.context)
|
||||||
|
|
||||||
if 'variation' in self.context['expand']:
|
if 'variation' in self.context['expand']:
|
||||||
self.fields['variation'] = InlineItemVariationSerializer(read_only=True)
|
self.fields['variation'] = InlineItemVariationSerializer(read_only=True, context=self.context)
|
||||||
|
|
||||||
if 'answers.question' in self.context['expand']:
|
if 'answers.question' in self.context['expand']:
|
||||||
self.fields['answers'].child.fields['question'] = QuestionSerializer(read_only=True)
|
self.fields['answers'].child.fields['question'] = QuestionSerializer(read_only=True)
|
||||||
@@ -616,7 +643,8 @@ class OrderPaymentDateField(serializers.DateField):
|
|||||||
class OrderFeeSerializer(I18nAwareModelSerializer):
|
class OrderFeeSerializer(I18nAwareModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = OrderFee
|
model = OrderFee
|
||||||
fields = ('id', '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',
|
||||||
|
'tax_code', 'canceled')
|
||||||
|
|
||||||
|
|
||||||
class PaymentURLField(serializers.URLField):
|
class PaymentURLField(serializers.URLField):
|
||||||
@@ -714,6 +742,11 @@ class OrderSerializer(I18nAwareModelSerializer):
|
|||||||
payment_provider = OrderPaymentTypeField(source='*', read_only=True)
|
payment_provider = OrderPaymentTypeField(source='*', read_only=True)
|
||||||
url = OrderURLField(source='*', read_only=True)
|
url = OrderURLField(source='*', read_only=True)
|
||||||
customer = serializers.SlugRelatedField(slug_field='identifier', read_only=True)
|
customer = serializers.SlugRelatedField(slug_field='identifier', read_only=True)
|
||||||
|
sales_channel = serializers.SlugRelatedField(
|
||||||
|
slug_field='identifier',
|
||||||
|
queryset=SalesChannel.objects.none(),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Order
|
model = Order
|
||||||
@@ -722,16 +755,20 @@ class OrderSerializer(I18nAwareModelSerializer):
|
|||||||
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
|
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
|
||||||
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
|
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
|
||||||
'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
|
'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
|
||||||
'url', 'customer', 'valid_if_pending'
|
'url', 'customer', 'valid_if_pending', 'api_meta', 'cancellation_date'
|
||||||
)
|
)
|
||||||
read_only_fields = (
|
read_only_fields = (
|
||||||
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
|
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
|
||||||
'payment_provider', 'fees', 'total', 'positions', 'downloads', 'customer',
|
'payment_provider', 'fees', 'total', 'positions', 'downloads', 'customer',
|
||||||
'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel'
|
'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel', 'cancellation_date'
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
if "organizer" in self.context:
|
||||||
|
self.fields["sales_channel"].queryset = self.context["organizer"].sales_channels.all()
|
||||||
|
else:
|
||||||
|
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
||||||
if not self.context['pdf_data']:
|
if not self.context['pdf_data']:
|
||||||
self.fields['positions'].child.fields.pop('pdf_data', None)
|
self.fields['positions'].child.fields.pop('pdf_data', None)
|
||||||
|
|
||||||
@@ -778,7 +815,7 @@ class OrderSerializer(I18nAwareModelSerializer):
|
|||||||
# Even though all fields that shouldn't be edited are marked as read_only in the serializer
|
# Even though all fields that shouldn't be edited are marked as read_only in the serializer
|
||||||
# (hopefully), we'll be extra careful here and be explicit about the model fields we update.
|
# (hopefully), we'll be extra careful here and be explicit about the model fields we update.
|
||||||
update_fields = ['comment', 'custom_followup_at', 'checkin_attention', 'checkin_text', 'email', 'locale',
|
update_fields = ['comment', 'custom_followup_at', 'checkin_attention', 'checkin_text', 'email', 'locale',
|
||||||
'phone', 'valid_if_pending']
|
'phone', 'valid_if_pending', 'api_meta']
|
||||||
|
|
||||||
if 'invoice_address' in validated_data:
|
if 'invoice_address' in validated_data:
|
||||||
iadata = validated_data.pop('invoice_address')
|
iadata = validated_data.pop('invoice_address')
|
||||||
@@ -1033,19 +1070,25 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
require_approval = serializers.BooleanField(default=False, required=False)
|
require_approval = serializers.BooleanField(default=False, required=False)
|
||||||
simulate = serializers.BooleanField(default=False, required=False)
|
simulate = serializers.BooleanField(default=False, required=False)
|
||||||
customer = serializers.SlugRelatedField(slug_field='identifier', queryset=Customer.objects.none(), required=False)
|
customer = serializers.SlugRelatedField(slug_field='identifier', queryset=Customer.objects.none(), required=False)
|
||||||
|
sales_channel = serializers.SlugRelatedField(
|
||||||
|
slug_field='identifier',
|
||||||
|
queryset=SalesChannel.objects.none(),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['positions'].child.fields['voucher'].queryset = self.context['event'].vouchers.all()
|
self.fields['positions'].child.fields['voucher'].queryset = self.context['event'].vouchers.all()
|
||||||
self.fields['customer'].queryset = self.context['event'].organizer.customers.all()
|
self.fields['customer'].queryset = self.context['event'].organizer.customers.all()
|
||||||
self.fields['expires'].required = False
|
self.fields['expires'].required = False
|
||||||
|
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Order
|
model = Order
|
||||||
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
||||||
'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date',
|
'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date',
|
||||||
'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at',
|
'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at',
|
||||||
'require_approval', 'valid_if_pending', 'expires')
|
'require_approval', 'valid_if_pending', 'expires', 'api_meta')
|
||||||
|
|
||||||
def validate_payment_provider(self, pp):
|
def validate_payment_provider(self, pp):
|
||||||
if pp is None:
|
if pp is None:
|
||||||
@@ -1059,11 +1102,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
raise ValidationError('Expiration date must be in the future.')
|
raise ValidationError('Expiration date must be in the future.')
|
||||||
return expires
|
return expires
|
||||||
|
|
||||||
def validate_sales_channel(self, channel):
|
|
||||||
if channel not in get_all_sales_channels():
|
|
||||||
raise ValidationError('Unknown sales channel.')
|
|
||||||
return channel
|
|
||||||
|
|
||||||
def validate_code(self, code):
|
def validate_code(self, code):
|
||||||
if code and Order.objects.filter(event__organizer=self.context['event'].organizer, code=code).exists():
|
if code and Order.objects.filter(event__organizer=self.context['event'].organizer, code=code).exists():
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
@@ -1125,20 +1163,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
raise ValidationError(errs)
|
raise ValidationError(errs)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def validate_testmode(self, testmode):
|
|
||||||
if 'sales_channel' in self.initial_data:
|
|
||||||
try:
|
|
||||||
sales_channel = get_all_sales_channels()[self.initial_data['sales_channel']]
|
|
||||||
|
|
||||||
if testmode and not sales_channel.testmode_supported:
|
|
||||||
raise ValidationError('This sales channel does not provide support for test mode.')
|
|
||||||
except KeyError:
|
|
||||||
# We do not need to raise a ValidationError here, since there is another check to validate the
|
|
||||||
# sales_channel
|
|
||||||
pass
|
|
||||||
|
|
||||||
return testmode
|
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
fees_data = validated_data.pop('fees') if 'fees' in validated_data else []
|
fees_data = validated_data.pop('fees') if 'fees' in validated_data else []
|
||||||
positions_data = validated_data.pop('positions') if 'positions' in validated_data else []
|
positions_data = validated_data.pop('positions') if 'positions' in validated_data else []
|
||||||
@@ -1147,9 +1171,16 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
payment_date = validated_data.pop('payment_date', now())
|
payment_date = validated_data.pop('payment_date', now())
|
||||||
force = validated_data.pop('force', False)
|
force = validated_data.pop('force', False)
|
||||||
simulate = validated_data.pop('simulate', False)
|
simulate = validated_data.pop('simulate', False)
|
||||||
|
|
||||||
|
if not validated_data.get("sales_channel"):
|
||||||
|
validated_data["sales_channel"] = self.context['event'].organizer.sales_channels.get(identifier="web")
|
||||||
|
|
||||||
|
if validated_data.get("testmode") and not validated_data["sales_channel"].type_instance.testmode_supported:
|
||||||
|
raise ValidationError({"testmode": ["This sales channel does not provide support for test mode."]})
|
||||||
|
|
||||||
self._send_mail = validated_data.pop('send_email', False)
|
self._send_mail = validated_data.pop('send_email', False)
|
||||||
if self._send_mail is None:
|
if self._send_mail is None:
|
||||||
self._send_mail = validated_data.get('sales_channel') in self.context['event'].settings.mail_sales_channel_placed_paid
|
self._send_mail = validated_data["sales_channel"].identifier in self.context['event'].settings.mail_sales_channel_placed_paid
|
||||||
|
|
||||||
if 'invoice_address' in validated_data:
|
if 'invoice_address' in validated_data:
|
||||||
iadata = validated_data.pop('invoice_address')
|
iadata = validated_data.pop('invoice_address')
|
||||||
@@ -1309,7 +1340,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
errs[i]['seat'] = ['The specified seat does not exist.']
|
errs[i]['seat'] = ['The specified seat does not exist.']
|
||||||
else:
|
else:
|
||||||
seat_usage[seat] += 1
|
seat_usage[seat] += 1
|
||||||
if (seat_usage[seat] > 0 and not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web'))) or seat_usage[seat] > 1:
|
sales_channel_id = validated_data['sales_channel'].identifier
|
||||||
|
if (seat_usage[seat] > 0 and not seat.is_available(sales_channel=sales_channel_id)) or seat_usage[seat] > 1:
|
||||||
errs[i]['seat'] = [gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
|
errs[i]['seat'] = [gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
|
||||||
elif seated:
|
elif seated:
|
||||||
errs[i]['seat'] = ['The specified product requires to choose a seat.']
|
errs[i]['seat'] = ['The specified product requires to choose a seat.']
|
||||||
@@ -1368,6 +1400,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
if validated_data.get('locale', None) is None:
|
if validated_data.get('locale', None) is None:
|
||||||
validated_data['locale'] = self.context['event'].settings.locale
|
validated_data['locale'] = self.context['event'].settings.locale
|
||||||
|
|
||||||
order = Order(event=self.context['event'], **validated_data)
|
order = Order(event=self.context['event'], **validated_data)
|
||||||
if not validated_data.get('expires'):
|
if not validated_data.get('expires'):
|
||||||
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
|
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
|
||||||
@@ -1484,6 +1517,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
pos.answers = answers
|
pos.answers = answers
|
||||||
pos.pseudonymization_id = "PREVIEW"
|
pos.pseudonymization_id = "PREVIEW"
|
||||||
pos.checkins = []
|
pos.checkins = []
|
||||||
|
pos.print_logs = []
|
||||||
pos_map[pos.positionid] = pos
|
pos_map[pos.positionid] = pos
|
||||||
else:
|
else:
|
||||||
if pos.voucher:
|
if pos.voucher:
|
||||||
@@ -1644,7 +1678,7 @@ class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = InvoiceLine
|
model = InvoiceLine
|
||||||
fields = ('position', 'description', 'item', 'variation', 'subevent', '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',
|
'event_date_to', 'gross_value', 'tax_value', 'tax_rate', 'tax_code', 'tax_name', 'fee_type',
|
||||||
'fee_internal_type', 'event_location')
|
'fee_internal_type', 'event_location')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
|
from pretix.api.auth.devicesecurity import get_all_security_profiles
|
||||||
from pretix.api.serializers import AsymmetricField
|
from pretix.api.serializers import AsymmetricField
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.api.serializers.order import CompatibleJSONField
|
from pretix.api.serializers.order import CompatibleJSONField
|
||||||
@@ -38,7 +39,7 @@ from pretix.base.i18n import get_language_without_region
|
|||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Customer, Device, GiftCard, GiftCardAcceptance, GiftCardTransaction,
|
Customer, Device, GiftCard, GiftCardAcceptance, GiftCardTransaction,
|
||||||
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium,
|
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium,
|
||||||
SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
||||||
)
|
)
|
||||||
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
||||||
from pretix.base.services.mail import SendMailException, mail
|
from pretix.base.services.mail import SendMailException, mail
|
||||||
@@ -165,6 +166,36 @@ class FlexibleTicketRelatedField(serializers.PrimaryKeyRelatedField):
|
|||||||
self.fail('incorrect_type', data_type=type(data).__name__)
|
self.fail('incorrect_type', data_type=type(data).__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SalesChannelSerializer(I18nAwareModelSerializer):
|
||||||
|
type = serializers.CharField(default="api")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SalesChannel
|
||||||
|
fields = ('identifier', 'type', 'label', 'position')
|
||||||
|
|
||||||
|
def validate_type(self, value):
|
||||||
|
if (not self.instance or not self.instance.pk) and value != "api":
|
||||||
|
raise ValidationError(
|
||||||
|
"You can currently only create channels of type 'api' through the API."
|
||||||
|
)
|
||||||
|
if value and self.instance and self.instance.pk and self.instance.type != value:
|
||||||
|
raise ValidationError(
|
||||||
|
"You cannot change the type of a sales channel."
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def validate_identifier(self, value):
|
||||||
|
if (not self.instance or not self.instance.pk) and not value.startswith("api."):
|
||||||
|
raise ValidationError(
|
||||||
|
"Your identifier needs to start with 'api.'."
|
||||||
|
)
|
||||||
|
if value and self.instance and self.instance.pk and self.instance.identifier != value:
|
||||||
|
raise ValidationError(
|
||||||
|
"You cannot change the identifier of a sales channel."
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
class GiftCardSerializer(I18nAwareModelSerializer):
|
class GiftCardSerializer(I18nAwareModelSerializer):
|
||||||
value = serializers.DecimalField(max_digits=13, decimal_places=2, min_value=Decimal('0.00'))
|
value = serializers.DecimalField(max_digits=13, decimal_places=2, min_value=Decimal('0.00'))
|
||||||
owner_ticket = FlexibleTicketRelatedField(required=False, allow_null=True, queryset=OrderPosition.all.none())
|
owner_ticket = FlexibleTicketRelatedField(required=False, allow_null=True, queryset=OrderPosition.all.none())
|
||||||
@@ -267,6 +298,7 @@ class DeviceSerializer(serializers.ModelSerializer):
|
|||||||
revoked = serializers.BooleanField(read_only=True)
|
revoked = serializers.BooleanField(read_only=True)
|
||||||
initialized = serializers.DateTimeField(read_only=True)
|
initialized = serializers.DateTimeField(read_only=True)
|
||||||
initialization_token = serializers.DateTimeField(read_only=True)
|
initialization_token = serializers.DateTimeField(read_only=True)
|
||||||
|
security_profile = serializers.ChoiceField(choices=[], required=False, default="full")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
@@ -276,6 +308,10 @@ class DeviceSerializer(serializers.ModelSerializer):
|
|||||||
'os_name', 'os_version', 'software_brand', 'software_version', 'security_profile'
|
'os_name', 'os_version', 'software_brand', 'software_version', 'security_profile'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['security_profile'].choices = [(k, v.verbose_name) for k, v in get_all_security_profiles().items()]
|
||||||
|
|
||||||
|
|
||||||
class TeamInviteSerializer(serializers.ModelSerializer):
|
class TeamInviteSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -32,10 +32,17 @@ from pretix.helpers.periodic import minimum_interval
|
|||||||
register_webhook_events = Signal()
|
register_webhook_events = Signal()
|
||||||
"""
|
"""
|
||||||
This signal is sent out to get all known webhook events. Receivers should return an
|
This signal is sent out to get all known webhook events. Receivers should return an
|
||||||
instance of a subclass of pretix.api.webhooks.WebhookEvent or a list of such
|
instance of a subclass of ``pretix.api.webhooks.WebhookEvent`` or a list of such
|
||||||
instances.
|
instances.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
register_device_security_profile = Signal()
|
||||||
|
"""
|
||||||
|
This signal is sent out to get all known device security_profiles. Receivers should
|
||||||
|
return an instance of a subclass of ``pretix.api.auth.devicesecurity.BaseSecurityProfile``
|
||||||
|
or a list of such instances.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
@receiver(periodic_task)
|
@receiver(periodic_task)
|
||||||
@scopes_disabled()
|
@scopes_disabled()
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ orga_router.register(r'webhooks', webhooks.WebHookViewSet)
|
|||||||
orga_router.register(r'seatingplans', organizer.SeatingPlanViewSet)
|
orga_router.register(r'seatingplans', organizer.SeatingPlanViewSet)
|
||||||
orga_router.register(r'giftcards', organizer.GiftCardViewSet)
|
orga_router.register(r'giftcards', organizer.GiftCardViewSet)
|
||||||
orga_router.register(r'customers', organizer.CustomerViewSet)
|
orga_router.register(r'customers', organizer.CustomerViewSet)
|
||||||
|
orga_router.register(r'saleschannels', organizer.SalesChannelViewSet)
|
||||||
orga_router.register(r'memberships', organizer.MembershipViewSet)
|
orga_router.register(r'memberships', organizer.MembershipViewSet)
|
||||||
orga_router.register(r'membershiptypes', organizer.MembershipTypeViewSet)
|
orga_router.register(r'membershiptypes', organizer.MembershipTypeViewSet)
|
||||||
orga_router.register(r'reusablemedia', media.ReusableMediaViewSet)
|
orga_router.register(r'reusablemedia', media.ReusableMediaViewSet)
|
||||||
@@ -86,6 +87,7 @@ event_router.register(r'invoices', order.InvoiceViewSet)
|
|||||||
event_router.register(r'revokedsecrets', order.RevokedSecretViewSet, basename='revokedsecrets')
|
event_router.register(r'revokedsecrets', order.RevokedSecretViewSet, basename='revokedsecrets')
|
||||||
event_router.register(r'blockedsecrets', order.BlockedSecretViewSet, basename='blockedsecrets')
|
event_router.register(r'blockedsecrets', order.BlockedSecretViewSet, basename='blockedsecrets')
|
||||||
event_router.register(r'taxrules', event.TaxRuleViewSet)
|
event_router.register(r'taxrules', event.TaxRuleViewSet)
|
||||||
|
event_router.register(r'seats', event.SeatViewSet)
|
||||||
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
|
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
|
||||||
event_router.register(r'checkinlists', checkin.CheckinListViewSet)
|
event_router.register(r'checkinlists', checkin.CheckinListViewSet)
|
||||||
event_router.register(r'cartpositions', cart.CartPositionViewSet)
|
event_router.register(r'cartpositions', cart.CartPositionViewSet)
|
||||||
@@ -94,6 +96,9 @@ event_router.register(r'exporters', exporters.EventExportersViewSet, basename='e
|
|||||||
event_router.register(r'shredders', shredders.EventShreddersViewSet, basename='shredders')
|
event_router.register(r'shredders', shredders.EventShreddersViewSet, basename='shredders')
|
||||||
event_router.register(r'item_meta_properties', event.ItemMetaPropertiesViewSet)
|
event_router.register(r'item_meta_properties', event.ItemMetaPropertiesViewSet)
|
||||||
|
|
||||||
|
subevent_router = routers.DefaultRouter()
|
||||||
|
subevent_router.register(r'seats', event.SeatViewSet)
|
||||||
|
|
||||||
checkinlist_router = routers.DefaultRouter()
|
checkinlist_router = routers.DefaultRouter()
|
||||||
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet, basename='checkinlistpos')
|
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet, basename='checkinlistpos')
|
||||||
|
|
||||||
@@ -131,6 +136,7 @@ urlpatterns = [
|
|||||||
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/settings/$', event.EventSettingsView.as_view(),
|
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/settings/$', event.EventSettingsView.as_view(),
|
||||||
name="event.settings"),
|
name="event.settings"),
|
||||||
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/', include(event_router.urls)),
|
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/', include(event_router.urls)),
|
||||||
|
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/subevents/(?P<subevent>\d+)/', include(subevent_router.urls)),
|
||||||
re_path(r'^organizers/(?P<organizer>[^/]+)/teams/(?P<team>[^/]+)/', include(team_router.urls)),
|
re_path(r'^organizers/(?P<organizer>[^/]+)/teams/(?P<team>[^/]+)/', include(team_router.urls)),
|
||||||
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/items/(?P<item>[^/]+)/', include(item_router.urls)),
|
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/items/(?P<item>[^/]+)/', include(item_router.urls)),
|
||||||
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/questions/(?P<question>[^/]+)/',
|
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/questions/(?P<question>[^/]+)/',
|
||||||
|
|||||||
@@ -211,8 +211,12 @@ class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
|
|||||||
|
|
||||||
if validated_data.get('seat'):
|
if validated_data.get('seat'):
|
||||||
# Assumption: Add-ons currently can't have seats, thus we only need to check the main product
|
# Assumption: Add-ons currently can't have seats, thus we only need to check the main product
|
||||||
|
if validated_data.get('sales_channel'):
|
||||||
|
sales_channel_id = validated_data.get('sales_channel').identifier
|
||||||
|
else:
|
||||||
|
sales_channel_id = "web"
|
||||||
if not validated_data['seat'].is_available(
|
if not validated_data['seat'].is_available(
|
||||||
sales_channel=validated_data.get('sales_channel', 'web'),
|
sales_channel=sales_channel_id,
|
||||||
distance_ignore_cart_id=validated_data['cart_id'],
|
distance_ignore_cart_id=validated_data['cart_id'],
|
||||||
ignore_voucher_id=validated_data['voucher'].pk if validated_data.get('voucher') else None,
|
ignore_voucher_id=validated_data['voucher'].pk if validated_data.get('voucher') else None,
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ from pretix.base.models import (
|
|||||||
CachedFile, Checkin, CheckinList, Device, Event, Order, OrderPosition,
|
CachedFile, Checkin, CheckinList, Device, Event, Order, OrderPosition,
|
||||||
Question, ReusableMedium, RevokedTicketSecret, TeamAPIToken,
|
Question, ReusableMedium, RevokedTicketSecret, TeamAPIToken,
|
||||||
)
|
)
|
||||||
|
from pretix.base.models.orders import PrintLog
|
||||||
from pretix.base.services.checkin import (
|
from pretix.base.services.checkin import (
|
||||||
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
|
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
|
||||||
)
|
)
|
||||||
@@ -115,7 +116,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
|||||||
if 'subevent' in self.request.query_params.getlist('expand'):
|
if 'subevent' in self.request.query_params.getlist('expand'):
|
||||||
qs = qs.prefetch_related(
|
qs = qs.prefetch_related(
|
||||||
'subevent', 'subevent__event', 'subevent__subeventitem_set', 'subevent__subeventitemvariation_set',
|
'subevent', 'subevent__event', 'subevent__subeventitem_set', 'subevent__subeventitemvariation_set',
|
||||||
'subevent__seat_category_mappings', 'subevent__meta_values'
|
'subevent__seat_category_mappings', 'subevent__meta_values',
|
||||||
)
|
)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
@@ -142,7 +143,9 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
|||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
|
instance.checkins.all().delete()
|
||||||
instance.log_action(
|
instance.log_action(
|
||||||
'pretix.event.checkinlist.deleted',
|
'pretix.event.checkinlist.deleted',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
@@ -365,8 +368,9 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
|||||||
qs = qs.prefetch_related(
|
qs = qs.prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
lookup='checkins',
|
lookup='checkins',
|
||||||
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists])
|
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device')
|
||||||
),
|
),
|
||||||
|
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||||
'answers', 'answers__options', 'answers__question',
|
'answers', 'answers__options', 'answers__question',
|
||||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
|
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
|
||||||
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
|
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
|
||||||
@@ -377,7 +381,8 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
|||||||
Prefetch(
|
Prefetch(
|
||||||
'positions',
|
'positions',
|
||||||
OrderPosition.objects.prefetch_related(
|
OrderPosition.objects.prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.all()),
|
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||||
|
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||||
'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -389,8 +394,9 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
|||||||
qs = qs.prefetch_related(
|
qs = qs.prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
lookup='checkins',
|
lookup='checkins',
|
||||||
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists])
|
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device')
|
||||||
),
|
),
|
||||||
|
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||||
'answers', 'answers__options', 'answers__question',
|
'answers', 'answers__options', 'answers__question',
|
||||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
|
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
|
||||||
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat')
|
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat')
|
||||||
@@ -406,7 +412,7 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
|||||||
'item__variations').select_related('item__tax_rule')
|
'item__variations').select_related('item__tax_rule')
|
||||||
|
|
||||||
if expand and 'variation' in expand:
|
if expand and 'variation' in expand:
|
||||||
qs = qs.prefetch_related('variation')
|
qs = qs.prefetch_related('variation', 'variation__meta_values')
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import base64
|
import base64
|
||||||
|
import copy
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from cryptography.hazmat.backends.openssl.backend import Backend
|
from cryptography.hazmat.backends.openssl.backend import Backend
|
||||||
@@ -146,6 +147,8 @@ class InitializeView(APIView):
|
|||||||
permission_classes = ()
|
permission_classes = ()
|
||||||
|
|
||||||
def post(self, request, format=None):
|
def post(self, request, format=None):
|
||||||
|
from pretix.base.signals import device_info_updated
|
||||||
|
|
||||||
serializer = InitializationRequestSerializer(data=request.data)
|
serializer = InitializationRequestSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
@@ -160,6 +163,8 @@ class InitializeView(APIView):
|
|||||||
if device.revoked:
|
if device.revoked:
|
||||||
raise ValidationError({'token': ['This initialization token has been revoked.']})
|
raise ValidationError({'token': ['This initialization token has been revoked.']})
|
||||||
|
|
||||||
|
old_instance = copy.copy(device)
|
||||||
|
|
||||||
device.initialized = now()
|
device.initialized = now()
|
||||||
device.hardware_brand = serializer.validated_data.get('hardware_brand')
|
device.hardware_brand = serializer.validated_data.get('hardware_brand')
|
||||||
device.hardware_model = serializer.validated_data.get('hardware_model')
|
device.hardware_model = serializer.validated_data.get('hardware_model')
|
||||||
@@ -174,6 +179,10 @@ class InitializeView(APIView):
|
|||||||
|
|
||||||
device.log_action('pretix.device.initialized', data=serializer.validated_data, auth=device)
|
device.log_action('pretix.device.initialized', data=serializer.validated_data, auth=device)
|
||||||
|
|
||||||
|
device_info_updated.send(
|
||||||
|
sender=Device, old_device=old_instance, new_device=device
|
||||||
|
)
|
||||||
|
|
||||||
serializer = DeviceSerializer(device)
|
serializer = DeviceSerializer(device)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
@@ -182,9 +191,12 @@ class UpdateView(APIView):
|
|||||||
authentication_classes = (DeviceTokenAuthentication,)
|
authentication_classes = (DeviceTokenAuthentication,)
|
||||||
|
|
||||||
def post(self, request, format=None):
|
def post(self, request, format=None):
|
||||||
|
from pretix.base.signals import device_info_updated
|
||||||
|
|
||||||
serializer = UpdateRequestSerializer(data=request.data)
|
serializer = UpdateRequestSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
device = request.auth
|
device = request.auth
|
||||||
|
old_instance = copy.copy(device)
|
||||||
device.hardware_brand = serializer.validated_data.get('hardware_brand')
|
device.hardware_brand = serializer.validated_data.get('hardware_brand')
|
||||||
device.hardware_model = serializer.validated_data.get('hardware_model')
|
device.hardware_model = serializer.validated_data.get('hardware_model')
|
||||||
device.os_name = serializer.validated_data.get('os_name')
|
device.os_name = serializer.validated_data.get('os_name')
|
||||||
@@ -200,6 +212,10 @@ class UpdateView(APIView):
|
|||||||
device.save()
|
device.save()
|
||||||
device.log_action('pretix.device.updated', data=serializer.validated_data, auth=device)
|
device.log_action('pretix.device.updated', data=serializer.validated_data, auth=device)
|
||||||
|
|
||||||
|
device_info_updated.send(
|
||||||
|
sender=Device, old_device=old_instance, new_device=device
|
||||||
|
)
|
||||||
|
|
||||||
serializer = DeviceSerializer(device)
|
serializer = DeviceSerializer(device)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,9 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
write_permission = 'can_change_items'
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.discounts.all()
|
return self.request.event.discounts.prefetch_related(
|
||||||
|
'limit_sales_channels',
|
||||||
|
)
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
serializer.save(event=self.request.event)
|
serializer.save(event=self.request.event)
|
||||||
|
|||||||
@@ -40,19 +40,24 @@ from django.utils.timezone import now
|
|||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from rest_framework import serializers, views, viewsets
|
from rest_framework import serializers, views, viewsets
|
||||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.exceptions import (
|
||||||
|
NotFound, PermissionDenied, ValidationError,
|
||||||
|
)
|
||||||
|
from rest_framework.generics import get_object_or_404
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from pretix.api.auth.permission import EventCRUDPermission
|
from pretix.api.auth.permission import EventCRUDPermission
|
||||||
from pretix.api.pagination import TotalOrderingFilter
|
from pretix.api.pagination import TotalOrderingFilter
|
||||||
from pretix.api.serializers.event import (
|
from pretix.api.serializers.event import (
|
||||||
CloneEventSerializer, DeviceEventSettingsSerializer, EventSerializer,
|
CloneEventSerializer, DeviceEventSettingsSerializer, EventSerializer,
|
||||||
EventSettingsSerializer, ItemMetaPropertiesSerializer, SubEventSerializer,
|
EventSettingsSerializer, ItemMetaPropertiesSerializer,
|
||||||
|
SeatBulkBlockInputSerializer, SeatSerializer, SubEventSerializer,
|
||||||
TaxRuleSerializer,
|
TaxRuleSerializer,
|
||||||
)
|
)
|
||||||
from pretix.api.views import ConditionalListView
|
from pretix.api.views import ConditionalListView
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CartPosition, Device, Event, ItemMetaProperty, SeatCategoryMapping,
|
CartPosition, Device, Event, ItemMetaProperty, Seat, SeatCategoryMapping,
|
||||||
TaxRule, TeamAPIToken,
|
TaxRule, TeamAPIToken,
|
||||||
)
|
)
|
||||||
from pretix.base.models.event import SubEvent
|
from pretix.base.models.event import SubEvent
|
||||||
@@ -113,7 +118,10 @@ with scopes_disabled():
|
|||||||
return queryset.exclude(expr)
|
return queryset.exclude(expr)
|
||||||
|
|
||||||
def sales_channel_qs(self, queryset, name, value):
|
def sales_channel_qs(self, queryset, name, value):
|
||||||
return queryset.filter(sales_channels__contains=value)
|
return queryset.filter(
|
||||||
|
Q(all_sales_channels=True) |
|
||||||
|
Q(limit_sales_channels__identifier=value)
|
||||||
|
)
|
||||||
|
|
||||||
def search_qs(self, queryset, name, value):
|
def search_qs(self, queryset, name, value):
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
@@ -135,6 +143,12 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
ordering_fields = ('date_from', 'slug')
|
ordering_fields = ('date_from', 'slug')
|
||||||
filterset_class = EventFilter
|
filterset_class = EventFilter
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
return {
|
||||||
|
**super().get_serializer_context(),
|
||||||
|
"organizer": self.request.organizer,
|
||||||
|
}
|
||||||
|
|
||||||
def get_copy_from_queryset(self):
|
def get_copy_from_queryset(self):
|
||||||
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||||
return self.request.auth.get_events_with_any_permission()
|
return self.request.auth.get_events_with_any_permission()
|
||||||
@@ -153,13 +167,20 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
qs = filter_qs_by_attr(qs, self.request)
|
qs = filter_qs_by_attr(qs, self.request)
|
||||||
|
|
||||||
if 'with_availability_for' in self.request.GET:
|
if 'with_availability_for' in self.request.GET:
|
||||||
qs = Event.annotated(qs, channel=self.request.GET.get('with_availability_for'))
|
qs = Event.annotated(
|
||||||
|
qs,
|
||||||
|
channel=get_object_or_404(
|
||||||
|
self.request.organizer.sales_channels,
|
||||||
|
identifier=self.request.GET.get('with_availability_for')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return qs.prefetch_related(
|
return qs.prefetch_related(
|
||||||
'organizer',
|
'organizer',
|
||||||
'meta_values',
|
'meta_values',
|
||||||
'meta_values__property',
|
'meta_values__property',
|
||||||
'item_meta_properties',
|
'item_meta_properties',
|
||||||
|
'limit_sales_channels',
|
||||||
Prefetch(
|
Prefetch(
|
||||||
'seat_category_mappings',
|
'seat_category_mappings',
|
||||||
to_attr='_seat_category_mappings',
|
to_attr='_seat_category_mappings',
|
||||||
@@ -218,9 +239,9 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
disabled = {m: 'disabled' for m in current_plugins_value if m not in updated_plugins_value}
|
disabled = {m: 'disabled' for m in current_plugins_value if m not in updated_plugins_value}
|
||||||
changed = merge_dicts(enabled, disabled)
|
changed = merge_dicts(enabled, disabled)
|
||||||
|
|
||||||
for module, action in changed.items():
|
for module, operation in changed.items():
|
||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.plugins.' + action,
|
'pretix.event.plugins.' + operation,
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
auth=self.request.auth,
|
auth=self.request.auth,
|
||||||
data={'plugin': module}
|
data={'plugin': module}
|
||||||
@@ -268,8 +289,6 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
new_event.is_public = serializer.validated_data['is_public']
|
new_event.is_public = serializer.validated_data['is_public']
|
||||||
if 'testmode' in serializer.validated_data:
|
if 'testmode' in serializer.validated_data:
|
||||||
new_event.testmode = serializer.validated_data['testmode']
|
new_event.testmode = serializer.validated_data['testmode']
|
||||||
if 'sales_channels' in serializer.validated_data:
|
|
||||||
new_event.sales_channels = serializer.validated_data['sales_channels']
|
|
||||||
if 'has_subevents' in serializer.validated_data:
|
if 'has_subevents' in serializer.validated_data:
|
||||||
new_event.has_subevents = serializer.validated_data['has_subevents']
|
new_event.has_subevents = serializer.validated_data['has_subevents']
|
||||||
if 'date_admission' in serializer.validated_data:
|
if 'date_admission' in serializer.validated_data:
|
||||||
@@ -277,6 +296,11 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
new_event.save()
|
new_event.save()
|
||||||
if 'timezone' in serializer.validated_data:
|
if 'timezone' in serializer.validated_data:
|
||||||
new_event.settings.timezone = serializer.validated_data['timezone']
|
new_event.settings.timezone = serializer.validated_data['timezone']
|
||||||
|
|
||||||
|
if 'all_sales_channels' in serializer.validated_data and 'sales_channels' in serializer.validated_data:
|
||||||
|
new_event.all_sales_channels = serializer.validated_data['all_sales_channels']
|
||||||
|
if not new_event.all_sales_channels:
|
||||||
|
new_event.limit_sales_channels.set(serializer.validated_data['limit_sales_channels'])
|
||||||
else:
|
else:
|
||||||
serializer.instance.set_defaults()
|
serializer.instance.set_defaults()
|
||||||
|
|
||||||
@@ -349,7 +373,7 @@ with scopes_disabled():
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SubEvent
|
model = SubEvent
|
||||||
fields = ['active', 'event__live']
|
fields = ['is_public', 'active', 'event__live']
|
||||||
|
|
||||||
def ends_after_qs(self, queryset, name, value):
|
def ends_after_qs(self, queryset, name, value):
|
||||||
expr = Q(
|
expr = Q(
|
||||||
@@ -379,7 +403,10 @@ with scopes_disabled():
|
|||||||
return queryset.exclude(expr)
|
return queryset.exclude(expr)
|
||||||
|
|
||||||
def sales_channel_qs(self, queryset, name, value):
|
def sales_channel_qs(self, queryset, name, value):
|
||||||
return queryset.filter(event__sales_channels__contains=value)
|
return queryset.filter(
|
||||||
|
Q(event__all_sales_channels=True) |
|
||||||
|
Q(event__limit_sales_channels__identifier=value)
|
||||||
|
)
|
||||||
|
|
||||||
def search_qs(self, queryset, name, value):
|
def search_qs(self, queryset, name, value):
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
@@ -421,13 +448,19 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
elif self.request.user.is_authenticated:
|
elif self.request.user.is_authenticated:
|
||||||
qs = SubEvent.objects.filter(
|
qs = SubEvent.objects.filter(
|
||||||
event__organizer=self.request.organizer,
|
event__organizer=self.request.organizer,
|
||||||
event__in=self.request.user.get_events_with_any_permission()
|
event__in=self.request.user.get_events_with_any_permission(request=self.request)
|
||||||
)
|
)
|
||||||
|
|
||||||
qs = filter_qs_by_attr(qs, self.request)
|
qs = filter_qs_by_attr(qs, self.request)
|
||||||
|
|
||||||
if 'with_availability_for' in self.request.GET:
|
if 'with_availability_for' in self.request.GET:
|
||||||
qs = SubEvent.annotated(qs, channel=self.request.GET.get('with_availability_for'))
|
qs = SubEvent.annotated(
|
||||||
|
qs,
|
||||||
|
channel=get_object_or_404(
|
||||||
|
self.request.organizer.sales_channels,
|
||||||
|
identifier=self.request.GET.get('with_availability_for')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return qs.prefetch_related(
|
return qs.prefetch_related(
|
||||||
'event',
|
'event',
|
||||||
@@ -639,3 +672,98 @@ class EventSettingsView(views.APIView):
|
|||||||
'request': request
|
'request': request
|
||||||
})
|
})
|
||||||
return Response(s.data)
|
return Response(s.data)
|
||||||
|
|
||||||
|
|
||||||
|
class SeatFilter(FilterSet):
|
||||||
|
is_available = django_filters.BooleanFilter(method="is_available_qs")
|
||||||
|
|
||||||
|
def is_available_qs(self, queryset, name, value):
|
||||||
|
expr = (
|
||||||
|
Q(orderposition_id__isnull=True, cartposition_id__isnull=True, voucher_id__isnull=True)
|
||||||
|
)
|
||||||
|
if self.request.event.settings.seating_minimal_distance:
|
||||||
|
expr = expr & Q(has_closeby_taken=False)
|
||||||
|
if value:
|
||||||
|
return queryset.filter(expr)
|
||||||
|
else:
|
||||||
|
return queryset.exclude(expr)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Seat
|
||||||
|
fields = ('zone_name', 'row_name', 'row_label', 'seat_number', 'seat_label', 'seat_guid', 'blocked',)
|
||||||
|
|
||||||
|
|
||||||
|
class SeatViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||||
|
serializer_class = SeatSerializer
|
||||||
|
queryset = Seat.objects.none()
|
||||||
|
write_permission = 'can_change_event_settings'
|
||||||
|
filter_backends = (DjangoFilterBackend, )
|
||||||
|
filterset_class = SeatFilter
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
if self.request.event.has_subevents and 'subevent' in self.request.resolver_match.kwargs:
|
||||||
|
try:
|
||||||
|
subevent = self.request.event.subevents.get(pk=self.request.resolver_match.kwargs['subevent'])
|
||||||
|
except SubEvent.DoesNotExist:
|
||||||
|
raise NotFound('Subevent not found')
|
||||||
|
qs = Seat.annotated(
|
||||||
|
event_id=self.request.event.id,
|
||||||
|
subevent=subevent,
|
||||||
|
qs=subevent.seats.all(),
|
||||||
|
annotate_ids=True,
|
||||||
|
minimal_distance=self.request.event.settings.seating_minimal_distance,
|
||||||
|
distance_only_within_row=self.request.event.settings.seating_distance_only_within_row,
|
||||||
|
)
|
||||||
|
elif not self.request.event.has_subevents and 'subevent' not in self.request.resolver_match.kwargs:
|
||||||
|
qs = Seat.annotated(
|
||||||
|
event_id=self.request.event.id,
|
||||||
|
subevent=None,
|
||||||
|
qs=self.request.event.seats.all(),
|
||||||
|
annotate_ids=True,
|
||||||
|
minimal_distance=self.request.event.settings.seating_minimal_distance,
|
||||||
|
distance_only_within_row=self.request.event.settings.seating_distance_only_within_row,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise NotFound('Please use the subevent-specific endpoint' if self.request.event.has_subevents
|
||||||
|
else 'This event has no subevents')
|
||||||
|
|
||||||
|
return qs
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
ctx = super().get_serializer_context()
|
||||||
|
ctx['expand_fields'] = self.request.query_params.getlist('expand')
|
||||||
|
ctx['order_context'] = {
|
||||||
|
'event': self.request.event,
|
||||||
|
'pdf_data': None,
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def perform_update(self, serializer):
|
||||||
|
super().perform_update(serializer)
|
||||||
|
serializer.instance.event.log_action(
|
||||||
|
"pretix.event.seats.blocks.changed",
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data={"seats": [serializer.instance.pk]},
|
||||||
|
)
|
||||||
|
|
||||||
|
def bulk_change_blocked(self, blocked):
|
||||||
|
s = SeatBulkBlockInputSerializer(
|
||||||
|
data=self.request.data,
|
||||||
|
context={"event": self.request.event, "queryset": self.get_queryset()},
|
||||||
|
)
|
||||||
|
s.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
seats = s.validated_data["seats"]
|
||||||
|
for seat in seats:
|
||||||
|
seat.blocked = blocked
|
||||||
|
Seat.objects.bulk_update(seats, ["blocked"], batch_size=1000)
|
||||||
|
return Response({})
|
||||||
|
|
||||||
|
@action(methods=["POST"], detail=False)
|
||||||
|
def bulk_block(self, request, *args, **kwargs):
|
||||||
|
return self.bulk_change_blocked(True)
|
||||||
|
|
||||||
|
@action(methods=["POST"], detail=False)
|
||||||
|
def bulk_unblock(self, request, *args, **kwargs):
|
||||||
|
return self.bulk_change_blocked(False)
|
||||||
|
|||||||
@@ -56,10 +56,17 @@ from pretix.base.models import (
|
|||||||
)
|
)
|
||||||
from pretix.base.services.quotas import QuotaAvailability
|
from pretix.base.services.quotas import QuotaAvailability
|
||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
|
from pretix.helpers.i18n import i18ncomp
|
||||||
|
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
class ItemFilter(FilterSet):
|
class ItemFilter(FilterSet):
|
||||||
tax_rate = django_filters.CharFilter(method='tax_rate_qs')
|
tax_rate = django_filters.CharFilter(method='tax_rate_qs')
|
||||||
|
search = django_filters.CharFilter(method='search_qs')
|
||||||
|
|
||||||
|
def search_qs(self, queryset, name, value):
|
||||||
|
return queryset.filter(
|
||||||
|
Q(internal_name__icontains=value) | Q(name__icontains=i18ncomp(value))
|
||||||
|
)
|
||||||
|
|
||||||
def tax_rate_qs(self, queryset, name, value):
|
def tax_rate_qs(self, queryset, name, value):
|
||||||
if value in ("0", "None", "0.00"):
|
if value in ("0", "None", "0.00"):
|
||||||
@@ -71,6 +78,18 @@ with scopes_disabled():
|
|||||||
model = Item
|
model = Item
|
||||||
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
|
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
|
||||||
|
|
||||||
|
class ItemVariationFilter(FilterSet):
|
||||||
|
search = django_filters.CharFilter(method='search_qs')
|
||||||
|
|
||||||
|
def search_qs(self, queryset, name, value):
|
||||||
|
return queryset.filter(
|
||||||
|
Q(value__icontains=i18ncomp(value))
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ItemVariation
|
||||||
|
fields = ['active']
|
||||||
|
|
||||||
|
|
||||||
class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||||
serializer_class = ItemSerializer
|
serializer_class = ItemSerializer
|
||||||
@@ -87,6 +106,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
'variations', 'addons', 'bundles', 'meta_values', 'meta_values__property',
|
'variations', 'addons', 'bundles', 'meta_values', 'meta_values__property',
|
||||||
'variations__meta_values', 'variations__meta_values__property',
|
'variations__meta_values', 'variations__meta_values__property',
|
||||||
'require_membership_types', 'variations__require_membership_types',
|
'require_membership_types', 'variations__require_membership_types',
|
||||||
|
'limit_sales_channels', 'variations__limit_sales_channels',
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
@@ -139,6 +159,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
|||||||
serializer_class = ItemVariationSerializer
|
serializer_class = ItemVariationSerializer
|
||||||
queryset = ItemVariation.objects.none()
|
queryset = ItemVariation.objects.none()
|
||||||
filter_backends = (DjangoFilterBackend, TotalOrderingFilter,)
|
filter_backends = (DjangoFilterBackend, TotalOrderingFilter,)
|
||||||
|
filterset_class = ItemVariationFilter
|
||||||
ordering_fields = ('id', 'position')
|
ordering_fields = ('id', 'position')
|
||||||
ordering = ('id',)
|
ordering = ('id',)
|
||||||
permission = None
|
permission = None
|
||||||
@@ -152,7 +173,8 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
|||||||
return self.item.variations.all().prefetch_related(
|
return self.item.variations.all().prefetch_related(
|
||||||
'meta_values',
|
'meta_values',
|
||||||
'meta_values__property',
|
'meta_values__property',
|
||||||
'require_membership_types'
|
'require_membership_types',
|
||||||
|
'limit_sales_channels',
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ from pretix.base.models import (
|
|||||||
Checkin, GiftCard, GiftCardAcceptance, GiftCardTransaction, OrderPosition,
|
Checkin, GiftCard, GiftCardAcceptance, GiftCardTransaction, OrderPosition,
|
||||||
ReusableMedium,
|
ReusableMedium,
|
||||||
)
|
)
|
||||||
|
from pretix.base.models.orders import PrintLog
|
||||||
from pretix.helpers import OF_SELF
|
from pretix.helpers import OF_SELF
|
||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
|
|
||||||
@@ -78,7 +79,8 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
|
|||||||
queryset=OrderPosition.objects.select_related(
|
queryset=OrderPosition.objects.select_related(
|
||||||
'order', 'order__event', 'order__event__organizer', 'seat',
|
'order', 'order__event', 'order__event__organizer', 'seat',
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.all()),
|
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||||
|
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||||
'answers', 'answers__options', 'answers__question',
|
'answers', 'answers__options', 'answers__question',
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ from django.db.models import (
|
|||||||
from django.db.models.functions import Coalesce, Concat
|
from django.db.models.functions import Coalesce, Concat
|
||||||
from django.http import FileResponse, HttpResponse
|
from django.http import FileResponse, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.utils import formats
|
||||||
from django.utils.timezone import make_aware, now
|
from django.utils.timezone import make_aware, now
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
@@ -49,6 +50,7 @@ from rest_framework.mixins import CreateModelMixin
|
|||||||
from rest_framework.permissions import SAFE_METHODS
|
from rest_framework.permissions import SAFE_METHODS
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from pretix.api.filters import MultipleCharFilter
|
||||||
from pretix.api.models import OAuthAccessToken
|
from pretix.api.models import OAuthAccessToken
|
||||||
from pretix.api.pagination import TotalOrderingFilter
|
from pretix.api.pagination import TotalOrderingFilter
|
||||||
from pretix.api.serializers.order import (
|
from pretix.api.serializers.order import (
|
||||||
@@ -56,7 +58,8 @@ from pretix.api.serializers.order import (
|
|||||||
OrderPaymentCreateSerializer, OrderPaymentSerializer,
|
OrderPaymentCreateSerializer, OrderPaymentSerializer,
|
||||||
OrderPositionSerializer, OrderRefundCreateSerializer,
|
OrderPositionSerializer, OrderRefundCreateSerializer,
|
||||||
OrderRefundSerializer, OrderSerializer, PriceCalcSerializer,
|
OrderRefundSerializer, OrderSerializer, PriceCalcSerializer,
|
||||||
RevokedTicketSecretSerializer, SimulatedOrderSerializer,
|
PrintLogSerializer, RevokedTicketSecretSerializer,
|
||||||
|
SimulatedOrderSerializer,
|
||||||
)
|
)
|
||||||
from pretix.api.serializers.orderchange import (
|
from pretix.api.serializers.orderchange import (
|
||||||
BlockNameSerializer, OrderChangeOperationSerializer,
|
BlockNameSerializer, OrderChangeOperationSerializer,
|
||||||
@@ -65,6 +68,7 @@ from pretix.api.serializers.orderchange import (
|
|||||||
OrderPositionInfoPatchSerializer,
|
OrderPositionInfoPatchSerializer,
|
||||||
)
|
)
|
||||||
from pretix.api.views import RichOrderingFilter
|
from pretix.api.views import RichOrderingFilter
|
||||||
|
from pretix.base.decimal import round_decimal
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CachedCombinedTicket, CachedTicket, Checkin, Device, EventMetaValue,
|
CachedCombinedTicket, CachedTicket, Checkin, Device, EventMetaValue,
|
||||||
@@ -74,7 +78,7 @@ from pretix.base.models import (
|
|||||||
TeamAPIToken, generate_secret,
|
TeamAPIToken, generate_secret,
|
||||||
)
|
)
|
||||||
from pretix.base.models.orders import (
|
from pretix.base.models.orders import (
|
||||||
BlockedTicketSecret, QuestionAnswer, RevokedTicketSecret,
|
BlockedTicketSecret, PrintLog, QuestionAnswer, RevokedTicketSecret,
|
||||||
)
|
)
|
||||||
from pretix.base.payment import PaymentException
|
from pretix.base.payment import PaymentException
|
||||||
from pretix.base.pdf import get_images
|
from pretix.base.pdf import get_images
|
||||||
@@ -95,7 +99,6 @@ from pretix.base.services.tickets import generate
|
|||||||
from pretix.base.signals import (
|
from pretix.base.signals import (
|
||||||
order_modified, order_paid, order_placed, register_ticket_outputs,
|
order_modified, order_paid, order_placed, register_ticket_outputs,
|
||||||
)
|
)
|
||||||
from pretix.base.templatetags.money import money_filter
|
|
||||||
from pretix.control.signals import order_search_filter_q
|
from pretix.control.signals import order_search_filter_q
|
||||||
from pretix.helpers import OF_SELF
|
from pretix.helpers import OF_SELF
|
||||||
|
|
||||||
@@ -108,6 +111,7 @@ with scopes_disabled():
|
|||||||
status = django_filters.CharFilter(field_name='status', lookup_expr='iexact')
|
status = django_filters.CharFilter(field_name='status', lookup_expr='iexact')
|
||||||
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
|
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
|
||||||
created_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte')
|
created_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte')
|
||||||
|
created_before = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='lt')
|
||||||
subevent_after = django_filters.IsoDateTimeFilter(method='subevent_after_qs')
|
subevent_after = django_filters.IsoDateTimeFilter(method='subevent_after_qs')
|
||||||
subevent_before = django_filters.IsoDateTimeFilter(method='subevent_before_qs')
|
subevent_before = django_filters.IsoDateTimeFilter(method='subevent_before_qs')
|
||||||
search = django_filters.CharFilter(method='search_qs')
|
search = django_filters.CharFilter(method='search_qs')
|
||||||
@@ -115,6 +119,8 @@ with scopes_disabled():
|
|||||||
variation = django_filters.CharFilter(field_name='all_positions', lookup_expr='variation_id', distinct=True)
|
variation = django_filters.CharFilter(field_name='all_positions', lookup_expr='variation_id', distinct=True)
|
||||||
subevent = django_filters.CharFilter(field_name='all_positions', lookup_expr='subevent_id', distinct=True)
|
subevent = django_filters.CharFilter(field_name='all_positions', lookup_expr='subevent_id', distinct=True)
|
||||||
customer = django_filters.CharFilter(field_name='customer__identifier')
|
customer = django_filters.CharFilter(field_name='customer__identifier')
|
||||||
|
sales_channel = django_filters.CharFilter(field_name='sales_channel__identifier')
|
||||||
|
payment_provider = django_filters.CharFilter(method='provider_qs')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Order
|
model = Order
|
||||||
@@ -138,6 +144,11 @@ with scopes_disabled():
|
|||||||
)
|
)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
def provider_qs(self, qs, name, value):
|
||||||
|
return qs.filter(Exists(
|
||||||
|
OrderPayment.objects.filter(order=OuterRef('pk'), provider=value)
|
||||||
|
))
|
||||||
|
|
||||||
def subevent_before_qs(self, qs, name, value):
|
def subevent_before_qs(self, qs, name, value):
|
||||||
if getattr(self.request, 'event', None):
|
if getattr(self.request, 'event', None):
|
||||||
subevents = self.request.event.subevents
|
subevents = self.request.event.subevents
|
||||||
@@ -205,7 +216,7 @@ class OrderViewSetMixin:
|
|||||||
queryset = Order.objects.none()
|
queryset = Order.objects.none()
|
||||||
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
||||||
ordering = ('datetime',)
|
ordering = ('datetime',)
|
||||||
ordering_fields = ('datetime', 'code', 'status', 'last_modified')
|
ordering_fields = ('datetime', 'code', 'status', 'last_modified', 'cancellation_date')
|
||||||
filterset_class = OrderFilter
|
filterset_class = OrderFilter
|
||||||
lookup_field = 'code'
|
lookup_field = 'code'
|
||||||
|
|
||||||
@@ -229,7 +240,7 @@ class OrderViewSetMixin:
|
|||||||
if 'customer' not in self.request.GET.getlist('exclude'):
|
if 'customer' not in self.request.GET.getlist('exclude'):
|
||||||
qs = qs.select_related('customer')
|
qs = qs.select_related('customer')
|
||||||
|
|
||||||
qs = qs.prefetch_related(self._positions_prefetch(self.request))
|
qs = qs.select_related('sales_channel').prefetch_related(self._positions_prefetch(self.request))
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def _positions_prefetch(self, request):
|
def _positions_prefetch(self, request):
|
||||||
@@ -249,7 +260,8 @@ class OrderViewSetMixin:
|
|||||||
return Prefetch(
|
return Prefetch(
|
||||||
'positions',
|
'positions',
|
||||||
opq.all().prefetch_related(
|
opq.all().prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.all()),
|
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||||
|
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||||
Prefetch('item', queryset=self.request.event.items.prefetch_related(
|
Prefetch('item', queryset=self.request.event.items.prefetch_related(
|
||||||
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'), to_attr='meta_values_cached')
|
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'), to_attr='meta_values_cached')
|
||||||
)),
|
)),
|
||||||
@@ -270,7 +282,8 @@ class OrderViewSetMixin:
|
|||||||
return Prefetch(
|
return Prefetch(
|
||||||
'positions',
|
'positions',
|
||||||
opq.all().prefetch_related(
|
opq.all().prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.all()),
|
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||||
|
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||||
'item', 'variation',
|
'item', 'variation',
|
||||||
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options', 'question').order_by('question__position')),
|
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options', 'question').order_by('question__position')),
|
||||||
'seat',
|
'seat',
|
||||||
@@ -316,6 +329,11 @@ class OrganizerOrderViewSet(OrderViewSetMixin, viewsets.ReadOnlyModelViewSet):
|
|||||||
else:
|
else:
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
ctx = super().get_serializer_context()
|
||||||
|
ctx['organizer'] = self.request.organizer
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
||||||
permission = 'can_view_orders'
|
permission = 'can_view_orders'
|
||||||
@@ -629,6 +647,8 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
|||||||
order = self.get_object()
|
order = self.get_object()
|
||||||
order.secret = generate_secret()
|
order.secret = generate_secret()
|
||||||
for op in order.all_positions.all():
|
for op in order.all_positions.all():
|
||||||
|
op.web_secret = generate_secret()
|
||||||
|
op.save(update_fields=["web_secret"])
|
||||||
assign_ticket_secret(
|
assign_ticket_secret(
|
||||||
request.event, op, force_invalidate=True, save=True
|
request.event, op, force_invalidate=True, save=True
|
||||||
)
|
)
|
||||||
@@ -1078,7 +1098,8 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
|||||||
'item_meta_properties',
|
'item_meta_properties',
|
||||||
)
|
)
|
||||||
qs = qs.prefetch_related(
|
qs = qs.prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.all()),
|
Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
|
||||||
|
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||||
Prefetch('item', queryset=self.request.event.items.prefetch_related(
|
Prefetch('item', queryset=self.request.event.items.prefetch_related(
|
||||||
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
|
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
|
||||||
to_attr='meta_values_cached')
|
to_attr='meta_values_cached')
|
||||||
@@ -1097,7 +1118,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
|||||||
Prefetch(
|
Prefetch(
|
||||||
'positions',
|
'positions',
|
||||||
qs.prefetch_related(
|
qs.prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.all()),
|
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||||
Prefetch('item', queryset=self.request.event.items.prefetch_related(
|
Prefetch('item', queryset=self.request.event.items.prefetch_related(
|
||||||
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
|
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
|
||||||
to_attr='meta_values_cached')
|
to_attr='meta_values_cached')
|
||||||
@@ -1121,7 +1142,8 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
qs = qs.prefetch_related(
|
qs = qs.prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.all()),
|
Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
|
||||||
|
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||||
'answers', 'answers__options', 'answers__question',
|
'answers', 'answers__options', 'answers__question',
|
||||||
).select_related(
|
).select_related(
|
||||||
'item', 'order', 'order__event', 'order__event__organizer', 'seat'
|
'item', 'order', 'order__event', 'order__event__organizer', 'seat'
|
||||||
@@ -1209,9 +1231,10 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
|||||||
price = get_price(**kwargs)
|
price = get_price(**kwargs)
|
||||||
tr = kwargs.get('tax_rule', kwargs.get('item').tax_rule)
|
tr = kwargs.get('tax_rule', kwargs.get('item').tax_rule)
|
||||||
with language(data.get('locale') or self.request.event.settings.locale, self.request.event.settings.region):
|
with language(data.get('locale') or self.request.event.settings.locale, self.request.event.settings.region):
|
||||||
|
gross_formatted = formats.localize_input(round_decimal(price.gross, self.request.event.currency))
|
||||||
return Response({
|
return Response({
|
||||||
'gross': price.gross,
|
'gross': price.gross,
|
||||||
'gross_formatted': money_filter(price.gross, self.request.event.currency, hide_currency=True),
|
'gross_formatted': gross_formatted,
|
||||||
'net': price.net,
|
'net': price.net,
|
||||||
'rate': price.rate,
|
'rate': price.rate,
|
||||||
'name': str(price.name),
|
'name': str(price.name),
|
||||||
@@ -1240,6 +1263,34 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
@action(detail=True, url_name="printlog", url_path="printlog", methods=["POST"])
|
||||||
|
def printlog(self, request, **kwargs):
|
||||||
|
pos = self.get_object()
|
||||||
|
serializer = PrintLogSerializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
serializer.save(
|
||||||
|
position=pos,
|
||||||
|
device=request.auth if isinstance(request.auth, Device) else None,
|
||||||
|
user=request.user if request.user.is_authenticated else None,
|
||||||
|
api_token=request.auth if isinstance(request.auth, TeamAPIToken) else None,
|
||||||
|
oauth_application=request.auth.application if isinstance(request.auth, OAuthAccessToken) else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
pos.order.log_action(
|
||||||
|
"pretix.event.order.print",
|
||||||
|
data={
|
||||||
|
"position": pos.pk,
|
||||||
|
"positionid": pos.positionid,
|
||||||
|
**serializer.validated_data,
|
||||||
|
},
|
||||||
|
auth=request.auth,
|
||||||
|
user=request.user,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
@action(detail=True, url_name='pdf_image', url_path=r'pdf_image/(?P<key>[^/]+)')
|
@action(detail=True, url_name='pdf_image', url_path=r'pdf_image/(?P<key>[^/]+)')
|
||||||
def pdf_image(self, request, key, **kwargs):
|
def pdf_image(self, request, key, **kwargs):
|
||||||
pos = self.get_object()
|
pos = self.get_object()
|
||||||
@@ -1812,17 +1863,14 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
|||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
class InvoiceFilter(FilterSet):
|
class InvoiceFilter(FilterSet):
|
||||||
refers = django_filters.CharFilter(method='refers_qs')
|
refers = django_filters.CharFilter(method='refers_qs')
|
||||||
number = django_filters.CharFilter(method='nr_qs')
|
number = MultipleCharFilter(field_name='nr', lookup_expr='iexact')
|
||||||
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
|
order = MultipleCharFilter(field_name='order', lookup_expr='code__iexact')
|
||||||
|
|
||||||
def refers_qs(self, queryset, name, value):
|
def refers_qs(self, queryset, name, value):
|
||||||
return queryset.annotate(
|
return queryset.annotate(
|
||||||
refers_nr=Concat('refers__prefix', 'refers__invoice_no')
|
refers_nr=Concat('refers__prefix', 'refers__invoice_no')
|
||||||
).filter(refers_nr__iexact=value)
|
).filter(refers_nr__iexact=value)
|
||||||
|
|
||||||
def nr_qs(self, queryset, name, value):
|
|
||||||
return queryset.filter(nr__iexact=value)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Invoice
|
model = Invoice
|
||||||
fields = ['order', 'number', 'is_cancellation', 'refers', 'locale']
|
fields = ['order', 'number', 'is_cancellation', 'refers', 'locale']
|
||||||
|
|||||||
@@ -24,10 +24,11 @@ from decimal import Decimal
|
|||||||
import django_filters
|
import django_filters
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import OuterRef, Subquery, Sum
|
from django.db.models import OuterRef, Q, Subquery, Sum
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
from django.utils.timezone import now
|
||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from rest_framework import mixins, serializers, status, views, viewsets
|
from rest_framework import mixins, serializers, status, views, viewsets
|
||||||
@@ -43,13 +44,13 @@ from pretix.api.serializers.organizer import (
|
|||||||
CustomerCreateSerializer, CustomerSerializer, DeviceSerializer,
|
CustomerCreateSerializer, CustomerSerializer, DeviceSerializer,
|
||||||
GiftCardSerializer, GiftCardTransactionSerializer, MembershipSerializer,
|
GiftCardSerializer, GiftCardTransactionSerializer, MembershipSerializer,
|
||||||
MembershipTypeSerializer, OrganizerSerializer, OrganizerSettingsSerializer,
|
MembershipTypeSerializer, OrganizerSerializer, OrganizerSettingsSerializer,
|
||||||
SeatingPlanSerializer, TeamAPITokenSerializer, TeamInviteSerializer,
|
SalesChannelSerializer, SeatingPlanSerializer, TeamAPITokenSerializer,
|
||||||
TeamMemberSerializer, TeamSerializer,
|
TeamInviteSerializer, TeamMemberSerializer, TeamSerializer,
|
||||||
)
|
)
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Customer, Device, GiftCard, GiftCardTransaction, Membership,
|
Customer, Device, GiftCard, GiftCardTransaction, Membership,
|
||||||
MembershipType, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite,
|
MembershipType, Organizer, SalesChannel, SeatingPlan, Team, TeamAPIToken,
|
||||||
User,
|
TeamInvite, User,
|
||||||
)
|
)
|
||||||
from pretix.helpers import OF_SELF
|
from pretix.helpers import OF_SELF
|
||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
@@ -136,11 +137,19 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
|
|||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
class GiftCardFilter(FilterSet):
|
class GiftCardFilter(FilterSet):
|
||||||
secret = django_filters.CharFilter(field_name='secret', lookup_expr='iexact')
|
secret = django_filters.CharFilter(field_name='secret', lookup_expr='iexact')
|
||||||
|
expired = django_filters.BooleanFilter(method='expired_qs')
|
||||||
|
value = django_filters.NumberFilter(field_name='cached_value')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = GiftCard
|
model = GiftCard
|
||||||
fields = ['secret', 'testmode']
|
fields = ['secret', 'testmode']
|
||||||
|
|
||||||
|
def expired_qs(self, qs, name, value):
|
||||||
|
if value:
|
||||||
|
return qs.filter(expires__isnull=False, expires__lt=now())
|
||||||
|
else:
|
||||||
|
return qs.filter(Q(expires__isnull=True) | Q(expires__gte=now()))
|
||||||
|
|
||||||
|
|
||||||
class GiftCardViewSet(viewsets.ModelViewSet):
|
class GiftCardViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = GiftCardSerializer
|
serializer_class = GiftCardSerializer
|
||||||
@@ -675,3 +684,68 @@ class MembershipViewSet(viewsets.ModelViewSet):
|
|||||||
data=self.request.data,
|
data=self.request.data,
|
||||||
)
|
)
|
||||||
return inst
|
return inst
|
||||||
|
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
|
class SalesChannelFilter(FilterSet):
|
||||||
|
class Meta:
|
||||||
|
model = SalesChannel
|
||||||
|
fields = ['type', 'identifier']
|
||||||
|
|
||||||
|
|
||||||
|
class SalesChannelViewSet(viewsets.ModelViewSet):
|
||||||
|
serializer_class = SalesChannelSerializer
|
||||||
|
queryset = SalesChannel.objects.none()
|
||||||
|
permission = 'can_change_organizer_settings'
|
||||||
|
write_permission = 'can_change_organizer_settings'
|
||||||
|
filter_backends = (DjangoFilterBackend,)
|
||||||
|
filterset_class = SalesChannelFilter
|
||||||
|
lookup_field = 'identifier'
|
||||||
|
lookup_url_kwarg = 'identifier'
|
||||||
|
lookup_value_regex = r"[a-zA-Z0-9.\-_]+"
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.request.organizer.sales_channels.all()
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
ctx = super().get_serializer_context()
|
||||||
|
ctx['organizer'] = self.request.organizer
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
inst = serializer.save(
|
||||||
|
organizer=self.request.organizer,
|
||||||
|
type="api"
|
||||||
|
)
|
||||||
|
inst.log_action(
|
||||||
|
'pretix.saleschannel.created',
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data=merge_dicts(self.request.data, {'id': inst.pk})
|
||||||
|
)
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
|
def perform_update(self, serializer):
|
||||||
|
inst = serializer.save(
|
||||||
|
type=serializer.instance.type,
|
||||||
|
identifier=serializer.instance.identifier,
|
||||||
|
)
|
||||||
|
inst.log_action(
|
||||||
|
'pretix.sales_channel.changed',
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data=self.request.data,
|
||||||
|
)
|
||||||
|
return inst
|
||||||
|
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
if not instance.allow_delete():
|
||||||
|
raise PermissionDenied("Can only be deleted if unused.")
|
||||||
|
instance.log_action(
|
||||||
|
'pretix.saleschannel.deleted',
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data={'id': instance.pk}
|
||||||
|
)
|
||||||
|
instance.delete()
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import django_filters
|
||||||
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
|
|
||||||
from pretix.api.models import WebHook
|
from pretix.api.models import WebHook
|
||||||
@@ -26,11 +28,17 @@ from pretix.api.serializers.webhooks import WebHookSerializer
|
|||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookFilter(FilterSet):
|
||||||
|
enabled = django_filters.rest_framework.BooleanFilter()
|
||||||
|
|
||||||
|
|
||||||
class WebHookViewSet(viewsets.ModelViewSet):
|
class WebHookViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = WebHookSerializer
|
serializer_class = WebHookSerializer
|
||||||
queryset = WebHook.objects.none()
|
queryset = WebHook.objects.none()
|
||||||
permission = 'can_change_organizer_settings'
|
permission = 'can_change_organizer_settings'
|
||||||
write_permission = 'can_change_organizer_settings'
|
write_permission = 'can_change_organizer_settings'
|
||||||
|
filter_backends = (DjangoFilterBackend,)
|
||||||
|
filterset_class = WebhookFilter
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.organizer.webhooks.prefetch_related('listeners')
|
return self.request.organizer.webhooks.prefetch_related('listeners')
|
||||||
|
|||||||
@@ -126,6 +126,17 @@ class ParametrizedOrderWebhookEvent(ParametrizedWebhookEvent):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DeletedOrderWebhookEvent(ParametrizedWebhookEvent):
|
||||||
|
def build_payload(self, logentry: LogEntry):
|
||||||
|
return {
|
||||||
|
'notification_id': logentry.pk,
|
||||||
|
'organizer': logentry.organizer.slug,
|
||||||
|
'event': logentry.event.slug,
|
||||||
|
'code': logentry.parsed_data.get("code"),
|
||||||
|
'action': logentry.action_type,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ParametrizedEventWebhookEvent(ParametrizedWebhookEvent):
|
class ParametrizedEventWebhookEvent(ParametrizedWebhookEvent):
|
||||||
|
|
||||||
def build_payload(self, logentry: LogEntry):
|
def build_payload(self, logentry: LogEntry):
|
||||||
@@ -297,6 +308,10 @@ def register_default_webhook_events(sender, **kwargs):
|
|||||||
'pretix.event.order.denied',
|
'pretix.event.order.denied',
|
||||||
_('Order denied'),
|
_('Order denied'),
|
||||||
),
|
),
|
||||||
|
DeletedOrderWebhookEvent(
|
||||||
|
'pretix.event.order.deleted',
|
||||||
|
_('Order deleted'),
|
||||||
|
),
|
||||||
ParametrizedOrderPositionCheckinWebhookEvent(
|
ParametrizedOrderPositionCheckinWebhookEvent(
|
||||||
'pretix.event.checkin',
|
'pretix.event.checkin',
|
||||||
_('Ticket checked in'),
|
_('Ticket checked in'),
|
||||||
|
|||||||
@@ -32,13 +32,16 @@
|
|||||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations under the License.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
|
import string
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import authenticate
|
from django.contrib.auth import authenticate
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.contrib.auth.hashers import check_password, make_password
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.utils.translation import gettext_lazy as _, ngettext
|
||||||
|
|
||||||
|
|
||||||
def get_auth_backends():
|
def get_auth_backends():
|
||||||
@@ -149,7 +152,7 @@ class NativeAuthBackend(BaseAuthBackend):
|
|||||||
to log in.
|
to log in.
|
||||||
"""
|
"""
|
||||||
d = OrderedDict([
|
d = OrderedDict([
|
||||||
('email', forms.EmailField(label=_("E-mail"), max_length=254,
|
('email', forms.EmailField(label=_("Email"), max_length=254,
|
||||||
widget=forms.EmailInput(attrs={'autofocus': 'autofocus'}))),
|
widget=forms.EmailInput(attrs={'autofocus': 'autofocus'}))),
|
||||||
('password', forms.CharField(label=_("Password"), widget=forms.PasswordInput,
|
('password', forms.CharField(label=_("Password"), widget=forms.PasswordInput,
|
||||||
max_length=4096)),
|
max_length=4096)),
|
||||||
@@ -160,3 +163,62 @@ class NativeAuthBackend(BaseAuthBackend):
|
|||||||
u = authenticate(request=request, email=form_data['email'].lower(), password=form_data['password'])
|
u = authenticate(request=request, email=form_data['email'].lower(), password=form_data['password'])
|
||||||
if u and u.auth_backend == self.identifier:
|
if u and u.auth_backend == self.identifier:
|
||||||
return u
|
return u
|
||||||
|
|
||||||
|
|
||||||
|
class NumericAndAlphabeticPasswordValidator:
|
||||||
|
|
||||||
|
def validate(self, password, user=None):
|
||||||
|
has_numeric = any(c in string.digits for c in password)
|
||||||
|
has_alpha = any(c in string.ascii_letters for c in password)
|
||||||
|
if not has_numeric or not has_alpha:
|
||||||
|
raise ValidationError(
|
||||||
|
_(
|
||||||
|
"Your password must contain both numeric and alphabetic characters.",
|
||||||
|
),
|
||||||
|
code="password_numeric_and_alphabetic",
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_help_text(self):
|
||||||
|
return _(
|
||||||
|
"Your password must contain both numeric and alphabetic characters.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HistoryPasswordValidator:
|
||||||
|
|
||||||
|
def __init__(self, history_length=4):
|
||||||
|
self.history_length = history_length
|
||||||
|
|
||||||
|
def validate(self, password, user=None):
|
||||||
|
from pretix.base.models import User
|
||||||
|
|
||||||
|
if not user or not user.pk or not isinstance(user, User):
|
||||||
|
return
|
||||||
|
|
||||||
|
for hp in user.historic_passwords.order_by("-created")[:self.history_length]:
|
||||||
|
if check_password(password, hp.password):
|
||||||
|
raise ValidationError(
|
||||||
|
ngettext(
|
||||||
|
"Your password may not be the same as your previous password.",
|
||||||
|
"Your password may not be the same as one of your %(history_length)s previous passwords.",
|
||||||
|
self.history_length,
|
||||||
|
),
|
||||||
|
code="password_history",
|
||||||
|
params={"history_length": self.history_length},
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_help_text(self):
|
||||||
|
return ngettext(
|
||||||
|
"Your password may not be the same as your previous password.",
|
||||||
|
"Your password may not be the same as one of your %(history_length)s previous passwords.",
|
||||||
|
self.history_length,
|
||||||
|
) % {"history_length": self.history_length}
|
||||||
|
|
||||||
|
def password_changed(self, password, user=None):
|
||||||
|
if not user:
|
||||||
|
pass
|
||||||
|
|
||||||
|
user.historic_passwords.create(password=make_password(password))
|
||||||
|
user.historic_passwords.filter(
|
||||||
|
pk__in=user.historic_passwords.order_by("-created")[self.history_length:].values_list("pk", flat=True),
|
||||||
|
).delete()
|
||||||
|
|||||||
@@ -20,56 +20,83 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import logging
|
import logging
|
||||||
|
import warnings
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.signals import register_sales_channels
|
from pretix.base.signals import (
|
||||||
|
register_sales_channel_types, register_sales_channels,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
_ALL_CHANNELS = None
|
_ALL_CHANNEL_TYPES = None
|
||||||
|
|
||||||
|
|
||||||
class SalesChannel:
|
class SalesChannelType:
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<SalesChannel: {}>'.format(self.identifier)
|
return '<SalesChannelType: {}>'.format(self.identifier)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def identifier(self) -> str:
|
def identifier(self) -> str:
|
||||||
"""
|
"""
|
||||||
The internal identifier of this sales channel.
|
The internal identifier of this sales channel type.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError() # NOQA
|
raise NotImplementedError() # NOQA
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def verbose_name(self) -> str:
|
def verbose_name(self) -> str:
|
||||||
"""
|
"""
|
||||||
A human-readable name of this sales channel.
|
A human-readable name of this sales channel type.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError() # NOQA
|
raise NotImplementedError() # NOQA
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self) -> str:
|
||||||
|
"""
|
||||||
|
A human-readable description of this sales channel type.
|
||||||
|
"""
|
||||||
|
return ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self) -> str:
|
def icon(self) -> str:
|
||||||
"""
|
"""
|
||||||
The name of a Font Awesome icon to represent this channel
|
This can be:
|
||||||
|
|
||||||
|
- The name of a Font Awesome icon to represent this channel type.
|
||||||
|
- The name of a SVG icon file that is resolvable through the static file system. We recommend to design for a size of 18x14 pixels.
|
||||||
"""
|
"""
|
||||||
return "circle"
|
return "circle"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_created(self) -> bool:
|
||||||
|
"""
|
||||||
|
Indication, if a sales channel of this type should automatically be created for every organizer
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def multiple_allowed(self) -> bool:
|
||||||
|
"""
|
||||||
|
Indication, if multiple sales channels of this type may exist in the same organizer
|
||||||
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def testmode_supported(self) -> bool:
|
def testmode_supported(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Indication, if a saleschannels supports test mode orders
|
Indication, if a sales channel of this type supports test mode orders
|
||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def payment_restrictions_supported(self) -> bool:
|
def payment_restrictions_supported(self) -> bool:
|
||||||
"""
|
"""
|
||||||
If this property is ``True``, organizers can restrict the usage of payment providers to this sales channel.
|
If this property is ``True``, organizers can restrict the usage of payment providers to this sales channel type.
|
||||||
|
|
||||||
Example: pretixPOS provides its own sales channel, ignores the configured payment providers completely and
|
Example: pretixPOS provides its own sales channel type, ignores the configured payment providers completely and
|
||||||
handles payments locally. Therefor, this property should be set to ``False`` for the pretixPOS sales channel as
|
handles payments locally. Therefore, this property should be set to ``False`` for the pretixPOS sales channel as
|
||||||
the event organizer cannot restrict the usage of any payment provider through the backend.
|
the event organizer cannot restrict the usage of any payment provider through the backend.
|
||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
@@ -77,8 +104,8 @@ class SalesChannel:
|
|||||||
@property
|
@property
|
||||||
def unlimited_items_per_order(self) -> bool:
|
def unlimited_items_per_order(self) -> bool:
|
||||||
"""
|
"""
|
||||||
If this property is ``True``, purchases made using this sales channel are not limited to the maximum amount of
|
If this property is ``True``, purchases made using sales channels of this type are not limited to the maximum
|
||||||
items defined in the event settings.
|
amount of items defined in the event settings.
|
||||||
"""
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -96,34 +123,67 @@ class SalesChannel:
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def required_event_plugin(self) -> str:
|
||||||
|
"""
|
||||||
|
Name of an event plugin that is required for this sales channel to be useful. Defaults to ``None``.
|
||||||
|
"""
|
||||||
|
return
|
||||||
|
|
||||||
def get_all_sales_channels():
|
|
||||||
global _ALL_CHANNELS
|
|
||||||
|
|
||||||
if _ALL_CHANNELS:
|
def get_all_sales_channel_types():
|
||||||
return _ALL_CHANNELS
|
from pretix.base.signals import register_sales_channel_types
|
||||||
|
global _ALL_CHANNEL_TYPES
|
||||||
|
|
||||||
|
if _ALL_CHANNEL_TYPES:
|
||||||
|
return _ALL_CHANNEL_TYPES
|
||||||
|
|
||||||
channels = []
|
channels = []
|
||||||
for recv, ret in register_sales_channels.send(None):
|
for recv, ret in register_sales_channel_types.send(None):
|
||||||
|
if isinstance(ret, (list, tuple)):
|
||||||
|
channels += ret
|
||||||
|
else:
|
||||||
|
channels.append(ret)
|
||||||
|
for recv, ret in register_sales_channels.send(None): # todo: remove me
|
||||||
if isinstance(ret, (list, tuple)):
|
if isinstance(ret, (list, tuple)):
|
||||||
channels += ret
|
channels += ret
|
||||||
else:
|
else:
|
||||||
channels.append(ret)
|
channels.append(ret)
|
||||||
channels.sort(key=lambda c: c.identifier)
|
channels.sort(key=lambda c: c.identifier)
|
||||||
_ALL_CHANNELS = OrderedDict([(c.identifier, c) for c in channels])
|
_ALL_CHANNEL_TYPES = OrderedDict([(c.identifier, c) for c in channels])
|
||||||
if 'web' in _ALL_CHANNELS:
|
if 'web' in _ALL_CHANNEL_TYPES:
|
||||||
_ALL_CHANNELS.move_to_end('web', last=False)
|
_ALL_CHANNEL_TYPES.move_to_end('web', last=False)
|
||||||
return _ALL_CHANNELS
|
return _ALL_CHANNEL_TYPES
|
||||||
|
|
||||||
|
|
||||||
class WebshopSalesChannel(SalesChannel):
|
def get_all_sales_channels():
|
||||||
|
# TODO: remove me
|
||||||
|
warnings.warn('Using get_all_sales_channels() is no longer appropriate, use get_al_sales_channel_types() instead.',
|
||||||
|
DeprecationWarning, stacklevel=2)
|
||||||
|
return get_all_sales_channel_types()
|
||||||
|
|
||||||
|
|
||||||
|
class WebshopSalesChannelType(SalesChannelType):
|
||||||
identifier = "web"
|
identifier = "web"
|
||||||
verbose_name = _('Online shop')
|
verbose_name = _('Online shop')
|
||||||
icon = "globe"
|
icon = "globe"
|
||||||
|
|
||||||
|
|
||||||
@receiver(register_sales_channels, dispatch_uid="base_register_default_sales_channels")
|
class ApiSalesChannelType(SalesChannelType):
|
||||||
|
identifier = "api"
|
||||||
|
verbose_name = _('API')
|
||||||
|
description = _('API sales channels come with no built-in functionality, but may be used for custom integrations.')
|
||||||
|
icon = "exchange"
|
||||||
|
default_created = False
|
||||||
|
multiple_allowed = True
|
||||||
|
|
||||||
|
|
||||||
|
SalesChannel = SalesChannelType # TODO: remove me
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(register_sales_channel_types, dispatch_uid="base_register_default_sales_channel_types")
|
||||||
def base_sales_channels(sender, **kwargs):
|
def base_sales_channels(sender, **kwargs):
|
||||||
return (
|
return (
|
||||||
WebshopSalesChannel(),
|
WebshopSalesChannelType(),
|
||||||
|
ApiSalesChannelType(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ This module contains utilities for implementing OpenID Connect for customer auth
|
|||||||
as well as an OpenID Provider (OP).
|
as well as an OpenID Provider (OP).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
pretix_token_endpoint_auth_methods = ['client_secret_basic', 'client_secret_post']
|
||||||
|
|
||||||
|
|
||||||
def _urljoin(base, path):
|
def _urljoin(base, path):
|
||||||
if not base.endswith("/"):
|
if not base.endswith("/"):
|
||||||
@@ -127,6 +129,16 @@ def oidc_validate_and_complete_config(config):
|
|||||||
fields=", ".join(provider_config.get("claims_supported", []))
|
fields=", ".join(provider_config.get("claims_supported", []))
|
||||||
))
|
))
|
||||||
|
|
||||||
|
if "token_endpoint_auth_methods_supported" in provider_config:
|
||||||
|
token_endpoint_auth_methods_supported = provider_config.get("token_endpoint_auth_methods_supported",
|
||||||
|
["client_secret_basic"])
|
||||||
|
if not any(x in pretix_token_endpoint_auth_methods for x in token_endpoint_auth_methods_supported):
|
||||||
|
raise ValidationError(
|
||||||
|
_(f'No supported Token Endpoint Auth Methods supported: {token_endpoint_auth_methods_supported}').format(
|
||||||
|
token_endpoint_auth_methods_supported=", ".join(token_endpoint_auth_methods_supported)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
config['provider_config'] = provider_config
|
config['provider_config'] = provider_config
|
||||||
return config
|
return config
|
||||||
|
|
||||||
@@ -147,6 +159,18 @@ def oidc_authorize_url(provider, state, redirect_uri):
|
|||||||
|
|
||||||
def oidc_validate_authorization(provider, code, redirect_uri):
|
def oidc_validate_authorization(provider, code, redirect_uri):
|
||||||
endpoint = provider.configuration['provider_config']['token_endpoint']
|
endpoint = provider.configuration['provider_config']['token_endpoint']
|
||||||
|
|
||||||
|
# Wall of shame and RFC ignorant IDPs
|
||||||
|
if endpoint == 'https://www.linkedin.com/oauth/v2/accessToken':
|
||||||
|
token_endpoint_auth_method = 'client_secret_post'
|
||||||
|
else:
|
||||||
|
token_endpoint_auth_methods = provider.configuration['provider_config'].get(
|
||||||
|
'token_endpoint_auth_methods_supported', ['client_secret_basic']
|
||||||
|
)
|
||||||
|
token_endpoint_auth_method = [
|
||||||
|
x for x in pretix_token_endpoint_auth_methods if x in token_endpoint_auth_methods
|
||||||
|
][0]
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
# https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
|
# https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
|
||||||
# https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
|
# https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
|
||||||
@@ -154,6 +178,11 @@ def oidc_validate_authorization(provider, code, redirect_uri):
|
|||||||
'code': code,
|
'code': code,
|
||||||
'redirect_uri': redirect_uri,
|
'redirect_uri': redirect_uri,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if token_endpoint_auth_method == 'client_secret_post':
|
||||||
|
params['client_id'] = provider.configuration['client_id']
|
||||||
|
params['client_secret'] = provider.configuration['client_secret']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp = requests.post(
|
resp = requests.post(
|
||||||
endpoint,
|
endpoint,
|
||||||
@@ -161,7 +190,10 @@ def oidc_validate_authorization(provider, code, redirect_uri):
|
|||||||
headers={
|
headers={
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
},
|
},
|
||||||
auth=(provider.configuration['client_id'], provider.configuration['client_secret']),
|
auth=(
|
||||||
|
provider.configuration['client_id'],
|
||||||
|
provider.configuration['client_secret']
|
||||||
|
) if token_endpoint_auth_method == 'client_secret_basic' else None,
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ from django.utils.translation import get_language, gettext_lazy as _
|
|||||||
from pretix.base.models import Event
|
from pretix.base.models import Event
|
||||||
from pretix.base.signals import register_html_mail_renderers
|
from pretix.base.signals import register_html_mail_renderers
|
||||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||||
|
from pretix.helpers.format import SafeFormatter, format_map
|
||||||
|
|
||||||
from pretix.base.services.placeholders import ( # noqa
|
from pretix.base.services.placeholders import ( # noqa
|
||||||
get_available_placeholders, PlaceholderContext
|
get_available_placeholders, PlaceholderContext
|
||||||
@@ -68,7 +69,7 @@ def test_custom_smtp_backend(backend: T, from_addr: str) -> None:
|
|||||||
|
|
||||||
class BaseHTMLMailRenderer:
|
class BaseHTMLMailRenderer:
|
||||||
"""
|
"""
|
||||||
This is the base class for all HTML e-mail renderers.
|
This is the base class for all HTML email renderers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, event: Event, organizer=None):
|
def __init__(self, event: Event, organizer=None):
|
||||||
@@ -79,7 +80,7 @@ class BaseHTMLMailRenderer:
|
|||||||
return self.identifier
|
return self.identifier
|
||||||
|
|
||||||
def render(self, plain_body: str, plain_signature: str, subject: str, order=None,
|
def render(self, plain_body: str, plain_signature: str, subject: str, order=None,
|
||||||
position=None) -> str:
|
position=None, context=None) -> str:
|
||||||
"""
|
"""
|
||||||
This method should generate the HTML part of the email.
|
This method should generate the HTML part of the email.
|
||||||
|
|
||||||
@@ -88,6 +89,7 @@ class BaseHTMLMailRenderer:
|
|||||||
:param subject: The email subject.
|
:param subject: The email subject.
|
||||||
:param order: The order if this email is connected to one, otherwise ``None``.
|
:param order: The order if this email is connected to one, otherwise ``None``.
|
||||||
:param position: The order position if this email is connected to one, otherwise ``None``.
|
:param position: The order position if this email is connected to one, otherwise ``None``.
|
||||||
|
:param context: Context to use to render placeholders in the plain body
|
||||||
:return: An HTML string
|
:return: An HTML string
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
@@ -134,8 +136,10 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
|||||||
def compile_markdown(self, plaintext):
|
def compile_markdown(self, plaintext):
|
||||||
return markdown_compile_email(plaintext)
|
return markdown_compile_email(plaintext)
|
||||||
|
|
||||||
def render(self, plain_body: str, plain_signature: str, subject: str, order, position) -> str:
|
def render(self, plain_body: str, plain_signature: str, subject: str, order, position, context) -> str:
|
||||||
body_md = self.compile_markdown(plain_body)
|
body_md = self.compile_markdown(plain_body)
|
||||||
|
if context:
|
||||||
|
body_md = format_map(body_md, context=context, mode=SafeFormatter.MODE_RICH_TO_HTML)
|
||||||
htmlctx = {
|
htmlctx = {
|
||||||
'site': settings.PRETIX_INSTANCE_NAME,
|
'site': settings.PRETIX_INSTANCE_NAME,
|
||||||
'site_url': settings.SITE_URL,
|
'site_url': settings.SITE_URL,
|
||||||
|
|||||||
@@ -207,10 +207,13 @@ class ListExporter(BaseExporter):
|
|||||||
def get_filename(self):
|
def get_filename(self):
|
||||||
return 'export'
|
return 'export'
|
||||||
|
|
||||||
|
def get_csv_encoding(self):
|
||||||
|
return 'utf-8'
|
||||||
|
|
||||||
def _render_csv(self, form_data, output_file=None, **kwargs):
|
def _render_csv(self, form_data, output_file=None, **kwargs):
|
||||||
if output_file:
|
if output_file:
|
||||||
if 'b' in output_file.mode:
|
if 'b' in output_file.mode:
|
||||||
output_file = io.TextIOWrapper(output_file, encoding='utf-8', newline='')
|
output_file = io.TextIOWrapper(output_file, encoding=self.get_csv_encoding(), errors='replace', newline='')
|
||||||
writer = csv.writer(output_file, **kwargs)
|
writer = csv.writer(output_file, **kwargs)
|
||||||
total = 0
|
total = 0
|
||||||
counter = 0
|
counter = 0
|
||||||
@@ -246,7 +249,7 @@ class ListExporter(BaseExporter):
|
|||||||
if counter % max(10, total // 100) == 0:
|
if counter % max(10, total // 100) == 0:
|
||||||
self.progress_callback(counter / total * 100)
|
self.progress_callback(counter / total * 100)
|
||||||
writer.writerow(line)
|
writer.writerow(line)
|
||||||
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
|
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode(self.get_csv_encoding(), errors='replace')
|
||||||
|
|
||||||
def prepare_xlsx_sheet(self, ws):
|
def prepare_xlsx_sheet(self, ws):
|
||||||
pass
|
pass
|
||||||
@@ -256,7 +259,7 @@ class ListExporter(BaseExporter):
|
|||||||
ws = wb.create_sheet()
|
ws = wb.create_sheet()
|
||||||
self.prepare_xlsx_sheet(ws)
|
self.prepare_xlsx_sheet(ws)
|
||||||
try:
|
try:
|
||||||
ws.title = str(self.verbose_name)
|
ws.title = str(self.verbose_name)[:30]
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
total = 0
|
total = 0
|
||||||
@@ -374,7 +377,7 @@ class MultiSheetListExporter(ListExporter):
|
|||||||
wb = SafeWorkbook(write_only=True)
|
wb = SafeWorkbook(write_only=True)
|
||||||
n_sheets = len(self.sheets)
|
n_sheets = len(self.sheets)
|
||||||
for i_sheet, (s, l) in enumerate(self.sheets):
|
for i_sheet, (s, l) in enumerate(self.sheets):
|
||||||
ws = wb.create_sheet(str(l))
|
ws = wb.create_sheet(str(l)[:30])
|
||||||
if hasattr(self, 'prepare_xlsx_sheet_' + s):
|
if hasattr(self, 'prepare_xlsx_sheet_' + s):
|
||||||
getattr(self, 'prepare_xlsx_sheet_' + s)(ws)
|
getattr(self, 'prepare_xlsx_sheet_' + s)(ws)
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class CustomerListExporter(OrganizerLevelExportMixin, ListExporter):
|
|||||||
_('Customer ID'),
|
_('Customer ID'),
|
||||||
_('SSO provider'),
|
_('SSO provider'),
|
||||||
_('External identifier'),
|
_('External identifier'),
|
||||||
_('E-mail'),
|
_('Email'),
|
||||||
_('Phone number'),
|
_('Phone number'),
|
||||||
_('Full name'),
|
_('Full name'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
|
|||||||
_('Invoice number'),
|
_('Invoice number'),
|
||||||
_('Date'),
|
_('Date'),
|
||||||
_('Order code'),
|
_('Order code'),
|
||||||
_('E-mail address'),
|
_('Email address'),
|
||||||
_('Invoice type'),
|
_('Invoice type'),
|
||||||
_('Cancellation of'),
|
_('Cancellation of'),
|
||||||
_('Language'),
|
_('Language'),
|
||||||
@@ -326,7 +326,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
|
|||||||
_('Event start date'),
|
_('Event start date'),
|
||||||
_('Date'),
|
_('Date'),
|
||||||
_('Order code'),
|
_('Order code'),
|
||||||
_('E-mail address'),
|
_('Email address'),
|
||||||
_('Invoice type'),
|
_('Invoice type'),
|
||||||
_('Cancellation of'),
|
_('Cancellation of'),
|
||||||
_('Invoice sender:') + ' ' + _('Name'),
|
_('Invoice sender:') + ' ' + _('Name'),
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ from openpyxl.styles import Alignment
|
|||||||
from openpyxl.utils import get_column_letter
|
from openpyxl.utils import get_column_letter
|
||||||
|
|
||||||
from ...helpers.safe_openpyxl import SafeCell
|
from ...helpers.safe_openpyxl import SafeCell
|
||||||
from ..channels import get_all_sales_channels
|
|
||||||
from ..exporter import ListExporter
|
from ..exporter import ListExporter
|
||||||
from ..models import ItemMetaValue, ItemVariation, ItemVariationMetaValue
|
from ..models import ItemMetaValue, ItemVariation, ItemVariationMetaValue
|
||||||
from ..signals import register_data_exporters
|
from ..signals import register_data_exporters
|
||||||
@@ -53,7 +52,7 @@ class ItemDataExporter(ListExporter):
|
|||||||
|
|
||||||
def iterate_list(self, form_data):
|
def iterate_list(self, form_data):
|
||||||
locales = self.event.settings.locales
|
locales = self.event.settings.locales
|
||||||
scs = get_all_sales_channels()
|
scs = self.organizer.sales_channels.all()
|
||||||
header = [
|
header = [
|
||||||
_("Product ID"),
|
_("Product ID"),
|
||||||
_("Variation ID"),
|
_("Variation ID"),
|
||||||
@@ -141,9 +140,15 @@ class ItemDataExporter(ListExporter):
|
|||||||
row.append(i.name.localize(l))
|
row.append(i.name.localize(l))
|
||||||
for l in locales:
|
for l in locales:
|
||||||
row.append(v.value.localize(l))
|
row.append(v.value.localize(l))
|
||||||
|
|
||||||
|
sales_channels = list(scs)
|
||||||
|
if not i.all_sales_channels:
|
||||||
|
sales_channels = [s for s in sales_channels if s.identifier in (p.identifier for p in i.limit_sales_channels.all())]
|
||||||
|
if not v.all_sales_channels:
|
||||||
|
sales_channels = [s for s in sales_channels if s.identifier in (p.identifier for p in v.limit_sales_channels.all())]
|
||||||
row += [
|
row += [
|
||||||
_("Yes") if i.active and v.active else "",
|
_("Yes") if i.active and v.active else "",
|
||||||
", ".join([str(sn.verbose_name) for s, sn in scs.items() if s in i.sales_channels and s in v.sales_channels]),
|
", ".join([str(sn.label) for sn in sales_channels]),
|
||||||
v.default_price or i.default_price,
|
v.default_price or i.default_price,
|
||||||
_("Yes") if i.free_price else "",
|
_("Yes") if i.free_price else "",
|
||||||
str(i.tax_rule) if i.tax_rule else "",
|
str(i.tax_rule) if i.tax_rule else "",
|
||||||
@@ -186,9 +191,12 @@ class ItemDataExporter(ListExporter):
|
|||||||
row.append(i.name.localize(l))
|
row.append(i.name.localize(l))
|
||||||
for l in locales:
|
for l in locales:
|
||||||
row.append("")
|
row.append("")
|
||||||
|
sales_channels = list(scs)
|
||||||
|
if not i.all_sales_channels:
|
||||||
|
sales_channels = [s for s in sales_channels if s.identifier in (p.identifier for p in i.limit_sales_channels.all())]
|
||||||
row += [
|
row += [
|
||||||
_("Yes") if i.active else "",
|
_("Yes") if i.active else "",
|
||||||
", ".join([str(sn.verbose_name) for s, sn in scs.items() if s in i.sales_channels]),
|
", ".join([str(sn.label) for sn in sales_channels]),
|
||||||
i.default_price,
|
i.default_price,
|
||||||
_("Yes") if i.free_price else "",
|
_("Yes") if i.free_price else "",
|
||||||
str(i.tax_rule) if i.tax_rule else "",
|
str(i.tax_rule) if i.tax_rule else "",
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ class JSONExporter(BaseExporter):
|
|||||||
'import in third-party systems.')
|
'import in third-party systems.')
|
||||||
|
|
||||||
def render(self, form_data):
|
def render(self, form_data):
|
||||||
|
all_sales_channels = self.event.organizer.sales_channels.all()
|
||||||
jo = {
|
jo = {
|
||||||
'event': {
|
'event': {
|
||||||
'name': str(self.event.name),
|
'name': str(self.event.name),
|
||||||
@@ -85,7 +86,7 @@ class JSONExporter(BaseExporter):
|
|||||||
'admission': item.admission,
|
'admission': item.admission,
|
||||||
'personalized': item.personalized,
|
'personalized': item.personalized,
|
||||||
'active': item.active,
|
'active': item.active,
|
||||||
'sales_channels': item.sales_channels,
|
'sales_channels': [c.identifier for c in (all_sales_channels if item.all_sales_channels else item.limit_sales_channels.all())],
|
||||||
'description': str(item.description),
|
'description': str(item.description),
|
||||||
'available_from': item.available_from,
|
'available_from': item.available_from,
|
||||||
'available_until': item.available_until,
|
'available_until': item.available_until,
|
||||||
@@ -114,7 +115,9 @@ class JSONExporter(BaseExporter):
|
|||||||
'checkin_text': variation.checkin_text,
|
'checkin_text': variation.checkin_text,
|
||||||
'require_approval': variation.require_approval,
|
'require_approval': variation.require_approval,
|
||||||
'require_membership': variation.require_membership,
|
'require_membership': variation.require_membership,
|
||||||
'sales_channels': variation.sales_channels,
|
'sales_channels': [
|
||||||
|
c.identifier for c in (all_sales_channels if variation.all_sales_channels else variation.limit_sales_channels.all())
|
||||||
|
],
|
||||||
'available_from': variation.available_from,
|
'available_from': variation.available_from,
|
||||||
'available_until': variation.available_until,
|
'available_until': variation.available_until,
|
||||||
'hide_without_voucher': variation.hide_without_voucher,
|
'hide_without_voucher': variation.hide_without_voucher,
|
||||||
@@ -122,6 +125,7 @@ class JSONExporter(BaseExporter):
|
|||||||
} for variation in item.variations.all()
|
} for variation in item.variations.all()
|
||||||
]
|
]
|
||||||
} for item in self.event.items.select_related('tax_rule').prefetch_related(
|
} for item in self.event.items.select_related('tax_rule').prefetch_related(
|
||||||
|
'limit_sales_channels',
|
||||||
Prefetch(
|
Prefetch(
|
||||||
'meta_values',
|
'meta_values',
|
||||||
ItemMetaValue.objects.select_related('property'),
|
ItemMetaValue.objects.select_related('property'),
|
||||||
@@ -130,6 +134,7 @@ class JSONExporter(BaseExporter):
|
|||||||
Prefetch(
|
Prefetch(
|
||||||
'variations',
|
'variations',
|
||||||
queryset=ItemVariation.objects.prefetch_related(
|
queryset=ItemVariation.objects.prefetch_related(
|
||||||
|
'limit_sales_channels',
|
||||||
Prefetch(
|
Prefetch(
|
||||||
'meta_values',
|
'meta_values',
|
||||||
ItemVariationMetaValue.objects.select_related('property'),
|
ItemVariationMetaValue.objects.select_related('property'),
|
||||||
@@ -167,7 +172,7 @@ class JSONExporter(BaseExporter):
|
|||||||
'require_approval': order.require_approval,
|
'require_approval': order.require_approval,
|
||||||
'checkin_attention': order.checkin_attention,
|
'checkin_attention': order.checkin_attention,
|
||||||
'checkin_text': order.checkin_text,
|
'checkin_text': order.checkin_text,
|
||||||
'sales_channel': order.sales_channel,
|
'sales_channel': order.sales_channel.identifier,
|
||||||
'expires': order.expires,
|
'expires': order.expires,
|
||||||
'datetime': order.datetime,
|
'datetime': order.datetime,
|
||||||
'fees': [
|
'fees': [
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
headers.append(_('Comment'))
|
headers.append(_('Comment'))
|
||||||
headers.append(_('Follow-up date'))
|
headers.append(_('Follow-up date'))
|
||||||
headers.append(_('Positions'))
|
headers.append(_('Positions'))
|
||||||
headers.append(_('E-mail address verified'))
|
headers.append(_('Email address verified'))
|
||||||
headers.append(_('External customer ID'))
|
headers.append(_('External customer ID'))
|
||||||
headers.append(_('Payment providers'))
|
headers.append(_('Payment providers'))
|
||||||
if form_data.get('include_payment_amounts'):
|
if form_data.get('include_payment_amounts'):
|
||||||
@@ -560,7 +560,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
),
|
),
|
||||||
).select_related(
|
).select_related(
|
||||||
'order', 'order__invoice_address', 'order__customer', 'item', 'variation',
|
'order', 'order__invoice_address', 'order__customer', 'item', 'variation',
|
||||||
'voucher', 'tax_rule'
|
'voucher', 'tax_rule', 'addon_to',
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'subevent', 'subevent__meta_values',
|
'subevent', 'subevent__meta_values',
|
||||||
'answers', 'answers__question', 'answers__options'
|
'answers', 'answers__question', 'answers__options'
|
||||||
@@ -619,6 +619,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
_('Valid until'),
|
_('Valid until'),
|
||||||
_('Order comment'),
|
_('Order comment'),
|
||||||
_('Follow-up date'),
|
_('Follow-up date'),
|
||||||
|
_('Add-on to position ID'),
|
||||||
]
|
]
|
||||||
|
|
||||||
questions = list(Question.objects.filter(event__in=self.events))
|
questions = list(Question.objects.filter(event__in=self.events))
|
||||||
@@ -652,8 +653,9 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
_('VAT ID'),
|
_('VAT ID'),
|
||||||
]
|
]
|
||||||
headers += [
|
headers += [
|
||||||
_('Sales channel'), _('Order locale'),
|
_('Sales channel'),
|
||||||
_('E-mail address verified'),
|
_('Order locale'),
|
||||||
|
_('Email address verified'),
|
||||||
_('External customer ID'),
|
_('External customer ID'),
|
||||||
_('Check-in lists'),
|
_('Check-in lists'),
|
||||||
_('Payment providers'),
|
_('Payment providers'),
|
||||||
@@ -743,6 +745,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
]
|
]
|
||||||
row.append(order.comment)
|
row.append(order.comment)
|
||||||
row.append(order.custom_followup_at.strftime("%Y-%m-%d") if order.custom_followup_at else "")
|
row.append(order.custom_followup_at.strftime("%Y-%m-%d") if order.custom_followup_at else "")
|
||||||
|
row.append(op.addon_to.positionid if op.addon_to_id else "")
|
||||||
acache = {}
|
acache = {}
|
||||||
for a in op.answers.all():
|
for a in op.answers.all():
|
||||||
# We do not want to localize Date, Time and Datetime question answers, as those can lead
|
# We do not want to localize Date, Time and Datetime question answers, as those can lead
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class MarkdownTextarea(forms.Textarea):
|
|||||||
|
|
||||||
|
|
||||||
class I18nMarkdownTextarea(i18nfield.forms.I18nTextarea):
|
class I18nMarkdownTextarea(i18nfield.forms.I18nTextarea):
|
||||||
def format_output(self, rendered_widgets) -> str:
|
def format_output(self, rendered_widgets, id_) -> str:
|
||||||
rendered_widgets = rendered_widgets + [
|
rendered_widgets = rendered_widgets + [
|
||||||
'<div class="i18n-field-markdown-note">%s</div>' % (
|
'<div class="i18n-field-markdown-note">%s</div>' % (
|
||||||
_("You can use {markup_name} in this field.").format(
|
_("You can use {markup_name} in this field.").format(
|
||||||
@@ -108,11 +108,11 @@ class I18nMarkdownTextarea(i18nfield.forms.I18nTextarea):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
return super().format_output(rendered_widgets)
|
return super().format_output(rendered_widgets, id_)
|
||||||
|
|
||||||
|
|
||||||
class I18nMarkdownTextInput(i18nfield.forms.I18nTextInput):
|
class I18nMarkdownTextInput(i18nfield.forms.I18nTextInput):
|
||||||
def format_output(self, rendered_widgets) -> str:
|
def format_output(self, rendered_widgets, id_) -> str:
|
||||||
rendered_widgets = rendered_widgets + [
|
rendered_widgets = rendered_widgets + [
|
||||||
'<div class="i18n-field-markdown-note">%s</div>' % (
|
'<div class="i18n-field-markdown-note">%s</div>' % (
|
||||||
_("You can use {markup_name} in this field.").format(
|
_("You can use {markup_name} in this field.").format(
|
||||||
@@ -120,7 +120,7 @@ class I18nMarkdownTextInput(i18nfield.forms.I18nTextInput):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
return super().format_output(rendered_widgets)
|
return super().format_output(rendered_widgets, id_)
|
||||||
|
|
||||||
|
|
||||||
SECRET_REDACTED = '*****'
|
SECRET_REDACTED = '*****'
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ class PasswordRecoverForm(forms.Form):
|
|||||||
|
|
||||||
class PasswordForgotForm(forms.Form):
|
class PasswordForgotForm(forms.Form):
|
||||||
email = forms.EmailField(
|
email = forms.EmailField(
|
||||||
label=_('E-mail'),
|
label=_('Email'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ from django.core.validators import (
|
|||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
from django.forms import Select, widgets
|
from django.forms import Select, widgets
|
||||||
from django.forms.widgets import FILE_INPUT_CONTRADICTION
|
from django.forms.widgets import FILE_INPUT_CONTRADICTION
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
@@ -77,7 +78,7 @@ from pretix.base.i18n import (
|
|||||||
get_babel_locale, get_language_without_region, language,
|
get_babel_locale, get_language_without_region, language,
|
||||||
)
|
)
|
||||||
from pretix.base.models import InvoiceAddress, Item, Question, QuestionOption
|
from pretix.base.models import InvoiceAddress, Item, Question, QuestionOption
|
||||||
from pretix.base.models.tax import VAT_ID_COUNTRIES, ask_for_vat_id
|
from pretix.base.models.tax import ask_for_vat_id
|
||||||
from pretix.base.services.tax import (
|
from pretix.base.services.tax import (
|
||||||
VATIDFinalError, VATIDTemporaryError, validate_vat_id,
|
VATIDFinalError, VATIDTemporaryError, validate_vat_id,
|
||||||
)
|
)
|
||||||
@@ -276,6 +277,10 @@ class NamePartsFormField(forms.MultiValueField):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def name_parts_is_empty(name_parts_dict):
|
||||||
|
return not any(k != "_scheme" and v for k, v in name_parts_dict.items())
|
||||||
|
|
||||||
|
|
||||||
class WrappedPhonePrefixSelect(Select):
|
class WrappedPhonePrefixSelect(Select):
|
||||||
initial = None
|
initial = None
|
||||||
|
|
||||||
@@ -602,6 +607,7 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
questions = pos.item.questions_to_ask
|
questions = pos.item.questions_to_ask
|
||||||
event = kwargs.pop('event')
|
event = kwargs.pop('event')
|
||||||
self.all_optional = kwargs.pop('all_optional', False)
|
self.all_optional = kwargs.pop('all_optional', False)
|
||||||
|
self.attendee_addresses_required = event.settings.attendee_addresses_required and not self.all_optional
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
@@ -676,7 +682,7 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
|
|
||||||
if item.ask_attendee_data and event.settings.attendee_addresses_asked:
|
if item.ask_attendee_data and event.settings.attendee_addresses_asked:
|
||||||
add_fields['street'] = forms.CharField(
|
add_fields['street'] = forms.CharField(
|
||||||
required=event.settings.attendee_addresses_required and not self.all_optional,
|
required=self.attendee_addresses_required,
|
||||||
label=_('Address'),
|
label=_('Address'),
|
||||||
widget=forms.Textarea(attrs={
|
widget=forms.Textarea(attrs={
|
||||||
'rows': 2,
|
'rows': 2,
|
||||||
@@ -686,7 +692,7 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
initial=(cartpos.street if cartpos else orderpos.street),
|
initial=(cartpos.street if cartpos else orderpos.street),
|
||||||
)
|
)
|
||||||
add_fields['zipcode'] = forms.CharField(
|
add_fields['zipcode'] = forms.CharField(
|
||||||
required=event.settings.attendee_addresses_required and not self.all_optional,
|
required=False,
|
||||||
max_length=30,
|
max_length=30,
|
||||||
label=_('ZIP code'),
|
label=_('ZIP code'),
|
||||||
initial=(cartpos.zipcode if cartpos else orderpos.zipcode),
|
initial=(cartpos.zipcode if cartpos else orderpos.zipcode),
|
||||||
@@ -695,7 +701,7 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
add_fields['city'] = forms.CharField(
|
add_fields['city'] = forms.CharField(
|
||||||
required=event.settings.attendee_addresses_required and not self.all_optional,
|
required=False,
|
||||||
label=_('City'),
|
label=_('City'),
|
||||||
max_length=255,
|
max_length=255,
|
||||||
initial=(cartpos.city if cartpos else orderpos.city),
|
initial=(cartpos.city if cartpos else orderpos.city),
|
||||||
@@ -707,11 +713,12 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
add_fields['country'] = CountryField(
|
add_fields['country'] = CountryField(
|
||||||
countries=CachedCountries
|
countries=CachedCountries
|
||||||
).formfield(
|
).formfield(
|
||||||
required=event.settings.attendee_addresses_required and not self.all_optional,
|
required=self.attendee_addresses_required,
|
||||||
label=_('Country'),
|
label=_('Country'),
|
||||||
initial=country,
|
initial=country,
|
||||||
widget=forms.Select(attrs={
|
widget=forms.Select(attrs={
|
||||||
'autocomplete': 'country',
|
'autocomplete': 'country',
|
||||||
|
'data-country-information-url': reverse('js_helpers.states'),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
c = [('', pgettext_lazy('address', 'Select state'))]
|
c = [('', pgettext_lazy('address', 'Select state'))]
|
||||||
@@ -946,9 +953,9 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
d = super().clean()
|
d = super().clean()
|
||||||
|
|
||||||
if self.address_validation:
|
if self.address_validation:
|
||||||
self.cleaned_data = d = validate_address(d, True)
|
self.cleaned_data = d = validate_address(d, all_optional=not self.attendee_addresses_required)
|
||||||
|
|
||||||
if d.get('city') and d.get('country') and str(d['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
if d.get('street') and d.get('country') and str(d['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||||
if not d.get('state'):
|
if not d.get('state'):
|
||||||
self.add_error('state', _('This field is required.'))
|
self.add_error('state', _('This field is required.'))
|
||||||
|
|
||||||
@@ -1005,7 +1012,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
'street': forms.Textarea(attrs={
|
'street': forms.Textarea(attrs={
|
||||||
'rows': 2,
|
'rows': 2,
|
||||||
'placeholder': _('Street and Number'),
|
'placeholder': _('Street and Number'),
|
||||||
'autocomplete': 'street-address'
|
'autocomplete': 'street-address',
|
||||||
}),
|
}),
|
||||||
'beneficiary': forms.Textarea(attrs={'rows': 3}),
|
'beneficiary': forms.Textarea(attrs={'rows': 3}),
|
||||||
'country': forms.Select(attrs={
|
'country': forms.Select(attrs={
|
||||||
@@ -1021,13 +1028,25 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
'data-display-dependency': '#id_is_business_1',
|
'data-display-dependency': '#id_is_business_1',
|
||||||
'autocomplete': 'organization',
|
'autocomplete': 'organization',
|
||||||
}),
|
}),
|
||||||
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1', 'data-countries-with-vat-id': ','.join(VAT_ID_COUNTRIES)}),
|
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
|
||||||
'internal_reference': forms.TextInput,
|
'internal_reference': forms.TextInput,
|
||||||
}
|
}
|
||||||
labels = {
|
labels = {
|
||||||
'is_business': ''
|
'is_business': ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ask_vat_id(self):
|
||||||
|
return self.event.settings.invoice_address_vatid
|
||||||
|
|
||||||
|
@property
|
||||||
|
def address_required(self):
|
||||||
|
return self.event.settings.invoice_address_required
|
||||||
|
|
||||||
|
@property
|
||||||
|
def company_required(self):
|
||||||
|
return self.event.settings.invoice_address_company_required
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.event = event = kwargs.pop('event')
|
self.event = event = kwargs.pop('event')
|
||||||
self.request = kwargs.pop('request', None)
|
self.request = kwargs.pop('request', None)
|
||||||
@@ -1039,7 +1058,11 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
kwargs['initial']['country'] = guess_country_from_request(self.request, self.event)
|
kwargs['initial']['country'] = guess_country_from_request(self.request, self.event)
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
if not event.settings.invoice_address_vatid:
|
|
||||||
|
self.fields["company"].widget.attrs["data-display-dependency"] = f'#id_{self.add_prefix("is_business")}_1'
|
||||||
|
self.fields["vat_id"].widget.attrs["data-display-dependency"] = f'#id_{self.add_prefix("is_business")}_1'
|
||||||
|
|
||||||
|
if not self.ask_vat_id:
|
||||||
del self.fields['vat_id']
|
del self.fields['vat_id']
|
||||||
elif self.validate_vat_id:
|
elif self.validate_vat_id:
|
||||||
self.fields['vat_id'].help_text = '<br/>'.join([
|
self.fields['vat_id'].help_text = '<br/>'.join([
|
||||||
@@ -1055,6 +1078,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
])
|
])
|
||||||
|
|
||||||
self.fields['country'].choices = CachedCountries()
|
self.fields['country'].choices = CachedCountries()
|
||||||
|
self.fields['country'].widget.attrs['data-country-information-url'] = reverse('js_helpers.states')
|
||||||
|
|
||||||
c = [('', pgettext_lazy('address', 'Select state'))]
|
c = [('', pgettext_lazy('address', 'Select state'))]
|
||||||
fprefix = self.prefix + '-' if self.prefix else ''
|
fprefix = self.prefix + '-' if self.prefix else ''
|
||||||
@@ -1083,18 +1107,22 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
)
|
)
|
||||||
self.fields['state'].widget.is_required = True
|
self.fields['state'].widget.is_required = True
|
||||||
|
|
||||||
|
self.fields['street'].required = False
|
||||||
|
self.fields['zipcode'].required = False
|
||||||
|
self.fields['city'].required = False
|
||||||
|
|
||||||
# Without JavaScript the VAT ID field is not hidden, so we empty the field if a country outside the EU is selected.
|
# Without JavaScript the VAT ID field is not hidden, so we empty the field if a country outside the EU is selected.
|
||||||
if cc and not ask_for_vat_id(cc) and fprefix + 'vat_id' in self.data:
|
if cc and not ask_for_vat_id(cc) and fprefix + 'vat_id' in self.data:
|
||||||
self.data = self.data.copy()
|
self.data = self.data.copy()
|
||||||
del self.data[fprefix + 'vat_id']
|
del self.data[fprefix + 'vat_id']
|
||||||
|
|
||||||
if not event.settings.invoice_address_required or self.all_optional:
|
if not self.address_required or self.all_optional:
|
||||||
for k, f in self.fields.items():
|
for k, f in self.fields.items():
|
||||||
f.required = False
|
f.required = False
|
||||||
f.widget.is_required = False
|
f.widget.is_required = False
|
||||||
if 'required' in f.widget.attrs:
|
if 'required' in f.widget.attrs:
|
||||||
del f.widget.attrs['required']
|
del f.widget.attrs['required']
|
||||||
elif event.settings.invoice_address_company_required and not self.all_optional:
|
elif self.company_required and not self.all_optional:
|
||||||
self.initial['is_business'] = True
|
self.initial['is_business'] = True
|
||||||
|
|
||||||
self.fields['is_business'].widget = BusinessBooleanRadio(require_business=True)
|
self.fields['is_business'].widget = BusinessBooleanRadio(require_business=True)
|
||||||
@@ -1111,17 +1139,18 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
label=_('Name'),
|
label=_('Name'),
|
||||||
initial=self.instance.name_parts,
|
initial=self.instance.name_parts,
|
||||||
)
|
)
|
||||||
if event.settings.invoice_address_required and not event.settings.invoice_address_company_required and not self.all_optional:
|
if self.address_required and not self.company_required and not self.all_optional:
|
||||||
if not event.settings.invoice_name_required:
|
if not event.settings.invoice_name_required:
|
||||||
self.fields['name_parts'].widget.attrs['data-required-if'] = '#id_is_business_0'
|
self.fields['name_parts'].widget.attrs['data-required-if'] = f'#id_{self.add_prefix("is_business")}_0'
|
||||||
self.fields['name_parts'].widget.attrs['data-no-required-attr'] = '1'
|
self.fields['name_parts'].widget.attrs['data-no-required-attr'] = '1'
|
||||||
self.fields['company'].widget.attrs['data-required-if'] = '#id_is_business_1'
|
self.fields['company'].widget.attrs['data-required-if'] = f'#id_{self.add_prefix("is_business")}_1'
|
||||||
|
|
||||||
if not event.settings.invoice_address_beneficiary:
|
if not event.settings.invoice_address_beneficiary:
|
||||||
del self.fields['beneficiary']
|
del self.fields['beneficiary']
|
||||||
|
|
||||||
if event.settings.invoice_address_custom_field:
|
if event.settings.invoice_address_custom_field:
|
||||||
self.fields['custom_field'].label = event.settings.invoice_address_custom_field
|
self.fields['custom_field'].label = event.settings.invoice_address_custom_field
|
||||||
|
self.fields['custom_field'].help_text = event.settings.invoice_address_custom_field_helptext
|
||||||
else:
|
else:
|
||||||
del self.fields['custom_field']
|
del self.fields['custom_field']
|
||||||
|
|
||||||
@@ -1134,16 +1163,19 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
validate_address # local import to prevent impact on startup time
|
validate_address # local import to prevent impact on startup time
|
||||||
|
|
||||||
data = self.cleaned_data
|
data = self.cleaned_data
|
||||||
|
|
||||||
if not data.get('is_business'):
|
if not data.get('is_business'):
|
||||||
data['company'] = ''
|
data['company'] = ''
|
||||||
data['vat_id'] = ''
|
data['vat_id'] = ''
|
||||||
if data.get('is_business') and not ask_for_vat_id(data.get('country')):
|
if data.get('is_business') and not ask_for_vat_id(data.get('country')):
|
||||||
data['vat_id'] = ''
|
data['vat_id'] = ''
|
||||||
if self.event.settings.invoice_address_required:
|
if self.address_validation and self.address_required and not self.all_optional:
|
||||||
if data.get('is_business') and not data.get('company'):
|
if data.get('is_business') and not data.get('company'):
|
||||||
raise ValidationError(_('You need to provide a company name.'))
|
raise ValidationError({"company": _('You need to provide a company name.')})
|
||||||
if not data.get('is_business') and not data.get('name_parts'):
|
if not data.get('is_business') and name_parts_is_empty(data.get('name_parts', {})):
|
||||||
raise ValidationError(_('You need to provide your name.'))
|
raise ValidationError(_('You need to provide your name.'))
|
||||||
|
if not data.get('street') and not data.get('zipcode') and not data.get('city'):
|
||||||
|
raise ValidationError({"street": _('This field is required.')})
|
||||||
|
|
||||||
if 'vat_id' in self.changed_data or not data.get('vat_id'):
|
if 'vat_id' in self.changed_data or not data.get('vat_id'):
|
||||||
self.instance.vat_id_validated = False
|
self.instance.vat_id_validated = False
|
||||||
@@ -1155,7 +1187,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
|
|
||||||
if all(
|
if all(
|
||||||
not v for k, v in data.items() if k not in ('is_business', 'country', 'name_parts')
|
not v for k, v in data.items() if k not in ('is_business', 'country', 'name_parts')
|
||||||
) and len(data.get('name_parts', {})) == 1:
|
) and name_parts_is_empty(data.get('name_parts', {})):
|
||||||
# Do not save the country if it is the only field set -- we don't know the user even checked it!
|
# Do not save the country if it is the only field set -- we don't know the user even checked it!
|
||||||
self.cleaned_data['country'] = ''
|
self.cleaned_data['country'] = ''
|
||||||
|
|
||||||
|
|||||||
@@ -48,10 +48,10 @@ from pretix.control.forms import SingleLanguageWidget
|
|||||||
|
|
||||||
class UserSettingsForm(forms.ModelForm):
|
class UserSettingsForm(forms.ModelForm):
|
||||||
error_messages = {
|
error_messages = {
|
||||||
'duplicate_identifier': _("There already is an account associated with this e-mail address. "
|
'duplicate_identifier': _("There already is an account associated with this email address. "
|
||||||
"Please choose a different one."),
|
"Please choose a different one."),
|
||||||
'pw_current': _("Please enter your current password if you want to change your e-mail "
|
'pw_current': _("Please enter your current password if you want to change your email address "
|
||||||
"address or password."),
|
"or password."),
|
||||||
'pw_current_wrong': _("The current password you entered was not correct."),
|
'pw_current_wrong': _("The current password you entered was not correct."),
|
||||||
'pw_mismatch': _("Please enter the same password twice"),
|
'pw_mismatch': _("Please enter the same password twice"),
|
||||||
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
|
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ from datetime import datetime
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.formats import get_format
|
from django.utils.formats import get_format
|
||||||
from django.utils.functional import lazy
|
from django.utils.functional import lazy
|
||||||
|
from django.utils.html import escape
|
||||||
from django.utils.timezone import get_current_timezone, now
|
from django.utils.timezone import get_current_timezone, now
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ def format_placeholders_help_text(placeholders, event=None):
|
|||||||
placeholders = [(k, v.render_sample(event) if event else v) for k, v in placeholders.items()]
|
placeholders = [(k, v.render_sample(event) if event else v) for k, v in placeholders.items()]
|
||||||
placeholders.sort(key=lambda x: x[0])
|
placeholders.sort(key=lambda x: x[0])
|
||||||
phs = [
|
phs = [
|
||||||
'<button type="button" class="content-placeholder" title="%s">{%s}</button>' % (_("Sample: %s") % v if v else "", k)
|
'<button type="button" class="content-placeholder" title="%s">{%s}</button>' % (escape(_("Sample: %s") % v) if v else "", escape(k))
|
||||||
for k, v in placeholders
|
for k, v in placeholders
|
||||||
]
|
]
|
||||||
return _('Available placeholders: {list}').format(
|
return _('Available placeholders: {list}').format(
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ from typing import Tuple
|
|||||||
|
|
||||||
import bleach
|
import bleach
|
||||||
import vat_moss.exchange_rates
|
import vat_moss.exchange_rates
|
||||||
from bidi.algorithm import get_display
|
from bidi import get_display
|
||||||
from django.contrib.staticfiles import finders
|
from django.contrib.staticfiles import finders
|
||||||
from django.db.models import Sum
|
from django.db.models import Sum
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
@@ -289,7 +289,7 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
|||||||
def _clean_text(self, text, tags=None):
|
def _clean_text(self, text, tags=None):
|
||||||
return self._normalize(bleach.clean(
|
return self._normalize(bleach.clean(
|
||||||
text,
|
text,
|
||||||
tags=tags or []
|
tags=set(tags) if tags else set()
|
||||||
).strip().replace('<br>', '<br />').replace('\n', '<br />\n'))
|
).strip().replace('<br>', '<br />').replace('\n', '<br />\n'))
|
||||||
|
|
||||||
|
|
||||||
@@ -461,7 +461,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
def _draw_event(self, canvas):
|
def _draw_event(self, canvas):
|
||||||
def shorten(txt):
|
def shorten(txt):
|
||||||
txt = str(txt)
|
txt = str(txt)
|
||||||
txt = bleach.clean(txt, tags=[]).strip()
|
txt = bleach.clean(txt, tags=set()).strip()
|
||||||
p = Paragraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
|
p = Paragraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
|
||||||
p_size = p.wrap(self.event_width, self.event_height)
|
p_size = p.wrap(self.event_width, self.event_height)
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import time
|
|||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.cache import cache
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.dispatch.dispatcher import NO_RECEIVERS
|
from django.dispatch.dispatcher import NO_RECEIVERS
|
||||||
|
|
||||||
@@ -50,17 +51,23 @@ class Command(BaseCommand):
|
|||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument('--tasks', action='store', type=str, help='Only execute the tasks with this name '
|
parser.add_argument('--tasks', action='store', type=str, help='Only execute the tasks with this name '
|
||||||
'(dotted path, comma separation)')
|
'(dotted path, comma separation)')
|
||||||
|
parser.add_argument('--list-tasks', action='store_true', help='Only list all tasks')
|
||||||
parser.add_argument('--exclude', action='store', type=str, help='Exclude the tasks with this name '
|
parser.add_argument('--exclude', action='store', type=str, help='Exclude the tasks with this name '
|
||||||
'(dotted path, comma separation)')
|
'(dotted path, comma separation)')
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
verbosity = int(options['verbosity'])
|
verbosity = int(options['verbosity'])
|
||||||
|
|
||||||
|
cache.set("pretix_runperiodic_executed", True, 3600 * 12)
|
||||||
|
|
||||||
if not periodic_task.receivers or periodic_task.sender_receivers_cache.get(self) is NO_RECEIVERS:
|
if not periodic_task.receivers or periodic_task.sender_receivers_cache.get(self) is NO_RECEIVERS:
|
||||||
return
|
return
|
||||||
|
|
||||||
for receiver in periodic_task._live_receivers(self):
|
for receiver in periodic_task._live_receivers(self):
|
||||||
name = f'{receiver.__module__}.{receiver.__name__}'
|
name = f'{receiver.__module__}.{receiver.__name__}'
|
||||||
|
if options['list_tasks']:
|
||||||
|
print(name)
|
||||||
|
continue
|
||||||
if options.get('tasks'):
|
if options.get('tasks'):
|
||||||
if name not in options.get('tasks').split(','):
|
if name not in options.get('tasks').split(','):
|
||||||
continue
|
continue
|
||||||
@@ -74,7 +81,7 @@ class Command(BaseCommand):
|
|||||||
try:
|
try:
|
||||||
r = receiver(signal=periodic_task, sender=self)
|
r = receiver(signal=periodic_task, sender=self)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
if isinstance(Exception, KeyboardInterrupt):
|
if isinstance(err, KeyboardInterrupt):
|
||||||
raise err
|
raise err
|
||||||
if settings.SENTRY_ENABLED:
|
if settings.SENTRY_ENABLED:
|
||||||
from sentry_sdk import capture_exception
|
from sentry_sdk import capture_exception
|
||||||
|
|||||||
@@ -37,6 +37,16 @@ class BaseMediaType:
|
|||||||
def verbose_name(self):
|
def verbose_name(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
"""
|
||||||
|
This can be:
|
||||||
|
|
||||||
|
- The name of a Font Awesome icon to represent this channel type.
|
||||||
|
- The name of a SVG icon file that is resolvable through the static file system. We recommend to design for a size of 18x14 pixels.
|
||||||
|
"""
|
||||||
|
return "circle"
|
||||||
|
|
||||||
def generate_identifier(self, organizer):
|
def generate_identifier(self, organizer):
|
||||||
if self.medium_created_by_server:
|
if self.medium_created_by_server:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
@@ -59,6 +69,7 @@ class BaseMediaType:
|
|||||||
class BarcodePlainMediaType(BaseMediaType):
|
class BarcodePlainMediaType(BaseMediaType):
|
||||||
identifier = 'barcode'
|
identifier = 'barcode'
|
||||||
verbose_name = _('Barcode / QR-Code')
|
verbose_name = _('Barcode / QR-Code')
|
||||||
|
icon = 'qrcode'
|
||||||
medium_created_by_server = True
|
medium_created_by_server = True
|
||||||
supports_giftcard = False
|
supports_giftcard = False
|
||||||
supports_orderposition = True
|
supports_orderposition = True
|
||||||
@@ -75,6 +86,7 @@ class BarcodePlainMediaType(BaseMediaType):
|
|||||||
class NfcUidMediaType(BaseMediaType):
|
class NfcUidMediaType(BaseMediaType):
|
||||||
identifier = 'nfc_uid'
|
identifier = 'nfc_uid'
|
||||||
verbose_name = _('NFC UID-based')
|
verbose_name = _('NFC UID-based')
|
||||||
|
icon = 'pretixbase/img/media/nfc_uid.svg'
|
||||||
medium_created_by_server = False
|
medium_created_by_server = False
|
||||||
supports_giftcard = True
|
supports_giftcard = True
|
||||||
supports_orderposition = False
|
supports_orderposition = False
|
||||||
@@ -114,6 +126,7 @@ class NfcUidMediaType(BaseMediaType):
|
|||||||
class NfcMf0aesMediaType(BaseMediaType):
|
class NfcMf0aesMediaType(BaseMediaType):
|
||||||
identifier = 'nfc_mf0aes'
|
identifier = 'nfc_mf0aes'
|
||||||
verbose_name = 'NFC Mifare Ultralight AES'
|
verbose_name = 'NFC Mifare Ultralight AES'
|
||||||
|
icon = 'pretixbase/img/media/nfc_secure.svg'
|
||||||
medium_created_by_server = False
|
medium_created_by_server = False
|
||||||
supports_giftcard = True
|
supports_giftcard = True
|
||||||
supports_orderposition = False
|
supports_orderposition = False
|
||||||
|
|||||||
@@ -256,8 +256,6 @@ class SecurityMiddleware(MiddlewareMixin):
|
|||||||
# pages
|
# pages
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
resp['X-XSS-Protection'] = '1'
|
|
||||||
|
|
||||||
# We just need to have a P3P, not matter whats in there
|
# We just need to have a P3P, not matter whats in there
|
||||||
# https://blogs.msdn.microsoft.com/ieinternals/2013/09/17/a-quick-look-at-p3p/
|
# https://blogs.msdn.microsoft.com/ieinternals/2013/09/17/a-quick-look-at-p3p/
|
||||||
# https://github.com/pretix/pretix/issues/765
|
# https://github.com/pretix/pretix/issues/765
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class Migration(migrations.Migration):
|
|||||||
('password', models.CharField(verbose_name='password', max_length=128)),
|
('password', models.CharField(verbose_name='password', max_length=128)),
|
||||||
('last_login', models.DateTimeField(verbose_name='last login', blank=True, null=True)),
|
('last_login', models.DateTimeField(verbose_name='last login', blank=True, null=True)),
|
||||||
('is_superuser', models.BooleanField(verbose_name='superuser status', default=False, help_text='Designates that this user has all permissions without explicitly assigning them.')),
|
('is_superuser', models.BooleanField(verbose_name='superuser status', default=False, help_text='Designates that this user has all permissions without explicitly assigning them.')),
|
||||||
('email', models.EmailField(max_length=191, blank=True, unique=True, verbose_name='E-mail', null=True,
|
('email', models.EmailField(max_length=191, blank=True, unique=True, verbose_name='Email', null=True,
|
||||||
db_index=True)),
|
db_index=True)),
|
||||||
('givenname', models.CharField(verbose_name='Given name', max_length=255, blank=True, null=True)),
|
('givenname', models.CharField(verbose_name='Given name', max_length=255, blank=True, null=True)),
|
||||||
('familyname', models.CharField(verbose_name='Family name', max_length=255, blank=True, null=True)),
|
('familyname', models.CharField(verbose_name='Family name', max_length=255, blank=True, null=True)),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from decimal import Decimal
|
|||||||
import django.core.validators
|
import django.core.validators
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import i18nfield.fields
|
import i18nfield.fields
|
||||||
|
from argon2.exceptions import HashingError
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
@@ -25,7 +26,14 @@ def initial_user(apps, schema_editor):
|
|||||||
user = User(email='admin@localhost')
|
user = User(email='admin@localhost')
|
||||||
user.is_staff = True
|
user.is_staff = True
|
||||||
user.is_superuser = True
|
user.is_superuser = True
|
||||||
user.password = make_password('admin')
|
try:
|
||||||
|
user.password = make_password('admin')
|
||||||
|
except HashingError:
|
||||||
|
raise Exception(
|
||||||
|
"Could not hash password of initial user with argon2id. If this is a system with less than 8 CPU cores, "
|
||||||
|
"you might need to disable argon2id by setting `passwords_argon2=off` in the `[django]` section of the "
|
||||||
|
"pretix.cfg configuration file."
|
||||||
|
)
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
|
|
||||||
@@ -48,7 +56,7 @@ class Migration(migrations.Migration):
|
|||||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
('email', models.EmailField(blank=True, db_index=True, max_length=254, null=True, unique=True, verbose_name='E-mail')),
|
('email', models.EmailField(blank=True, db_index=True, max_length=254, null=True, unique=True, verbose_name='Email')),
|
||||||
('givenname', models.CharField(blank=True, max_length=255, null=True, verbose_name='Given name')),
|
('givenname', models.CharField(blank=True, max_length=255, null=True, verbose_name='Given name')),
|
||||||
('familyname', models.CharField(blank=True, max_length=255, null=True, verbose_name='Family name')),
|
('familyname', models.CharField(blank=True, max_length=255, null=True, verbose_name='Family name')),
|
||||||
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
|
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
|
||||||
@@ -232,7 +240,7 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('code', models.CharField(max_length=16, verbose_name='Order code')),
|
('code', models.CharField(max_length=16, verbose_name='Order code')),
|
||||||
('status', models.CharField(choices=[('n', 'pending'), ('p', 'paid'), ('e', 'expired'), ('c', 'cancelled'), ('r', 'refunded')], max_length=3, verbose_name='Status')),
|
('status', models.CharField(choices=[('n', 'pending'), ('p', 'paid'), ('e', 'expired'), ('c', 'cancelled'), ('r', 'refunded')], max_length=3, verbose_name='Status')),
|
||||||
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-mail')),
|
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email')),
|
||||||
('locale', models.CharField(blank=True, max_length=32, null=True, verbose_name='Locale')),
|
('locale', models.CharField(blank=True, max_length=32, null=True, verbose_name='Locale')),
|
||||||
('secret', models.CharField(default=pretix.base.models.orders.generate_secret, max_length=32)),
|
('secret', models.CharField(default=pretix.base.models.orders.generate_secret, max_length=32)),
|
||||||
('datetime', models.DateTimeField(verbose_name='Date')),
|
('datetime', models.DateTimeField(verbose_name='Date')),
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('code', models.CharField(max_length=16, verbose_name='Order code')),
|
('code', models.CharField(max_length=16, verbose_name='Order code')),
|
||||||
('status', models.CharField(choices=[('n', 'pending'), ('p', 'paid'), ('e', 'expired'), ('c', 'cancelled'), ('r', 'refunded')], max_length=3, verbose_name='Status')),
|
('status', models.CharField(choices=[('n', 'pending'), ('p', 'paid'), ('e', 'expired'), ('c', 'cancelled'), ('r', 'refunded')], max_length=3, verbose_name='Status')),
|
||||||
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-mail')),
|
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email')),
|
||||||
('locale', models.CharField(blank=True, max_length=32, null=True, verbose_name='Locale')),
|
('locale', models.CharField(blank=True, max_length=32, null=True, verbose_name='Locale')),
|
||||||
('secret', models.CharField(default=pretix.base.models.orders.generate_secret, max_length=32)),
|
('secret', models.CharField(default=pretix.base.models.orders.generate_secret, max_length=32)),
|
||||||
('datetime', models.DateTimeField(verbose_name='Date')),
|
('datetime', models.DateTimeField(verbose_name='Date')),
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='On waiting list since')),
|
('created', models.DateTimeField(auto_now_add=True, verbose_name='On waiting list since')),
|
||||||
('email', models.EmailField(max_length=254, verbose_name='E-mail address')),
|
('email', models.EmailField(max_length=254, verbose_name='Email address')),
|
||||||
('locale', models.CharField(default='en', max_length=190)),
|
('locale', models.CharField(default='en', max_length=190)),
|
||||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Event', verbose_name='Event')),
|
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Event', verbose_name='Event')),
|
||||||
('item', models.ForeignKey(help_text='The product the user waits for.', on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Item', verbose_name='Product')),
|
('item', models.ForeignKey(help_text='The product the user waits for.', on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Item', verbose_name='Product')),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user