mirror of
https://github.com/pretix/pretix.git
synced 2025-12-12 04:42:28 +00:00
Compare commits
704 Commits
fix-tests
...
missing-re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35892bc186 | ||
|
|
ca8d253114 | ||
|
|
f014a9bbd3 | ||
|
|
3e5bfb44d2 | ||
|
|
1736efbdc3 | ||
|
|
5cd7959e86 | ||
|
|
b847612e1a | ||
|
|
832f4e4d68 | ||
|
|
0a23aeece4 | ||
|
|
9622bf41a1 | ||
|
|
dd4bac70be | ||
|
|
1187757b56 | ||
|
|
abe5b4ef53 | ||
|
|
b1fb391d08 | ||
|
|
a95c6d94ee | ||
|
|
4809558343 | ||
|
|
a6d1af01d2 | ||
|
|
23e58996bc | ||
|
|
15e05dae2f | ||
|
|
ce40524ae8 | ||
|
|
46aefc10f3 | ||
|
|
1f49b577f0 | ||
|
|
b3aa405bcc | ||
|
|
f29b60b3db | ||
|
|
603e7821cc | ||
|
|
ffdc73e0a3 | ||
|
|
00b4622afa | ||
|
|
045edc7cec | ||
|
|
1635118772 | ||
|
|
87c987fee5 | ||
|
|
1267bf8ba8 | ||
|
|
a8d1ed8ee1 | ||
|
|
b7736d5e82 | ||
|
|
cfefe5bfc3 | ||
|
|
f0f272b304 | ||
|
|
e8b159e6d4 | ||
|
|
b0de6815db | ||
|
|
92ceea2680 | ||
|
|
c2a9f9f76a | ||
|
|
1e1f0e5d86 | ||
|
|
9634907539 | ||
|
|
70dd688ec1 | ||
|
|
5ad0213195 | ||
|
|
c40cf45179 | ||
|
|
a72839fd0e | ||
|
|
5071db0a8b | ||
|
|
0dce6464ad | ||
|
|
e1d3f16819 | ||
|
|
6c0b266260 | ||
|
|
e4692ed746 | ||
|
|
a99bb283e2 | ||
|
|
06f09dda49 | ||
|
|
da1de7d646 | ||
|
|
63a2e2e058 | ||
|
|
5d83f70f75 | ||
|
|
f8badea1d3 | ||
|
|
d727d58bc9 | ||
|
|
c8d4815c9e | ||
|
|
c25d6988a7 | ||
|
|
89f1f61b73 | ||
|
|
ecd0cbe00d | ||
|
|
6cb9dc5b35 | ||
|
|
6846348a7f | ||
|
|
df785f8c6d | ||
|
|
bc32fe91a6 | ||
|
|
0664127e70 | ||
|
|
5cf2f1beef | ||
|
|
ff6b4ae7bf | ||
|
|
6b5fd07535 | ||
|
|
7a423ebead | ||
|
|
62c028d9ac | ||
|
|
57f5eeff82 | ||
|
|
d92e82a133 | ||
|
|
5704142fe1 | ||
|
|
8c5768912c | ||
|
|
934567c811 | ||
|
|
b16f54fad9 | ||
|
|
5dcd1f229b | ||
|
|
40e4f8a05a | ||
|
|
8d9295a628 | ||
|
|
063e1e16a3 | ||
|
|
b5733a433d | ||
|
|
454354f148 | ||
|
|
8480f45997 | ||
|
|
b909b0b4bd | ||
|
|
c4b45b7d95 | ||
|
|
3ce6030122 | ||
|
|
28e26284a6 | ||
|
|
aaf1b7776e | ||
|
|
d545b15d0d | ||
|
|
0a334d1c67 | ||
|
|
1a2fb688de | ||
|
|
c1333d6edb | ||
|
|
e3e432a9e4 | ||
|
|
36ea18970b | ||
|
|
bde2d50828 | ||
|
|
e8334df908 | ||
|
|
c8943785d7 | ||
|
|
41b878b667 | ||
|
|
4f63ba967b | ||
|
|
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 |
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.
|
||||||
@@ -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**:
|
||||||
|
|
||||||
|
|||||||
@@ -44,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
|
||||||
|
|||||||
@@ -69,6 +69,10 @@ hidden_if_available integer **DEPRECATED*
|
|||||||
hidden_if_item_available integer The internal ID of a different item, or ``null``. If
|
hidden_if_item_available integer The internal ID of a different item, or ``null``. If
|
||||||
set, this item won't be shown publicly as long as this
|
set, this item won't be shown publicly as long as this
|
||||||
other item is available.
|
other item is available.
|
||||||
|
hidden_if_item_available_mode string If ``hide`` (the default), this item is hidden in the shop
|
||||||
|
if unavailable due to the ``hidden_if_item_available`` setting.
|
||||||
|
If ``info``, the item is visible, but can't be purchased,
|
||||||
|
and a note explaining the unavailability is displayed.
|
||||||
require_voucher boolean If ``true``, this item can only be bought using a
|
require_voucher boolean If ``true``, this item can only be bought using a
|
||||||
voucher that is specifically assigned to this item.
|
voucher that is specifically assigned to this item.
|
||||||
hide_without_voucher boolean If ``true``, this item is only shown during the voucher
|
hide_without_voucher boolean If ``true``, this item is only shown during the voucher
|
||||||
@@ -239,6 +243,10 @@ meta_data object Values set fo
|
|||||||
The ``hidden_if_item_available`` attributes has been added, the ``hidden_if_available`` attribute has been
|
The ``hidden_if_item_available`` attributes has been added, the ``hidden_if_available`` attribute has been
|
||||||
deprecated.
|
deprecated.
|
||||||
|
|
||||||
|
.. versionchanged:: 2025.01
|
||||||
|
|
||||||
|
The ``hidden_if_item_available_mode`` attributes has been added.
|
||||||
|
|
||||||
Notes
|
Notes
|
||||||
-----
|
-----
|
||||||
|
|
||||||
@@ -308,6 +316,7 @@ Endpoints
|
|||||||
"available_until_mode": "hide",
|
"available_until_mode": "hide",
|
||||||
"hidden_if_available": null,
|
"hidden_if_available": null,
|
||||||
"hidden_if_item_available": null,
|
"hidden_if_item_available": null,
|
||||||
|
"hidden_if_item_available_mode": "hide",
|
||||||
"require_voucher": false,
|
"require_voucher": false,
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"allow_cancel": true,
|
"allow_cancel": true,
|
||||||
@@ -459,6 +468,7 @@ Endpoints
|
|||||||
"available_until_mode": "hide",
|
"available_until_mode": "hide",
|
||||||
"hidden_if_available": null,
|
"hidden_if_available": null,
|
||||||
"hidden_if_item_available": null,
|
"hidden_if_item_available": null,
|
||||||
|
"hidden_if_item_available_mode": "hide",
|
||||||
"require_voucher": false,
|
"require_voucher": false,
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"allow_cancel": true,
|
"allow_cancel": true,
|
||||||
@@ -589,6 +599,7 @@ Endpoints
|
|||||||
"available_until_mode": "hide",
|
"available_until_mode": "hide",
|
||||||
"hidden_if_available": null,
|
"hidden_if_available": null,
|
||||||
"hidden_if_item_available": null,
|
"hidden_if_item_available": null,
|
||||||
|
"hidden_if_item_available_mode": "hide",
|
||||||
"require_voucher": false,
|
"require_voucher": false,
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"allow_cancel": true,
|
"allow_cancel": true,
|
||||||
@@ -705,6 +716,7 @@ Endpoints
|
|||||||
"available_until_mode": "hide",
|
"available_until_mode": "hide",
|
||||||
"hidden_if_available": null,
|
"hidden_if_available": null,
|
||||||
"hidden_if_item_available": null,
|
"hidden_if_item_available": null,
|
||||||
|
"hidden_if_item_available_mode": "hide",
|
||||||
"require_voucher": false,
|
"require_voucher": false,
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"allow_cancel": true,
|
"allow_cancel": true,
|
||||||
@@ -855,6 +867,7 @@ Endpoints
|
|||||||
"available_until_mode": "hide",
|
"available_until_mode": "hide",
|
||||||
"hidden_if_available": null,
|
"hidden_if_available": null,
|
||||||
"hidden_if_item_available": null,
|
"hidden_if_item_available": null,
|
||||||
|
"hidden_if_item_available_mode": "hide",
|
||||||
"require_voucher": false,
|
"require_voucher": false,
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"generate_tickets": null,
|
"generate_tickets": null,
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -230,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
|
||||||
@@ -381,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,
|
||||||
@@ -396,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,
|
||||||
@@ -435,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)
|
||||||
@@ -562,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,
|
||||||
@@ -607,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,
|
||||||
@@ -622,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,
|
||||||
@@ -661,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
|
||||||
@@ -742,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)
|
||||||
@@ -790,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 ``web_secret`` attributes for both the order and all order positions.
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
@@ -821,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**:
|
||||||
|
|
||||||
@@ -971,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``
|
||||||
|
|
||||||
@@ -1560,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",
|
||||||
@@ -1575,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,
|
||||||
@@ -1674,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,
|
||||||
@@ -1689,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,
|
||||||
@@ -1789,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:
|
||||||
@@ -2048,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
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ Endpoints
|
|||||||
"results": [
|
"results": [
|
||||||
{
|
{
|
||||||
"identifier": "web",
|
"identifier": "web",
|
||||||
"name": {
|
"label": {
|
||||||
"en": "Online shop"
|
"en": "Online shop"
|
||||||
},
|
},
|
||||||
"type": "web",
|
"type": "web",
|
||||||
@@ -88,7 +88,7 @@ Endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"identifier": "web",
|
"identifier": "web",
|
||||||
"name": {
|
"label": {
|
||||||
"en": "Online shop"
|
"en": "Online shop"
|
||||||
},
|
},
|
||||||
"type": "web",
|
"type": "web",
|
||||||
@@ -116,7 +116,7 @@ Endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"identifier": "api.custom",
|
"identifier": "api.custom",
|
||||||
"name": {
|
"label": {
|
||||||
"en": "Custom integration"
|
"en": "Custom integration"
|
||||||
},
|
},
|
||||||
"type": "api",
|
"type": "api",
|
||||||
@@ -133,7 +133,7 @@ Endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"identifier": "api.custom",
|
"identifier": "api.custom",
|
||||||
"name": {
|
"label": {
|
||||||
"en": "Custom integration"
|
"en": "Custom integration"
|
||||||
},
|
},
|
||||||
"type": "api",
|
"type": "api",
|
||||||
@@ -178,7 +178,7 @@ Endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"identifier": "web",
|
"identifier": "web",
|
||||||
"name": {
|
"label": {
|
||||||
"en": "Online shop"
|
"en": "Online shop"
|
||||||
},
|
},
|
||||||
"type": "web",
|
"type": "web",
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Core
|
|||||||
: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_channel_types, 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
|
||||||
|
|
||||||
|
|
||||||
@@ -39,18 +41,21 @@ Frontend
|
|||||||
|
|
||||||
|
|
||||||
.. 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
|
||||||
|
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ This will automatically make pretix discover this plugin as soon as it is instal
|
|||||||
through ``pip``. During development, you can just run ``python setup.py develop`` inside
|
through ``pip``. During development, you can just run ``python setup.py develop`` inside
|
||||||
your plugin source directory to make it discoverable.
|
your plugin source directory to make it discoverable.
|
||||||
|
|
||||||
|
.. _`signals`:
|
||||||
Signals
|
Signals
|
||||||
-------
|
-------
|
||||||
|
|
||||||
@@ -153,6 +154,25 @@ in the ``installed`` method:
|
|||||||
Note that ``installed`` will *not* be called if the plugin is indirectly activated for an event
|
Note that ``installed`` will *not* be called if the plugin is indirectly activated for an event
|
||||||
because the event is created with settings copied from another event.
|
because the event is created with settings copied from another event.
|
||||||
|
|
||||||
|
.. _`registries`:
|
||||||
|
Registries
|
||||||
|
----------
|
||||||
|
|
||||||
|
Many signals in pretix are used so that plugins can "register" a class, e.g. a payment provider or a
|
||||||
|
ticket renderer.
|
||||||
|
|
||||||
|
However, for some of them (types of :ref:`Log Entries <logging>`) we use a different method to keep track of them:
|
||||||
|
In a ``Registry``, classes are collected at application startup, along with a unique key (in case
|
||||||
|
of LogEntryType, the ``action_type``) as well as which plugin registered them.
|
||||||
|
|
||||||
|
To register a class, you can use one of several decorators provided by the Registry object:
|
||||||
|
|
||||||
|
.. autoclass:: pretix.base.logentrytypes.LogEntryTypeRegistry
|
||||||
|
:members: register, new, new_from_dict
|
||||||
|
|
||||||
|
All files in which classes are registered need to be imported in the ``AppConfig.ready`` as explained
|
||||||
|
in `Signals <signals>`_ above.
|
||||||
|
|
||||||
Views
|
Views
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ To actually log an action, you can just call the ``log_action`` method on your o
|
|||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
order.log_action('pretix.event.order.canceled', user=user, data={})
|
order.log_action('pretix.event.order.comment', user=user,
|
||||||
|
data={"new_comment": "Hello, world."})
|
||||||
|
|
||||||
The positional ``action`` argument should represent the type of action and should be globally unique, we
|
The positional ``action`` argument should represent the type of action and should be globally unique, we
|
||||||
recommend to prefix it with your package name, e.g. ``paypal.payment.rejected``. The ``user`` argument is
|
recommend to prefix it with your package name, e.g. ``paypal.payment.rejected``. The ``user`` argument is
|
||||||
@@ -72,24 +73,101 @@ following ready-to-include template::
|
|||||||
{% include "pretixcontrol/includes/logs.html" with obj=order %}
|
{% include "pretixcontrol/includes/logs.html" with obj=order %}
|
||||||
|
|
||||||
We now need a way to translate the action codes like ``pretix.event.changed`` into human-readable
|
We now need a way to translate the action codes like ``pretix.event.changed`` into human-readable
|
||||||
strings. The :py:attr:`pretix.base.signals.logentry_display` signals allows you to do so. A simple
|
strings. The :py:attr:`pretix.base.logentrytypes.log_entry_types` :ref:`registry <registries>` allows you to do so. A simple
|
||||||
implementation could look like:
|
implementation could look like:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from pretix.base.signals import logentry_display
|
from pretix.base.logentrytypes import log_entry_types
|
||||||
|
|
||||||
|
|
||||||
|
@log_entry_types.new_from_dict({
|
||||||
|
'pretix.event.order.comment': _('The order\'s internal comment has been updated to: {new_comment}'),
|
||||||
|
'pretix.event.order.paid': _('The order has been marked as paid.'),
|
||||||
|
# ...
|
||||||
|
})
|
||||||
|
class CoreOrderLogEntryType(OrderLogEntryType):
|
||||||
|
pass
|
||||||
|
|
||||||
|
Please note that you always need to define your own inherited ``LogEntryType`` class in your plugin. If you would just
|
||||||
|
register an instance of a ``LogEntryType`` class defined in pretix core, it cannot be automatically detected as belonging
|
||||||
|
to your plugin, leading to confusing user interface situations.
|
||||||
|
|
||||||
|
|
||||||
|
Customizing log entry display
|
||||||
|
"""""""""""""""""""""""""""""
|
||||||
|
|
||||||
|
The base ``LogEntryType`` classes allow for varying degree of customization in their descendants.
|
||||||
|
|
||||||
|
If you want to add another log message for an existing core object (e.g. an :class:`Order <pretix.base.models.Order>`,
|
||||||
|
:class:`Item <pretix.base.models.Item>`, or :class:`Voucher <pretix.base.models.Voucher>`), you can inherit
|
||||||
|
from its predefined :class:`LogEntryType <pretix.base.logentrytypes.LogEntryType>`, e.g.
|
||||||
|
:class:`OrderLogEntryType <pretix.base.logentrytypes.OrderLogEntryType>`, and just specify a new plaintext string.
|
||||||
|
You can use format strings to insert information from the LogEntry's `data` object as shown in the section above.
|
||||||
|
|
||||||
|
If you define a new model object in your plugin, you should make sure proper object links in the user interface are
|
||||||
|
displayed for it. If your model object belongs logically to a pretix :class:`Event <pretix.base.models.Event>`, you can inherit from :class:`EventLogEntryType <pretix.base.logentrytypes.EventLogEntryType>`,
|
||||||
|
and set the ``object_link_*`` fields accordingly. ``object_link_viewname`` refers to a django url name, which needs to
|
||||||
|
accept the arguments `organizer` and `event`, containing the respective slugs, and additional arguments provided by
|
||||||
|
``object_link_args``. The default implementation of ``object_link_args`` will return an argument named by
|
||||||
|
````object_link_argname``, with a value of ``content_object.pk`` (the primary key of the model object).
|
||||||
|
If you want to customize the name displayed for the object (instead of the result of calling ``str()`` on it),
|
||||||
|
overwrite ``object_link_display_name``.
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class ItemLogEntryType(EventLogEntryType):
|
||||||
|
object_link_wrapper = _('Product {val}')
|
||||||
|
|
||||||
|
# link will be generated as reverse('control:event.item', {'organizer': ..., 'event': ..., 'item': item.pk})
|
||||||
|
object_link_viewname = 'control:event.item'
|
||||||
|
object_link_argname = 'item'
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class OrderLogEntryType(EventLogEntryType):
|
||||||
|
object_link_wrapper = _('Order {val}')
|
||||||
|
|
||||||
|
# link will be generated as reverse('control:event.order', {'organizer': ..., 'event': ..., 'code': order.code})
|
||||||
|
object_link_viewname = 'control:event.order'
|
||||||
|
|
||||||
|
def object_link_args(self, order):
|
||||||
|
return {'code': order.code}
|
||||||
|
|
||||||
|
def object_link_display_name(self, order):
|
||||||
|
return order.code
|
||||||
|
|
||||||
|
To show more sophisticated message strings, e.g. varying the message depending on information from the :class:`LogEntry <pretix.base.models.log.LogEntry>`'s
|
||||||
|
`data` object, override the `display` method:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@log_entry_types.new()
|
||||||
|
class PaypalEventLogEntryType(EventLogEntryType):
|
||||||
|
action_type = 'pretix.plugins.paypal.event'
|
||||||
|
|
||||||
|
def display(self, logentry):
|
||||||
|
event_type = logentry.parsed_data.get('event_type')
|
||||||
|
text = {
|
||||||
|
'PAYMENT.SALE.COMPLETED': _('Payment completed.'),
|
||||||
|
'PAYMENT.SALE.DENIED': _('Payment denied.'),
|
||||||
|
# ...
|
||||||
|
}.get(event_type, f"({event_type})")
|
||||||
|
return _('PayPal reported an event: {}').format(text)
|
||||||
|
|
||||||
|
.. automethod:: pretix.base.logentrytypes.LogEntryType.display
|
||||||
|
|
||||||
|
If your new model object does not belong to an :class:`Event <pretix.base.models.Event>`, you need to inherit directly from ``LogEntryType`` instead
|
||||||
|
of ``EventLogEntryType``, providing your own implementation of ``get_object_link_info`` if object links should be
|
||||||
|
displayed.
|
||||||
|
|
||||||
|
.. autoclass:: pretix.base.logentrytypes.LogEntryType
|
||||||
|
:members: get_object_link_info
|
||||||
|
|
||||||
|
|
||||||
@receiver(signal=logentry_display)
|
|
||||||
def pretixcontrol_logentry_display(sender, logentry, **kwargs):
|
|
||||||
plains = {
|
|
||||||
'pretix.event.order.paid': _('The order has been marked as paid.'),
|
|
||||||
'pretix.event.order.refunded': _('The order has been refunded.'),
|
|
||||||
'pretix.event.order.canceled': _('The order has been canceled.'),
|
|
||||||
...
|
|
||||||
}
|
|
||||||
if logentry.action_type in plains:
|
|
||||||
return plains[logentry.action_type]
|
|
||||||
|
|
||||||
Sending notifications
|
Sending notifications
|
||||||
---------------------
|
---------------------
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -96,6 +96,18 @@ attribute::
|
|||||||
<pretix-widget event="https://pretix.eu/demo/democon/" disable-iframe></pretix-widget>
|
<pretix-widget event="https://pretix.eu/demo/democon/" disable-iframe></pretix-widget>
|
||||||
|
|
||||||
|
|
||||||
|
Always show event’s info
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
If you want the widget to show the event’s info such as title, location and frontpage text, you can pass the optional
|
||||||
|
``display-event-info`` attribute with either a value of ``"false"``, ``"true"`` or ``"auto"`` – the latter being the
|
||||||
|
default if the attribute is not present at all.
|
||||||
|
|
||||||
|
Note that any other value than ``"false"`` or ``"auto"`` means ``"true"``::
|
||||||
|
|
||||||
|
<pretix-widget event="https://pretix.eu/demo/democon/" display-event-info></pretix-widget>
|
||||||
|
|
||||||
|
|
||||||
Pre-selecting a voucher
|
Pre-selecting a voucher
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
@@ -449,6 +461,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.10.*",
|
||||||
"phonenumberslite==8.13.*",
|
"phonenumberslite==8.13.*",
|
||||||
"Pillow==10.4.*",
|
"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.20.*",
|
||||||
"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.5.*",
|
||||||
"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',
|
||||||
@@ -74,11 +75,20 @@ FORMAT_MODULE_PATH = [
|
|||||||
'pretix.helpers.formats',
|
'pretix.helpers.formats',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
CORE_MODULES = {
|
||||||
|
"pretix.base",
|
||||||
|
"pretix.presale",
|
||||||
|
"pretix.control",
|
||||||
|
"pretix.plugins.checkinlists",
|
||||||
|
"pretix.plugins.reports",
|
||||||
|
}
|
||||||
|
|
||||||
ALL_LANGUAGES = [
|
ALL_LANGUAGES = [
|
||||||
('en', _('English')),
|
('en', _('English')),
|
||||||
('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)')),
|
||||||
@@ -92,6 +102,7 @@ ALL_LANGUAGES = [
|
|||||||
('el', _('Greek')),
|
('el', _('Greek')),
|
||||||
('id', _('Indonesian')),
|
('id', _('Indonesian')),
|
||||||
('it', _('Italian')),
|
('it', _('Italian')),
|
||||||
|
('ja', _('Japanese')),
|
||||||
('lv', _('Latvian')),
|
('lv', _('Latvian')),
|
||||||
('nb-no', _('Norwegian Bokmål')),
|
('nb-no', _('Norwegian Bokmål')),
|
||||||
('pl', _('Polish')),
|
('pl', _('Polish')),
|
||||||
@@ -152,6 +163,12 @@ EXTRA_LANG_INFO = {
|
|||||||
'name': 'Portuguese',
|
'name': 'Portuguese',
|
||||||
'name_local': 'Português',
|
'name_local': 'Português',
|
||||||
},
|
},
|
||||||
|
'nb-no': {
|
||||||
|
'bidi': False,
|
||||||
|
'code': 'nb-no',
|
||||||
|
'name': 'Norwegian Bokmal',
|
||||||
|
'name_local': 'norsk (bokmål)',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
django.conf.locale.LANG_INFO.update(EXTRA_LANG_INFO)
|
django.conf.locale.LANG_INFO.update(EXTRA_LANG_INFO)
|
||||||
@@ -285,7 +302,7 @@ PILLOW_FORMATS_QUESTIONS_IMAGE = ('PNG', 'GIF', 'JPEG', 'BMP', 'TIFF')
|
|||||||
FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT = (
|
FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT = (
|
||||||
".png", ".jpg", ".gif", ".jpeg", ".pdf", ".txt", ".docx", ".gif", ".svg",
|
".png", ".jpg", ".gif", ".jpeg", ".pdf", ".txt", ".docx", ".gif", ".svg",
|
||||||
".pptx", ".ppt", ".doc", ".xlsx", ".xls", ".jfif", ".heic", ".heif", ".pages",
|
".pptx", ".ppt", ".doc", ".xlsx", ".xls", ".jfif", ".heic", ".heif", ".pages",
|
||||||
".bmp", ".tif", ".tiff"
|
".bmp", ".tif", ".tiff", ".ics",
|
||||||
)
|
)
|
||||||
FILE_UPLOAD_EXTENSIONS_OTHER = FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT
|
FILE_UPLOAD_EXTENSIONS_OTHER = FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT
|
||||||
|
|
||||||
|
|||||||
@@ -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,6 +21,7 @@
|
|||||||
#
|
#
|
||||||
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
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
@@ -80,35 +81,45 @@ class SalesChannelMigrationMixin:
|
|||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
if "sales_channels" in data:
|
if "sales_channels" in data:
|
||||||
|
prefetch_related_objects([self.organizer], "sales_channels")
|
||||||
all_channels = {
|
all_channels = {
|
||||||
s.identifier for s in
|
s.identifier for s in
|
||||||
self.organizer.sales_channels.all()
|
self.organizer.sales_channels.all()
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.get("all_sales_channels") and set(data["sales_channels"]) != all_channels:
|
if data.get("all_sales_channels") and set(data["sales_channels"]) != all_channels:
|
||||||
raise ValidationError(
|
raise ValidationError({
|
||||||
"If 'all_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
"limit_sales_channels": [
|
||||||
"the list of all 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"]):
|
if data.get("limit_sales_channels") and set(data["sales_channels"]) != set(data["limit_sales_channels"]):
|
||||||
raise ValidationError(
|
raise ValidationError({
|
||||||
"If 'limit_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
"limit_sales_channels": [
|
||||||
"the same list."
|
"If 'limit_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
||||||
)
|
"the same list."
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
if data["sales_channels"] == all_channels:
|
if set(data["sales_channels"]) == all_channels:
|
||||||
data["all_sales_channels"] = True
|
data["all_sales_channels"] = True
|
||||||
data["limit_sales_channels"] = []
|
data["limit_sales_channels"] = []
|
||||||
else:
|
else:
|
||||||
data["all_sales_channels"] = False
|
data["all_sales_channels"] = False
|
||||||
data["limit_sales_channels"] = data["sales_channels"]
|
data["limit_sales_channels"] = data["sales_channels"]
|
||||||
del data["sales_channels"]
|
del data["sales_channels"]
|
||||||
|
|
||||||
|
if data.get("all_sales_channels"):
|
||||||
|
data["limit_sales_channels"] = []
|
||||||
|
|
||||||
return super().to_internal_value(data)
|
return super().to_internal_value(data)
|
||||||
|
|
||||||
def to_representation(self, value):
|
def to_representation(self, value):
|
||||||
value = super().to_representation(value)
|
value = super().to_representation(value)
|
||||||
if value.get("all_sales_channels"):
|
if value.get("all_sales_channels"):
|
||||||
|
prefetch_related_objects([self.organizer], "sales_channels")
|
||||||
value["sales_channels"] = sorted([
|
value["sales_channels"] = sorted([
|
||||||
s.identifier for s in
|
s.identifier for s in
|
||||||
self.organizer.sales_channels.all()
|
self.organizer.sales_channels.all()
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ class CartPositionCreateSerializer(BaseCartPositionCreateSerializer):
|
|||||||
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)
|
||||||
|
|
||||||
|
|||||||
@@ -26,31 +26,22 @@ 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.media import MEDIA_TYPES
|
from pretix.base.media import MEDIA_TYPES
|
||||||
from pretix.base.models import Checkin, CheckinList, SalesChannel
|
from pretix.base.models import Checkin, CheckinList
|
||||||
|
|
||||||
|
|
||||||
class CheckinListSerializer(I18nAwareModelSerializer):
|
class CheckinListSerializer(I18nAwareModelSerializer):
|
||||||
checkin_count = serializers.IntegerField(read_only=True)
|
checkin_count = serializers.IntegerField(read_only=True)
|
||||||
position_count = serializers.IntegerField(read_only=True)
|
position_count = serializers.IntegerField(read_only=True)
|
||||||
auto_checkin_sales_channels = serializers.SlugRelatedField(
|
|
||||||
slug_field="identifier",
|
|
||||||
queryset=SalesChannel.objects.none(),
|
|
||||||
required=False,
|
|
||||||
allow_empty=True,
|
|
||||||
many=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CheckinList
|
model = CheckinList
|
||||||
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):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.fields['auto_checkin_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
|
||||||
|
|
||||||
if 'subevent' in self.context['request'].query_params.getlist('expand'):
|
if 'subevent' in self.context['request'].query_params.getlist('expand'):
|
||||||
self.fields['subevent'] = SubEventSerializer(read_only=True)
|
self.fields['subevent'] = SubEventSerializer(read_only=True)
|
||||||
|
|
||||||
|
|||||||
@@ -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,6 +43,7 @@ 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
|
||||||
|
|
||||||
@@ -52,7 +53,8 @@ from pretix.api.serializers import (
|
|||||||
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 (
|
from pretix.base.models import (
|
||||||
Device, Event, SalesChannel, TaxRule, TeamAPIToken,
|
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 (
|
||||||
@@ -435,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})
|
||||||
|
|
||||||
@@ -448,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:
|
||||||
@@ -677,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):
|
||||||
@@ -771,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',
|
||||||
@@ -844,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
|
||||||
@@ -894,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',
|
||||||
@@ -913,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',
|
||||||
@@ -969,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",
|
||||||
|
]
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
||||||
'min_per_order', 'max_per_order', 'checkin_attention', 'checkin_text', 'has_variations', 'variations',
|
'min_per_order', 'max_per_order', 'checkin_attention', 'checkin_text', 'has_variations', 'variations',
|
||||||
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
|
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
|
||||||
'show_quota_left', 'hidden_if_available', 'hidden_if_item_available', 'allow_waitinglist',
|
'show_quota_left', 'hidden_if_available', 'hidden_if_item_available', 'hidden_if_item_available_mode', 'allow_waitinglist',
|
||||||
'issue_giftcard', 'meta_data',
|
'issue_giftcard', 'meta_data',
|
||||||
'require_membership', 'require_membership_types', 'require_membership_hidden', 'grant_membership_type',
|
'require_membership', 'require_membership_types', 'require_membership_hidden', 'grant_membership_type',
|
||||||
'grant_membership_duration_like_event', 'grant_membership_duration_days',
|
'grant_membership_duration_like_event', 'grant_membership_duration_days',
|
||||||
@@ -369,7 +369,7 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
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', [])
|
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:
|
if limit_sales_channels and not validated_data.get('all_sales_channels'):
|
||||||
item.limit_sales_channels.add(*limit_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)
|
||||||
@@ -441,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):
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ from pretix.base.models import (
|
|||||||
)
|
)
|
||||||
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
|
||||||
@@ -273,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):
|
||||||
@@ -470,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)
|
||||||
@@ -484,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):
|
||||||
@@ -571,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)
|
||||||
@@ -615,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):
|
||||||
@@ -726,12 +755,12 @@ 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):
|
||||||
@@ -786,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')
|
||||||
@@ -1059,7 +1088,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
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:
|
||||||
@@ -1488,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:
|
||||||
@@ -1648,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
|
||||||
@@ -297,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
|
||||||
@@ -306,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()
|
||||||
|
|||||||
@@ -87,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)
|
||||||
@@ -95,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')
|
||||||
|
|
||||||
@@ -132,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>[^/]+)/',
|
||||||
|
|||||||
@@ -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', 'auto_checkin_sales_channels'
|
'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')
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,10 @@ 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.generics import get_object_or_404
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
@@ -48,12 +51,13 @@ 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
|
||||||
@@ -235,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}
|
||||||
@@ -295,7 +299,8 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
if 'all_sales_channels' in serializer.validated_data and 'sales_channels' in serializer.validated_data:
|
if 'all_sales_channels' in serializer.validated_data and 'sales_channels' in serializer.validated_data:
|
||||||
new_event.all_sales_channels = serializer.validated_data['all_sales_channels']
|
new_event.all_sales_channels = serializer.validated_data['all_sales_channels']
|
||||||
new_event.limit_sales_channels.set(serializer.validated_data['limit_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()
|
||||||
|
|
||||||
@@ -368,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(
|
||||||
@@ -443,7 +448,7 @@ 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)
|
||||||
@@ -667,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)
|
||||||
|
|||||||
@@ -619,7 +619,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
def availability(self, request, *args, **kwargs):
|
def availability(self, request, *args, **kwargs):
|
||||||
quota = self.get_object()
|
quota = self.get_object()
|
||||||
|
|
||||||
qa = QuotaAvailability()
|
qa = QuotaAvailability(full_results=True)
|
||||||
qa.queue(quota)
|
qa.queue(quota)
|
||||||
qa.compute()
|
qa.compute()
|
||||||
avail = qa.results[quota]
|
avail = qa.results[quota]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -213,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'
|
||||||
|
|
||||||
@@ -257,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')
|
||||||
)),
|
)),
|
||||||
@@ -278,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',
|
||||||
@@ -597,7 +602,7 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
|||||||
order.status in (Order.STATUS_PAID, Order.STATUS_PENDING)
|
order.status in (Order.STATUS_PAID, Order.STATUS_PENDING)
|
||||||
and order.invoices.filter(is_cancellation=True).count() >= order.invoices.filter(is_cancellation=False).count()
|
and order.invoices.filter(is_cancellation=True).count() >= order.invoices.filter(is_cancellation=False).count()
|
||||||
)
|
)
|
||||||
if self.request.event.settings.get('invoice_generate') not in ('admin', 'user', 'paid', 'True') or not invoice_qualified(order):
|
if self.request.event.settings.get('invoice_generate') not in ('admin', 'user', 'paid', 'user_paid', 'True') or not invoice_qualified(order):
|
||||||
return Response(
|
return Response(
|
||||||
{'detail': _('You cannot generate an invoice for this order.')},
|
{'detail': _('You cannot generate an invoice for this order.')},
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
@@ -642,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
|
||||||
)
|
)
|
||||||
@@ -1091,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')
|
||||||
@@ -1110,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')
|
||||||
@@ -1134,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'
|
||||||
@@ -1222,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),
|
||||||
@@ -1253,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()
|
||||||
@@ -1825,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']
|
||||||
|
|||||||
@@ -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,74 @@ 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()
|
||||||
|
|
||||||
|
|
||||||
|
def has_event_access_permission(request, permission='can_change_event_settings'):
|
||||||
|
return (
|
||||||
|
request.user.is_authenticated and
|
||||||
|
request.user.has_event_permission(request.organizer, request.event, permission, request=request)
|
||||||
|
) or (
|
||||||
|
getattr(request, 'event_access_user', None) and
|
||||||
|
request.event_access_user.is_authenticated and
|
||||||
|
request.event_access_user.has_event_permission(request.organizer, request.event, permission,
|
||||||
|
session_key=request.event_access_parent_session_key)
|
||||||
|
)
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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,13 @@ 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:
|
|
||||||
|
# If an individual or company address is acceptable, #id_is_business_0 == individual, _1 == company.
|
||||||
|
# However, if only company addresses are acceptable, #id_is_business_0 == company and is the only choice
|
||||||
|
self.fields["company"].widget.attrs["data-display-dependency"] = f'#id_{self.add_prefix("is_business")}_{int(not self.company_required)}'
|
||||||
|
self.fields["vat_id"].widget.attrs["data-display-dependency"] = f'#id_{self.add_prefix("is_business")}_{int(not self.company_required)}'
|
||||||
|
|
||||||
|
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 +1080,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 +1109,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 +1141,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 +1165,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 +1189,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'))
|
||||||
|
|
||||||
|
|
||||||
@@ -388,6 +388,15 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
except:
|
except:
|
||||||
logger.exception("Can not resize image")
|
logger.exception("Can not resize image")
|
||||||
pass
|
pass
|
||||||
|
try:
|
||||||
|
# Valid ZUGFeRD invoices must be compliant with PDF/A-3. pretix-zugferd ensures this by passing them
|
||||||
|
# through ghost script. Unfortunately, if the logo contains transparency, this will still fail.
|
||||||
|
# I was unable to figure out a way to fix this in GhostScript, so the easy fix is to remove the
|
||||||
|
# transparency, as our invoices always have a white background anyways.
|
||||||
|
ir.remove_transparency()
|
||||||
|
except:
|
||||||
|
logger.exception("Can not remove transparency from logo")
|
||||||
|
pass
|
||||||
canvas.drawImage(ir,
|
canvas.drawImage(ir,
|
||||||
self.logo_left,
|
self.logo_left,
|
||||||
self.pagesize[1] - self.logo_height - self.logo_top,
|
self.pagesize[1] - self.logo_height - self.logo_top,
|
||||||
@@ -461,7 +470,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)
|
||||||
|
|
||||||
@@ -775,7 +784,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
|
|
||||||
for idx, gross in grossvalue_map.items():
|
for idx, gross in grossvalue_map.items():
|
||||||
rate, name = idx
|
rate, name = idx
|
||||||
if rate == 0:
|
if rate == 0 and gross == 0:
|
||||||
continue
|
continue
|
||||||
tax = taxvalue_map[idx]
|
tax = taxvalue_map[idx]
|
||||||
tdata.append([
|
tdata.append([
|
||||||
@@ -792,7 +801,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return localize(val) + ' ' + self.invoice.foreign_currency_display
|
return localize(val) + ' ' + self.invoice.foreign_currency_display
|
||||||
|
|
||||||
if len(tdata) > 1 and has_taxes:
|
if any(rate != 0 and gross != 0 for (rate, name), gross in grossvalue_map.items()) and has_taxes:
|
||||||
colwidths = [a * doc.width for a in (.25, .15, .15, .15, .3)]
|
colwidths = [a * doc.width for a in (.25, .15, .15, .15, .3)]
|
||||||
table = Table(tdata, colWidths=colwidths, repeatRows=2, hAlign=TA_LEFT)
|
table = Table(tdata, colWidths=colwidths, repeatRows=2, hAlign=TA_LEFT)
|
||||||
table.setStyle(TableStyle(tstyledata))
|
table.setStyle(TableStyle(tstyledata))
|
||||||
|
|||||||
165
src/pretix/base/logentrytype_registry.py
Normal file
165
src/pretix/base/logentrytype_registry.py
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
#
|
||||||
|
# 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 collections import defaultdict
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from pretix.base.signals import EventPluginRegistry
|
||||||
|
|
||||||
|
|
||||||
|
def make_link(a_map, wrapper, is_active=True, event=None, plugin_name=None):
|
||||||
|
if a_map:
|
||||||
|
if 'href' not in a_map:
|
||||||
|
a_map['val'] = format_html('<i>{val}</i>', **a_map)
|
||||||
|
elif is_active:
|
||||||
|
a_map['val'] = format_html('<a href="{href}">{val}</a>', **a_map)
|
||||||
|
elif event and plugin_name:
|
||||||
|
a_map['val'] = format_html(
|
||||||
|
'<i>{val}</i> <a href="{plugin_href}">'
|
||||||
|
'<span data-toggle="tooltip" title="{errmes}" class="fa fa-warning fa-fw"></span></a>',
|
||||||
|
**a_map,
|
||||||
|
errmes=_("The relevant plugin is currently not active. To activate it, click here to go to the plugin settings."),
|
||||||
|
plugin_href=reverse('control:event.settings.plugins', kwargs={
|
||||||
|
'organizer': event.organizer.slug,
|
||||||
|
'event': event.slug,
|
||||||
|
}) + '#plugin_' + plugin_name,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
a_map['val'] = format_html(
|
||||||
|
'<i>{val}</i> <span data-toggle="tooltip" title="{errmes}" class="fa fa-warning fa-fw"></span>',
|
||||||
|
**a_map,
|
||||||
|
errmes=_("The relevant plugin is currently not active."),
|
||||||
|
)
|
||||||
|
return format_html(wrapper, **a_map)
|
||||||
|
|
||||||
|
|
||||||
|
class LogEntryTypeRegistry(EventPluginRegistry):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__({'action_type': lambda o: getattr(o, 'action_type')})
|
||||||
|
|
||||||
|
def register(self, *objs):
|
||||||
|
for obj in objs:
|
||||||
|
if not isinstance(obj, LogEntryType):
|
||||||
|
raise TypeError('Entries must be derived from LogEntryType')
|
||||||
|
|
||||||
|
if obj.__module__.startswith('pretix.base.'):
|
||||||
|
raise TypeError('Must not register base classes, only derived ones')
|
||||||
|
|
||||||
|
return super().register(*objs)
|
||||||
|
|
||||||
|
def new_from_dict(self, data):
|
||||||
|
"""
|
||||||
|
Register multiple instance of a `LogEntryType` class with different `action_type`
|
||||||
|
and plain text strings, as given by the items of the specified data dictionary.
|
||||||
|
|
||||||
|
This method is designed to be used as a decorator as follows:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@log_entry_types.new_from_dict({
|
||||||
|
'pretix.event.item.added': _('The product has been created.'),
|
||||||
|
'pretix.event.item.changed': _('The product has been changed.'),
|
||||||
|
# ...
|
||||||
|
})
|
||||||
|
class CoreItemLogEntryType(ItemLogEntryType):
|
||||||
|
# ...
|
||||||
|
|
||||||
|
:param data: action types and descriptions
|
||||||
|
``{"some_action_type": "Plain text description", ...}``
|
||||||
|
"""
|
||||||
|
def reg(clz):
|
||||||
|
for action_type, plain in data.items():
|
||||||
|
self.register(clz(action_type=action_type, plain=plain))
|
||||||
|
return clz
|
||||||
|
return reg
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Registry for LogEntry types.
|
||||||
|
|
||||||
|
Each entry in this registry should be an instance of a subclass of ``LogEntryType``.
|
||||||
|
They are annotated with their ``action_type`` and the defining ``plugin``.
|
||||||
|
"""
|
||||||
|
log_entry_types = LogEntryTypeRegistry()
|
||||||
|
|
||||||
|
|
||||||
|
class LogEntryType:
|
||||||
|
"""
|
||||||
|
Base class for a type of LogEntry, identified by its action_type.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, action_type=None, plain=None):
|
||||||
|
if action_type:
|
||||||
|
self.action_type = action_type
|
||||||
|
if plain:
|
||||||
|
self.plain = plain
|
||||||
|
|
||||||
|
def display(self, logentry, data):
|
||||||
|
"""
|
||||||
|
Returns the message to be displayed for a given logentry of this type.
|
||||||
|
|
||||||
|
:return: `str` or `LazyI18nString`
|
||||||
|
"""
|
||||||
|
if hasattr(self, 'plain'):
|
||||||
|
plain = str(self.plain)
|
||||||
|
if '{' in plain:
|
||||||
|
data = defaultdict(lambda: '?', data)
|
||||||
|
return plain.format_map(data)
|
||||||
|
else:
|
||||||
|
return plain
|
||||||
|
|
||||||
|
def get_object_link_info(self, logentry) -> Optional[dict]:
|
||||||
|
"""
|
||||||
|
Return information to generate a link to the `content_object` of a given log entry.
|
||||||
|
|
||||||
|
Not implemented in the base class, causing the object link to be omitted.
|
||||||
|
|
||||||
|
:return: Dictionary with the keys ``href`` (URL to view/edit the object) and
|
||||||
|
``val`` (text for the anchor element)
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_object_link(self, logentry):
|
||||||
|
a_map = self.get_object_link_info(logentry)
|
||||||
|
return make_link(a_map, self.object_link_wrapper)
|
||||||
|
|
||||||
|
object_link_wrapper = '{val}'
|
||||||
|
|
||||||
|
def shred_pii(self, logentry):
|
||||||
|
"""
|
||||||
|
To be used for shredding personally identified information contained in the data field of a LogEntry of this
|
||||||
|
type.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class NoOpShredderMixin:
|
||||||
|
def shred_pii(self, logentry):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ClearDataShredderMixin:
|
||||||
|
def shred_pii(self, logentry):
|
||||||
|
logentry.data = None
|
||||||
147
src/pretix/base/logentrytypes.py
Normal file
147
src/pretix/base/logentrytypes.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
#
|
||||||
|
# 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 typing import Optional
|
||||||
|
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
|
|
||||||
|
from pretix.base.models import (
|
||||||
|
Discount, Item, ItemCategory, Order, Question, Quota, SubEvent, TaxRule,
|
||||||
|
Voucher,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .logentrytype_registry import ( # noqa
|
||||||
|
ClearDataShredderMixin, LogEntryType, NoOpShredderMixin, log_entry_types,
|
||||||
|
make_link, LogEntryTypeRegistry,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EventLogEntryType(LogEntryType):
|
||||||
|
"""
|
||||||
|
Base class for any `LogEntry` type whose `content_object` is either an `Event` itself or belongs to a specific `Event`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_object_link_info(self, logentry) -> Optional[dict]:
|
||||||
|
if hasattr(self, 'object_link_viewname'):
|
||||||
|
content = logentry.content_object
|
||||||
|
if not content:
|
||||||
|
if logentry.content_type_id:
|
||||||
|
return {
|
||||||
|
'val': _('(deleted)'),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
if hasattr(self, 'content_type') and not isinstance(content, self.content_type):
|
||||||
|
return
|
||||||
|
|
||||||
|
return {
|
||||||
|
'href': reverse(self.object_link_viewname, kwargs={
|
||||||
|
'event': logentry.event.slug,
|
||||||
|
'organizer': logentry.event.organizer.slug,
|
||||||
|
**self.object_link_args(content),
|
||||||
|
}),
|
||||||
|
'val': self.object_link_display_name(logentry.content_object),
|
||||||
|
}
|
||||||
|
|
||||||
|
def object_link_args(self, content_object):
|
||||||
|
"""Return the kwargs for the url used in a link to content_object."""
|
||||||
|
if hasattr(self, 'object_link_argname'):
|
||||||
|
return {self.object_link_argname: content_object.pk}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def object_link_display_name(self, content_object):
|
||||||
|
"""Return the display name to refer to content_object in the user interface."""
|
||||||
|
return str(content_object)
|
||||||
|
|
||||||
|
|
||||||
|
class OrderLogEntryType(EventLogEntryType):
|
||||||
|
object_link_wrapper = _('Order {val}')
|
||||||
|
object_link_viewname = 'control:event.order'
|
||||||
|
content_type = Order
|
||||||
|
|
||||||
|
def object_link_args(self, order):
|
||||||
|
return {'code': order.code}
|
||||||
|
|
||||||
|
def object_link_display_name(self, order):
|
||||||
|
return order.code
|
||||||
|
|
||||||
|
|
||||||
|
class VoucherLogEntryType(EventLogEntryType):
|
||||||
|
object_link_wrapper = _('Voucher {val}…')
|
||||||
|
object_link_viewname = 'control:event.voucher'
|
||||||
|
object_link_argname = 'voucher'
|
||||||
|
content_type = Voucher
|
||||||
|
|
||||||
|
def object_link_display_name(self, voucher):
|
||||||
|
if len(voucher.code) > 6:
|
||||||
|
return voucher.code[:6] + "…"
|
||||||
|
return voucher.code
|
||||||
|
|
||||||
|
|
||||||
|
class ItemLogEntryType(EventLogEntryType):
|
||||||
|
object_link_wrapper = _('Product {val}')
|
||||||
|
object_link_viewname = 'control:event.item'
|
||||||
|
object_link_argname = 'item'
|
||||||
|
content_type = Item
|
||||||
|
|
||||||
|
|
||||||
|
class SubEventLogEntryType(EventLogEntryType):
|
||||||
|
object_link_wrapper = pgettext_lazy('subevent', 'Date {val}')
|
||||||
|
object_link_viewname = 'control:event.subevent'
|
||||||
|
object_link_argname = 'subevent'
|
||||||
|
content_type = SubEvent
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaLogEntryType(EventLogEntryType):
|
||||||
|
object_link_wrapper = _('Quota {val}')
|
||||||
|
object_link_viewname = 'control:event.items.quotas.show'
|
||||||
|
object_link_argname = 'quota'
|
||||||
|
content_type = Quota
|
||||||
|
|
||||||
|
|
||||||
|
class DiscountLogEntryType(EventLogEntryType):
|
||||||
|
object_link_wrapper = _('Discount {val}')
|
||||||
|
object_link_viewname = 'control:event.items.discounts.edit'
|
||||||
|
object_link_argname = 'discount'
|
||||||
|
content_type = Discount
|
||||||
|
|
||||||
|
|
||||||
|
class ItemCategoryLogEntryType(EventLogEntryType):
|
||||||
|
object_link_wrapper = _('Category {val}')
|
||||||
|
object_link_viewname = 'control:event.items.categories.edit'
|
||||||
|
object_link_argname = 'category'
|
||||||
|
content_type = ItemCategory
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionLogEntryType(EventLogEntryType):
|
||||||
|
object_link_wrapper = _('Question {val}')
|
||||||
|
object_link_viewname = 'control:event.items.questions.show'
|
||||||
|
object_link_argname = 'question'
|
||||||
|
content_type = Question
|
||||||
|
|
||||||
|
|
||||||
|
class TaxRuleLogEntryType(EventLogEntryType):
|
||||||
|
object_link_wrapper = _('Tax rule {val}')
|
||||||
|
object_link_viewname = 'control:event.settings.tax.edit'
|
||||||
|
object_link_argname = 'rule'
|
||||||
|
content_type = TaxRule
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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')),
|
||||||
|
|||||||
@@ -35,7 +35,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')),
|
||||||
|
|||||||
@@ -163,7 +163,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')),
|
||||||
('action_type', models.CharField(max_length=255)),
|
('action_type', models.CharField(max_length=255)),
|
||||||
('method', models.CharField(choices=[('mail', 'E-mail')], max_length=255)),
|
('method', models.CharField(choices=[('mail', 'Email')], max_length=255)),
|
||||||
('event', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
|
('event', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
|
||||||
to='pretixbase.Event')),
|
to='pretixbase.Event')),
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
|||||||
@@ -21,7 +21,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')),
|
||||||
('action_type', models.CharField(max_length=255)),
|
('action_type', models.CharField(max_length=255)),
|
||||||
('method', models.CharField(choices=[('mail', 'E-mail')], max_length=255)),
|
('method', models.CharField(choices=[('mail', 'Email')], max_length=255)),
|
||||||
('event', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Event')),
|
('event', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Event')),
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
('enabled', models.BooleanField(default=True)),
|
('enabled', models.BooleanField(default=True)),
|
||||||
|
|||||||
18
src/pretix/base/migrations/0269_order_api_meta.py
Normal file
18
src/pretix/base/migrations/0269_order_api_meta.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.13 on 2024-07-17 14:03
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0268_remove_subevent_items_remove_subevent_variations_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='order',
|
||||||
|
name='api_meta',
|
||||||
|
field=models.JSONField(default=dict),
|
||||||
|
),
|
||||||
|
]
|
||||||
36
src/pretix/base/migrations/0270_historicpassword.py
Normal file
36
src/pretix/base/migrations/0270_historicpassword.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Generated by Django 4.2.15 on 2024-09-16 15:10
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("pretixbase", "0269_order_api_meta"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="HistoricPassword",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True, primary_key=True, serialize=False
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("password", models.CharField(max_length=128)),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="historic_passwords",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Generated by Django 4.2.11 on 2024-05-27 13:19
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import pretix.base.models.orders
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("pretixbase", "0270_historicpassword"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="itemcategory",
|
||||||
|
name="cross_selling_condition",
|
||||||
|
field=models.CharField(null=True, max_length=10),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="itemcategory",
|
||||||
|
name="cross_selling_mode",
|
||||||
|
field=models.CharField(null=True, max_length=5),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="itemcategory",
|
||||||
|
name="cross_selling_match_products",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
related_name="matched_by_cross_selling_categories", to="pretixbase.item"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
79
src/pretix/base/migrations/0272_printlog.py
Normal file
79
src/pretix/base/migrations/0272_printlog.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# Generated by Django 4.2.16 on 2024-09-19 10:41
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
|
||||||
|
("pretixbase", "0271_itemcategory_cross_selling"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="PrintLog",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True, primary_key=True, serialize=False
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("datetime", models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
("created", models.DateTimeField(auto_now_add=True, null=True)),
|
||||||
|
("successful", models.BooleanField(default=True)),
|
||||||
|
("source", models.CharField(max_length=255)),
|
||||||
|
("type", models.CharField(max_length=255)),
|
||||||
|
("info", models.JSONField(default=dict)),
|
||||||
|
(
|
||||||
|
"api_token",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="pretixbase.teamapitoken",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"device",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="print_logs",
|
||||||
|
to="pretixbase.device",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"oauth_application",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"position",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="print_logs",
|
||||||
|
to="pretixbase.orderposition",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="print_logs",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ("-datetime",),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Generated by Django 4.2.16 on 2024-10-29 15:03
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_autocheckin(apps, schema_editor):
|
||||||
|
CheckinList = apps.get_model("pretixbase", "CheckinList")
|
||||||
|
AutoCheckinRule = apps.get_model("autocheckin", "AutoCheckinRule")
|
||||||
|
|
||||||
|
for cl in CheckinList.objects.filter(auto_checkin_sales_channels__isnull=False).select_related("event", "event__organizer"):
|
||||||
|
sales_channels = cl.auto_checkin_sales_channels.all()
|
||||||
|
all_sales_channels = cl.event.organizer.sales_channels.all()
|
||||||
|
|
||||||
|
if "pretix.plugins.autocheckin" not in cl.event.plugins:
|
||||||
|
cl.event.plugins = cl.event.plugins + ",pretix.plugins.autocheckin"
|
||||||
|
cl.event.save()
|
||||||
|
|
||||||
|
r = AutoCheckinRule.objects.get_or_create(
|
||||||
|
list=cl,
|
||||||
|
event=cl.event,
|
||||||
|
all_products=True,
|
||||||
|
all_payment_methods=True,
|
||||||
|
defaults=dict(
|
||||||
|
mode="placed",
|
||||||
|
all_sales_channels=len(sales_channels) == len(all_sales_channels),
|
||||||
|
)
|
||||||
|
)[0]
|
||||||
|
if len(sales_channels) != len(all_sales_channels):
|
||||||
|
r.limit_sales_channels.set(sales_channels)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("pretixbase", "0272_printlog"),
|
||||||
|
("autocheckin", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
migrate_autocheckin,
|
||||||
|
migrations.RunPython.noop,
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="checkinlist",
|
||||||
|
name="auto_checkin_sales_channels",
|
||||||
|
),
|
||||||
|
]
|
||||||
41
src/pretix/base/migrations/0274_tax_codes.py
Normal file
41
src/pretix/base/migrations/0274_tax_codes.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Generated by Django 4.2.8 on 2024-07-02 10:34
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
(
|
||||||
|
"pretixbase",
|
||||||
|
"0273_remove_checkinlist_auto_checkin_sales_channels",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="invoiceline",
|
||||||
|
name="tax_code",
|
||||||
|
field=models.CharField(max_length=190, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="orderfee",
|
||||||
|
name="tax_code",
|
||||||
|
field=models.CharField(max_length=190, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="orderposition",
|
||||||
|
name="tax_code",
|
||||||
|
field=models.CharField(max_length=190, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="taxrule",
|
||||||
|
name="code",
|
||||||
|
field=models.CharField(max_length=190, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="transaction",
|
||||||
|
name="tax_code",
|
||||||
|
field=models.CharField(max_length=190, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 4.2.17 on 2025-01-13 14:55
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("pretixbase", "0274_tax_codes"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="question",
|
||||||
|
name="valid_number_max",
|
||||||
|
field=models.DecimalField(decimal_places=6, max_digits=30, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="question",
|
||||||
|
name="valid_number_min",
|
||||||
|
field=models.DecimalField(decimal_places=6, max_digits=30, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user