mirror of
https://github.com/pretix/pretix.git
synced 2025-12-05 21:32:28 +00:00
Compare commits
1216 Commits
v2.8.2
...
gha-migrat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90b5d721cb | ||
|
|
1bcf1ec26a | ||
|
|
d224b5387d | ||
|
|
27d6e49c4a | ||
|
|
50bb66a32b | ||
|
|
207149f681 | ||
|
|
e0ed25a1d3 | ||
|
|
c0c177d755 | ||
|
|
f2844ac686 | ||
|
|
d9fd4b33a0 | ||
|
|
d2415f46df | ||
|
|
ad7c745465 | ||
|
|
b260cca412 | ||
|
|
82c84a0fdc | ||
|
|
d703c4de7a | ||
|
|
ff5fbf1c15 | ||
|
|
41ff4bca7f | ||
|
|
37f45de8a5 | ||
|
|
71f01c17bd | ||
|
|
43b1df572f | ||
|
|
ecda905ceb | ||
|
|
4e59b02bb1 | ||
|
|
234bf093ff | ||
|
|
ae9bd1b6ba | ||
|
|
b84b51250f | ||
|
|
d0dd2116ca | ||
|
|
945218035e | ||
|
|
89f8436e9a | ||
|
|
4992b17966 | ||
|
|
f7d057f9fa | ||
|
|
96ba32b1bb | ||
|
|
02792e3ae8 | ||
|
|
a87fe3ef41 | ||
|
|
bb673e0cc9 | ||
|
|
0b02bcea8b | ||
|
|
1d8668aaf1 | ||
|
|
0783add3b9 | ||
|
|
2030aaf12e | ||
|
|
7ce4c30922 | ||
|
|
f1e772c829 | ||
|
|
31d1fc31cd | ||
|
|
597211d83a | ||
|
|
17679d4304 | ||
|
|
0fb70c78a9 | ||
|
|
e254e90e49 | ||
|
|
9c6e5f025d | ||
|
|
3c86532218 | ||
|
|
3834ae566f | ||
|
|
6766b2b19e | ||
|
|
b6d2f67c7c | ||
|
|
e70f593a94 | ||
|
|
ed5726fc0c | ||
|
|
5400d26c60 | ||
|
|
0bb6104532 | ||
|
|
16aa403735 | ||
|
|
1c279a92a7 | ||
|
|
35985dcb11 | ||
|
|
b0dcbe31fa | ||
|
|
b3c3ee3b22 | ||
|
|
aab340fd87 | ||
|
|
1871324ef4 | ||
|
|
d799d560b7 | ||
|
|
01e2851a76 | ||
|
|
ef2a4244ed | ||
|
|
55539dc8e5 | ||
|
|
ef303bfcc4 | ||
|
|
fff9ac04a9 | ||
|
|
76d27fbfaa | ||
|
|
2b1123b487 | ||
|
|
3607d8706d | ||
|
|
31fdf8721b | ||
|
|
128a1f349a | ||
|
|
7d432f0639 | ||
|
|
1ffc799c4d | ||
|
|
25dd8f2e2f | ||
|
|
b121596e4b | ||
|
|
cf835df62e | ||
|
|
7a3b7d4f02 | ||
|
|
b151d8f455 | ||
|
|
06de74d877 | ||
|
|
2ae9e3e0d9 | ||
|
|
0c0fe58bbf | ||
|
|
7b1e1a48ef | ||
|
|
c7dd50de0d | ||
|
|
a1caa65776 | ||
|
|
260973345d | ||
|
|
2c9b2620ea | ||
|
|
909c80e710 | ||
|
|
5a218ae6a9 | ||
|
|
b498d45621 | ||
|
|
b02196434b | ||
|
|
c0edce7760 | ||
|
|
cc46d55f5e | ||
|
|
ea8abb8dab | ||
|
|
f765d094b4 | ||
|
|
86f222870d | ||
|
|
19b5270d76 | ||
|
|
db76b9b0ef | ||
|
|
d23e53873f | ||
|
|
c116a4b998 | ||
|
|
2471d4bca5 | ||
|
|
8e04dbdcca | ||
|
|
0928358396 | ||
|
|
23f783c15c | ||
|
|
edae96c84f | ||
|
|
242ebdfae9 | ||
|
|
0ee502abec | ||
|
|
29cb1e93d8 | ||
|
|
c89242855c | ||
|
|
61a1368ed2 | ||
|
|
ac3e00fa03 | ||
|
|
d9d0f7b6f3 | ||
|
|
ad5e2df3be | ||
|
|
ec34561815 | ||
|
|
e1540b1648 | ||
|
|
a6b265455d | ||
|
|
8a6334bd86 | ||
|
|
173a23722a | ||
|
|
ab8eb2a34d | ||
|
|
30dcda616b | ||
|
|
3eafec9d6e | ||
|
|
a5910016fd | ||
|
|
0a49b93b26 | ||
|
|
7449bea836 | ||
|
|
0fc4478332 | ||
|
|
0df4a6e7ed | ||
|
|
a37cd380c8 | ||
|
|
11b2bd8887 | ||
|
|
8986db0975 | ||
|
|
2921611cb1 | ||
|
|
785fb29513 | ||
|
|
81c3d7fa17 | ||
|
|
8ff963698d | ||
|
|
6da63e0169 | ||
|
|
f84903ae27 | ||
|
|
a0a7859b33 | ||
|
|
af23d6e4bf | ||
|
|
7e9c9beace | ||
|
|
ac2fc2de5c | ||
|
|
45e548873e | ||
|
|
f484eb65df | ||
|
|
027a785ab5 | ||
|
|
25b80cbb57 | ||
|
|
589fa0f9de | ||
|
|
6d2989d15a | ||
|
|
5bb27b29ae | ||
|
|
d17f8a71e6 | ||
|
|
b664cc712a | ||
|
|
d61e8a9204 | ||
|
|
f00012a63e | ||
|
|
bd238f76ce | ||
|
|
703ae97820 | ||
|
|
1a60c5ea64 | ||
|
|
1d3ac5f02f | ||
|
|
8d23d75dfd | ||
|
|
9a32668ee1 | ||
|
|
ca0407a133 | ||
|
|
1de77b0784 | ||
|
|
d0907d3dcf | ||
|
|
81cc4bd768 | ||
|
|
262639e063 | ||
|
|
dedd93fb89 | ||
|
|
45f94aee03 | ||
|
|
d36e7d033f | ||
|
|
b94bd277bf | ||
|
|
e5095185d9 | ||
|
|
d76ce47597 | ||
|
|
58717850c2 | ||
|
|
29d52d4fe5 | ||
|
|
34c9c40ddc | ||
|
|
39d05a6c40 | ||
|
|
b664222c62 | ||
|
|
1ee48a10b5 | ||
|
|
2431a8b767 | ||
|
|
af84354e51 | ||
|
|
b04de880fc | ||
|
|
11f3057f76 | ||
|
|
ba164c16f6 | ||
|
|
7ef766ddfa | ||
|
|
bcafcc7dd8 | ||
|
|
e5b7102abc | ||
|
|
3601dd6bee | ||
|
|
a1d5854fbf | ||
|
|
09544a688d | ||
|
|
58a5892cc0 | ||
|
|
c9af76b46e | ||
|
|
91753935cf | ||
|
|
23a52eb12a | ||
|
|
79ecb231f2 | ||
|
|
08de7f59a3 | ||
|
|
0de3c33bab | ||
|
|
a4ae8b0e66 | ||
|
|
be1bf81298 | ||
|
|
b7528ae1cf | ||
|
|
4f6712ccbe | ||
|
|
939335f94b | ||
|
|
c849276a35 | ||
|
|
8e9f0f07a1 | ||
|
|
389884d191 | ||
|
|
d8c2c82da7 | ||
|
|
c3ed3d4899 | ||
|
|
e9235cd433 | ||
|
|
975b6d800a | ||
|
|
ee260c8231 | ||
|
|
f7fddc05dd | ||
|
|
eaa61c7795 | ||
|
|
d4994258e6 | ||
|
|
9b50ec2d74 | ||
|
|
447b6b7fee | ||
|
|
40f763c999 | ||
|
|
6a3d05be9e | ||
|
|
766447f021 | ||
|
|
5fbeb90f00 | ||
|
|
c01cc85eda | ||
|
|
4a054da6ee | ||
|
|
583a2b6572 | ||
|
|
fbe025afb2 | ||
|
|
66e6191122 | ||
|
|
0f26f0787c | ||
|
|
62a86c9b4a | ||
|
|
07318be4c9 | ||
|
|
3c8ef2c620 | ||
|
|
a858f47220 | ||
|
|
381fa5e1cd | ||
|
|
1539eea664 | ||
|
|
3d41d1331a | ||
|
|
00848b3339 | ||
|
|
d174b11c6a | ||
|
|
a501ce496a | ||
|
|
de277cc959 | ||
|
|
12b3ae81d6 | ||
|
|
fcdb40dda0 | ||
|
|
f65cf8e86a | ||
|
|
12540238b7 | ||
|
|
398a30e33a | ||
|
|
3410640618 | ||
|
|
7b45cfccc2 | ||
|
|
33f503aea1 | ||
|
|
3fd650081b | ||
|
|
b622854be6 | ||
|
|
6d00daa9ee | ||
|
|
f27148998a | ||
|
|
4a0369cc37 | ||
|
|
76aaf61e19 | ||
|
|
dd1e5fa929 | ||
|
|
4a2516e303 | ||
|
|
cf06712eca | ||
|
|
6185d675f0 | ||
|
|
a53cd3abce | ||
|
|
ebe86a17fb | ||
|
|
d189b16ee7 | ||
|
|
d70ce4491a | ||
|
|
607ff48d70 | ||
|
|
4bab44ca85 | ||
|
|
a5cdb485d0 | ||
|
|
282ef792c4 | ||
|
|
6cd888a1dc | ||
|
|
2e5b80c83c | ||
|
|
4511110069 | ||
|
|
1af1d8c658 | ||
|
|
9f6a3f9a6a | ||
|
|
1c03d5d305 | ||
|
|
69a1fccd20 | ||
|
|
2a54aa2d83 | ||
|
|
2269c8dee0 | ||
|
|
46295ea887 | ||
|
|
e41863229b | ||
|
|
ca5a6ddba1 | ||
|
|
4d4dafb5dd | ||
|
|
9c2af952b7 | ||
|
|
dc6e425c2a | ||
|
|
5f65b9528f | ||
|
|
8957c2f106 | ||
|
|
2bbbc88a9c | ||
|
|
162b7c1b52 | ||
|
|
755f3d53b6 | ||
|
|
f6db62d6ce | ||
|
|
aa1ffc402c | ||
|
|
2c9b96f0c5 | ||
|
|
16599e242d | ||
|
|
19c13d7f38 | ||
|
|
65db8cd583 | ||
|
|
d0794d7b94 | ||
|
|
a770f5a8e7 | ||
|
|
80a3063799 | ||
|
|
34ec11ecfa | ||
|
|
a1da2eafdc | ||
|
|
6bc2175ea9 | ||
|
|
21dcb4f43d | ||
|
|
e9722bcdbd | ||
|
|
e7eb8e3111 | ||
|
|
a895d83764 | ||
|
|
b6697b838b | ||
|
|
0d8c4271a9 | ||
|
|
d226bbda5c | ||
|
|
38d0198dea | ||
|
|
0a920ac21c | ||
|
|
7acee9458d | ||
|
|
82e40ce664 | ||
|
|
4632269ac3 | ||
|
|
6d7e1ef53d | ||
|
|
3ea4cdc3b3 | ||
|
|
e4619eeca3 | ||
|
|
bb5c7c5ad7 | ||
|
|
9984fe97ba | ||
|
|
242dd24caa | ||
|
|
2482d9390a | ||
|
|
3b4923ccae | ||
|
|
8a2e4385ff | ||
|
|
e83b8ac218 | ||
|
|
b387fba5f4 | ||
|
|
da68cb618e | ||
|
|
eb11dac21e | ||
|
|
6e531ee067 | ||
|
|
c8e6daa7a1 | ||
|
|
b3e3d427cb | ||
|
|
6e88054af7 | ||
|
|
22dfa0e61d | ||
|
|
833cd32578 | ||
|
|
fd1c964c92 | ||
|
|
87b10ef055 | ||
|
|
734f65b10b | ||
|
|
0f826a6f76 | ||
|
|
35e521cc55 | ||
|
|
63c845574f | ||
|
|
5a675cc75d | ||
|
|
994dc9bf76 | ||
|
|
cc4a07e3b0 | ||
|
|
2ca88d5328 | ||
|
|
0bca9b9bf1 | ||
|
|
742d2f11be | ||
|
|
5ea5b82994 | ||
|
|
81245cf125 | ||
|
|
c6bcd05404 | ||
|
|
1999a25095 | ||
|
|
62f7c5ba0f | ||
|
|
d11b0e92f1 | ||
|
|
662bdea45b | ||
|
|
d37cc4f641 | ||
|
|
6b2bc71be9 | ||
|
|
f267940562 | ||
|
|
7140406f35 | ||
|
|
e275e2e240 | ||
|
|
75c0920f5e | ||
|
|
b6efe9ae1e | ||
|
|
a28378bac9 | ||
|
|
a940fa9eb7 | ||
|
|
332fba6168 | ||
|
|
41655532e9 | ||
|
|
6cc9801fe1 | ||
|
|
29ff5b9416 | ||
|
|
889dd651ef | ||
|
|
8c7d7a3055 | ||
|
|
ff67931c04 | ||
|
|
faa6f0e0a3 | ||
|
|
68ec37605f | ||
|
|
2ef8b89da0 | ||
|
|
dfc746ea7a | ||
|
|
661546f130 | ||
|
|
5e61342ff5 | ||
|
|
4eadfdeec2 | ||
|
|
8284a9de44 | ||
|
|
ae2e70245f | ||
|
|
a92e283a66 | ||
|
|
9e2c0d8152 | ||
|
|
57453a5b00 | ||
|
|
1ccf677ea2 | ||
|
|
0a9daf0d3a | ||
|
|
934217ee4f | ||
|
|
deff282a63 | ||
|
|
bcd687764c | ||
|
|
8d7224fecc | ||
|
|
3fff3378c0 | ||
|
|
91ae89d463 | ||
|
|
5c0d112def | ||
|
|
f7ae90811e | ||
|
|
6ec8c33ecc | ||
|
|
f991d5434f | ||
|
|
7cf1688de5 | ||
|
|
298b3c3660 | ||
|
|
5ea1c96e19 | ||
|
|
d29b8eba01 | ||
|
|
f566b353f2 | ||
|
|
c2221fad32 | ||
|
|
06a8a804f4 | ||
|
|
5832429540 | ||
|
|
7913de971c | ||
|
|
9b91e3e4f6 | ||
|
|
402730df43 | ||
|
|
c1d6d9bf1a | ||
|
|
ddbe27f351 | ||
|
|
35f2b10069 | ||
|
|
0d2a534982 | ||
|
|
a1ad00a30c | ||
|
|
07d2463960 | ||
|
|
08de722525 | ||
|
|
891e740ede | ||
|
|
87645a0b1f | ||
|
|
d7a575683e | ||
|
|
ca83a44489 | ||
|
|
40da03f979 | ||
|
|
fdd45a85f0 | ||
|
|
47579d0517 | ||
|
|
8704a7f3dd | ||
|
|
244e0695b1 | ||
|
|
8e2821b398 | ||
|
|
b738e3bd9d | ||
|
|
fa224fd17e | ||
|
|
76359c859f | ||
|
|
ff98ae3200 | ||
|
|
2ffb4edee9 | ||
|
|
902f589ee6 | ||
|
|
2a6dc22d7b | ||
|
|
4d9ec7c8e4 | ||
|
|
b84a0e93ae | ||
|
|
534f09bdc6 | ||
|
|
370aa63ead | ||
|
|
fb7e859e72 | ||
|
|
80a7c45e05 | ||
|
|
38a19bb58b | ||
|
|
19873e2a09 | ||
|
|
eb7e938af6 | ||
|
|
614c40596f | ||
|
|
4db56e218e | ||
|
|
ed9b96a41c | ||
|
|
e839dbc7d4 | ||
|
|
982fb0149d | ||
|
|
4597cb9849 | ||
|
|
9f629fc1c9 | ||
|
|
387e1b4998 | ||
|
|
84415864e5 | ||
|
|
82feca6e38 | ||
|
|
28242e52aa | ||
|
|
488ee19b11 | ||
|
|
ba4f00cfc0 | ||
|
|
eb392ebf20 | ||
|
|
3cef690779 | ||
|
|
294fc4735a | ||
|
|
6d17cad529 | ||
|
|
668380cc3f | ||
|
|
3bf8de39a0 | ||
|
|
1986cdf4b9 | ||
|
|
577729a271 | ||
|
|
9033e5b6f7 | ||
|
|
c1fa0d1559 | ||
|
|
e1a4dd6e43 | ||
|
|
089a468a5d | ||
|
|
018d345008 | ||
|
|
529e2a0286 | ||
|
|
1d36ef3c24 | ||
|
|
282ad2c869 | ||
|
|
e67ff83378 | ||
|
|
21be22e489 | ||
|
|
3da79ad36b | ||
|
|
8a17fedaa6 | ||
|
|
7d6b3e7140 | ||
|
|
f80ba365a5 | ||
|
|
8156cdd539 | ||
|
|
cd55146867 | ||
|
|
3cb7482bae | ||
|
|
99f3db04a9 | ||
|
|
352942b7d6 | ||
|
|
6d3ccc0182 | ||
|
|
49b73fc096 | ||
|
|
24b931e1c3 | ||
|
|
1c99e01af9 | ||
|
|
66183e805e | ||
|
|
d33c9332c6 | ||
|
|
2284def607 | ||
|
|
15c25a5a0d | ||
|
|
cf5ac6af4b | ||
|
|
2a929200b5 | ||
|
|
3f77d34026 | ||
|
|
a395b24b80 | ||
|
|
984ef60099 | ||
|
|
5b6f0df963 | ||
|
|
509c7d98cc | ||
|
|
3bd4959efe | ||
|
|
4faaa8e521 | ||
|
|
0e8832fd54 | ||
|
|
4faa76d9c7 | ||
|
|
8d1f9bf0f3 | ||
|
|
4afef62cbd | ||
|
|
3d5cfdd9c7 | ||
|
|
b3b1d09690 | ||
|
|
381ecd6d75 | ||
|
|
a12fea71e5 | ||
|
|
a6dd6ac537 | ||
|
|
c3041aa8c4 | ||
|
|
e275677a0a | ||
|
|
fff14c31ba | ||
|
|
a74bde60eb | ||
|
|
12b9d23efb | ||
|
|
afec39ce57 | ||
|
|
4ae22c4a1e | ||
|
|
a928d8c781 | ||
|
|
27b7a756fb | ||
|
|
7252167cc7 | ||
|
|
d6ac337bde | ||
|
|
aa9d3ab614 | ||
|
|
186fb851e6 | ||
|
|
97f7507dad | ||
|
|
e4582dd9ed | ||
|
|
090d5bfe94 | ||
|
|
b4e98e21a1 | ||
|
|
4571abe496 | ||
|
|
f86520af37 | ||
|
|
1a5b8a2e07 | ||
|
|
dddf91d3bf | ||
|
|
f88a09a78c | ||
|
|
adebcc31d4 | ||
|
|
5645a07cdc | ||
|
|
1d732e9675 | ||
|
|
61de010bbb | ||
|
|
22eb9d3493 | ||
|
|
4e8bda0c96 | ||
|
|
0c83e5a50c | ||
|
|
745d7f74a1 | ||
|
|
8c86169d3f | ||
|
|
3c87272fdc | ||
|
|
f1142560f6 | ||
|
|
d46278f04f | ||
|
|
098b7363e6 | ||
|
|
11d0c37415 | ||
|
|
99607aa74a | ||
|
|
d44b75388e | ||
|
|
531c8aedc2 | ||
|
|
0474651070 | ||
|
|
fd7ad3cb16 | ||
|
|
d5b932e0d9 | ||
|
|
5462e256ac | ||
|
|
46d4d97c13 | ||
|
|
8b5241d520 | ||
|
|
ee06844b0d | ||
|
|
d4a491fc1b | ||
|
|
bb3348022f | ||
|
|
a10082a46f | ||
|
|
ef555eaff1 | ||
|
|
31adc37599 | ||
|
|
1d02764be2 | ||
|
|
f80f2e9bf9 | ||
|
|
f5e7e0e309 | ||
|
|
bbc70447a2 | ||
|
|
530632d624 | ||
|
|
370130f047 | ||
|
|
14575693b8 | ||
|
|
436dcc68f2 | ||
|
|
b49e02d988 | ||
|
|
037644421b | ||
|
|
654faa5a10 | ||
|
|
53a22e0e88 | ||
|
|
f8a080d180 | ||
|
|
b013737d70 | ||
|
|
24d6816dac | ||
|
|
2eb92bef9b | ||
|
|
8d5b4d190c | ||
|
|
50dabb5b26 | ||
|
|
84fb25e4d9 | ||
|
|
ee4f75c2fb | ||
|
|
1e63eb743c | ||
|
|
09477e1431 | ||
|
|
2be49d8cab | ||
|
|
808ccfee75 | ||
|
|
485766e247 | ||
|
|
bab27f5ab6 | ||
|
|
26141da1b3 | ||
|
|
4c6cf63d68 | ||
|
|
24fa251b05 | ||
|
|
d65eaf7b95 | ||
|
|
e24a7f3887 | ||
|
|
ec66b20798 | ||
|
|
2c3c9a4ba1 | ||
|
|
4c8e1cef7f | ||
|
|
180963c787 | ||
|
|
ee96fb2ca1 | ||
|
|
5563259b11 | ||
|
|
e7b86e0deb | ||
|
|
067c3b1abc | ||
|
|
3fff04bcda | ||
|
|
150ebc7ddb | ||
|
|
2f906bae5c | ||
|
|
82d1927fe7 | ||
|
|
cde87b437a | ||
|
|
9cbf3490cf | ||
|
|
56cfdfc5b3 | ||
|
|
8564d20183 | ||
|
|
c3155ad16d | ||
|
|
7fce062c2a | ||
|
|
05311fda54 | ||
|
|
0396c851ed | ||
|
|
1d2094f5ea | ||
|
|
44b8531614 | ||
|
|
5e9610eecf | ||
|
|
1c40351b27 | ||
|
|
3a4196c087 | ||
|
|
080b5f02cb | ||
|
|
21c052918c | ||
|
|
9515249098 | ||
|
|
a0dd495cc3 | ||
|
|
46ce9904cd | ||
|
|
600310da0f | ||
|
|
3b306de1bb | ||
|
|
a2c1c69d7e | ||
|
|
f79df47b78 | ||
|
|
1703f3d636 | ||
|
|
580ae7a2e9 | ||
|
|
34703cf6de | ||
|
|
1999495638 | ||
|
|
00798d9207 | ||
|
|
ac2ff6c2aa | ||
|
|
35ac2f0d2b | ||
|
|
3432bb089a | ||
|
|
69b03b24a1 | ||
|
|
520dd3e254 | ||
|
|
d98fce3594 | ||
|
|
aa5ae8b2bd | ||
|
|
b148182240 | ||
|
|
339d7f06ed | ||
|
|
b876293453 | ||
|
|
54091b9721 | ||
|
|
06a744ea2d | ||
|
|
c24ab642ce | ||
|
|
2ab3e1d420 | ||
|
|
0e9d2cfc10 | ||
|
|
63a46d0156 | ||
|
|
6896682dd1 | ||
|
|
384e7f8fc1 | ||
|
|
d403b3d433 | ||
|
|
552e523229 | ||
|
|
95ca6da74a | ||
|
|
bce8879804 | ||
|
|
a05cce8af7 | ||
|
|
dd6f1ee70f | ||
|
|
ad91ed4a30 | ||
|
|
e3c3154469 | ||
|
|
54cd38bbaa | ||
|
|
8dbba1cfd3 | ||
|
|
0bf3ee1a6f | ||
|
|
da7e1dee3e | ||
|
|
df3cc1499f | ||
|
|
2d066e3b5e | ||
|
|
25cfa8973c | ||
|
|
3f7f04ed21 | ||
|
|
88b2f4738a | ||
|
|
ea71b1f04e | ||
|
|
03a06b6997 | ||
|
|
1cd319357d | ||
|
|
4c653aa4f1 | ||
|
|
5b4e27c47b | ||
|
|
919afb94c4 | ||
|
|
8f112f8d9a | ||
|
|
d15e37d93e | ||
|
|
47cf019079 | ||
|
|
f51521143b | ||
|
|
a0ca76f0ec | ||
|
|
d7f71948c1 | ||
|
|
7d72611851 | ||
|
|
2e9e5a8994 | ||
|
|
c8289877bb | ||
|
|
bb0ad6fb11 | ||
|
|
6dcdb87725 | ||
|
|
c2c8f36724 | ||
|
|
11c0a38665 | ||
|
|
c1c5388923 | ||
|
|
c7054fe72d | ||
|
|
13be3b7899 | ||
|
|
d9fe3e51da | ||
|
|
e381e50fbf | ||
|
|
73a323528b | ||
|
|
3a0aaa3f92 | ||
|
|
2a9c105e51 | ||
|
|
038533ad63 | ||
|
|
fcf9f0054e | ||
|
|
621b0c8c95 | ||
|
|
4a40312a32 | ||
|
|
b156efaae8 | ||
|
|
f473439f77 | ||
|
|
9ed49fb379 | ||
|
|
70e95b42fa | ||
|
|
1e0e8184c8 | ||
|
|
161f4a8132 | ||
|
|
8ac978062f | ||
|
|
619435d0f6 | ||
|
|
7e9e03cef4 | ||
|
|
dc3e76d6d3 | ||
|
|
df2c9bdce5 | ||
|
|
20a6decb3a | ||
|
|
3d31b95201 | ||
|
|
cc970caad8 | ||
|
|
845f231446 | ||
|
|
538d1b6b58 | ||
|
|
601fefc57f | ||
|
|
645b9696b7 | ||
|
|
ee5ce900d2 | ||
|
|
ab0be57106 | ||
|
|
ee6c4551b3 | ||
|
|
d63956fe25 | ||
|
|
27fb9cd8b0 | ||
|
|
ada2de8bc9 | ||
|
|
7fe3ac3b5e | ||
|
|
13cf24d7fa | ||
|
|
6d95603aa8 | ||
|
|
97fbec804b | ||
|
|
8f16145dda | ||
|
|
3f5e835367 | ||
|
|
fe92af10fb | ||
|
|
5e88dcf329 | ||
|
|
d7c8460c28 | ||
|
|
837775c8d4 | ||
|
|
2a51969b04 | ||
|
|
357972c8f8 | ||
|
|
08460f9918 | ||
|
|
277b7ad71c | ||
|
|
d0340044d3 | ||
|
|
8abaa16fab | ||
|
|
5e4a16bd44 | ||
|
|
b219faafaa | ||
|
|
3006d35622 | ||
|
|
a879a86dff | ||
|
|
926b94c88f | ||
|
|
638f1324fe | ||
|
|
be7fea4e2e | ||
|
|
adf7b6a4ed | ||
|
|
fc4c6444cb | ||
|
|
03c760c2bb | ||
|
|
27538d220e | ||
|
|
19b10e3ca4 | ||
|
|
2b18621c76 | ||
|
|
e8a2f7e349 | ||
|
|
a326ee8f75 | ||
|
|
147b90b9e8 | ||
|
|
1a461531eb | ||
|
|
f854a0f8d6 | ||
|
|
137ed5da6a | ||
|
|
6695fef3ee | ||
|
|
4a8827cc2b | ||
|
|
328b4ec633 | ||
|
|
974a84fefe | ||
|
|
94b6b86696 | ||
|
|
8fe9b35dea | ||
|
|
2c87d5ece3 | ||
|
|
f8433b5cc9 | ||
|
|
0a200de41c | ||
|
|
25a998a510 | ||
|
|
a1c7a6f2b0 | ||
|
|
1fe93ac6b7 | ||
|
|
4b2f25ce8a | ||
|
|
33cee65d7f | ||
|
|
302966808e | ||
|
|
767e679140 | ||
|
|
1a07686b79 | ||
|
|
7b4c3a00a0 | ||
|
|
7db87af754 | ||
|
|
da9c30089a | ||
|
|
89a85392a9 | ||
|
|
02b6cc5aad | ||
|
|
b3e6f44027 | ||
|
|
ac2df35db6 | ||
|
|
ac212b798d | ||
|
|
195d418e00 | ||
|
|
ba286d96cb | ||
|
|
9842fcf7da | ||
|
|
e97ae04581 | ||
|
|
346f215c50 | ||
|
|
e099fad0ca | ||
|
|
4aeada0bfb | ||
|
|
73dd94fe73 | ||
|
|
8c50b7409f | ||
|
|
b07d9d167d | ||
|
|
f22d5915ea | ||
|
|
e37d85f517 | ||
|
|
c68f715e07 | ||
|
|
db71ec92be | ||
|
|
85e7a16880 | ||
|
|
ed370fa913 | ||
|
|
f7f00fe735 | ||
|
|
37b05fb4f4 | ||
|
|
defd00ffdf | ||
|
|
9f44d7bbf5 | ||
|
|
28e27e91b2 | ||
|
|
e519a50805 | ||
|
|
b11b79b559 | ||
|
|
88378be14e | ||
|
|
8a8f8ae10a | ||
|
|
404d88a220 | ||
|
|
a48d461c22 | ||
|
|
5e59d41f6e | ||
|
|
7fb77eef34 | ||
|
|
f1321b67f9 | ||
|
|
8a6a515b6a | ||
|
|
e34511b984 | ||
|
|
fa3b40f53c | ||
|
|
b870dde301 | ||
|
|
a4d8c810ce | ||
|
|
4152ee4e50 | ||
|
|
7eb5b09b84 | ||
|
|
2f7c16d18b | ||
|
|
49bff3cc33 | ||
|
|
80c9d1ff9e | ||
|
|
dc51d51338 | ||
|
|
8aa8a2265f | ||
|
|
971275e774 | ||
|
|
91b586ce08 | ||
|
|
f10d1bd236 | ||
|
|
eafed2e213 | ||
|
|
02c81e0fa7 | ||
|
|
8e9a5e371c | ||
|
|
25345275c7 | ||
|
|
b9a911dd97 | ||
|
|
361488f3e6 | ||
|
|
b704d21e88 | ||
|
|
4bfe0e3784 | ||
|
|
d4d046ca60 | ||
|
|
fb3fc05522 | ||
|
|
fdcd750487 | ||
|
|
92754136a6 | ||
|
|
3b4d39ec27 | ||
|
|
88bb3b483c | ||
|
|
247370839b | ||
|
|
b0510f47b3 | ||
|
|
4fa086bbc5 | ||
|
|
cb03e7c843 | ||
|
|
d012d0804a | ||
|
|
3731d5f431 | ||
|
|
9f04d53564 | ||
|
|
748a389acb | ||
|
|
05a1df244b | ||
|
|
9f7d5156cc | ||
|
|
143fe6c1a6 | ||
|
|
1e3ccc4449 | ||
|
|
d09000b716 | ||
|
|
fc4572767e | ||
|
|
eeedd9a9c2 | ||
|
|
1d0c148170 | ||
|
|
cb37e7435d | ||
|
|
749ddbf21c | ||
|
|
9f6634025f | ||
|
|
ad039ae08e | ||
|
|
eeaaca574d | ||
|
|
bef15ec442 | ||
|
|
447a8b0a8c | ||
|
|
05b4d954d9 | ||
|
|
515d8c4899 | ||
|
|
82497cfb89 | ||
|
|
4ade9d39cd | ||
|
|
4360d5652b | ||
|
|
9fca3188b2 | ||
|
|
0ed48fac7f | ||
|
|
27a32173e6 | ||
|
|
81b6188777 | ||
|
|
9e85d3c94c | ||
|
|
4e58ba7594 | ||
|
|
248493dbf2 | ||
|
|
f1bd240096 | ||
|
|
0b673dc68c | ||
|
|
b2a7fe13da | ||
|
|
d28f735fca | ||
|
|
7a3b450bc6 | ||
|
|
f1ec129c0a | ||
|
|
ce6e46dfd2 | ||
|
|
f296f262e6 | ||
|
|
7f8d290ae1 | ||
|
|
ca0c0f4ae3 | ||
|
|
ac1e69f2d8 | ||
|
|
6338cc69ae | ||
|
|
39eaf3ad6a | ||
|
|
76e75bef65 | ||
|
|
a39822aedc | ||
|
|
73d5a2cec0 | ||
|
|
9f668e5fd6 | ||
|
|
1b92a891d7 | ||
|
|
827925e3c9 | ||
|
|
c0be574974 | ||
|
|
738413e8fd | ||
|
|
2c6125adeb | ||
|
|
3f7807d242 | ||
|
|
59b7f0a03d | ||
|
|
b7dea16db3 | ||
|
|
3a81706aeb | ||
|
|
ad3369b059 | ||
|
|
ab0709558d | ||
|
|
e2e64ac01d | ||
|
|
cf14dcf889 | ||
|
|
a884a25d2b | ||
|
|
8493778028 | ||
|
|
36a276c360 | ||
|
|
795c423d73 | ||
|
|
3ce9ec79f0 | ||
|
|
debc5e255b | ||
|
|
145aa0d7fd | ||
|
|
3997bdd098 | ||
|
|
a51a905512 | ||
|
|
d3ac9e8880 | ||
|
|
07402c9ea0 | ||
|
|
5f81bf55ca | ||
|
|
0120a5a930 | ||
|
|
ba555f956e | ||
|
|
5e3876ddde | ||
|
|
f5719687aa | ||
|
|
18449efcc7 | ||
|
|
3bb20d943a | ||
|
|
586e544fce | ||
|
|
3a4fc69db1 | ||
|
|
8b5d49d82f | ||
|
|
7665faa39f | ||
|
|
027d28e646 | ||
|
|
babcf66a2e | ||
|
|
20f9e88d61 | ||
|
|
9594607a8c | ||
|
|
ad3bcaf43a | ||
|
|
2c4ee3b3c7 | ||
|
|
21451db412 | ||
|
|
12b1f7d90e | ||
|
|
c3901c567e | ||
|
|
0a3eddcd5c | ||
|
|
dfa99cd325 | ||
|
|
62a040255e | ||
|
|
0e2b02c778 | ||
|
|
0f0ed90be9 | ||
|
|
b1e19d776c | ||
|
|
0bcc784aaf | ||
|
|
262fb82237 | ||
|
|
adbe959314 | ||
|
|
02d0a68d57 | ||
|
|
d6985123b4 | ||
|
|
f7a356c340 | ||
|
|
c6265b4517 | ||
|
|
1ee352e114 | ||
|
|
6c830a7d36 | ||
|
|
129c360fff | ||
|
|
b5c7ad92b6 | ||
|
|
33efd8c157 | ||
|
|
f318c8e017 | ||
|
|
319334706d | ||
|
|
1a25138bef | ||
|
|
daa5383b89 | ||
|
|
31333280d2 | ||
|
|
b78f8d70e8 | ||
|
|
a847538a2e | ||
|
|
e3ef9eba9e | ||
|
|
9e4a4402fb | ||
|
|
96ed9f5cf5 | ||
|
|
7216cebce5 | ||
|
|
a3892fd4de | ||
|
|
7718462528 | ||
|
|
60b20829f3 | ||
|
|
e11d03f418 | ||
|
|
69e4db58fd | ||
|
|
9f27a84f52 | ||
|
|
e5c204dc95 | ||
|
|
7fc7dd0163 | ||
|
|
aa99dbc830 | ||
|
|
e3a4ec93fc | ||
|
|
67da6a18a8 | ||
|
|
adc4128f9f | ||
|
|
eed217262f | ||
|
|
4bae824a03 | ||
|
|
be1a1f7995 | ||
|
|
f4b81aa032 | ||
|
|
2559439c4e | ||
|
|
fce9117dfd | ||
|
|
4c8dc8f31c | ||
|
|
b4f69fb13f | ||
|
|
2aed894bd4 | ||
|
|
2ff5416afb | ||
|
|
59f7098a70 | ||
|
|
83dd865b78 | ||
|
|
ebf411b7a0 | ||
|
|
733a4ce8f4 | ||
|
|
ad94263374 | ||
|
|
102772ec55 | ||
|
|
9a826b694f | ||
|
|
bcf8e9cd04 | ||
|
|
200ce93bb4 | ||
|
|
0582a4d9e5 | ||
|
|
1cbab04108 | ||
|
|
98c18b162f | ||
|
|
d972cd4c49 | ||
|
|
985f354293 | ||
|
|
9c23216bd1 | ||
|
|
f8bf44c262 | ||
|
|
6badfdf576 | ||
|
|
c2eba21359 | ||
|
|
5cda04a994 | ||
|
|
bc9d8f5bd8 | ||
|
|
9d6ff20191 | ||
|
|
82684e6df3 | ||
|
|
d681ae4dce | ||
|
|
213e724e18 | ||
|
|
6e0b80706c | ||
|
|
5363f4206e | ||
|
|
9bdb715874 | ||
|
|
90a9709838 | ||
|
|
669b438c91 | ||
|
|
a1353b3773 | ||
|
|
f5c611982a | ||
|
|
e9ab56486a | ||
|
|
74bc495eb7 | ||
|
|
b0b0f7474d | ||
|
|
663ff60d0a | ||
|
|
7bafb0bc76 | ||
|
|
1ab225f40a | ||
|
|
3b09456755 | ||
|
|
d919605d79 | ||
|
|
547f71aac6 | ||
|
|
191729c07a | ||
|
|
8f0a5d859d | ||
|
|
9286ca14f9 | ||
|
|
c5f9a78bdb | ||
|
|
08eb5bfb8f | ||
|
|
804e33b773 | ||
|
|
c264d8bd5b | ||
|
|
cd7be48cf2 | ||
|
|
2290b00161 | ||
|
|
9fb2d3a43b | ||
|
|
d0f3c24b2a | ||
|
|
94e2c2fa3c | ||
|
|
a0e3bbcc82 | ||
|
|
9a9de523e0 | ||
|
|
6dd1c927ef | ||
|
|
51446574e2 | ||
|
|
cfbfb74996 | ||
|
|
527a250435 | ||
|
|
87fb5f06ff | ||
|
|
661cba876f | ||
|
|
be37e3635b | ||
|
|
8bc4793f4e | ||
|
|
1604d0bf7a | ||
|
|
f042932d1d | ||
|
|
bfc6422e6e | ||
|
|
942feb09fc | ||
|
|
b372ce84a5 | ||
|
|
7c0c7202da | ||
|
|
b8bf5ce2d3 | ||
|
|
d25a9d077d | ||
|
|
4a4dad3d5c | ||
|
|
7b6b83eaf4 | ||
|
|
bf54222cac | ||
|
|
d37939bc2a | ||
|
|
4b3f6ba94b | ||
|
|
18c8933c64 | ||
|
|
6a6a84e8c8 | ||
|
|
32edf4b833 | ||
|
|
35ae7e4968 | ||
|
|
b5fb48a55f | ||
|
|
814364fbda | ||
|
|
5bff5053be | ||
|
|
32f4813d33 | ||
|
|
871a677e5e | ||
|
|
95a777516e | ||
|
|
ad8f109e77 | ||
|
|
c60d1c8a5d | ||
|
|
49288ff4e5 | ||
|
|
e90356546f | ||
|
|
a664d51dbc | ||
|
|
79ee851fae | ||
|
|
00905836dc | ||
|
|
e5f57c8ff4 | ||
|
|
c4cbfc726c | ||
|
|
869694a026 | ||
|
|
843f28d94e | ||
|
|
ce35551e97 | ||
|
|
3dae8bcdec | ||
|
|
3763edbc57 | ||
|
|
c1d89284a4 | ||
|
|
609f0b632c | ||
|
|
10aeadf835 | ||
|
|
26726043c2 | ||
|
|
34d1fcf077 | ||
|
|
e83e8cdcc0 | ||
|
|
2dd75ea252 | ||
|
|
4857cfad6e | ||
|
|
55f8e1c123 | ||
|
|
6df1960f79 | ||
|
|
3091139aab | ||
|
|
020c7faaef | ||
|
|
e9b26cc51e | ||
|
|
7948cefee1 | ||
|
|
62195f14be | ||
|
|
5a216b7be9 | ||
|
|
20d79152a6 | ||
|
|
d97a0b1941 | ||
|
|
fe6e65ccb0 | ||
|
|
9886f22b83 | ||
|
|
591ed969b8 | ||
|
|
3ab475ba6d | ||
|
|
307b1a2748 | ||
|
|
cb3f3f5084 | ||
|
|
85edbe4837 | ||
|
|
6d12b3780c | ||
|
|
a99616b1e0 | ||
|
|
a5ba7440fe | ||
|
|
a02ea45dba | ||
|
|
c1e2fb36ba | ||
|
|
b67c684969 | ||
|
|
dc42dbb837 | ||
|
|
44ffc0685e | ||
|
|
a79a156a28 | ||
|
|
fb1f6c65af | ||
|
|
8d674965d1 | ||
|
|
020122b44f | ||
|
|
f55fff6495 | ||
|
|
08316129d3 | ||
|
|
a39563aa3e | ||
|
|
a3707a962b | ||
|
|
4bb8c3991e | ||
|
|
0d5c2f6329 | ||
|
|
17c0cfb395 | ||
|
|
e55f0cdf11 | ||
|
|
a2fbc376a5 | ||
|
|
be310a4e47 | ||
|
|
35037c79cc | ||
|
|
f8bb139651 | ||
|
|
77046136f2 | ||
|
|
53a0d62d93 | ||
|
|
d994fc674a | ||
|
|
f066ed01ff | ||
|
|
fb66434fc9 | ||
|
|
3f9269f6e5 | ||
|
|
2a30a1a039 | ||
|
|
846f20692d | ||
|
|
2eb5adb6c1 | ||
|
|
491753008d | ||
|
|
6d6cd3b7cf | ||
|
|
eaf6da7272 | ||
|
|
22ce7a388d | ||
|
|
e687eee9f1 | ||
|
|
e7baca952b | ||
|
|
eef713816e | ||
|
|
5a03033255 | ||
|
|
59daeba477 | ||
|
|
c1a4b8d343 | ||
|
|
0ac98f5127 | ||
|
|
55d423af18 | ||
|
|
285694955c | ||
|
|
2352f3b811 | ||
|
|
08bfe13dc3 | ||
|
|
ec522ed7e5 | ||
|
|
197ec84f05 | ||
|
|
42af8b1602 | ||
|
|
f6a4c5271e | ||
|
|
fb53beee2d | ||
|
|
ca1c387a41 | ||
|
|
5180b5e48b | ||
|
|
a5e94bf63f | ||
|
|
09ef7aac6e | ||
|
|
d90510a1bd | ||
|
|
48790e7743 | ||
|
|
cbeaf399df | ||
|
|
779a3698a8 | ||
|
|
a5e2caf438 | ||
|
|
ce79769293 | ||
|
|
9fbb8fa781 | ||
|
|
83c551c1ba | ||
|
|
328cd9bdc5 | ||
|
|
4ce7655958 | ||
|
|
bccc73f1dc | ||
|
|
5eeba88283 | ||
|
|
4c2fe9fc20 | ||
|
|
f2ba409b03 | ||
|
|
296c2b6e28 | ||
|
|
ab27bcca42 | ||
|
|
b0a365a099 | ||
|
|
97fc095d20 | ||
|
|
cfb1cd8fdb | ||
|
|
446cf68377 | ||
|
|
b727207e79 | ||
|
|
fcc4170a4a | ||
|
|
c7f345e98e | ||
|
|
d30fbf4e6a | ||
|
|
5326aa7486 | ||
|
|
53147c0f0c | ||
|
|
fe31318413 | ||
|
|
bb4821eeb5 | ||
|
|
003d958cc5 | ||
|
|
93089d87e3 | ||
|
|
f79d17cb6a | ||
|
|
0e8db3181c | ||
|
|
23031642bd | ||
|
|
93cca34eab | ||
|
|
e29c8a1708 | ||
|
|
acfec59abc | ||
|
|
7adf203863 | ||
|
|
3c2de09216 | ||
|
|
26a96f107f | ||
|
|
819dd7eee6 | ||
|
|
ccc662228c | ||
|
|
99a2fde373 | ||
|
|
dda48d92c6 | ||
|
|
0a1429ed60 | ||
|
|
8487a5446d | ||
|
|
64833c0bab | ||
|
|
3f40525af5 | ||
|
|
7b6b3b1348 | ||
|
|
0b9f4cd739 | ||
|
|
885eefbcb0 | ||
|
|
573757e2bf | ||
|
|
c5a2bd35b7 | ||
|
|
4b65b94bd5 | ||
|
|
d716f7e014 | ||
|
|
d85ddb5bda | ||
|
|
b1db5dbb3e | ||
|
|
fed389b990 | ||
|
|
1ce613ff89 | ||
|
|
44bef85b66 | ||
|
|
49c4acefd0 | ||
|
|
61e111742d | ||
|
|
b2274039b3 | ||
|
|
4913190730 | ||
|
|
276a087fdb | ||
|
|
3639f2cea1 | ||
|
|
692e9c38f1 | ||
|
|
dd4075b2cc | ||
|
|
b549cb451a | ||
|
|
576132b2d0 | ||
|
|
e0c432d014 |
42
.github/workflows/docs.yml
vendored
Normal file
42
.github/workflows/docs.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- 'src/pretix/locale/**'
|
||||
- 'src/pretix/static/**'
|
||||
- 'src/tests/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- 'src/pretix/locale/**'
|
||||
- 'src/pretix/static/**'
|
||||
- 'src/tests/**'
|
||||
|
||||
jobs:
|
||||
spelling:
|
||||
name: Spellcheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install system packages
|
||||
run: sudo apt install enchant hunspell aspell-en
|
||||
- name: Install Dependencies
|
||||
run: pip3 install --no-use-pep517 -Ur doc/requirements.txt
|
||||
- name: Spellcheck docs
|
||||
run: make spelling
|
||||
working-directory: ./doc
|
||||
- name:
|
||||
run: '[ ! -s _build/spelling/output.txt ]'
|
||||
working-directory: ./doc
|
||||
60
.github/workflows/strings.yml
vendored
Normal file
60
.github/workflows/strings.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
name: Strings
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'src/pretix/locale/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'src/pretix/locale/**'
|
||||
|
||||
jobs:
|
||||
compile:
|
||||
runs-on: ubuntu-latest
|
||||
name: Check gettext syntax
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install system packages
|
||||
run: sudo apt install gettext
|
||||
- name: Install Dependencies
|
||||
run: pip3 install --no-use-pep517 -Ur src/requirements.txt
|
||||
- name: Compile messages
|
||||
run: python manage.py compilemessages
|
||||
working-directory: ./src
|
||||
- name: Compile jsi18n
|
||||
run: python manage.py compilejsi18n
|
||||
working-directory: ./src
|
||||
spelling:
|
||||
runs-on: ubuntu-latest
|
||||
name: Spellcheck
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install system packages
|
||||
run: sudo apt install enchant hunspell hunspell-de-de aspell-en aspell-de
|
||||
- name: Install Dependencies
|
||||
run: pip3 install --no-use-pep517 -Ur src/requirements/dev.txt
|
||||
- name: Spellcheck translations
|
||||
run: potypo
|
||||
working-directory: ./src
|
||||
55
.github/workflows/style.yml
vendored
Normal file
55
.github/workflows/style.yml
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
name: Code Style
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- 'src/pretix/locale/**'
|
||||
- 'src/pretix/static/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- 'src/pretix/locale/**'
|
||||
- 'src/pretix/static/**'
|
||||
|
||||
jobs:
|
||||
isort:
|
||||
name: isort
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install Dependencies
|
||||
run: pip3 install --no-use-pep517 -Ur src/requirements/dev.txt
|
||||
- name: Run isort
|
||||
run: isort -c -rc -df .
|
||||
working-directory: ./src
|
||||
flake:
|
||||
name: flake8
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install Dependencies
|
||||
run: pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt
|
||||
- name: Run flake8
|
||||
run: flake8 .
|
||||
working-directory: ./src
|
||||
73
.github/workflows/tests.yml
vendored
Normal file
73
.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- 'doc/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- 'doc/**'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
name: Tests
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.6, 3.7, 3.8]
|
||||
database: [sqlite, postgres, mysql]
|
||||
exclude:
|
||||
- database: mysql
|
||||
python-version: 3.7
|
||||
- database: sqlite
|
||||
python-version: 3.7
|
||||
- database: mysql
|
||||
python-version: 3.6
|
||||
- database: sqlite
|
||||
python-version: 3.6
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: getong/mariadb-action@v1.1
|
||||
with:
|
||||
mariadb version: '10.4'
|
||||
mysql database: 'pretix'
|
||||
mysql root password: ''
|
||||
if: matrix.database == 'mysql'
|
||||
- uses: harmon758/postgresql-action@v1
|
||||
with:
|
||||
postgresql version: '11'
|
||||
postgresql db: 'pretix'
|
||||
postgresql user: 'postgres'
|
||||
postgresql password: 'postgres'
|
||||
if: matrix.database == 'postgres'
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install system dependencies
|
||||
run: sudo apt install gettext mysql-client
|
||||
- name: Install Python dependencies
|
||||
run: pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt mysqlclient psycopg2-binary
|
||||
- name: Run checks
|
||||
run: python manage.py check
|
||||
working-directory: ./src
|
||||
- name: Compile
|
||||
working-directory: ./src
|
||||
run: make all compress
|
||||
- name: Run tests
|
||||
working-directory: ./src
|
||||
run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test -v -n 3 -p no:sugar --cov=./ --cov-report=xml --reruns 3 tests --maxfail=100
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
file: src/coverage.xml
|
||||
fail_ci_if_error: true
|
||||
if: matrix.database == 'postgres' && matrix.python-version == '3.8'
|
||||
@@ -5,7 +5,11 @@ tests:
|
||||
- virtualenv env
|
||||
- source env/bin/activate
|
||||
- pip install -U pip wheel setuptools
|
||||
- XDG_CACHE_HOME=/cache bash .travis.sh tests
|
||||
- XDG_CACHE_HOME=/cache pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt
|
||||
- cd src
|
||||
- python manage.py check
|
||||
- make all compress
|
||||
- py.test --reruns 3 -n 3 tests
|
||||
tags:
|
||||
- python3
|
||||
except:
|
||||
|
||||
68
.travis.sh
68
.travis.sh
@@ -1,68 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
set -x
|
||||
|
||||
echo "Executing job $1"
|
||||
|
||||
if [ "$PRETIX_CONFIG_FILE" == "tests/travis_mysql.cfg" ]; then
|
||||
mysql -u root -e 'CREATE DATABASE pretix DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;'
|
||||
pip3 install -Ur src/requirements/mysql.txt
|
||||
fi
|
||||
|
||||
if [ "$PRETIX_CONFIG_FILE" == "tests/travis_postgres.cfg" ]; then
|
||||
psql -c 'create database travis_ci_test;' -U postgres
|
||||
fi
|
||||
|
||||
if [ "$1" == "style" ]; then
|
||||
XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements.txt -r src/requirements/dev.txt
|
||||
cd src
|
||||
flake8 .
|
||||
isort -c -rc -df .
|
||||
fi
|
||||
if [ "$1" == "doctests" ]; then
|
||||
XDG_CACHE_HOME=/cache pip3 install -Ur doc/requirements.txt
|
||||
cd doc
|
||||
make doctest
|
||||
fi
|
||||
if [ "$1" == "doc-spelling" ]; then
|
||||
XDG_CACHE_HOME=/cache pip3 install -Ur doc/requirements.txt
|
||||
cd doc
|
||||
make spelling
|
||||
if [ -s _build/spelling/output.txt ]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
if [ "$1" == "translation-spelling" ]; then
|
||||
XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements/dev.txt
|
||||
cd src
|
||||
potypo
|
||||
fi
|
||||
if [ "$1" == "tests" ]; then
|
||||
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt
|
||||
cd src
|
||||
python manage.py check
|
||||
make all compress
|
||||
py.test --reruns 5 -n 3 tests
|
||||
fi
|
||||
if [ "$1" == "tests-cov" ]; then
|
||||
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt
|
||||
cd src
|
||||
python manage.py check
|
||||
make all compress
|
||||
coverage run -m py.test --reruns 5 tests && codecov
|
||||
fi
|
||||
if [ "$1" == "plugins" ]; then
|
||||
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt
|
||||
cd src
|
||||
python setup.py develop
|
||||
make all compress
|
||||
|
||||
pushd ~
|
||||
git clone --depth 1 https://github.com/pretix/pretix-cartshare.git
|
||||
cd pretix-cartshare
|
||||
python setup.py develop
|
||||
make
|
||||
py.test --reruns 5 tests
|
||||
popd
|
||||
|
||||
fi
|
||||
47
.travis.yml
47
.travis.yml
@@ -1,47 +0,0 @@
|
||||
language: python
|
||||
dist: xenial
|
||||
sudo: false
|
||||
install:
|
||||
- pip install -U pip wheel setuptools
|
||||
script:
|
||||
- bash .travis.sh $JOB
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/pip
|
||||
services:
|
||||
- mysql
|
||||
- postgresql
|
||||
matrix:
|
||||
include:
|
||||
- python: 3.7
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
|
||||
- python: 3.7
|
||||
env: JOB=tests-cov PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||
- python: 3.7
|
||||
env: JOB=style
|
||||
- python: 3.7
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
|
||||
- python: 3.7
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||
- python: 3.5
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||
- python: 3.7
|
||||
env: JOB=plugins
|
||||
- python: 3.7
|
||||
env: JOB=doc-spelling
|
||||
- python: 3.7
|
||||
env: JOB=translation-spelling
|
||||
addons:
|
||||
postgresql: "9.4"
|
||||
mariadb: '10.3'
|
||||
apt:
|
||||
packages:
|
||||
- enchant
|
||||
- myspell-de-de
|
||||
- aspell-en
|
||||
- sqlite3
|
||||
sources:
|
||||
- travis-ci/sqlite3
|
||||
branches:
|
||||
except:
|
||||
- /^weblate-.*/
|
||||
@@ -3,7 +3,7 @@ Contributing to pretix
|
||||
|
||||
Hey there and welcome to pretix!
|
||||
|
||||
We've got an contributors guide in [our documentation](https://docs.pretix.eu/en/latest/development/contribution/)
|
||||
We've got a contributors guide in [our documentation](https://docs.pretix.eu/en/latest/development/contribution/)
|
||||
together with notes on the [development setup](https://docs.pretix.eu/en/latest/development/setup.html).
|
||||
|
||||
Please note that we have a [Code of Conduct](https://docs.pretix.eu/en/latest/development/contribution/codeofconduct.html)
|
||||
|
||||
@@ -57,6 +57,8 @@ COPY deployment/docker/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY deployment/docker/production_settings.py /pretix/src/production_settings.py
|
||||
COPY src /pretix/src
|
||||
|
||||
RUN cd /pretix/src && pip3 install .
|
||||
|
||||
RUN chmod +x /usr/local/bin/pretix && \
|
||||
rm /etc/nginx/sites-enabled/default && \
|
||||
cd /pretix/src && \
|
||||
|
||||
@@ -4,11 +4,10 @@ pretix
|
||||
.. image:: https://img.shields.io/pypi/v/pretix.svg
|
||||
:target: https://pypi.python.org/pypi/pretix
|
||||
|
||||
.. image:: https://readthedocs.org/projects/pretix/badge/?version=latest
|
||||
.. image:: https://github.com/pretix/pretix/workflows/Documentation/badge.svg
|
||||
:target: https://docs.pretix.eu/en/latest/
|
||||
|
||||
.. image:: https://travis-ci.org/pretix/pretix.svg?branch=master
|
||||
:target: https://travis-ci.org/pretix/pretix
|
||||
.. image:: https://github.com/pretix/pretix/workflows/Tests/badge.svg
|
||||
|
||||
.. image:: https://codecov.io/gh/pretix/pretix/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/pretix/pretix
|
||||
|
||||
@@ -4,7 +4,7 @@ pid /var/run/nginx.pid;
|
||||
daemon off;
|
||||
|
||||
events {
|
||||
worker_connections 768;
|
||||
worker_connections 4096;
|
||||
}
|
||||
|
||||
http {
|
||||
@@ -39,7 +39,7 @@ http {
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen 80 backlog=4096 default_server;
|
||||
listen [::]:80 ipv6only=on default_server;
|
||||
server_name _;
|
||||
index index.php index.html;
|
||||
|
||||
@@ -57,6 +57,9 @@ Example::
|
||||
A comma-separated list of plugins that are not available even though they are installed.
|
||||
Defaults to an empty string.
|
||||
|
||||
``auth_backends``
|
||||
A comma-separated list of available auth backends. Defaults to ``pretix.base.auth.NativeAuthBackend``.
|
||||
|
||||
``cookie_domain``
|
||||
The cookie domain to be set. Defaults to ``None``.
|
||||
|
||||
@@ -78,6 +81,20 @@ Example::
|
||||
Enables or disables nagging staff users for leaving comments on their sessions for auditability.
|
||||
Defaults to ``off``.
|
||||
|
||||
``obligatory_2fa``
|
||||
Enables or disables obligatory usage of Two-Factor Authentication for users of the pretix backend.
|
||||
Defaults to ``False``
|
||||
|
||||
``trust_x_forwarded_for``
|
||||
Specifies whether the ``X-Forwarded-For`` header can be trusted. Only set to ``on`` if you have a reverse
|
||||
proxy that actively removes and re-adds the header to make sure the correct client IP is the first value.
|
||||
Defaults to ``off``.
|
||||
|
||||
``trust_x_forwarded_proto``
|
||||
Specifies whether the ``X-Forwarded-Proto`` header can be trusted. Only set to ``on`` if you have a reverse
|
||||
proxy that actively removes and re-adds the header to make sure the correct client IP is the first value.
|
||||
Defaults to ``off``.
|
||||
|
||||
|
||||
Locale settings
|
||||
---------------
|
||||
|
||||
@@ -125,6 +125,8 @@ Fill the configuration file ``/etc/pretix/pretix.cfg`` with the following conten
|
||||
; DO NOT change the following value, it has to be set to the location of the
|
||||
; directory *inside* the docker container
|
||||
datadir=/data
|
||||
trust_x_forwarded_for=on
|
||||
trust_x_forwarded_proto=on
|
||||
|
||||
[database]
|
||||
; Replace postgresql with mysql for MySQL
|
||||
@@ -180,6 +182,7 @@ named ``/etc/systemd/system/pretix.service`` with the following content::
|
||||
-v /var/pretix-data:/data \
|
||||
-v /etc/pretix:/etc/pretix \
|
||||
-v /var/run/redis:/var/run/redis \
|
||||
--sysctl net.core.somaxconn=4096 \
|
||||
pretix/standalone:stable all
|
||||
ExecStop=/usr/bin/docker stop %n
|
||||
|
||||
@@ -276,7 +279,7 @@ choice)::
|
||||
|
||||
Then, go to that directory and build the image::
|
||||
|
||||
$ docker build -t mypretix
|
||||
$ docker build . -t mypretix
|
||||
|
||||
You can now use that image ``mypretix`` instead of ``pretix/standalone`` in your service file (see above). Be sure
|
||||
to re-build your custom image after you pulled ``pretix/standalone`` if you want to perform an update.
|
||||
|
||||
@@ -68,7 +68,7 @@ generated key and installs the plugin from the URL we told you::
|
||||
mkdir -p /etc/ssh && \
|
||||
ssh-keyscan -t rsa -p 10022 code.rami.io >> /root/.ssh/known_hosts && \
|
||||
echo StrictHostKeyChecking=no >> /root/.ssh/config && \
|
||||
pip3 install -Ue "git+ssh://git@code.rami.io:10022/pretix/pretix-slack.git@stable#egg=pretix-slack" && \
|
||||
DJANGO_SETTINGS_MODULE=pretix.settings pip3 install -U "git+ssh://git@code.rami.io:10022/pretix/pretix-slack.git@stable#egg=pretix-slack" && \
|
||||
cd /pretix/src && \
|
||||
sudo -u pretixuser make production
|
||||
USER pretixuser
|
||||
|
||||
@@ -12,7 +12,7 @@ solution with many things readily set-up, look at :ref:`dockersmallscale`.
|
||||
get it right. If you're not feeling comfortable managing a Linux server, check out our hosting and service
|
||||
offers at `pretix.eu`_.
|
||||
|
||||
We tested this guide on the Linux distribution **Debian 8.0** but it should work very similar on other
|
||||
We tested this guide on the Linux distribution **Debian 10.0** but it should work very similar on other
|
||||
modern distributions, especially on all systemd-based ones.
|
||||
|
||||
Requirements
|
||||
@@ -85,6 +85,8 @@ Fill the configuration file ``/etc/pretix/pretix.cfg`` with the following conten
|
||||
url=https://pretix.mydomain.com
|
||||
currency=EUR
|
||||
datadir=/var/pretix/data
|
||||
trust_x_forwarded_for=on
|
||||
trust_x_forwarded_proto=on
|
||||
|
||||
[database]
|
||||
; For MySQL, replace with "mysql"
|
||||
@@ -131,7 +133,7 @@ command if you're running MySQL::
|
||||
|
||||
(venv)$ pip3 install "pretix[postgres]" gunicorn
|
||||
|
||||
Note that you need Python 3.5 or newer. You can find out your Python version using ``python -V``.
|
||||
Note that you need Python 3.6 or newer. You can find out your Python version using ``python -V``.
|
||||
|
||||
We also need to create a data directory::
|
||||
|
||||
|
||||
@@ -170,6 +170,19 @@ Date String in ISO 8601 format ``2017-12-27``
|
||||
Multi-lingual string Object of strings ``{"en": "red", "de": "rot", "de_Informal": "rot"}``
|
||||
Money String with decimal number ``"23.42"``
|
||||
Currency String with ISO 4217 code ``"EUR"``, ``"USD"``
|
||||
Relative datetime *either* String in ISO 8601 ``"2017-12-27T10:00:00.596934Z"``,
|
||||
format *or* specification of ``"RELDATE/3/12:00:00/presale_start/"``
|
||||
a relative datetime,
|
||||
constructed from a number of
|
||||
days before the base point,
|
||||
a time of day, and the base
|
||||
point.
|
||||
Relative date *either* String in ISO 8601 ``"2017-12-27"``,
|
||||
format *or* specification of ``"RELDATE/3/-/presale_start/"``
|
||||
a relative date,
|
||||
constructed from a number of
|
||||
days before the base point
|
||||
and the base point.
|
||||
===================== ============================ ===================================
|
||||
|
||||
Query parameters
|
||||
|
||||
132
doc/api/guides/custom_checkout.rst
Normal file
132
doc/api/guides/custom_checkout.rst
Normal file
@@ -0,0 +1,132 @@
|
||||
Creating an external checkout process
|
||||
=====================================
|
||||
|
||||
Occasionally, we get asked whether it is possible to just use pretix' powerful backend as a ticketing engine but use
|
||||
a fully-customized checkout process that only communicates via the API. This is possible, but with a few limitations.
|
||||
If you go down this route, you will miss out on many of pretix features and safeguards, as well as the added flexibility
|
||||
by most of pretix' plugins. We strongly recommend to talk this through with us before you decide this is the way to go.
|
||||
|
||||
However, this is really useful if you need to tightly integrate pretix into existing web applications that e.g. control
|
||||
the pricing of your products in a way that cannot be mapped to pretix' product structures.
|
||||
|
||||
Creating orders
|
||||
---------------
|
||||
|
||||
After letting your user select the products to buy in your application, you should create a new order object inside
|
||||
pretix. Below, you can see an example of such an order, but most fields are optional and there are some more features
|
||||
supported. Read :ref:`rest-orders-create` to learn more about this endpoint.
|
||||
|
||||
Please note that this endpoint assumes trustworthy input for the most part. By default, the endpoint checks that
|
||||
you do not exceed any quotas, do not sell any seats twice, or do not use any redeemed vouchers. However, it will not
|
||||
complain about violation of any other availability constraints, such as violation of time frames or minimum/maximum
|
||||
amounts of either your product or event. Bundled products will not be added in automatically and fees will not be
|
||||
calculated automatically.
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/democon/events/3vjrh/orders/ HTTP/1.1
|
||||
Host: test.pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Authorization: …
|
||||
|
||||
{
|
||||
"email": "dummy@example.org",
|
||||
"locale": "en",
|
||||
"sales_channel": "web",
|
||||
"payment_provider": "banktransfer",
|
||||
"invoice_address": {
|
||||
"is_business": false,
|
||||
"company": "Sample company",
|
||||
"name_parts": {"full_name": "John Doe"},
|
||||
"street": "Sesam Street 12",
|
||||
"zipcode": "12345",
|
||||
"city": "Sample City",
|
||||
"country": "US",
|
||||
"state": "NY",
|
||||
"internal_reference": "",
|
||||
"vat_id": ""
|
||||
},
|
||||
"positions": [
|
||||
{
|
||||
"item": 21,
|
||||
"variation": null,
|
||||
"attendee_name_parts": {
|
||||
"full_name": "Peter"
|
||||
},
|
||||
"answers": [
|
||||
{
|
||||
"question": 1,
|
||||
"answer": "23",
|
||||
"options": []
|
||||
}
|
||||
],
|
||||
"subevent": null
|
||||
}
|
||||
],
|
||||
"fees": []
|
||||
}
|
||||
|
||||
You will be returned a full order object that you can inspect, store, or use to build emails or confirmation pages for
|
||||
the user. If you don't want to do that yourself, it will also contain the URL to our confirmation page in the ``url``
|
||||
attribute. If you pass the ``"send_mail": true`` option, pretix will also send order confirmations for you.
|
||||
|
||||
Handling payments yourself
|
||||
--------------------------
|
||||
|
||||
If you want to handle payments in your application, you can either just create the orders with status "paid" or you can
|
||||
create them in "pending" state (the default) and later confirm the payment. We strongly advise to use the payment
|
||||
provider ``"manual"`` in this case to avoid interference with payment code with pretix.
|
||||
|
||||
However, it is often unfeasible to implement the payment process yourself, and it also requires you to give up a
|
||||
lot of pretix functionality, such as automatic refunds. Therefore, it is also possible to utilize pretix' native
|
||||
payment process even in this case:
|
||||
|
||||
Using pretix payment providers
|
||||
------------------------------
|
||||
|
||||
If you passed a ``payment_provider`` during order creation above, pretix will have created a payment object with state
|
||||
``created`` that you can see in the returned order object. This payment object will have an attribute ``payment_url``
|
||||
that you can use to let the user pay. For example, you could link or redirect to this page.
|
||||
|
||||
If you want the user to return to your application after the payment is complete, you can pass a query parameter
|
||||
``return_url``. To prepare your event for this, open your event in the pretix backend and go to "Settings", then
|
||||
"Plugins". Enable the plugin "Redirection from order page". Then, go to the new page "Settings", then "Redirection".
|
||||
Enter the base URL of your web application. This will allow you to redirect to pages under this base URL later on.
|
||||
For example, if you want users to be redirected to ``https://example.org/order/return?tx_id=1234``, you could now
|
||||
either enter ``https://example.org`` or ``https://example.org/order/``.
|
||||
|
||||
The user will be redirected back to your page instead of pretix' order confirmation page after the payment,
|
||||
**regardless of whether it was successful or not**. Make sure you use our API to check if the payment actually
|
||||
worked! Your final URL could look like this::
|
||||
|
||||
https://test.pretix.eu/democon/3vjrh/order/NSLEZ/ujbrnsjzbq4dzhck/pay/123/?return_url=https%3A%2F%2Fexample.org%2Forder%2Freturn%3Ftx_id%3D1234
|
||||
|
||||
You can also embed this page in an ``<iframe>`` instead. Note, however, that this causes problems with some payment
|
||||
methods such as PayPal which do not allow being opened in an iframe. pretix can partly work around these issues by
|
||||
opening a new window, but will only to so if you also append an ``iframe=1`` parameter to the URL::
|
||||
|
||||
https://test.pretix.eu/democon/3vjrh/order/NSLEZ/ujbrnsjzbq4dzhck/pay/123/?return_url=https%3A%2F%2Fexample.org%2Forder%2Freturn%3Ftx_id%3D1234&iframe=1
|
||||
|
||||
If you did **not** pass a payment method since you want us to ask the user which payment method they want to use, you
|
||||
need to construct the URL from the ``url`` attribute of the order and the sub-path ``pay/change```. For example, you
|
||||
would end up with the following URL::
|
||||
|
||||
https://test.pretix.eu/democon/3vjrh/order/NSLEZ/ujbrnsjzbq4dzhck/pay/change
|
||||
|
||||
Of course, you can also use the ``iframe`` and ``return_url`` parameters here.
|
||||
|
||||
Optional: Cart reservations
|
||||
---------------------------
|
||||
|
||||
Creating orders is an atomic operation: The order is either created as a whole or not at all. However, pretix'
|
||||
built-in checkout automatically reserves tickets in a user's cart for a configurable amount of time to ensure users
|
||||
will actually get their tickets once they started entering all their details. If you want a similar behavior in your
|
||||
application, you need to create :ref:`rest-carts` through the API.
|
||||
|
||||
When creating your order, you can pass a ``consume_carts`` parameter with the cart ID(s) of your user. This way, the
|
||||
quota reserved by the cart will be credited towards the order and the carts will be destroyed if (and only if) the
|
||||
order creation succeeds.
|
||||
|
||||
Cart creation is currently even more limited than the order creation endpoints, as cart creation currently does not
|
||||
support vouchers or automatic price calculation. If you require these features, please get in touch with us.
|
||||
11
doc/api/guides/index.rst
Normal file
11
doc/api/guides/index.rst
Normal file
@@ -0,0 +1,11 @@
|
||||
.. _`rest-api-guides`:
|
||||
|
||||
API Usage Guides
|
||||
================
|
||||
|
||||
This part of the documentation contains how-to guides on some special use cases of our API.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
custom_checkout
|
||||
@@ -18,3 +18,4 @@ in functionality over time.
|
||||
resources/index
|
||||
ratelimit
|
||||
webhooks
|
||||
guides/index
|
||||
|
||||
@@ -61,7 +61,7 @@ access to the API. The ``token`` endpoint expects you to authenticate using `HTT
|
||||
ID as a username and your client secret as a password. You are also required to again supply the same ``redirect_uri``
|
||||
parameter that you used for the authorization.
|
||||
|
||||
.. http:get:: /api/v1/oauth/token
|
||||
.. http:post:: /api/v1/oauth/token
|
||||
|
||||
Request a new access token
|
||||
|
||||
|
||||
148
doc/api/resources/billing_var.rst
Normal file
148
doc/api/resources/billing_var.rst
Normal file
@@ -0,0 +1,148 @@
|
||||
pretix Hosted reseller API
|
||||
==========================
|
||||
|
||||
This API is only accessible to our `value-added reseller partners`_ on pretix Hosted.
|
||||
|
||||
.. note:: This API is only accessible with user-level permissions, not with API tokens. Therefore, you will need to
|
||||
create an :ref:`OAuth application <rest-oauth>` and obtain an OAuth access token for a user account that has
|
||||
permission to your reseller account.
|
||||
|
||||
Reseller account resource
|
||||
-------------------------
|
||||
|
||||
The resource contains the following public fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Your reseller ID
|
||||
name string Internal name of your reseller account
|
||||
public_name string Public name of your reseller account
|
||||
public_url string Public URL of your company
|
||||
support_email string Your support email address
|
||||
support_phone string Your support phone number
|
||||
communication_language string Language code we use to communicate with you
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. http:get:: /api/v1/var/
|
||||
|
||||
Returns a list of all reseller accounts you have access to.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/var/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "ticketshop.live Ltd & Co. KG",
|
||||
"public_name": "ticketshop.live",
|
||||
"public_url": "https://ticketshop.live",
|
||||
"support_email": "support@ticketshop.live",
|
||||
"support_phone": "+4962213217750",
|
||||
"communication_language": "de"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
|
||||
.. http:get:: /api/v1/var/(id)/
|
||||
|
||||
Returns information on one reseller account, identified by its ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/var/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "ticketshop.live Ltd & Co. KG",
|
||||
"public_name": "ticketshop.live",
|
||||
"public_url": "https://ticketshop.live",
|
||||
"support_email": "support@ticketshop.live",
|
||||
"support_phone": "+4962213217750",
|
||||
"communication_language": "de"
|
||||
}
|
||||
|
||||
:param id: The ``id`` field of the reseller account to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 404: The requested account does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:post:: /api/v1/var/(id)/create_organizer/
|
||||
|
||||
Creates a new organizer account that will be associated with a given reseller account.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/var/1/create_organizer/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 123
|
||||
|
||||
{
|
||||
"name": "My new client",
|
||||
"slug": "New client"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "My new client",
|
||||
"slug": "New client"
|
||||
}
|
||||
|
||||
:param id: The ``id`` field of the reseller account to fetch
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: Invalid request body, usually the slug is invalid or already taken.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 404: The requested account does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. _value-added reseller partners: https://pretix.eu/about/en/var
|
||||
@@ -36,12 +36,20 @@ answers list of objects Answers to user
|
||||
├ question_identifier string The question's ``identifier`` field
|
||||
├ options list of integers Internal IDs of selected option(s)s (only for choice types)
|
||||
└ option_identifiers list of strings The ``identifier`` fields of the selected option(s)s
|
||||
seat objects The assigned seat. Can be ``null``.
|
||||
├ id integer Internal ID of the seat instance
|
||||
├ name string Human-readable seat name
|
||||
└ seat_guid string Identifier of the seat within the seating plan
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.17
|
||||
|
||||
This resource has been added.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
|
||||
This ``seat`` attribute has been added.
|
||||
|
||||
|
||||
Cart position endpoints
|
||||
-----------------------
|
||||
@@ -87,6 +95,7 @@ Cart position endpoints
|
||||
"datetime": "2018-06-11T10:00:00Z",
|
||||
"expires": "2018-06-11T10:00:00Z",
|
||||
"includes_tax": true,
|
||||
"seat": null,
|
||||
"answers": []
|
||||
}
|
||||
]
|
||||
@@ -132,6 +141,7 @@ Cart position endpoints
|
||||
"datetime": "2018-06-11T10:00:00Z",
|
||||
"expires": "2018-06-11T10:00:00Z",
|
||||
"includes_tax": true,
|
||||
"seat": null,
|
||||
"answers": []
|
||||
}
|
||||
|
||||
@@ -178,11 +188,13 @@ Cart position endpoints
|
||||
* ``item``
|
||||
* ``variation`` (optional)
|
||||
* ``price``
|
||||
* ``seat`` (The ``seat_guid`` attribute of a seat. Required when the specified ``item`` requires a seat, otherwise must be ``null``.)
|
||||
* ``attendee_name`` **or** ``attendee_name_parts`` (optional)
|
||||
* ``attendee_email`` (optional)
|
||||
* ``subevent`` (optional)
|
||||
* ``expires`` (optional)
|
||||
* ``includes_tax`` (optional)
|
||||
* ``sales_channel`` (optional)
|
||||
* ``answers``
|
||||
|
||||
* ``question``
|
||||
@@ -196,7 +208,7 @@ Cart position endpoints
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/cartpositions/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"item": 1,
|
||||
|
||||
@@ -131,7 +131,7 @@ Endpoints
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/categories/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": {"en": "Tickets"},
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
.. spelling:: checkin
|
||||
|
||||
Check-in lists
|
||||
==============
|
||||
|
||||
@@ -27,6 +29,7 @@ subevent integer ID of the date
|
||||
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).
|
||||
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.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.10
|
||||
@@ -41,6 +44,10 @@ include_pending boolean If ``true``, th
|
||||
|
||||
The ``include_pending`` field has been added.
|
||||
|
||||
.. versionchanged:: 3.2
|
||||
|
||||
The ``auto_checkin_sales_channels`` field has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -81,7 +88,10 @@ Endpoints
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"include_pending": false,
|
||||
"subevent": null
|
||||
"subevent": null,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -122,7 +132,10 @@ Endpoints
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"include_pending": false,
|
||||
"subevent": null
|
||||
"subevent": null,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
@@ -209,13 +222,16 @@ Endpoints
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/checkinlists/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "VIP entry",
|
||||
"all_products": false,
|
||||
"limit_products": [1, 2],
|
||||
"subevent": null
|
||||
"subevent": null,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -234,7 +250,10 @@ Endpoints
|
||||
"all_products": false,
|
||||
"limit_products": [1, 2],
|
||||
"include_pending": false,
|
||||
"subevent": null
|
||||
"subevent": null,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event/item to create a list for
|
||||
@@ -283,7 +302,10 @@ Endpoints
|
||||
"all_products": false,
|
||||
"limit_products": [1, 2],
|
||||
"include_pending": false,
|
||||
"subevent": null
|
||||
"subevent": null,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
@@ -342,6 +364,11 @@ Order position endpoints
|
||||
``ignore_status`` filter. The ``attendee_name`` field is now "smart" (see below) and the redemption endpoint
|
||||
returns ``400`` instead of ``404`` on tickets which are known but not paid.
|
||||
|
||||
.. versionchanged:: 3.2
|
||||
|
||||
The ``checkins`` dict now also contains a ``auto_checked_in`` value to indicate if the check-in has been performed
|
||||
automatically by the system.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(list)/positions/
|
||||
|
||||
Returns a list of all order positions within a given event. The result is the same as
|
||||
@@ -396,10 +423,12 @@ Order position endpoints
|
||||
"addon_to": null,
|
||||
"subevent": null,
|
||||
"pseudonymization_id": "MQLJvANO3B",
|
||||
"seat": null,
|
||||
"checkins": [
|
||||
{
|
||||
"list": 1,
|
||||
"datetime": "2017-12-25T12:45:23Z"
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": true
|
||||
}
|
||||
],
|
||||
"answers": [
|
||||
@@ -505,10 +534,12 @@ Order position endpoints
|
||||
"addon_to": null,
|
||||
"subevent": null,
|
||||
"pseudonymization_id": "MQLJvANO3B",
|
||||
"seat": null,
|
||||
"checkins": [
|
||||
{
|
||||
"list": 1,
|
||||
"datetime": "2017-12-25T12:45:23Z"
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": true
|
||||
}
|
||||
],
|
||||
"answers": [
|
||||
@@ -546,6 +577,8 @@ Order position endpoints
|
||||
you do not implement question handling in your user interface, you **must**
|
||||
set this to ``false``. In that case, questions will just be ignored. Defaults
|
||||
to ``true``.
|
||||
:<json boolean canceled_supported: When this parameter is set to ``true``, the response code ``canceled`` may be
|
||||
returned. Otherwise, canceled orders will return ``unpaid``.
|
||||
:<json datetime datetime: Specifies the datetime of the check-in. If not supplied, the current time will be used.
|
||||
:<json boolean force: Specifies that the check-in should succeed regardless of previous check-ins or required
|
||||
questions that have not been filled. Defaults to ``false``.
|
||||
@@ -574,6 +607,7 @@ Order position endpoints
|
||||
"nonce": "Pvrk50vUzQd0DhdpNRL4I4OcXsvg70uA",
|
||||
"datetime": null,
|
||||
"questions_supported": true,
|
||||
"canceled_supported": true,
|
||||
"answers": {
|
||||
"4": "XS"
|
||||
}
|
||||
@@ -657,7 +691,9 @@ Order position endpoints
|
||||
|
||||
Possible error reasons:
|
||||
|
||||
* ``unpaid`` - Ticket is not paid for or has been refunded
|
||||
* ``unpaid`` - Ticket is not paid for
|
||||
* ``canceled`` – Ticket is canceled or expired. This reason is only sent when your request sets
|
||||
``canceled_supported`` to ``true``, otherwise these orders return ``unpaid``.
|
||||
* ``already_redeemed`` - Ticket already has been redeemed
|
||||
* ``product`` - Tickets with this product may not be scanned at this device
|
||||
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
.. spelling::
|
||||
|
||||
geo
|
||||
lat
|
||||
lon
|
||||
|
||||
Events
|
||||
======
|
||||
|
||||
@@ -25,11 +31,19 @@ is_public boolean If ``true``, th
|
||||
presale_start datetime The date at which the ticket shop opens (or ``null``)
|
||||
presale_end datetime The date at which the ticket shop closes (or ``null``)
|
||||
location multi-lingual string The event location (or ``null``)
|
||||
geo_lat float Latitude of the location (or ``null``)
|
||||
geo_lon float Longitude of the location (or ``null``)
|
||||
has_subevents boolean ``true`` if the event series feature is active for this
|
||||
event. Cannot change after event is created.
|
||||
meta_data dict Values set for organizer-specific meta data parameters.
|
||||
meta_data object Values set for organizer-specific meta data parameters.
|
||||
plugins list A list of package names of the enabled plugins for this
|
||||
event.
|
||||
seating_plan integer If reserved seating is in use, the ID of a seating
|
||||
plan. Otherwise ``null``.
|
||||
seat_category_mapping object An object mapping categories of the seating plan
|
||||
(strings) to items in the event (integers or ``null``).
|
||||
timezone string Event timezone name
|
||||
item_meta_properties object Item-specific meta data parameters and default values.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
@@ -54,9 +68,29 @@ plugins list A list of packa
|
||||
|
||||
When cloning events, the ``testmode`` attribute will now be cloned, too.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
|
||||
The attributes ``seating_plan`` and ``seat_category_mapping`` have been added.
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
|
||||
The attributes ``geo_lat`` and ``geo_lon`` have been added.
|
||||
|
||||
.. versionchanged:: 3.4
|
||||
|
||||
The attribute ``timezone`` has been added.
|
||||
|
||||
.. versionchanged:: 3.7
|
||||
|
||||
The attribute ``item_meta_properties`` has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
|
||||
The events resource can now be filtered by meta data attributes.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/
|
||||
|
||||
Returns a list of all events within a given organizer the authenticated user/token has access to.
|
||||
@@ -97,8 +131,14 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"has_subevents": false,
|
||||
"meta_data": {},
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"timezone": "Europe/Berlin",
|
||||
"item_meta_properties": {},
|
||||
"plugins": [
|
||||
"pretix.plugins.banktransfer"
|
||||
"pretix.plugins.stripe"
|
||||
@@ -119,6 +159,10 @@ Endpoints
|
||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``date_from`` and
|
||||
``slug``. Keep in mind that ``date_from`` of event series does not really tell you anything.
|
||||
Default: ``slug``.
|
||||
:query array attr[meta_data_key]: By providing the key and value of a meta data attribute, the list of events will
|
||||
only contain the events matching the set criteria. Providing ``?attr[Format]=Seminar`` would return only those
|
||||
events having set their ``Format`` meta data to ``Seminar``, ``?attr[Format]=`` only those, that have no value
|
||||
set. Please note that this filter will respect default values set on organizer level.
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
@@ -159,8 +203,14 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"has_subevents": false,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"meta_data": {},
|
||||
"timezone": "Europe/Berlin",
|
||||
"item_meta_properties": {},
|
||||
"plugins": [
|
||||
"pretix.plugins.banktransfer"
|
||||
"pretix.plugins.stripe"
|
||||
@@ -191,7 +241,7 @@ Endpoints
|
||||
POST /api/v1/organizers/bigevents/events/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": {"en": "Sample Conference"},
|
||||
@@ -205,9 +255,15 @@ Endpoints
|
||||
"is_public": false,
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"has_subevents": false,
|
||||
"meta_data": {},
|
||||
"timezone": "Europe/Berlin",
|
||||
"item_meta_properties": {},
|
||||
"plugins": [
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal"
|
||||
@@ -235,8 +291,14 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"has_subevents": false,
|
||||
"meta_data": {},
|
||||
"timezone": "Europe/Berlin",
|
||||
"item_meta_properties": {},
|
||||
"plugins": [
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal"
|
||||
@@ -252,11 +314,11 @@ Endpoints
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/clone/
|
||||
|
||||
Creates a new event with properties as set in the request body. The properties that are copied are: 'is_public',
|
||||
`testmode`, settings, plugin settings, items, variations, add-ons, quotas, categories, tax rules, questions.
|
||||
Creates a new event with properties as set in the request body. The properties that are copied are: ``is_public``,
|
||||
``testmode``, ``has_subevents``, settings, plugin settings, items, variations, add-ons, quotas, categories, tax rules, questions.
|
||||
|
||||
If the 'plugins' and/or 'is_public' fields are present in the post body this will determine their value. Otherwise
|
||||
their value will be copied from the existing event.
|
||||
If the ``plugins``, ``has_subevents`` and/or ``is_public`` fields are present in the post body this will determine their
|
||||
value. Otherwise their value will be copied from the existing event.
|
||||
|
||||
Please note that you can only copy from events under the same organizer.
|
||||
|
||||
@@ -269,7 +331,7 @@ Endpoints
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/clone/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": {"en": "Sample Conference"},
|
||||
@@ -284,8 +346,14 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"has_subevents": false,
|
||||
"meta_data": {},
|
||||
"timezone": "Europe/Berlin",
|
||||
"item_meta_properties": {},
|
||||
"plugins": [
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal"
|
||||
@@ -313,8 +381,14 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"has_subevents": false,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"meta_data": {},
|
||||
"timezone": "Europe/Berlin",
|
||||
"item_meta_properties": {},
|
||||
"plugins": [
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal"
|
||||
@@ -342,7 +416,7 @@ Endpoints
|
||||
PATCH /api/v1/organizers/bigevents/events/sampleconf/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"plugins": [
|
||||
@@ -374,8 +448,14 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"has_subevents": false,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"meta_data": {},
|
||||
"timezone": "Europe/Berlin",
|
||||
"item_meta_properties": {},
|
||||
"plugins": [
|
||||
"pretix.plugins.banktransfer",
|
||||
"pretix.plugins.stripe",
|
||||
@@ -418,3 +498,123 @@ Endpoints
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
|
||||
|
||||
Event settings
|
||||
--------------
|
||||
|
||||
pretix events have lots and lots of parameters of different types that are stored in a key-value store on our system.
|
||||
Since many of these settings depend on each other in complex ways, we can not give direct access to all of these
|
||||
settings through the API. However, we do expose many of the simple and useful flags through the API.
|
||||
|
||||
Please note that the available settings flags change between pretix versions and also between events, depending on the
|
||||
installed plugins, and we do not give a guarantee on backwards-compatibility like with other parts of the API.
|
||||
Therefore, we're also not including a list of the options here, but instead recommend to look at the endpoint output
|
||||
to see available options. The ``explain=true`` flag enables a verbose mode that provides you with human-readable
|
||||
information about the properties.
|
||||
|
||||
.. note:: Please note that this is not a complete representation of all event settings. You will find more settings
|
||||
in the web interface.
|
||||
|
||||
.. warning:: This API is intended for advanced users. Even though we take care to validate your input, you will be
|
||||
able to break your event using this API by creating situations of conflicting settings. Please take care.
|
||||
|
||||
.. versionchanged:: 3.6
|
||||
|
||||
Initial support for settings has been added to the API.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/settings/
|
||||
|
||||
Get current values of event settings.
|
||||
|
||||
Permission required: "Can change event settings"
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/settings/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example standard response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"imprint_url": "https://pretix.eu",
|
||||
…
|
||||
}
|
||||
|
||||
**Example verbose response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"imprint_url":
|
||||
{
|
||||
"value": "https://pretix.eu",
|
||||
"label": "Imprint URL",
|
||||
"help_text": "This should point e.g. to a part of your website that has your contact details and legal information."
|
||||
}
|
||||
},
|
||||
…
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event to access
|
||||
:param event: The ``slug`` field of the event to access
|
||||
:query explain: Set to ``true`` to enable verbose response mode
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/settings/
|
||||
|
||||
Updates event settings. Note that ``PUT`` is not allowed here, only ``PATCH``.
|
||||
|
||||
.. warning::
|
||||
|
||||
Settings can be stored at different levels in pretix. If a value is not set on event level, a default setting
|
||||
from a higher level (organizer, global) will be returned. If you explicitly set a setting on event level, it
|
||||
will no longer be inherited from the higher levels. Therefore, we recommend you to send only settings that you
|
||||
explicitly want to set on event level. To unset a settings, pass ``null``.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/events/sampleconf/settings/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"imprint_url": "https://example.org/imprint/"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"imprint_url": "https://example.org/imprint/",
|
||||
…
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event to update
|
||||
:param event: The ``slug`` field of the event to update
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The event could not be updated due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
|
||||
|
||||
251
doc/api/resources/giftcards.rst
Normal file
251
doc/api/resources/giftcards.rst
Normal file
@@ -0,0 +1,251 @@
|
||||
.. _`rest-giftcards`:
|
||||
|
||||
Gift cards
|
||||
==========
|
||||
|
||||
Resource description
|
||||
--------------------
|
||||
|
||||
The gift card resource contains the following public fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the gift card
|
||||
secret string Gift card code (can not be modified later)
|
||||
value money (string) Current gift card value
|
||||
currency string Currency of the value (can not be modified later)
|
||||
testmode boolean Whether this is a test gift card
|
||||
expires datetime Expiry date (or ``null``)
|
||||
conditions string Special terms and conditions for this card (or ``null``)
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/giftcards/
|
||||
|
||||
Returns a list of all gift cards issued by a given organizer.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/giftcards/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"secret": "HLBYVELFRC77NCQY",
|
||||
"currency": "EUR",
|
||||
"testmode": false,
|
||||
"expires": null,
|
||||
"conditions": null,
|
||||
"value": "13.37"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:query string secret: Only show gift cards with the given secret.
|
||||
:query boolean testmode: Filter for gift cards that are (not) in test mode.
|
||||
:query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer.
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/giftcards/(id)/
|
||||
|
||||
Returns information on one gift card, identified by its ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/giftcards/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"secret": "HLBYVELFRC77NCQY",
|
||||
"currency": "EUR",
|
||||
"testmode": false,
|
||||
"expires": null,
|
||||
"conditions": null,
|
||||
"value": "13.37"
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param id: The ``id`` field of the gift card to fetch
|
||||
:query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer.
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/giftcards/
|
||||
|
||||
Creates a new gift card
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/giftcards/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"secret": "HLBYVELFRC77NCQY",
|
||||
"currency": "EUR",
|
||||
"value": "13.37"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"secret": "HLBYVELFRC77NCQY",
|
||||
"testmode": false,
|
||||
"currency": "EUR",
|
||||
"expires": null,
|
||||
"conditions": null,
|
||||
"value": "13.37"
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to create a gift card for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The gift card could not be created due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/giftcards/(id)/
|
||||
|
||||
Update a gift card. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||
want to change.
|
||||
|
||||
You can change all fields of the resource except the ``id``, ``secret``, ``testmode``, and ``currency`` fields. Be
|
||||
careful when modifying the ``value`` field to avoid race conditions. We recommend to use the ``transact`` method
|
||||
described below.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/giftcards/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 94
|
||||
|
||||
{
|
||||
"value": "14.00"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"secret": "HLBYVELFRC77NCQY",
|
||||
"testmode": false,
|
||||
"currency": "EUR",
|
||||
"expires": null,
|
||||
"conditions": null,
|
||||
"value": "14.00"
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param id: The ``id`` field of the gift card to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The gift card could not be modified due to invalid submitted data
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/giftcards/(id)/transact/
|
||||
|
||||
Atomically change the value of a gift card. A positive amount will increase the value of the gift card,
|
||||
a negative amount will decrease it.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/giftcards/1/transact/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 94
|
||||
|
||||
{
|
||||
"value": "2.00"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"secret": "HLBYVELFRC77NCQY",
|
||||
"currency": "EUR",
|
||||
"testmode": false,
|
||||
"expires": null,
|
||||
"conditions": null,
|
||||
"value": "15.37"
|
||||
}
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
This endpoint now returns status code ``409`` if the transaction would lead to a negative gift card value.
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param id: The ``id`` field of the gift card to modify
|
||||
:query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer.
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The gift card could not be modified due to invalid submitted data
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
|
||||
:statuscode 409: There is not sufficient credit on the gift card.
|
||||
@@ -21,6 +21,10 @@ Resources and endpoints
|
||||
vouchers
|
||||
checkinlists
|
||||
waitinglist
|
||||
giftcards
|
||||
carts
|
||||
teams
|
||||
webhooks
|
||||
seatingplans
|
||||
billing_invoices
|
||||
billing_var
|
||||
|
||||
@@ -28,6 +28,7 @@ payment_provider_text string Text to be prin
|
||||
payment information
|
||||
footer_text string Text to be printed in the page footer area
|
||||
lines list of objects The actual invoice contents
|
||||
├ position integer Number of the line within an invoice.
|
||||
├ description string Text representing the invoice line (e.g. product name)
|
||||
├ gross_value money (string) Price including taxes
|
||||
├ tax_value money (string) Tax amount included
|
||||
@@ -63,6 +64,11 @@ internal_reference string Customer's refe
|
||||
The attribute ``internal_reference`` has been added.
|
||||
|
||||
|
||||
.. versionchanged:: 3.4
|
||||
|
||||
The attribute ``lines.number`` has been added.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -107,6 +113,7 @@ Endpoints
|
||||
"footer_text": "Big Events LLC - Registration No. 123456 - VAT ID: EU0987654321",
|
||||
"lines": [
|
||||
{
|
||||
"position": 1,
|
||||
"description": "Budget Ticket",
|
||||
"gross_value": "23.00",
|
||||
"tax_value": "0.00",
|
||||
@@ -171,6 +178,7 @@ Endpoints
|
||||
"footer_text": "Big Events LLC - Registration No. 123456 - VAT ID: EU0987654321",
|
||||
"lines": [
|
||||
{
|
||||
"position": 1,
|
||||
"description": "Budget Ticket",
|
||||
"gross_value": "23.00",
|
||||
"tax_value": "0.00",
|
||||
|
||||
@@ -134,7 +134,7 @@ Endpoints
|
||||
POST /api/v1/organizers/(organizer)/events/(event)/items/(item)/addons/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"addon_category": 1,
|
||||
|
||||
@@ -134,7 +134,7 @@ Endpoints
|
||||
POST /api/v1/organizers/(organizer)/events/(event)/items/(item)/bundles/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"bundled_item": 3,
|
||||
|
||||
@@ -152,7 +152,7 @@ Endpoints
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/items/1/variations/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"value": {"en": "Student"},
|
||||
|
||||
@@ -44,6 +44,9 @@ available_from datetime The first date
|
||||
(or ``null``).
|
||||
available_until datetime The last date time at which this item can be bought
|
||||
(or ``null``).
|
||||
hidden_if_available integer The internal ID of a quota object, or ``null``. If
|
||||
set, this item won't be shown publicly as long as this
|
||||
quota is available.
|
||||
require_voucher boolean If ``true``, this item can only be bought using a
|
||||
voucher that is specifically assigned to this item.
|
||||
hide_without_voucher boolean If ``true``, this item is only shown during the voucher
|
||||
@@ -72,6 +75,11 @@ generate_tickets boolean If ``false``, t
|
||||
non-admission or add-on product, regardless of event
|
||||
settings. If this is ``null``, regular ticketing
|
||||
rules apply.
|
||||
allow_waitinglist boolean If ``false``, no waiting list will be shown for this
|
||||
product when it is sold out.
|
||||
issue_giftcard boolean If ``true``, buying this product will yield a gift card.
|
||||
show_quota_left boolean Publicly show how many tickets are still available.
|
||||
If this is ``null``, the event default is used.
|
||||
has_variations boolean Shows whether or not this item has variations.
|
||||
variations list of objects A list with one object for each variation of this item.
|
||||
Can be empty. Only writable during creation,
|
||||
@@ -106,6 +114,7 @@ bundles list of objects Definition of b
|
||||
└ designated_price money (string) Designated price of the bundled product. This will be
|
||||
used to split the price of the base item e.g. for mixed
|
||||
taxation. This is not added to the price.
|
||||
meta_data object Values set for event-specific meta data parameters.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 2.7
|
||||
@@ -142,6 +151,14 @@ bundles list of objects Definition of b
|
||||
|
||||
The ``bundles`` and ``require_bundling`` attributes have been added.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
|
||||
The ``show_quota_left``, ``allow_waitinglist``, and ``hidden_if_available`` attributes have been added.
|
||||
|
||||
.. versionchanged:: 3.7
|
||||
|
||||
The attribute ``meta_data`` has been added.
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
@@ -195,10 +212,13 @@ Endpoints
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
"issue_giftcard": false,
|
||||
"meta_data": {},
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"hidden_if_available": null,
|
||||
"require_voucher": false,
|
||||
"hide_without_voucher": false,
|
||||
"allow_cancel": true,
|
||||
@@ -207,6 +227,8 @@ Endpoints
|
||||
"checkin_attention": false,
|
||||
"has_variations": false,
|
||||
"generate_tickets": null,
|
||||
"allow_waitinglist": true,
|
||||
"show_quota_left": null,
|
||||
"require_approval": false,
|
||||
"require_bundling": false,
|
||||
"variations": [
|
||||
@@ -286,14 +308,19 @@ Endpoints
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
"issue_giftcard": false,
|
||||
"meta_data": {},
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"hidden_if_available": null,
|
||||
"require_voucher": false,
|
||||
"hide_without_voucher": false,
|
||||
"allow_cancel": true,
|
||||
"generate_tickets": null,
|
||||
"allow_waitinglist": true,
|
||||
"show_quota_left": null,
|
||||
"min_per_order": null,
|
||||
"max_per_order": null,
|
||||
"checkin_attention": false,
|
||||
@@ -342,7 +369,7 @@ Endpoints
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/items/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
@@ -358,14 +385,19 @@ Endpoints
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
"issue_giftcard": false,
|
||||
"meta_data": {},
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"hidden_if_available": null,
|
||||
"require_voucher": false,
|
||||
"hide_without_voucher": false,
|
||||
"allow_cancel": true,
|
||||
"generate_tickets": null,
|
||||
"allow_waitinglist": true,
|
||||
"show_quota_left": null,
|
||||
"min_per_order": null,
|
||||
"max_per_order": null,
|
||||
"checkin_attention": false,
|
||||
@@ -417,16 +449,21 @@ Endpoints
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
"issue_giftcard": false,
|
||||
"meta_data": {},
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"hidden_if_available": null,
|
||||
"require_voucher": false,
|
||||
"hide_without_voucher": false,
|
||||
"allow_cancel": true,
|
||||
"min_per_order": null,
|
||||
"max_per_order": null,
|
||||
"generate_tickets": null,
|
||||
"allow_waitinglist": true,
|
||||
"show_quota_left": null,
|
||||
"checkin_attention": false,
|
||||
"has_variations": true,
|
||||
"require_approval": false,
|
||||
@@ -508,13 +545,18 @@ Endpoints
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
"issue_giftcard": false,
|
||||
"meta_data": {},
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"hidden_if_available": null,
|
||||
"require_voucher": false,
|
||||
"hide_without_voucher": false,
|
||||
"generate_tickets": null,
|
||||
"allow_waitinglist": true,
|
||||
"show_quota_left": null,
|
||||
"allow_cancel": true,
|
||||
"min_per_order": null,
|
||||
"max_per_order": null,
|
||||
|
||||
@@ -53,15 +53,18 @@ invoice_address object Invoice address
|
||||
├ street string Customer street
|
||||
├ zipcode string Customer ZIP code
|
||||
├ city string Customer city
|
||||
├ country string Customer country
|
||||
├ country string Customer country code
|
||||
├ state string Customer state (ISO 3166-2 code). Only supported in
|
||||
AU, BR, CA, CN, MY, MX, and US.
|
||||
├ internal_reference string Customer's internal reference to be printed on the invoice
|
||||
├ vat_id string Customer VAT ID
|
||||
└ vat_id_validated string ``true``, if the VAT ID has been validated against the
|
||||
EU VAT service and validation was successful. This only
|
||||
happens in rare cases.
|
||||
positions list of objects List of non-canceled order positions (see below)
|
||||
fees list of objects List of non-canceled fees included in the order total
|
||||
(i.e. payment fees)
|
||||
positions list of objects List of order positions (see below). By default, only
|
||||
non-canceled positions are included.
|
||||
fees list of objects List of fees included in the order total. By default, only
|
||||
non-canceled fees are included.
|
||||
├ fee_type string Type of fee (currently ``payment``, ``passbook``,
|
||||
``other``)
|
||||
├ value money (string) Fee amount
|
||||
@@ -70,7 +73,8 @@ fees list of objects List of non-can
|
||||
can be empty
|
||||
├ tax_rate decimal (string) VAT rate applied for 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``)
|
||||
└ canceled boolean Whether or not this fee has been canceled.
|
||||
downloads list of objects List of ticket download options for order-wise ticket
|
||||
downloading. This might be a multi-page PDF or a ZIP
|
||||
file of tickets for outputs that do not support
|
||||
@@ -82,6 +86,7 @@ require_approval boolean If ``true`` and
|
||||
needs approval by an organizer before it can
|
||||
continue. If ``true`` and the order is canceled,
|
||||
this order has been denied by the event organizer.
|
||||
url string The full URL to the order confirmation page
|
||||
payments list of objects List of payment processes (see below)
|
||||
refunds list of objects List of refund processes (see below)
|
||||
last_modified datetime Last modification of this object
|
||||
@@ -128,15 +133,29 @@ last_modified datetime Last modificati
|
||||
|
||||
The ``sales_channel`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 2.4:
|
||||
.. versionchanged:: 2.4
|
||||
|
||||
``order.status`` can no longer be ``r``, ``…/mark_canceled/`` now accepts a ``cancellation_fee`` parameter and
|
||||
``…/mark_refunded/`` has been deprecated.
|
||||
|
||||
.. versionchanged:: 2.5:
|
||||
.. versionchanged:: 2.5
|
||||
|
||||
The ``testmode`` attribute has been added and ``DELETE`` has been implemented for orders.
|
||||
|
||||
.. versionchanged:: 3.1
|
||||
|
||||
The ``invoice_address.state`` and ``url`` attributes have been added. When creating orders through the API,
|
||||
vouchers are now supported and many fields are now optional.
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
The ``order.fees.canceled`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 3.8
|
||||
|
||||
The ``reactivate`` operation has been added.
|
||||
|
||||
|
||||
.. _order-position-resource:
|
||||
|
||||
Order position resource
|
||||
@@ -150,12 +169,21 @@ Field Type Description
|
||||
id integer Internal ID of the order position
|
||||
order string Order code of the order the position belongs to
|
||||
positionid integer Number of the position within the order
|
||||
canceled boolean Whether or not this position has been canceled. Note that
|
||||
by default, only non-canceled positions are shown.
|
||||
item integer ID of the purchased item
|
||||
variation integer ID of the purchased variation (or ``null``)
|
||||
price money (string) Price of this position
|
||||
attendee_name string Specified attendee name for this position (or ``null``)
|
||||
attendee_name_parts object of strings Decomposition of attendee name (i.e. given name, family name)
|
||||
attendee_email string Specified attendee email address for this position (or ``null``)
|
||||
company string Attendee company name (or ``null``)
|
||||
street string Attendee street (or ``null``)
|
||||
zipcode string Attendee ZIP code (or ``null``)
|
||||
city string Attendee city (or ``null``)
|
||||
country string Attendee country code (or ``null``)
|
||||
state string Attendee state (ISO 3166-2 code). Only supported in
|
||||
AU, BR, CA, CN, MY, MX, and US, otherwise ``null``.
|
||||
voucher integer Internal ID of the voucher used for this position (or ``null``)
|
||||
tax_rate decimal (string) VAT rate applied for this position
|
||||
tax_value money (string) VAT included in this position
|
||||
@@ -166,7 +194,8 @@ subevent integer ID of the date
|
||||
pseudonymization_id string A random ID, e.g. for use in lead scanning apps
|
||||
checkins list of objects List of check-ins with this ticket
|
||||
├ list integer Internal ID of the check-in list
|
||||
└ datetime datetime Time of check-in
|
||||
├ datetime datetime Time of check-in
|
||||
└ auto_checked_in boolean Indicates if this check-in been performed automatically by the system
|
||||
downloads list of objects List of ticket download options
|
||||
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
|
||||
└ url string Download URL
|
||||
@@ -176,6 +205,10 @@ answers list of objects Answers to user
|
||||
├ question_identifier string The question's ``identifier`` field
|
||||
├ options list of integers Internal IDs of selected option(s)s (only for choice types)
|
||||
└ option_identifiers list of strings The ``identifier`` fields of the selected option(s)s
|
||||
seat objects The assigned seat. Can be ``null``.
|
||||
├ id integer Internal ID of the seat instance
|
||||
├ name string Human-readable seat name
|
||||
└ seat_guid string Identifier of the seat within the seating plan
|
||||
pdf_data object Data object required for ticket PDF generation. By default,
|
||||
this field is missing. It will be added only if you add the
|
||||
``pdf_data=true`` query parameter to your request.
|
||||
@@ -197,6 +230,27 @@ pdf_data object Data object req
|
||||
|
||||
The attributes ``pseudonymization_id`` and ``pdf_data`` have been added.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
|
||||
The attribute ``seat`` has been added.
|
||||
|
||||
.. versionchanged:: 3.2
|
||||
|
||||
The value ``auto_checked_in`` has been added to the ``checkins``-attribute.
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
|
||||
The ``url`` of a ticket ``download`` can now also return a ``text/uri-list`` instead of a file. See
|
||||
:ref:`order-position-ticket-download` for details.
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
The attribute ``canceled`` has been added.
|
||||
|
||||
.. versionchanged:: 3.8
|
||||
|
||||
The attributes ``company``, ``street``, ``zipcode``, ``city``, ``country``, and ``state`` have been added.
|
||||
|
||||
.. _order-payment-resource:
|
||||
|
||||
Order payment resource
|
||||
@@ -213,13 +267,27 @@ amount money (string) Payment amount
|
||||
created datetime Date and time of creation of this payment
|
||||
payment_date datetime Date and time of completion of this payment (or ``null``)
|
||||
provider string Identification string of the payment provider
|
||||
payment_url string The URL where an user can continue with the payment (or ``null``)
|
||||
details object Payment-specific information. This is a dictionary
|
||||
with various fields that can be different between
|
||||
payment providers, versions, payment states, etc. If
|
||||
you read this field, you always need to be able to
|
||||
deal with situations where values that you expect are
|
||||
missing. Mostly, the field contains various IDs that
|
||||
can be used for matching with other systems. If a
|
||||
payment provider does not implement this feature,
|
||||
the object is empty.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
|
||||
This resource has been added.
|
||||
|
||||
.. _order-payment-resource:
|
||||
.. versionchanged:: 3.1
|
||||
|
||||
The attributes ``payment_url`` and ``details`` have been added.
|
||||
|
||||
.. _order-refund-resource:
|
||||
|
||||
Order refund resource
|
||||
---------------------
|
||||
@@ -249,6 +317,10 @@ List of all orders
|
||||
|
||||
Filtering for emails or order codes is now case-insensitive.
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
The ``include_canceled_positions`` and ``include_canceled_fees`` query parameters have been added.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/
|
||||
|
||||
Returns a list of all orders within a given event.
|
||||
@@ -280,6 +352,7 @@ List of all orders
|
||||
"status": "p",
|
||||
"testmode": false,
|
||||
"secret": "k24fiuwvu8kxz3y1",
|
||||
"url": "https://test.pretix.eu/dummy/dummy/order/ABC12/k24fiuwvu8kxz3y1/",
|
||||
"email": "tester@example.org",
|
||||
"locale": "en",
|
||||
"sales_channel": "web",
|
||||
@@ -302,7 +375,8 @@ List of all orders
|
||||
"street": "Test street 12",
|
||||
"zipcode": "12345",
|
||||
"city": "Testington",
|
||||
"country": "Testikistan",
|
||||
"country": "DE",
|
||||
"state": "",
|
||||
"internal_reference": "",
|
||||
"vat_id": "EU123456789",
|
||||
"vat_id_validated": false
|
||||
@@ -312,6 +386,7 @@ List of all orders
|
||||
"id": 23442,
|
||||
"order": "ABC12",
|
||||
"positionid": 1,
|
||||
"canceled": false,
|
||||
"item": 1345,
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
@@ -320,6 +395,12 @@ List of all orders
|
||||
"full_name": "Peter",
|
||||
},
|
||||
"attendee_email": null,
|
||||
"company": "Sample company",
|
||||
"street": "Test street 12",
|
||||
"zipcode": "12345",
|
||||
"city": "Testington",
|
||||
"country": "DE",
|
||||
"state": null,
|
||||
"voucher": null,
|
||||
"tax_rate": "0.00",
|
||||
"tax_value": "0.00",
|
||||
@@ -328,10 +409,12 @@ List of all orders
|
||||
"addon_to": null,
|
||||
"subevent": null,
|
||||
"pseudonymization_id": "MQLJvANO3B",
|
||||
"seat": null,
|
||||
"checkins": [
|
||||
{
|
||||
"list": 44,
|
||||
"datetime": "2017-12-25T12:45:23Z"
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": false
|
||||
}
|
||||
],
|
||||
"answers": [
|
||||
@@ -364,6 +447,8 @@ List of all orders
|
||||
"amount": "23.00",
|
||||
"created": "2017-12-01T10:00:00Z",
|
||||
"payment_date": "2017-12-04T12:13:12Z",
|
||||
"payment_url": null,
|
||||
"details": {},
|
||||
"provider": "banktransfer"
|
||||
}
|
||||
],
|
||||
@@ -380,6 +465,9 @@ List of all orders
|
||||
:query boolean testmode: Only return orders with ``testmode`` set to ``true`` or ``false``
|
||||
:query boolean require_approval: If set to ``true`` or ``false``, only categories with this value for the field
|
||||
``require_approval`` will be returned.
|
||||
:query include_canceled_positions: If set to ``true``, the output will contain canceled order positions. Note that this
|
||||
only affects position-level cancellations, not fully-canceled orders.
|
||||
:query include_canceled_fees: If set to ``true``, the output will contain canceled order fees.
|
||||
:query string email: Only return orders created with the given email address
|
||||
:query string locale: Only return orders with the given customer locale
|
||||
:query datetime modified_since: Only return orders that have changed since the given date. Be careful: We only
|
||||
@@ -397,6 +485,10 @@ List of all orders
|
||||
Fetching individual orders
|
||||
--------------------------
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
The ``include_canceled_positions`` and ``include_canceled_fees`` query parameters have been added.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/
|
||||
|
||||
Returns information on one order, identified by its order code.
|
||||
@@ -422,6 +514,7 @@ Fetching individual orders
|
||||
"status": "p",
|
||||
"testmode": false,
|
||||
"secret": "k24fiuwvu8kxz3y1",
|
||||
"url": "https://test.pretix.eu/dummy/dummy/order/ABC12/k24fiuwvu8kxz3y1/",
|
||||
"email": "tester@example.org",
|
||||
"locale": "en",
|
||||
"sales_channel": "web",
|
||||
@@ -444,7 +537,8 @@ Fetching individual orders
|
||||
"street": "Test street 12",
|
||||
"zipcode": "12345",
|
||||
"city": "Testington",
|
||||
"country": "Testikistan",
|
||||
"country": "DE",
|
||||
"state": "",
|
||||
"internal_reference": "",
|
||||
"vat_id": "EU123456789",
|
||||
"vat_id_validated": false
|
||||
@@ -454,6 +548,7 @@ Fetching individual orders
|
||||
"id": 23442,
|
||||
"order": "ABC12",
|
||||
"positionid": 1,
|
||||
"canceled": false,
|
||||
"item": 1345,
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
@@ -462,6 +557,12 @@ Fetching individual orders
|
||||
"full_name": "Peter",
|
||||
},
|
||||
"attendee_email": null,
|
||||
"company": "Sample company",
|
||||
"street": "Test street 12",
|
||||
"zipcode": "12345",
|
||||
"city": "Testington",
|
||||
"country": "DE",
|
||||
"state": null,
|
||||
"voucher": null,
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": null,
|
||||
@@ -470,10 +571,12 @@ Fetching individual orders
|
||||
"addon_to": null,
|
||||
"subevent": null,
|
||||
"pseudonymization_id": "MQLJvANO3B",
|
||||
"seat": null,
|
||||
"checkins": [
|
||||
{
|
||||
"list": 44,
|
||||
"datetime": "2017-12-25T12:45:23Z"
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": false
|
||||
}
|
||||
],
|
||||
"answers": [
|
||||
@@ -506,6 +609,8 @@ Fetching individual orders
|
||||
"amount": "23.00",
|
||||
"created": "2017-12-01T10:00:00Z",
|
||||
"payment_date": "2017-12-04T12:13:12Z",
|
||||
"payment_url": null,
|
||||
"details": {},
|
||||
"provider": "banktransfer"
|
||||
}
|
||||
],
|
||||
@@ -515,6 +620,9 @@ Fetching individual orders
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:param code: The ``code`` field of the order to fetch
|
||||
:query include_canceled_positions: If set to ``true``, the output will contain canceled order positions. Note that this
|
||||
only affects position-level cancellations, not fully-canceled orders.
|
||||
:query include_canceled_fees: If set to ``true``, the output will contain canceled order fees.
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
@@ -681,6 +789,8 @@ Deleting orders
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource **or** the order may not be deleted.
|
||||
:statuscode 404: The requested order does not exist.
|
||||
|
||||
.. _rest-orders-create:
|
||||
|
||||
Creating orders
|
||||
---------------
|
||||
|
||||
@@ -688,8 +798,6 @@ Creating orders
|
||||
|
||||
Creates a new order.
|
||||
|
||||
.. warning:: This endpoint is considered **experimental**. It might change at any time without prior notice.
|
||||
|
||||
.. warning::
|
||||
|
||||
This endpoint is intended for advanced users. It is not designed to be used to build your own shop frontend,
|
||||
@@ -708,25 +816,21 @@ Creating orders
|
||||
|
||||
* does not validate the number of items per order or the number of times an item can be included in an order
|
||||
|
||||
* does not validate any requirements related to add-on products
|
||||
* does not validate any requirements related to add-on products and does not add bundled products automatically
|
||||
|
||||
* does not check or calculate prices but believes any prices you send
|
||||
|
||||
* does not support the redemption of vouchers
|
||||
* does not check prices but believes any prices you send
|
||||
|
||||
* does not prevent you from buying items that can only be bought with a voucher
|
||||
|
||||
* does not calculate fees
|
||||
* does not calculate fees automatically
|
||||
|
||||
* does not allow to pass data to plugins and will therefore cause issues with some plugins like the shipping
|
||||
module
|
||||
|
||||
* does not send order confirmations via email
|
||||
|
||||
* does not support reverse charge taxation
|
||||
|
||||
* does not support file upload questions
|
||||
|
||||
* does not support redeeming gift cards
|
||||
|
||||
You can supply the following fields of the resource:
|
||||
|
||||
* ``code`` (optional)
|
||||
@@ -737,14 +841,15 @@ Creating orders
|
||||
then call the ``mark_paid`` API method.
|
||||
* ``testmode`` (optional) – Defaults to ``false``
|
||||
* ``consume_carts`` (optional) – A list of cart IDs. All cart positions with these IDs will be deleted if the
|
||||
order creation is successful. Any quotas that become free by this operation will be credited to your order
|
||||
order creation is successful. Any quotas or seats that become free by this operation will be credited to your order
|
||||
creation.
|
||||
* ``email``
|
||||
* ``email`` (optional)
|
||||
* ``locale``
|
||||
* ``sales_channel``
|
||||
* ``payment_provider`` – The identifier of the payment provider set for this order. This needs to be an existing
|
||||
payment provider. You should use ``"free"`` for free orders, and we strongly advise to use ``"manual"`` for all
|
||||
orders you create as paid.
|
||||
* ``sales_channel`` (optional)
|
||||
* ``payment_provider`` (optional) – The identifier of the payment provider set for this order. This needs to be an
|
||||
existing payment provider. You should use ``"free"`` for free orders, and we strongly advise to use ``"manual"``
|
||||
for all orders you create as paid. This field is optional when the order status is ``"n"`` or the order total is
|
||||
zero, otherwise it is required.
|
||||
* ``payment_info`` (optional) – You can pass a nested JSON object that will be set as the internal ``info``
|
||||
value of the payment object that will be created. How this value is handled is up to the payment provider and you
|
||||
should only use this if you know the specific payment provider in detail. Please keep in mind that the payment
|
||||
@@ -762,20 +867,32 @@ Creating orders
|
||||
* ``zipcode``
|
||||
* ``city``
|
||||
* ``country``
|
||||
* ``state``
|
||||
* ``internal_reference``
|
||||
* ``vat_id``
|
||||
* ``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
|
||||
trigger reverse charge taxation. Don't forget to set ``is_business`` as well!
|
||||
|
||||
* ``positions``
|
||||
|
||||
* ``positionid`` (optional, see below)
|
||||
* ``item``
|
||||
* ``variation``
|
||||
* ``price``
|
||||
* ``attendee_name`` **or** ``attendee_name_parts``
|
||||
* ``attendee_email``
|
||||
* ``variation`` (optional)
|
||||
* ``price`` (optional, if set to ``null`` or missing the price will be computed from the given product)
|
||||
* ``seat`` (The ``seat_guid`` attribute of a seat. Required when the specified ``item`` requires a seat, otherwise must be ``null``.)
|
||||
* ``attendee_name`` **or** ``attendee_name_parts`` (optional)
|
||||
* ``voucher`` (optional, the ``code`` attribute of a valid voucher)
|
||||
* ``attendee_email`` (optional)
|
||||
* ``company`` (optional)
|
||||
* ``street`` (optional)
|
||||
* ``zipcode`` (optional)
|
||||
* ``city`` (optional)
|
||||
* ``country`` (optional)
|
||||
* ``state`` (optional)
|
||||
* ``secret`` (optional)
|
||||
* ``addon_to`` (optional, see below)
|
||||
* ``subevent``
|
||||
* ``subevent`` (optional)
|
||||
* ``answers``
|
||||
|
||||
* ``question``
|
||||
@@ -789,14 +906,31 @@ Creating orders
|
||||
* ``description``
|
||||
* ``internal_type``
|
||||
* ``tax_rule``
|
||||
* ``_treat_value_as_percentage`` (Optional convenience flag. If set to ``true``, your ``value`` parameter will
|
||||
be treated as a percentage and the fee will be calculated using that percentage and the sum of all product
|
||||
prices. Note that this will not include other fees and is calculated once during order generation and will not
|
||||
be respected automatically when the order changes later.)
|
||||
* ``_split_taxes_like_products`` (Optional convenience flag. If set to ``true``, your ``tax_rule`` will be ignored
|
||||
and the fee will be taxed like the products in the order. If the products have multiple tax rates, multiple fees
|
||||
will be generated with weights adjusted to the net price of the products. Note that this will be calculated once
|
||||
during order generation and is not respected automatically when the order changes later.)
|
||||
|
||||
* ``force`` (optional). If set to ``true``, quotas will be ignored.
|
||||
* ``send_mail`` (optional). If set to ``true``, the same emails will be sent as for a regular order. Defaults to
|
||||
``false``.
|
||||
|
||||
If you want to use add-on products, you need to set the ``positionid`` fields of all positions manually
|
||||
to incrementing integers starting with ``1``. Then, you can reference one of these
|
||||
IDs in the ``addon_to`` field of another position. Note that all add_ons for a specific position need to come
|
||||
immediately after the position itself.
|
||||
|
||||
Starting with pretix 3.7, you can add ``"simulate": true`` to the body to do a "dry run" of your order. This will
|
||||
validate your order and return you an order object with the resulting prices, but will not create an actual order.
|
||||
You can use this for testing or to look up prices. In this case, some attributes are ignored, such as whether
|
||||
to send an email or what payment provider will be used. Note that some returned fields will contain empty values
|
||||
(e.g. all ``id`` fields of positions will be zero) and some will contain fake values (e.g. the order code will
|
||||
always be ``PREVIEW``). pretix plugins will not be triggered, so some special behavior might be missing as well.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@@ -828,6 +962,7 @@ Creating orders
|
||||
"zipcode": "12345",
|
||||
"city": "Sample City",
|
||||
"country": "UK",
|
||||
"state": "",
|
||||
"internal_reference": "",
|
||||
"vat_id": ""
|
||||
},
|
||||
@@ -851,7 +986,7 @@ Creating orders
|
||||
],
|
||||
"subevent": null
|
||||
}
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -955,6 +1090,42 @@ Order state operations
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
:statuscode 404: The requested order does not exist.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/reactivate/
|
||||
|
||||
Reactivates a canceled order. This will set the order to pending or paid state. Only possible if all products are
|
||||
still available.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/reactivate/ 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
|
||||
|
||||
{
|
||||
"code": "ABC12",
|
||||
"status": "n",
|
||||
...
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param code: The ``code`` field of the order to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The order cannot be reactivated
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
:statuscode 404: The requested order does not exist.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_pending/
|
||||
|
||||
Marks a paid order as unpaid.
|
||||
@@ -1242,7 +1413,13 @@ List of all order positions
|
||||
The order positions endpoint has been extended by the filter queries ``voucher``, ``voucher__code`` and
|
||||
``pseudonymization_id``.
|
||||
|
||||
.. note:: Individually canceled order positions are currently not visible via the API at all.
|
||||
.. versionchanged:: 3.2
|
||||
|
||||
The value ``auto_checked_in`` has been added to the ``checkins``-attribute.
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
The ``include_canceled_positions`` and ``include_canceled_fees`` query parameters have been added.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/
|
||||
|
||||
@@ -1273,6 +1450,7 @@ List of all order positions
|
||||
"id": 23442,
|
||||
"order": "ABC12",
|
||||
"positionid": 1,
|
||||
"canceled": false,
|
||||
"item": 1345,
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
@@ -1287,12 +1465,14 @@ List of all order positions
|
||||
"tax_value": "0.00",
|
||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||
"pseudonymization_id": "MQLJvANO3B",
|
||||
"seat": null,
|
||||
"addon_to": null,
|
||||
"subevent": null,
|
||||
"checkins": [
|
||||
{
|
||||
"list": 44,
|
||||
"datetime": "2017-12-25T12:45:23Z"
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": false
|
||||
}
|
||||
],
|
||||
"answers": [
|
||||
@@ -1340,6 +1520,8 @@ List of all order positions
|
||||
comma-separated IDs.
|
||||
:query string voucher: Only return positions with a specific voucher.
|
||||
:query string voucher__code: Only return positions with a specific voucher code.
|
||||
:query include_canceled_positions: If set to ``true``, the output will contain canceled order positions. Note that this
|
||||
only affects position-level cancellations, not fully-canceled orders.
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:statuscode 200: no error
|
||||
@@ -1373,6 +1555,7 @@ Fetching individual positions
|
||||
"id": 23442,
|
||||
"order": "ABC12",
|
||||
"positionid": 1,
|
||||
"canceled": false,
|
||||
"item": 1345,
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
@@ -1389,10 +1572,12 @@ Fetching individual positions
|
||||
"addon_to": null,
|
||||
"subevent": null,
|
||||
"pseudonymization_id": "MQLJvANO3B",
|
||||
"seat": null,
|
||||
"checkins": [
|
||||
{
|
||||
"list": 44,
|
||||
"datetime": "2017-12-25T12:45:23Z"
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": false
|
||||
}
|
||||
],
|
||||
"answers": [
|
||||
@@ -1415,11 +1600,14 @@ Fetching individual positions
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:param id: The ``id`` field of the order position to fetch
|
||||
:query include_canceled_positions: If set to ``true``, canceled positions may be returned (otherwise, they return 404).
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
:statuscode 404: The requested order position does not exist.
|
||||
|
||||
.. _`order-position-ticket-download`:
|
||||
|
||||
Order position ticket download
|
||||
------------------------------
|
||||
|
||||
@@ -1429,6 +1617,11 @@ Order position ticket download
|
||||
Depending on the chosen output, the response might be a ZIP file, PDF file or something else. The order details
|
||||
response contains a list of output options for this particular order position.
|
||||
|
||||
Be aware that the output does not have to be a file, but can also be a regular HTTP response with a ``Content-Type``
|
||||
set to ``text/uri-list``. In this case, the user is expected to navigate to that URL in order to access their ticket.
|
||||
The referenced URL can provide a download or a regular, human-viewable website - so it is advised to open this URL
|
||||
in a webbrowser and leave it up to the user to handle the result.
|
||||
|
||||
Tickets can be only downloaded if the order is paid and if ticket downloads are active. Also, depending on event
|
||||
configuration downloads might be only unavailable for add-on products or non-admission products.
|
||||
Note that in some cases the ticket file might not yet have been created. In that case, you will receive a status
|
||||
@@ -1504,6 +1697,10 @@ Order payment endpoints
|
||||
|
||||
These endpoints have been added.
|
||||
|
||||
.. versionchanged:: 3.6
|
||||
|
||||
Payments can now be created through the API.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/payments/
|
||||
|
||||
Returns a list of all payments for an order.
|
||||
@@ -1535,6 +1732,8 @@ Order payment endpoints
|
||||
"amount": "23.00",
|
||||
"created": "2017-12-01T10:00:00Z",
|
||||
"payment_date": "2017-12-04T12:13:12Z",
|
||||
"payment_url": null,
|
||||
"details": {},
|
||||
"provider": "banktransfer"
|
||||
}
|
||||
]
|
||||
@@ -1575,6 +1774,8 @@ Order payment endpoints
|
||||
"amount": "23.00",
|
||||
"created": "2017-12-01T10:00:00Z",
|
||||
"payment_date": "2017-12-04T12:13:12Z",
|
||||
"payment_url": null,
|
||||
"details": {},
|
||||
"provider": "banktransfer"
|
||||
}
|
||||
|
||||
@@ -1708,6 +1909,61 @@ Order payment endpoints
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
:statuscode 404: The requested order or payment does not exist.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/payments/
|
||||
|
||||
Creates a new payment.
|
||||
|
||||
Be careful with the ``info`` parameter: You can pass a nested JSON object that will be set as the internal ``info``
|
||||
value of the payment object that will be created. How this value is handled is up to the payment provider and you
|
||||
should only use this if you know the specific payment provider in detail. Please keep in mind that the payment
|
||||
provider will not be called to do anything about this (i.e. if you pass a bank account to a debit provider, *no*
|
||||
charge will be created), this is just informative in case you *handled the payment already*.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/payments/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"state": "confirmed",
|
||||
"amount": "23.00",
|
||||
"payment_date": "2017-12-04T12:13:12Z",
|
||||
"info": {},
|
||||
"provider": "banktransfer"
|
||||
}
|
||||
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"local_id": 1,
|
||||
"state": "confirmed",
|
||||
"amount": "23.00",
|
||||
"created": "2017-12-01T10:00:00Z",
|
||||
"payment_date": "2017-12-04T12:13:12Z",
|
||||
"payment_url": null,
|
||||
"details": {},
|
||||
"provider": "banktransfer"
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to access
|
||||
:param event: The ``slug`` field of the event to access
|
||||
:param order: The ``code`` field of the order to access
|
||||
: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.
|
||||
:statuscode 404: The requested order does not exist.
|
||||
|
||||
|
||||
Order refund endpoints
|
||||
----------------------
|
||||
@@ -1826,7 +2082,8 @@ Order refund endpoints
|
||||
"payment": 1,
|
||||
"execution_date": null,
|
||||
"provider": "manual",
|
||||
"mark_canceled": false
|
||||
"mark_canceled": false,
|
||||
"mark_pending": true
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
@@ -18,6 +18,7 @@ Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the question
|
||||
question multi-lingual string The field label shown to the customer
|
||||
help_text multi-lingual string The help text shown to the customer
|
||||
type string The expected type of answer. Valid options:
|
||||
|
||||
* ``N`` – number
|
||||
@@ -31,6 +32,7 @@ type string The expected ty
|
||||
* ``H`` – time
|
||||
* ``W`` – date and time
|
||||
* ``CC`` – country code (ISO 3666-1 alpha-2)
|
||||
* ``TEL`` – telephone number
|
||||
required boolean If ``true``, the question needs to be filled out.
|
||||
position integer An integer, used for sorting
|
||||
items list of integers List of item IDs this question is assigned to.
|
||||
@@ -41,6 +43,8 @@ ask_during_checkin boolean If ``true``, th
|
||||
the ticket instead.
|
||||
hidden boolean If ``true``, the question will only be shown in the
|
||||
backend.
|
||||
print_on_invoice boolean If ``true``, the question will only be shown on
|
||||
invoices.
|
||||
options list of objects In case of question type ``C`` or ``M``, this lists the
|
||||
available objects. Only writable during creation,
|
||||
use separate endpoint to modify this later.
|
||||
@@ -54,11 +58,12 @@ dependency_question integer Internal ID of
|
||||
this attribute is set to the value given in
|
||||
``dependency_value``. This cannot be combined with
|
||||
``ask_during_checkin``.
|
||||
dependency_value string The value ``dependency_question`` needs to be set to.
|
||||
If ``dependency_question`` is set to a boolean
|
||||
question, this should be ``"true"`` or ``"false"``.
|
||||
Otherwise, it should be the ``identifier`` of a
|
||||
question option.
|
||||
dependency_values list of strings If ``dependency_question`` is set to a boolean
|
||||
question, this should be ``["True"]`` or ``["False"]``.
|
||||
Otherwise, it should be a list of ``identifier`` values
|
||||
of question options.
|
||||
dependency_value string An old version of ``dependency_values`` that only allows
|
||||
for one value. **Deprecated.**
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.12
|
||||
@@ -75,6 +80,18 @@ dependency_value string The value ``dep
|
||||
|
||||
The attribute ``hidden`` and the question type ``CC`` have been added.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
|
||||
The attribute ``dependency_values`` has been added.
|
||||
|
||||
.. versionchanged:: 3.1
|
||||
|
||||
The attribute ``print_on_invoice`` has been added.
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
The attribute ``help_text`` has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -111,6 +128,7 @@ Endpoints
|
||||
{
|
||||
"id": 1,
|
||||
"question": {"en": "T-Shirt size"},
|
||||
"help_text": {"en": "Choose your preferred t-shirt-size"},
|
||||
"type": "C",
|
||||
"required": false,
|
||||
"items": [1, 2],
|
||||
@@ -118,8 +136,10 @@ Endpoints
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": false,
|
||||
"hidden": false,
|
||||
"print_on_invoice": false,
|
||||
"dependency_question": null,
|
||||
"dependency_value": null,
|
||||
"dependency_values": [],
|
||||
"options": [
|
||||
{
|
||||
"id": 1,
|
||||
@@ -179,6 +199,7 @@ Endpoints
|
||||
{
|
||||
"id": 1,
|
||||
"question": {"en": "T-Shirt size"},
|
||||
"help_text": {"en": "Choose your preferred t-shirt-size"},
|
||||
"type": "C",
|
||||
"required": false,
|
||||
"items": [1, 2],
|
||||
@@ -186,8 +207,10 @@ Endpoints
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": false,
|
||||
"hidden": false,
|
||||
"print_on_invoice": false,
|
||||
"dependency_question": null,
|
||||
"dependency_value": null,
|
||||
"dependency_values": [],
|
||||
"options": [
|
||||
{
|
||||
"id": 1,
|
||||
@@ -228,18 +251,20 @@ Endpoints
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/questions/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"question": {"en": "T-Shirt size"},
|
||||
"help_text": {"en": "Choose your preferred t-shirt-size"},
|
||||
"type": "C",
|
||||
"required": false,
|
||||
"items": [1, 2],
|
||||
"position": 1,
|
||||
"ask_during_checkin": false,
|
||||
"hidden": false,
|
||||
"print_on_invoice": false,
|
||||
"dependency_question": null,
|
||||
"dependency_value": null,
|
||||
"dependency_values": [],
|
||||
"options": [
|
||||
{
|
||||
"answer": {"en": "S"}
|
||||
@@ -265,6 +290,7 @@ Endpoints
|
||||
{
|
||||
"id": 1,
|
||||
"question": {"en": "T-Shirt size"},
|
||||
"help_text": {"en": "Choose your preferred t-shirt-size"},
|
||||
"type": "C",
|
||||
"required": false,
|
||||
"items": [1, 2],
|
||||
@@ -272,8 +298,10 @@ Endpoints
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": false,
|
||||
"hidden": false,
|
||||
"print_on_invoice": false,
|
||||
"dependency_question": null,
|
||||
"dependency_value": null,
|
||||
"dependency_values": [],
|
||||
"options": [
|
||||
{
|
||||
"id": 1,
|
||||
@@ -337,6 +365,7 @@ Endpoints
|
||||
{
|
||||
"id": 1,
|
||||
"question": {"en": "T-Shirt size"},
|
||||
"help_text": {"en": "Choose your preferred t-shirt-size"},
|
||||
"type": "C",
|
||||
"required": false,
|
||||
"items": [1, 2],
|
||||
@@ -344,8 +373,10 @@ Endpoints
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": false,
|
||||
"hidden": false,
|
||||
"print_on_invoice": false,
|
||||
"dependency_question": null,
|
||||
"dependency_value": null,
|
||||
"dependency_values": [],
|
||||
"options": [
|
||||
{
|
||||
"id": 1,
|
||||
|
||||
@@ -20,12 +20,22 @@ size integer The size of the
|
||||
items list of integers List of item IDs this quota acts on.
|
||||
variations list of integers List of item variation IDs this quota acts on.
|
||||
subevent integer ID of the date inside an event series this quota belongs to (or ``null``).
|
||||
close_when_sold_out boolean If ``true``, the quota will "close" as soon as it is
|
||||
sold out once. Even if tickets become available again,
|
||||
they will not be sold unless the quota is set to open
|
||||
again.
|
||||
closed boolean Whether the quota is currently closed (see above
|
||||
field).
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.10
|
||||
|
||||
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
|
||||
The attributes ``close_when_sold_out`` and ``closed`` have been added.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
@@ -61,7 +71,9 @@ Endpoints
|
||||
"size": 200,
|
||||
"items": [1, 2],
|
||||
"variations": [1, 4, 5, 7],
|
||||
"subevent": null
|
||||
"subevent": null,
|
||||
"close_when_sold_out": false,
|
||||
"closed": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -102,7 +114,9 @@ Endpoints
|
||||
"size": 200,
|
||||
"items": [1, 2],
|
||||
"variations": [1, 4, 5, 7],
|
||||
"subevent": null
|
||||
"subevent": null,
|
||||
"close_when_sold_out": false,
|
||||
"closed": false
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
@@ -123,14 +137,16 @@ Endpoints
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/quotas/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "Ticket Quota",
|
||||
"size": 200,
|
||||
"items": [1, 2],
|
||||
"variations": [1, 4, 5, 7],
|
||||
"subevent": null
|
||||
"subevent": null,
|
||||
"close_when_sold_out": false,
|
||||
"closed": false
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -147,7 +163,9 @@ Endpoints
|
||||
"size": 200,
|
||||
"items": [1, 2],
|
||||
"variations": [1, 4, 5, 7],
|
||||
"subevent": null
|
||||
"subevent": null,
|
||||
"close_when_sold_out": false,
|
||||
"closed": false
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event/item to create a quota for
|
||||
@@ -200,7 +218,9 @@ Endpoints
|
||||
1,
|
||||
2
|
||||
],
|
||||
"subevent": null
|
||||
"subevent": null,
|
||||
"close_when_sold_out": false,
|
||||
"closed": false
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
|
||||
209
doc/api/resources/seatingplans.rst
Normal file
209
doc/api/resources/seatingplans.rst
Normal file
@@ -0,0 +1,209 @@
|
||||
.. _`rest-seatingplans`:
|
||||
|
||||
Seating plans
|
||||
=============
|
||||
|
||||
Resource description
|
||||
--------------------
|
||||
|
||||
The seating plan resource contains the following public fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the plan
|
||||
name string Human-readable name of the plan
|
||||
layout object JSON representation of the seating plan. These
|
||||
representations follow a JSON schema that currently
|
||||
still evolves. The version in use can be found `here`_.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
|
||||
This endpoint has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/seatingplans/
|
||||
|
||||
Returns a list of all seating plans within a given organizer.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/seatingplans/ 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": 2,
|
||||
"name": "Main plan",
|
||||
"layout": { … }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/seatingplans/(id)/
|
||||
|
||||
Returns information on one plan, identified by its ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/seatingplans/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": 2,
|
||||
"name": "Main plan",
|
||||
"layout": { … }
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param id: The ``id`` field of the seating plan to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/seatingplans/
|
||||
|
||||
Creates a new seating plan
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/seatingplans/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "Main plan",
|
||||
"layout": { … }
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Main plan",
|
||||
"layout": { … }
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to create a seating plan for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The seating plan could not be created due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/seatingplans/(id)/
|
||||
|
||||
Update a plan. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||
want to change.
|
||||
|
||||
You can change all fields of the resource except the ``id`` field. **You can not change a plan while it is in use for
|
||||
any events.**
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/seatingplans/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 94
|
||||
|
||||
{
|
||||
"name": "Old plan"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Old plan",
|
||||
"layout": { … }
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param id: The ``id`` field of the plan to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The plan could not be modified due to invalid submitted data
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource **or** the plan is currently in use.
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/seatingplans/(id)/
|
||||
|
||||
Delete a plan. You can not delete plans which are currently in use by any events.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/seatingplans/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 id: The ``id`` field of the plan to delete
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource **or** the plan is currently in use.
|
||||
|
||||
|
||||
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/seating/seating-plan.schema.json
|
||||
@@ -1,3 +1,9 @@
|
||||
.. spelling::
|
||||
|
||||
geo
|
||||
lat
|
||||
lon
|
||||
|
||||
.. _rest-subevents:
|
||||
|
||||
Event series dates / Sub-events
|
||||
@@ -28,6 +34,8 @@ date_admission datetime The sub-event's
|
||||
presale_start datetime The sub-date at which the ticket shop opens (or ``null``)
|
||||
presale_end datetime The sub-date at which the ticket shop closes (or ``null``)
|
||||
location multi-lingual string The sub-event location (or ``null``)
|
||||
geo_lat float Latitude of the location (or ``null``)
|
||||
geo_lon float Longitude of the location (or ``null``)
|
||||
item_price_overrides list of objects List of items for which this sub-event overrides the
|
||||
default price
|
||||
├ item integer The internal item ID
|
||||
@@ -36,7 +44,11 @@ variation_price_overrides list of objects List of variati
|
||||
the default price
|
||||
├ variation integer The internal variation ID
|
||||
└ price money (string) The price or ``null`` for the default price
|
||||
meta_data dict Values set for organizer-specific meta data parameters.
|
||||
meta_data object Values set for organizer-specific meta data parameters.
|
||||
seating_plan integer If reserved seating is in use, the ID of a seating
|
||||
plan. Otherwise ``null``.
|
||||
seat_category_mapping object An object mapping categories of the seating plan
|
||||
(strings) to items in the event (integers or ``null``).
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
@@ -54,9 +66,21 @@ meta_data dict Values set for
|
||||
|
||||
The attribute ``is_public`` has been added.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
|
||||
The attributes ``seating_plan`` and ``seat_category_mapping`` have been added.
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
|
||||
The attributes ``geo_lat`` and ``geo_lon`` have been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
|
||||
The sub-events resource can now be filtered by meta data attributes.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/
|
||||
|
||||
Returns a list of all sub-events of an event.
|
||||
@@ -93,7 +117,11 @@ Endpoints
|
||||
"date_admission": null,
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": 2,
|
||||
@@ -113,6 +141,11 @@ Endpoints
|
||||
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned.
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of the main event
|
||||
:query array attr[meta_data_key]: By providing the key and value of a meta data attribute, the list of sub-events
|
||||
will only contain the sub-events matching the set criteria. Providing ``?attr[Format]=Seminar`` would return
|
||||
only those sub-events having set their ``Format`` meta data to ``Seminar``, ``?attr[Format]=`` only those, that
|
||||
have no value set. Please note that this filter will respect default values set on
|
||||
organizer or event level.
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||
@@ -130,7 +163,7 @@ Endpoints
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/subevents/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": {"en": "First Sample Conference"},
|
||||
@@ -142,6 +175,10 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": 2,
|
||||
@@ -172,6 +209,10 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": 2,
|
||||
@@ -223,6 +264,10 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": 2,
|
||||
@@ -255,7 +300,7 @@ Endpoints
|
||||
PATCH /api/v1/organizers/bigevents/events/sampleconf/subevents/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": {"en": "New Subevent Name"},
|
||||
@@ -287,6 +332,10 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": 2,
|
||||
@@ -371,6 +420,10 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": 2,
|
||||
|
||||
671
doc/api/resources/teams.rst
Normal file
671
doc/api/resources/teams.rst
Normal file
@@ -0,0 +1,671 @@
|
||||
.. spelling:: fullname
|
||||
|
||||
.. _`rest-teams`:
|
||||
|
||||
Teams
|
||||
=====
|
||||
|
||||
.. warning:: Unlike our user interface, the team API **does** allow you to lock yourself out by deleting or modifying
|
||||
the team your user or API key belongs to. Be careful around here!
|
||||
|
||||
Team resource
|
||||
-------------
|
||||
|
||||
The team resource contains the following public fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the team
|
||||
name string Team name
|
||||
all_events boolean Whether this team has access to all events
|
||||
limit_events list List of event slugs this team has access to
|
||||
can_create_events boolean
|
||||
can_change_teams boolean
|
||||
can_change_organizer_settings boolean
|
||||
can_manage_gift_cards boolean
|
||||
can_change_event_settings boolean
|
||||
can_change_items boolean
|
||||
can_view_orders boolean
|
||||
can_change_orders boolean
|
||||
can_view_vouchers boolean
|
||||
can_change_vouchers boolean
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
Team member resource
|
||||
--------------------
|
||||
|
||||
The team member resource contains the following public fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the user
|
||||
email string The user's email address
|
||||
fullname string The user's full name (or ``null``)
|
||||
require_2fa boolean Whether this user uses two-factor-authentication
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
Team invite resource
|
||||
--------------------
|
||||
|
||||
The team invite resource contains the following public fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the invite
|
||||
email string The invitee's email address
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
Team API token resource
|
||||
-----------------------
|
||||
|
||||
The team API token resource contains the following public fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the invite
|
||||
name string Name of this API token
|
||||
active boolean Whether this API token is active (can never be set to
|
||||
``true`` again once ``false``)
|
||||
token string The actual API token. Will only be sent back during
|
||||
token creation.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
Team endpoints
|
||||
--------------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/teams/
|
||||
|
||||
Returns a list of all teams within a given organizer.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/teams/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Admin team",
|
||||
"all_events": true,
|
||||
"limit_events": [],
|
||||
"can_create_events": true,
|
||||
...
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/teams/(id)/
|
||||
|
||||
Returns information on one team, identified by its ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/teams/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Admin team",
|
||||
"all_events": true,
|
||||
"limit_events": [],
|
||||
"can_create_events": true,
|
||||
...
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param id: The ``id`` field of the team to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/teams/
|
||||
|
||||
Creates a new team
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/teams/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "Admin team",
|
||||
"all_events": true,
|
||||
"limit_events": [],
|
||||
"can_create_events": true,
|
||||
...
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Admin team",
|
||||
"all_events": true,
|
||||
"limit_events": [],
|
||||
"can_create_events": true,
|
||||
...
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to create a team for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The team could not be created due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/teams/(id)/
|
||||
|
||||
Update a team. 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/teams/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 94
|
||||
|
||||
{
|
||||
"can_create_events": true
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Admin team",
|
||||
"all_events": true,
|
||||
"limit_events": [],
|
||||
"can_create_events": true,
|
||||
...
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param id: The ``id`` field of the team to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The team could not be modified due to invalid submitted data
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/teams/(id)/
|
||||
|
||||
Deletes a team.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/teams/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204 No Content
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param id: The ``id`` field of the team to delete
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
|
||||
|
||||
Team member endpoints
|
||||
---------------------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/teams/(team)/members/
|
||||
|
||||
Returns a list of all members of a team.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/teams/1/members/ 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,
|
||||
"fullname": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"require_2fa": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param team: The ``id`` field of the team 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: The requested team does not exist
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/teams/(team)/members/(id)/
|
||||
|
||||
Returns information on one team member, identified by their ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/teams/1/members/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,
|
||||
"fullname": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"require_2fa": true
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param team: The ``id`` field of the team to fetch
|
||||
:param id: The ``id`` field of the member 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: The requested team or member does not exist
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/teams/(team)/members/(id)/
|
||||
|
||||
Removes a member from the team.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/teams/1/members/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204 No Content
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param team: The ``id`` field of the team to modify
|
||||
:param id: The ``id`` field of the member to delete
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
||||
:statuscode 404: The requested team or member does not exist
|
||||
|
||||
Team invite endpoints
|
||||
---------------------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/teams/(team)/invites/
|
||||
|
||||
Returns a list of all invitations to a team.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/teams/1/invites/ 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,
|
||||
"email": "john@example.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param team: The ``id`` field of the team 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: The requested team does not exist
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/teams/(team)/invites/(id)/
|
||||
|
||||
Returns information on one invite, identified by its ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/teams/1/invites/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,
|
||||
"email": "john@example.org"
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param team: The ``id`` field of the team to fetch
|
||||
:param id: The ``id`` field of the invite 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: The requested team or invite does not exist
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/teams/(team)/invites/
|
||||
|
||||
Invites someone into the team. Note that if the user already has a pretix account, you will receive a response without
|
||||
an ``id`` and instead of an invite being created, the user will be directly added to the team.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/teams/1/invites/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 94
|
||||
|
||||
{
|
||||
"email": "mark@example.org"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"email": "mark@example.org"
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param team: The ``id`` field of the team to modify
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
||||
:statuscode 404: The requested team does not exist
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/teams/(team)/invites/(id)/
|
||||
|
||||
Revokes an invite.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/teams/1/invites/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204 No Content
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param team: The ``id`` field of the team to modify
|
||||
:param id: The ``id`` field of the invite to delete
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
||||
:statuscode 404: The requested team or invite does not exist
|
||||
|
||||
Team API token endpoints
|
||||
------------------------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/teams/(team)/tokens/
|
||||
|
||||
Returns a list of all API tokens of a team.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/teams/1/tokens/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"active": true,
|
||||
"name": "Test token"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param team: The ``id`` field of the team 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: The requested team does not exist
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/teams/(team)/tokens/(id)/
|
||||
|
||||
Returns information on one token, identified by its ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/teams/1/tokens/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"active": true,
|
||||
"name": "Test token"
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param team: The ``id`` field of the team to fetch
|
||||
:param id: The ``id`` field of the token 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: The requested team or token does not exist
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/teams/(team)/tokens/
|
||||
|
||||
Creates a new token.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/teams/1/tokens/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 94
|
||||
|
||||
{
|
||||
"name": "New token"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 2,
|
||||
"name": "New token",
|
||||
"active": true,
|
||||
"token": "",
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param team: The ``id`` field of the team to create a token for
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
||||
:statuscode 404: The requested team does not exist
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/teams/(team)/tokens/(id)/
|
||||
|
||||
Disables a token.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/teams/1/tokens/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "My token",
|
||||
"active": false
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param team: The ``id`` field of the team to modify
|
||||
:param id: The ``id`` field of the token to delete
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
||||
:statuscode 404: The requested team or token does not exist
|
||||
@@ -38,9 +38,11 @@ quota integer An ID of a quot
|
||||
attached either to a specific product or to all
|
||||
products within one quota or it can be available
|
||||
for all items without restriction.
|
||||
seat string ``seat_guid`` attribute of a specific seat (or ``null``)
|
||||
tag string A string that is used for grouping vouchers
|
||||
comment string An internal comment on the voucher
|
||||
subevent integer ID of the date inside an event series this voucher belongs to (or ``null``).
|
||||
show_hidden_items boolean Only if set to ``true``, this voucher allows to buy products with the property ``hide_without_voucher``. Defaults to ``true``.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
@@ -48,6 +50,14 @@ subevent integer ID of the date
|
||||
|
||||
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
|
||||
The attribute ``show_hidden_items`` has been added.
|
||||
|
||||
.. versionchanged:: 3.4
|
||||
|
||||
The attribute ``seat`` has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -91,7 +101,8 @@ Endpoints
|
||||
"quota": null,
|
||||
"tag": "testvoucher",
|
||||
"comment": "",
|
||||
"subevent": null
|
||||
"seat": null,
|
||||
"subevent": null,
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -157,6 +168,7 @@ Endpoints
|
||||
"quota": null,
|
||||
"tag": "testvoucher",
|
||||
"comment": "",
|
||||
"seat": null,
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
@@ -220,6 +232,7 @@ Endpoints
|
||||
"quota": null,
|
||||
"tag": "testvoucher",
|
||||
"comment": "",
|
||||
"seat": null,
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
@@ -347,6 +360,7 @@ Endpoints
|
||||
"quota": null,
|
||||
"tag": "testvoucher",
|
||||
"comment": "",
|
||||
"seat": null,
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ Endpoints
|
||||
POST /api/v1/organizers/bigevents/webhooks/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"enabled": true,
|
||||
|
||||
12
doc/contents.rst
Normal file
12
doc/contents.rst
Normal file
@@ -0,0 +1,12 @@
|
||||
Table of contents
|
||||
=================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
user/index
|
||||
admin/index
|
||||
api/index
|
||||
development/index
|
||||
plugins/index
|
||||
|
||||
70
doc/development/api/auth.rst
Normal file
70
doc/development/api/auth.rst
Normal file
@@ -0,0 +1,70 @@
|
||||
.. highlight:: python
|
||||
:linenothreshold: 5
|
||||
|
||||
Pluggable authentication backends
|
||||
=================================
|
||||
|
||||
Plugins can supply additional authentication backends. This is mainly useful in self-hosted installations
|
||||
and allows you to use company-wide login mechanisms such as LDAP or OAuth for accessing pretix' backend.
|
||||
|
||||
Every authentication backend contains an implementation of the interface defined in ``pretix.base.auth.BaseAuthBackend``
|
||||
(see below). Note that pretix authentication backends work differently than plain Django authentication backends.
|
||||
Basically, three pre-defined flows are supported:
|
||||
|
||||
* Authentication mechanisms that rely on a **set of input parameters**, e.g. a username and a password. These can be
|
||||
implemented by supplying the ``login_form_fields`` property and a ``form_authenticate`` method.
|
||||
|
||||
* Authentication mechanisms that rely on **external sessions**, e.g. a cookie or a proxy HTTP header. These can be
|
||||
implemented by supplying a ``request_authenticate`` method.
|
||||
|
||||
* Authentication mechanisms that rely on **redirection**, e.g. to an OAuth provider. These can be implemented by
|
||||
supplying a ``authentication_url`` method and implementing a custom return view.
|
||||
|
||||
Authentication backends are *not* collected through a signal. Instead, they must explicitly be set through the
|
||||
``auth_backends`` directive in the ``pretix.cfg`` :ref:`configuration file <config>`.
|
||||
|
||||
In each of these methods (``form_authenticate``, ``request_authenticate`` or your custom view) you are supposed to
|
||||
either get an existing :py:class:`pretix.base.models.User` object from the database or create a new one. There are a
|
||||
few rules you need to follow:
|
||||
|
||||
* You **MUST** only return users with the ``auth_backend`` attribute set to the ``identifier`` value of your backend.
|
||||
|
||||
* You **MUST** create new users with the ``auth_backend`` attribute set to the ``identifier`` value of your backend.
|
||||
|
||||
* Every user object **MUST** have an email address. Email addresses are globally unique. If the email address is
|
||||
already registered to a user who signs in through a different backend, you **SHOULD** refuse the login.
|
||||
|
||||
The backend interface
|
||||
---------------------
|
||||
|
||||
.. class:: pretix.base.auth.BaseAuthBackend
|
||||
|
||||
The central object of each backend is the subclass of ``BaseAuthBackend``.
|
||||
|
||||
.. autoattribute:: identifier
|
||||
|
||||
This is an abstract attribute, you **must** override this!
|
||||
|
||||
.. autoattribute:: verbose_name
|
||||
|
||||
This is an abstract attribute, you **must** override this!
|
||||
|
||||
.. autoattribute:: login_form_fields
|
||||
|
||||
.. autoattribute:: visible
|
||||
|
||||
.. automethod:: form_authenticate
|
||||
|
||||
.. automethod:: request_authenticate
|
||||
|
||||
.. automethod:: authentication_url
|
||||
|
||||
Logging users in
|
||||
----------------
|
||||
|
||||
If you return a user from ``form_authenticate`` or ``request_authenticate``, the system will handle everything else
|
||||
for you correctly. However, if you use a redirection method and build a custom view to verify the login, we strongly
|
||||
recommend that you use the following utility method to correctly set session values and enforce two-factor
|
||||
authentication (if activated):
|
||||
|
||||
.. autofunction:: pretix.control.views.auth.process_login
|
||||
@@ -66,7 +66,7 @@ event-related views, there is also a signal that allows you to add the view to t
|
||||
|
||||
from django.urls import resolve, reverse
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from pretix.control.signals import nav_event
|
||||
|
||||
|
||||
|
||||
@@ -101,9 +101,12 @@ The template is passed the following context variables:
|
||||
The ``Event`` object
|
||||
|
||||
``signature`` (optional, only if configured)
|
||||
The body as markdown (render with ``{{ signature|safe }}``)
|
||||
The signature with event organizer contact details as markdown (render with ``{{ signature|safe }}``)
|
||||
|
||||
``order`` (optional, only if applicable)
|
||||
The ``Order`` object
|
||||
|
||||
``position`` (optional, only if applicable)
|
||||
The ``OrderPosition`` object
|
||||
|
||||
.. _inlinestyler: https://pypi.org/project/inlinestyler/
|
||||
|
||||
@@ -12,7 +12,7 @@ Core
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types,
|
||||
item_copy_data, register_sales_channels, register_global_settings
|
||||
item_copy_data, register_sales_channels, register_global_settings, quota_availability, global_email_filter
|
||||
|
||||
Order events
|
||||
""""""""""""
|
||||
@@ -20,13 +20,24 @@ Order events
|
||||
There are multiple signals that will be sent out in the ordering cycle:
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: validate_cart, validate_order, order_fee_calculation, order_paid, order_placed, order_canceled, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download
|
||||
:members: validate_cart, validate_cart_addons, validate_order, 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
|
||||
"""""""""
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: checkin_created
|
||||
|
||||
|
||||
Frontend
|
||||
--------
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, checkout_flow_steps, order_info, order_meta_from_request, position_info
|
||||
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description
|
||||
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
:members: order_info, order_info_top, order_meta_from_request
|
||||
|
||||
Request flow
|
||||
""""""""""""
|
||||
@@ -45,8 +56,8 @@ Backend
|
||||
|
||||
.. automodule:: pretix.control.signals
|
||||
:members: nav_event, html_head, html_page_start, quota_detail_html, nav_topbar, nav_global, nav_organizer, nav_event_settings,
|
||||
order_info, event_settings_widget, oauth_application_registered, order_position_buttons
|
||||
|
||||
order_info, event_settings_widget, oauth_application_registered, order_position_buttons, subevent_forms,
|
||||
item_formsets, order_search_filter_q
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: logentry_display, logentry_object_link, requiredaction_display, timeline_events
|
||||
@@ -74,3 +85,12 @@ Ticket designs
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: layout_text_variables
|
||||
|
||||
.. automodule:: pretix.plugins.ticketoutputpdf.signals
|
||||
:members: override_layout
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: validate_event_settings, api_event_settings_fields
|
||||
|
||||
112
doc/development/api/import.rst
Normal file
112
doc/development/api/import.rst
Normal file
@@ -0,0 +1,112 @@
|
||||
.. highlight:: python
|
||||
:linenothreshold: 5
|
||||
|
||||
.. _`importcol`:
|
||||
|
||||
Extending the order import process
|
||||
==================================
|
||||
|
||||
It's possible through the backend to import orders into pretix, for example from a legacy ticketing system. If your
|
||||
plugins defines additional data structures around orders, it might be useful to make it possible to import them as well.
|
||||
|
||||
Import process
|
||||
--------------
|
||||
|
||||
Here's a short description of pretix' import process to show you where the system will need to interact with your plugin.
|
||||
You can find more detailed descriptions of the attributes and methods further below.
|
||||
|
||||
1. The user uploads a CSV file. The system tries to parse the CSV file and understand its column headers.
|
||||
|
||||
2. A preview of the file is shown to the user and the user is asked to assign the various different input parameters to
|
||||
columns of the file or static values. For example, the user either needs to manually select a product or specify a
|
||||
column that contains a product. For this purpose, a select field is rendered for every possible input column,
|
||||
allowing the user to choose between a default/empty value (defined by your ``default_value``/``default_label``)
|
||||
attributes, the columns of the uploaded file, or a static value (defined by your ``static_choices`` method).
|
||||
|
||||
3. The user submits its assignment and the system uses the ``resolve`` method of all columns to get the raw value for
|
||||
all columns.
|
||||
|
||||
4. The system uses the ``clean`` method of all columns to verify that all input fields are valid and transformed to the
|
||||
correct data type.
|
||||
|
||||
5. The system prepares internal model objects (``Order`` etc) and uses the ``assign`` method of all columns to assign
|
||||
these objects with actual values.
|
||||
|
||||
6. The system saves all of these model objects to the database in a database transaction. Plugins can create additional
|
||||
objects in this stage through their ``save`` method.
|
||||
|
||||
Column registration
|
||||
-------------------
|
||||
|
||||
The import API does not make a lot of usage from signals, however, it
|
||||
does use a signal to get a list of all available import columns. Your plugin
|
||||
should listen for this signal and return the subclass of ``pretix.base.orderimport.ImportColumn``
|
||||
that we'll provide in this plugin:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
from django.dispatch import receiver
|
||||
|
||||
from pretix.base.signals import order_import_columns
|
||||
|
||||
|
||||
@receiver(order_import_columns, dispatch_uid="custom_columns")
|
||||
def register_column(sender, **kwargs):
|
||||
return [
|
||||
EmailColumn(sender),
|
||||
]
|
||||
|
||||
The column class API
|
||||
--------------------
|
||||
|
||||
.. class:: pretix.base.orderimport.ImportColumn
|
||||
|
||||
The central object of each import extension is the subclass of ``ImportColumn``.
|
||||
|
||||
.. py:attribute:: ImportColumn.event
|
||||
|
||||
The default constructor sets this property to the event we are currently
|
||||
working for.
|
||||
|
||||
.. autoattribute:: identifier
|
||||
|
||||
This is an abstract attribute, you **must** override this!
|
||||
|
||||
.. autoattribute:: verbose_name
|
||||
|
||||
This is an abstract attribute, you **must** override this!
|
||||
|
||||
.. autoattribute:: default_value
|
||||
|
||||
.. autoattribute:: default_label
|
||||
|
||||
.. autoattribute:: initial
|
||||
|
||||
.. automethod:: static_choices
|
||||
|
||||
.. automethod:: resolve
|
||||
|
||||
.. automethod:: clean
|
||||
|
||||
.. automethod:: assign
|
||||
|
||||
.. automethod:: save
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
For example, the import column responsible for assigning email addresses looks like this:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
class EmailColumn(ImportColumn):
|
||||
identifier = 'email'
|
||||
verbose_name = _('E-mail address')
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if value:
|
||||
EmailValidator()(value)
|
||||
return value
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
order.email = value
|
||||
@@ -12,8 +12,11 @@ Contents:
|
||||
payment
|
||||
payment_2.0
|
||||
email
|
||||
placeholder
|
||||
invoice
|
||||
shredder
|
||||
import
|
||||
customview
|
||||
auth
|
||||
general
|
||||
quality
|
||||
|
||||
@@ -62,6 +62,8 @@ The provider class
|
||||
|
||||
.. autoattribute:: is_enabled
|
||||
|
||||
.. autoattribute:: priority
|
||||
|
||||
.. autoattribute:: settings_form_fields
|
||||
|
||||
.. automethod:: settings_form_clean
|
||||
@@ -108,8 +110,16 @@ The provider class
|
||||
|
||||
.. automethod:: execute_refund
|
||||
|
||||
.. automethod:: refund_control_render
|
||||
|
||||
.. automethod:: api_payment_details
|
||||
|
||||
.. automethod:: matching_id
|
||||
|
||||
.. automethod:: shred_payment_info
|
||||
|
||||
.. automethod:: cancel_payment
|
||||
|
||||
.. autoattribute:: is_implicit
|
||||
|
||||
.. autoattribute:: is_meta
|
||||
|
||||
79
doc/development/api/placeholder.rst
Normal file
79
doc/development/api/placeholder.rst
Normal file
@@ -0,0 +1,79 @@
|
||||
.. highlight:: python
|
||||
:linenothreshold: 5
|
||||
|
||||
Writing an e-mail placeholder plugin
|
||||
====================================
|
||||
|
||||
An email placeholder is a dynamic value that pretix users can use in their email templates.
|
||||
|
||||
Please read :ref:`Creating a plugin <pluginsetup>` first, if you haven't already.
|
||||
|
||||
Placeholder registration
|
||||
------------------------
|
||||
|
||||
The placeholder API does not make a lot of usage from signals, however, it
|
||||
does use a signal to get a list of all available email placeholders. Your plugin
|
||||
should listen for this signal and return an instance of a subclass of ``pretix.base.email.BaseMailTextPlaceholder``::
|
||||
|
||||
from django.dispatch import receiver
|
||||
|
||||
from pretix.base.signals import register_mail_placeholders
|
||||
|
||||
|
||||
@receiver(register_mail_placeholders, dispatch_uid="placeholder_custom")
|
||||
def register_mail_renderers(sender, **kwargs):
|
||||
from .email import MyPlaceholderClass
|
||||
return MyPlaceholder()
|
||||
|
||||
|
||||
Context mechanism
|
||||
-----------------
|
||||
|
||||
Emails are sent in different "contexts" within pretix. For example, many emails are sent in the
|
||||
the context of an order, but some are not, such as the notification of a waiting list voucher.
|
||||
|
||||
Not all placeholders make sense in every email, and placeholders usually depend some parameters
|
||||
themselves, such as the ``Order`` object. Therefore, placeholders are expected to explicitly declare
|
||||
what values they depend on and they will only be available in an email if all those dependencies are
|
||||
met. Currently, placeholders can depend on the following context parameters:
|
||||
|
||||
* ``event``
|
||||
* ``order``
|
||||
* ``position``
|
||||
* ``waiting_list_entry``
|
||||
* ``invoice_address``
|
||||
* ``payment``
|
||||
|
||||
There are a few more that are only to be used internally but not by plugins.
|
||||
|
||||
The placeholder class
|
||||
---------------------
|
||||
|
||||
.. class:: pretix.base.email.BaseMailTextPlaceholder
|
||||
|
||||
.. autoattribute:: identifier
|
||||
|
||||
This is an abstract attribute, you **must** override this!
|
||||
|
||||
.. autoattribute:: required_context
|
||||
|
||||
This is an abstract attribute, you **must** override this!
|
||||
|
||||
.. automethod:: render
|
||||
|
||||
This is an abstract method, you **must** implement this!
|
||||
|
||||
.. automethod:: render_sample
|
||||
|
||||
This is an abstract method, you **must** implement this!
|
||||
|
||||
Helper class for simple placeholders
|
||||
------------------------------------
|
||||
|
||||
pretix ships with a helper class that makes it easy to provide placeholders based on simple
|
||||
functions::
|
||||
|
||||
placeholder = SimpleFunctionalMailTextPlaceholder(
|
||||
'code', ['order'], lambda order: order.code, sample='F8VVL'
|
||||
)
|
||||
|
||||
@@ -46,6 +46,9 @@ name string The human-readable name of your plugin
|
||||
author string Your name
|
||||
version string A human-readable version code of your plugin
|
||||
description string A more verbose description of what your plugin does.
|
||||
category string Category of a plugin. Either one of ``"FEATURE"``, ``"PAYMENT"``,
|
||||
``"INTEGRATION"``, ``"CUSTOMIZATION"``, ``"FORMAT"``, or ``"API"``,
|
||||
or any other string.
|
||||
visible boolean (optional) ``True`` by default, can hide a plugin so it cannot be normally activated.
|
||||
restricted boolean (optional) ``False`` by default, restricts a plugin such that it can only be enabled
|
||||
for an event by system administrators / superusers.
|
||||
@@ -58,7 +61,7 @@ A working example would be::
|
||||
from pretix.base.plugins import PluginConfig
|
||||
except ImportError:
|
||||
raise RuntimeError("Please use pretix 2.7 or above to run this plugin!")
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class PaypalApp(PluginConfig):
|
||||
@@ -69,6 +72,7 @@ A working example would be::
|
||||
name = _("PayPal")
|
||||
author = _("the pretix team")
|
||||
version = '1.0.0'
|
||||
category = 'PAYMENT
|
||||
visible = True
|
||||
restricted = False
|
||||
description = _("This plugin allows you to receive payments via PayPal")
|
||||
|
||||
@@ -35,9 +35,9 @@ The shredder class
|
||||
|
||||
.. class:: pretix.base.shredder.BaseDataShredder
|
||||
|
||||
The central object of each invoice renderer is the subclass of ``BaseInvoiceRenderer``.
|
||||
The central object of each data shredder is the subclass of ``BaseDataShredder``.
|
||||
|
||||
.. py:attribute:: BaseInvoiceRenderer.event
|
||||
.. py:attribute:: BaseDataShredder.event
|
||||
|
||||
The default constructor sets this property to the event we are currently
|
||||
working for.
|
||||
|
||||
@@ -69,3 +69,13 @@ The output class
|
||||
.. automethod:: generate_order
|
||||
|
||||
.. autoattribute:: download_button_text
|
||||
|
||||
.. autoattribute:: download_button_icon
|
||||
|
||||
.. autoattribute:: multi_download_button_text
|
||||
|
||||
.. autoattribute:: long_download_button_text
|
||||
|
||||
.. autoattribute:: preview_allowed
|
||||
|
||||
.. autoattribute:: javascript_required
|
||||
|
||||
@@ -18,7 +18,7 @@ Coding style and quality
|
||||
* We expect all new code to come with proper tests. When writing new tests, please write them using `pytest-style`_
|
||||
test functions and raw ``assert`` statements. Use `fixtures`_ to prevent repetitive code. Some old parts of pretix'
|
||||
test suite are in the style of Python's unit test module. If you extend those files, you might continue in this style,
|
||||
but please use pytest style for any new test files.
|
||||
but please use ``pytest`` style for any new test files.
|
||||
|
||||
* Please keep the first line of your commit messages short. When referencing an issue, please phrase it like
|
||||
``Fix #123 -- Problems with order creation`` or ``Refs #123 -- Fix this part of that bug``.
|
||||
|
||||
@@ -69,7 +69,7 @@ We now need a way to translate the action codes like ``pretix.event.changed`` in
|
||||
strings. The :py:attr:`pretix.base.signals.logentry_display` signals allows you to do so. A simple
|
||||
implementation could look like::
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from pretix.base.signals import logentry_display
|
||||
|
||||
@receiver(signal=logentry_display)
|
||||
|
||||
@@ -65,9 +65,7 @@ Then, create the local database::
|
||||
python manage.py migrate
|
||||
|
||||
A first user with username ``admin@localhost`` and password ``admin`` will be automatically
|
||||
created. If you want to generate more test data, run::
|
||||
|
||||
python make_testdata.py
|
||||
created.
|
||||
|
||||
If you want to see pretix in a different language than English, you have to compile our language
|
||||
files::
|
||||
@@ -83,8 +81,7 @@ To run the local development webserver, execute::
|
||||
and head to http://localhost:8000/
|
||||
|
||||
As we did not implement an overall front page yet, you need to go directly to
|
||||
http://localhost:8000/control/ for the admin view or, if you imported the test
|
||||
data as suggested above, to the event page at http://localhost:8000/bigevents/2019/
|
||||
http://localhost:8000/control/ for the admin view.
|
||||
|
||||
.. note:: If you want the development server to listen on a different interface or
|
||||
port (for example because you develop on `pretixdroid`_), you can check
|
||||
|
||||
@@ -1,12 +1 @@
|
||||
Table of contents
|
||||
=================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
user/index
|
||||
admin/index
|
||||
api/index
|
||||
development/index
|
||||
plugins/index
|
||||
|
||||
.. include:: contents.rst
|
||||
224
doc/plugins/campaigns.rst
Normal file
224
doc/plugins/campaigns.rst
Normal file
@@ -0,0 +1,224 @@
|
||||
Campaigns
|
||||
=========
|
||||
|
||||
The campaigns plugin provides a HTTP API that allows you to create new campaigns.
|
||||
|
||||
Resource description
|
||||
--------------------
|
||||
|
||||
The campaign resource contains the following public fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal campaign ID
|
||||
code string The URL component of the campaign, e.g. with code ``BAR``
|
||||
the campaign URL would to be ``https://<server>/<organizer>/<event>/c/BAR/``.
|
||||
This value needs to be *globally unique* and we do not
|
||||
recommend setting it manually. If you omit it, a random
|
||||
value will be chosen.
|
||||
description string An internal, human-readable name of the campaign.
|
||||
external_target string An URL to redirect to from the tracking link. To redirect to
|
||||
the ticket shop, use an empty string.
|
||||
order_count integer Number of orders tracked on this campaign (read-only)
|
||||
click_count integer Number of clicks tracked on this campaign (read-only)
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/campaigns/
|
||||
|
||||
Returns a list of all campaigns configured for an event.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/campaigns/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"code": "wZnL11fjq",
|
||||
"description": "Facebook",
|
||||
"external_target": "",
|
||||
"order_count:" 0,
|
||||
"click_count:" 0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query page: The page number in case of a multi-page result set, default is 1
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to view it.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/campaigns/(id)/
|
||||
|
||||
Returns information on one campaign, identified by its ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/campaigns/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,
|
||||
"code": "wZnL11fjq",
|
||||
"description": "Facebook",
|
||||
"external_target": "",
|
||||
"order_count:" 0,
|
||||
"click_count:" 0
|
||||
}
|
||||
|
||||
: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 campaign to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event/campaign does not exist **or** you have no permission to view it.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/campaigns/
|
||||
|
||||
Create a new campaign.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/campaigns/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 166
|
||||
|
||||
{
|
||||
"description": "Twitter"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 2,
|
||||
"code": "IfVJQzSBL",
|
||||
"description": "Twitter",
|
||||
"external_target": "",
|
||||
"order_count:" 0,
|
||||
"click_count:" 0
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to create a campaign for
|
||||
:param event: The ``slug`` field of the event to create a campaign for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The campaign 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 campaigns.
|
||||
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/campaigns/(id)/
|
||||
|
||||
Update a campaign. 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/campaigns/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 34
|
||||
|
||||
{
|
||||
"external_target": "https://mywebsite.com"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 2,
|
||||
"code": "IfVJQzSBL",
|
||||
"description": "Twitter",
|
||||
"external_target": "https://mywebsite.com",
|
||||
"order_count:" 0,
|
||||
"click_count:" 0
|
||||
}
|
||||
|
||||
: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 campaign to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The campaign could not be modified due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event/campaign does not exist **or** you have no permission to change it.
|
||||
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/campaigns/(id)/
|
||||
|
||||
Delete a campaign and all associated data.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/events/sampleconf/campaigns/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 campaign to delete
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event/campaign does not exist **or** you have no permission to change it
|
||||
277
doc/plugins/digital.rst
Normal file
277
doc/plugins/digital.rst
Normal file
@@ -0,0 +1,277 @@
|
||||
Digital content
|
||||
===============
|
||||
|
||||
The digital content plugin provides a HTTP API that allows you to create new digital content for your ticket holders,
|
||||
such as live streams, videos, or material downloads.
|
||||
|
||||
Resource description
|
||||
--------------------
|
||||
|
||||
The digital content resource contains the following public fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal content ID
|
||||
title multi-lingual string The content title (required)
|
||||
content_type string The type of content, valid values are ``webinar``, ``video``, ``livestream``, ``link``, ``file``
|
||||
url string The location of the digital content
|
||||
description multi-lingual string A public description of the item. May contain Markdown
|
||||
syntax and is not required.
|
||||
available_from datetime The first date time at which this content will be shown
|
||||
(or ``null``).
|
||||
available_until datetime The last date time at which this content will b e shown
|
||||
(or ``null``).
|
||||
all_products boolean If ``true``, the content is available to all buyers of tickets for this event. The ``limit_products`` field is ignored in this case.
|
||||
limit_products list of integers List of product/item IDs. This content is only shown to buyers of these ticket types.
|
||||
position integer An integer, used for sorting
|
||||
subevent integer Date in an event series this content should be shown for. Should be ``null`` if this is not an event series or if this should be shown to all customers.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/
|
||||
|
||||
Returns a list of all digital content configured for an event.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/ 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,
|
||||
"subevent": null,
|
||||
"title": {
|
||||
"en": "Concert livestream"
|
||||
},
|
||||
"content_type": "link",
|
||||
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"description": {
|
||||
"en": "Watch our event live here on YouTube!"
|
||||
},
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"available_from": "2020-03-22T23:00:00Z",
|
||||
"available_until": null,
|
||||
"position": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query page: The page number in case of a multi-page result set, default is 1
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to view it.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/(id)/
|
||||
|
||||
Returns information on one content item, identified by its ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/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,
|
||||
"subevent": null,
|
||||
"title": {
|
||||
"en": "Concert livestream"
|
||||
},
|
||||
"content_type": "link",
|
||||
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"description": {
|
||||
"en": "Watch our event live here on YouTube!"
|
||||
},
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"available_from": "2020-03-22T23:00:00Z",
|
||||
"available_until": null,
|
||||
"position": 1
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:param id: The ``id`` field of the content to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event/content does not exist **or** you have no permission to view it.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/
|
||||
|
||||
Create a new digital content.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 166
|
||||
|
||||
{
|
||||
"subevent": null,
|
||||
"title": {
|
||||
"en": "Concert livestream"
|
||||
},
|
||||
"content_type": "link",
|
||||
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"description": {
|
||||
"en": "Watch our event live here on YouTube!"
|
||||
},
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"available_from": "2020-03-22T23:00:00Z",
|
||||
"available_until": null,
|
||||
"position": 1
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 2,
|
||||
"subevent": null,
|
||||
"title": {
|
||||
"en": "Concert livestream"
|
||||
},
|
||||
"content_type": "link",
|
||||
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"description": {
|
||||
"en": "Watch our event live here on YouTube!"
|
||||
},
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"available_from": "2020-03-22T23:00:00Z",
|
||||
"available_until": null,
|
||||
"position": 1
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to create new content for
|
||||
:param event: The ``slug`` field of the event to create new content for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The content 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 digital contents.
|
||||
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/(id)/
|
||||
|
||||
Update a content. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||
want to change.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 34
|
||||
|
||||
{
|
||||
"url": "https://mywebsite.com"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 2,
|
||||
"subevent": null,
|
||||
"title": {
|
||||
"en": "Concert livestream"
|
||||
},
|
||||
"content_type": "link",
|
||||
"url": "https://mywebsite.com",
|
||||
"description": {
|
||||
"en": "Watch our event live here on YouTube!"
|
||||
},
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"available_from": "2020-03-22T23:00:00Z",
|
||||
"available_until": null,
|
||||
"position": 1
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param id: The ``id`` field of the content to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The content could not be modified due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event/content does not exist **or** you have no permission to change it.
|
||||
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/(id)/
|
||||
|
||||
Delete a digital content.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/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 content to delete
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event/content does not exist **or** you have no permission to change it
|
||||
@@ -14,3 +14,5 @@ If you want to **create** a plugin, please go to the
|
||||
banktransfer
|
||||
ticketoutputpdf
|
||||
badges
|
||||
campaigns
|
||||
digital
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
-r ../src/requirements.txt
|
||||
sphinx==1.6.*
|
||||
sphinx==2.3.*
|
||||
sphinx-rtd-theme
|
||||
sphinxcontrib-httpdomain
|
||||
sphinxcontrib-images
|
||||
sphinxcontrib-spelling
|
||||
pygments-markdown-lexer
|
||||
# See https://github.com/rfk/pyenchant/pull/130
|
||||
git+https://github.com/raphaelm/pyenchant.git@patch-1#egg=pyenchant
|
||||
|
||||
@@ -31,18 +31,24 @@ deprovision
|
||||
discoverable
|
||||
django
|
||||
dockerfile
|
||||
downloadable
|
||||
durations
|
||||
eu
|
||||
filename
|
||||
filesystem
|
||||
fontawesome
|
||||
formset
|
||||
formsets
|
||||
frontend
|
||||
frontpage
|
||||
gettext
|
||||
giftcard
|
||||
gunicorn
|
||||
guid
|
||||
hardcoded
|
||||
hostname
|
||||
idempotency
|
||||
iframe
|
||||
incrementing
|
||||
inofficial
|
||||
invalidations
|
||||
@@ -51,6 +57,7 @@ Jimdo
|
||||
libpretixprint
|
||||
libsass
|
||||
linters
|
||||
login
|
||||
memcached
|
||||
metadata
|
||||
middleware
|
||||
@@ -72,6 +79,7 @@ overpayment
|
||||
param
|
||||
passphrase
|
||||
percental
|
||||
pluggable
|
||||
positionid
|
||||
pre
|
||||
prepend
|
||||
@@ -95,15 +103,18 @@ regex
|
||||
renderer
|
||||
renderers
|
||||
reportlab
|
||||
reseller
|
||||
SaaS
|
||||
scalability
|
||||
screenshot
|
||||
scss
|
||||
searchable
|
||||
selectable
|
||||
serializers
|
||||
serializable
|
||||
serializer
|
||||
serializers
|
||||
sexualized
|
||||
SQL
|
||||
startup
|
||||
stdout
|
||||
stylesheet
|
||||
@@ -130,10 +141,13 @@ untrusted
|
||||
uptime
|
||||
username
|
||||
url
|
||||
validator
|
||||
versa
|
||||
versioning
|
||||
viewable
|
||||
viewset
|
||||
viewsets
|
||||
waitinglist
|
||||
webhook
|
||||
webhooks
|
||||
webserver
|
||||
|
||||
71
doc/user/events/giftcards.rst
Normal file
71
doc/user/events/giftcards.rst
Normal file
@@ -0,0 +1,71 @@
|
||||
.. spelling::
|
||||
|
||||
Warengutschein
|
||||
Wertgutschein
|
||||
|
||||
Gift cards
|
||||
==========
|
||||
|
||||
Gift cards, also known as "gift coupons" or "gift certificates" are a mechanism that allows you to sell tokens that
|
||||
can later be used to pay for tickets.
|
||||
|
||||
Gift cards are very different feature than **vouchers**. The difference is:
|
||||
|
||||
* Vouchers can be used to give a discount. When a voucher is used, the price of a ticket is reduced by the configured
|
||||
discount and sold at a lower price. They therefore reduce both revenue as well as taxes. Vouchers (in pretix) are
|
||||
always specific to a certain product in an order. Vouchers are usually not sold but given out as part of a
|
||||
marketing campaign or to specific groups of people. Vouchers in pretix are bound to a specific event.
|
||||
|
||||
* Gift cards are not a discount, but rather a means of payment. If you buy a €20 ticket with a €10 gift card, it is
|
||||
still a €20 ticket and will still count towards your revenue with €20. Gift cards are usually bought for the money
|
||||
that they are worth. Gift cards in pretix can be used across events (and even organizers).
|
||||
|
||||
Selling gift cards
|
||||
------------------
|
||||
|
||||
Selling gift cards works like selling every other type of product in pretix: Create a new product, then head to
|
||||
"Additional settings" and select the option "This product is a gift card". Whenever someone buys this product and
|
||||
pays for it, a new gift card will be created.
|
||||
|
||||
In this case, the gift card code corresponds to the "ticket secret" in the PDF ticket. Therefore, if selling gift cards,
|
||||
you can use ticket downloads just as with normal tickets and use our ticket editor to create beautiful gift certificates
|
||||
people can give to their loved ones.
|
||||
|
||||
Of course, you can use pretix' flexible options to modify your product. For example, you can configure that the customer
|
||||
can freely choose the price of the gift card.
|
||||
|
||||
.. note::
|
||||
|
||||
pretix currently does not support charging sales tax or VAT when selling gift cards, but instead charges VAT on
|
||||
the full price when the gift card is redeemed. This is the correct behavior in Germany and some other countries for
|
||||
gift cards which are not bound to a very specific service ("Warengutschein"), but instead to a monetary amount
|
||||
("Wertgutschein").
|
||||
|
||||
.. note::
|
||||
|
||||
The ticket PDF will not contain the correct gift card code before the order has been paid, so we recommend not
|
||||
selling gift cards in events where tickets are issued before payments arrive.
|
||||
|
||||
|
||||
Accepting gift cards
|
||||
--------------------
|
||||
|
||||
All your events have have the payment provider "Gift card" enabled by default, but it will only show up in the ticket
|
||||
shop once the very first gift card has been issued on your organizer account. Of course, you can turn off gift card
|
||||
payments if you do not want them for a specific event.
|
||||
|
||||
If gift card payments are enabled, buyers will be able to select "Gift card" as a payment method during checkout. If
|
||||
a gift card with a value less than the order total is used, the buyer will be asked to select a second payment method
|
||||
for the remaining payment. If a gift card with a value greater than the order total is used, the surplus amount
|
||||
remains on the gift card and can be used in a different purchase.
|
||||
|
||||
If it possible to accept gift cards across organizer accounts. To do so, you need to have access to both organizer
|
||||
accounts. Then, you will see a configuration section at the bottom of the "Gift cards" page of your organizer settings
|
||||
where you can specify which gift cards should be accepted.
|
||||
|
||||
Manually issuing or using gift cards
|
||||
------------------------------------
|
||||
|
||||
Of course, you can also issue or redeem gift cards manually through our backend using the "Gift cards" menu item in your
|
||||
organizer profile or using our API. These gift cards will be tracked by pretix, but do not correspond to any purchase
|
||||
within pretix. You will therefore need to account for them in your books separately.
|
||||
@@ -45,8 +45,8 @@ In addition, you will need quotas. If you do not care how many of your tickets a
|
||||
|
||||
If you want to limit the number of student tickets to 50 to ensure a certain minimum revenue, but do not want to limit the number of regular tickets artificially, we suggest you to create the same quota of 200 that is linked to both products, and then create a **second quota** of 50 that is only linked to the student ticket. This way, the system will reduce both quotas whenever a student ticket is sold and only the larger quota when a regular ticket is sold.
|
||||
|
||||
Use case: Early-bird tiers
|
||||
--------------------------
|
||||
Use case: Early-bird tiers based on dates
|
||||
-----------------------------------------
|
||||
|
||||
Let's say you run a conference that has the following pricing scheme:
|
||||
|
||||
@@ -58,9 +58,53 @@ Of course, you could just set up one product and change its price at the given d
|
||||
|
||||
Create three products (e.g. "super early bird", "early bird", "regular ticket") with the respective prices and one shared quota of your total event capacity. Then, set the **available from** and **available until** configuration fields of the products to automatically turn them on and off based on the current date.
|
||||
|
||||
.. note::
|
||||
Use case: Early-bird tiers based on ticket numbers
|
||||
--------------------------------------------------
|
||||
|
||||
pretix currently can't do early-bird tiers based on **ticket number** instead of time. We're planning this feature for later in 2019. For now, you'll need to monitor that manually.
|
||||
Let's say you run a conference with 400 tickets that has the following pricing scheme:
|
||||
|
||||
* First 100 tickets ("super early bird"): € 450
|
||||
* Next 100 tickets ("early bird"): € 550
|
||||
* Remaining tickets ("regular"): € 650
|
||||
|
||||
First of all, create three products:
|
||||
|
||||
* "Super early bird ticket"
|
||||
* "Early bird ticket"
|
||||
* "Regular ticket"
|
||||
|
||||
Then, create three quotas:
|
||||
|
||||
* "Super early bird" with a **size of 100** and the "Super early bird ticket" product selected. At "Advanced options",
|
||||
select the box "Close this quota permanently once it is sold out".
|
||||
|
||||
* "Early bird and lower" with a **size of 200** and both of the "Super early bird ticket" and "Early bird ticket"
|
||||
products selected. At "Advanced options", select the box "Close this quota permanently once it is sold out".
|
||||
|
||||
* "All participants" with a **size of 400**, all three products selected and **no additional options**.
|
||||
|
||||
Next, modify the product "Regular ticket". In the section "Availability", you should look for the option "Only show
|
||||
after sellout of" and select your quota "Early bird and lower". Do the same for the "Early bird ticket" with the quota
|
||||
"Super early bird ticket".
|
||||
|
||||
This will ensure the following things:
|
||||
|
||||
* Each ticket level is only visible after the previous level is sold out.
|
||||
|
||||
* As soon as one level is really sold out, it's not coming back, because the quota "closes", i.e. locks in place.
|
||||
|
||||
* By creating a total quota of 400 with all tickets included, you can still make sure to sell the maximum number of
|
||||
tickets, even if e.g. early-bird tickets are canceled.
|
||||
|
||||
Optionally, if you want to hide the early bird prices once they are sold out, go to "Settings", then "Display" and
|
||||
select "Hide all products that are sold out". Of course, it might be a nice idea to keep showing the prices to remind
|
||||
people to buy earlier next time ;)
|
||||
|
||||
Please note that there might be short time intervals where the prices switch back and forth: When the last early bird
|
||||
tickets are in someone's cart (but not yet sold!), the early bird tickets will show as "Reserved" and the regular
|
||||
tickets start showing up. However, if the customers holding the reservations do not complete their order,
|
||||
the early bird tickets will become available again. This is not avoidable if we want to prevent malicious users
|
||||
from blocking all the cheap tickets without an actual sale happening.
|
||||
|
||||
Use case: Up-selling of ticket extras
|
||||
-------------------------------------
|
||||
@@ -85,8 +129,14 @@ Use case: Conference with workshops
|
||||
|
||||
When running a conference, you might also organize a number of workshops with smaller capacity. To be able to plan, it would be great to know which workshops an attendee plans to attend.
|
||||
|
||||
Option A: Questions
|
||||
"""""""""""""""""""
|
||||
|
||||
Your first and simplest option is to just create a multiple-choice question. This has the upside of making it easy for users to change their mind later on, but will not allow you to restrict the number of attendees signing up for a given workshop – or even charge extra for a given workshop.
|
||||
|
||||
Option B: Add-on products with fixed time slots
|
||||
"""""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
The usually better option is to go with add-on products. Let's take for example the following conference schedule, in which the lecture can be attended by anyone, but the workshops only have space for 20 persons each:
|
||||
|
||||
==================== =================================== ===================================
|
||||
@@ -117,6 +167,42 @@ Assuming you already created one or more products for your general conference ad
|
||||
|
||||
* One add-on configuration on your base product that allows users to choose between 0 and 2 products from the category "Workshops"
|
||||
|
||||
Option C: Add-on products with variable time slots
|
||||
""""""""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
The above option only works if your conference uses fixed time slots and every workshop uses exactly one time slot. If
|
||||
your schedule looks like this, it's not going to work great:
|
||||
|
||||
+-------------+------------+-----------+
|
||||
| Time | Room A | Room B |
|
||||
+=============+============+===========+
|
||||
| 09:00-11:00 | Talk 1 | Long |
|
||||
+-------------+------------+ Workshop 1|
|
||||
| 11:00-13:00 | Talk 2 | |
|
||||
+-------------+------------+-----------+
|
||||
| 14:00-16:00 | Long | Talk 3 |
|
||||
+-------------+ workshop 2 +-----------+
|
||||
| 16:00-18:00 | | Talk 4 |
|
||||
+-------------+------------+-----------+
|
||||
|
||||
In this case, we recommend that you go to *Settings*, then *Plugins* and activate the plugin **Agenda constraints**.
|
||||
|
||||
Then, create a product (without variations) for every single part that should be bookable (talks 1-4 and long workshops
|
||||
1 and 2) as well as appropriate quotas for each of them.
|
||||
|
||||
All of these products should be part of the same category. In your base product (e.g. your conference ticket), you
|
||||
can then create an add-on product configuration allowing users to add products from this category.
|
||||
|
||||
If you edit these products, you will be able to enter the "Start date" and "End date" of the talk or workshop close
|
||||
to the bottom of the page. If you fill in these values, pretix will automatically ensure no overlapping talks are
|
||||
booked.
|
||||
|
||||
.. note::
|
||||
|
||||
This option is currently only available on pretix Hosted. If you are interested in using it with pretix Enterprise,
|
||||
please contact sales@pretix.eu.
|
||||
|
||||
|
||||
Use case: Discounted packages
|
||||
-----------------------------
|
||||
|
||||
|
||||
@@ -114,6 +114,17 @@ If you want to disable voucher input in the widget, you can pass the ``disable-v
|
||||
|
||||
<pretix-widget event="https://pretix.eu/demo/democon/" disable-vouchers></pretix-widget>
|
||||
|
||||
Filtering products
|
||||
------------------
|
||||
|
||||
You can filter the products shown in the widget by passing in a list of product IDs::
|
||||
|
||||
<pretix-widget event="https://pretix.eu/demo/democon/" items="23,42"></pretix-widget>
|
||||
|
||||
Alternatively, you can select one or more categories to be shown::
|
||||
|
||||
<pretix-widget event="https://pretix.eu/demo/democon/" categories="12,25"></pretix-widget>
|
||||
|
||||
Multi-event selection
|
||||
---------------------
|
||||
|
||||
@@ -143,6 +154,11 @@ You can see an example here:
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
You can filter events by meta data attributes. You can create those attributes in your order profile and set their values in both event and series date
|
||||
settings. For example, if you set up a meta data property called "Promoted" that you set to "Yes" on some events, you can pass a filter like this::
|
||||
|
||||
<pretix-widget event="https://pretix.eu/demo/series/" style="list" filter="attr[Promoted]=Yes"></pretix-widget>
|
||||
|
||||
pretix Button
|
||||
-------------
|
||||
|
||||
@@ -178,6 +194,24 @@ Just as the widget, the button supports the optional attributes ``voucher`` and
|
||||
|
||||
You can style the button using the ``pretix-button`` CSS class.
|
||||
|
||||
Dynamically opening the widget
|
||||
------------------------------
|
||||
|
||||
You can get the behavior of the pretix Button without a button at all, so you can trigger it from your own code in
|
||||
response to a user action. Usually, this will open an overlay with your ticket shop, however in some cases, such as
|
||||
missing HTTPS encryption on your case or a really small screen (mobile), it will open a new tab instead of an overlay.
|
||||
Therefore, make sure you call this *in direct response to a user action*, otherwise most browser will block it as an
|
||||
unwanted pop-up.
|
||||
|
||||
.. js:function:: window.PretixWidget.open(target_url [, voucher [, subevent [, items, [, widget_data [, skip_ssl_check ]]]]])
|
||||
|
||||
:param string target_url: The URL of the ticket shop.
|
||||
:param string voucher: A voucher code to be pre-selected, or ``null``.
|
||||
:param string subevent: A subevent to be pre-selected, or ``null``.
|
||||
:param array items: A collection of items to be put in the cart, of the form ``[{"item": "item_3", "count": 1}, {"item": "variation_5_6", "count": 4}]``
|
||||
:param object widget_data: Additional data to be passed to the shop, see below.
|
||||
:param boolean skip_ssl_check: Whether to ignore the check for HTTPS. Only to be used during development.
|
||||
|
||||
Dynamically loading the widget
|
||||
------------------------------
|
||||
|
||||
@@ -201,6 +235,21 @@ If you want, you can suppress us loading the widget and/or modify the user data
|
||||
|
||||
If you then later want to trigger loading the widgets, just call ``window.PretixWidget.buildWidgets()``.
|
||||
|
||||
Waiting for the widget to load
|
||||
------------------------------
|
||||
|
||||
If you want to run custom JavaScript once the widget is fully loaded, you can register a callback function. Note that
|
||||
this function might be run multiple times, for example if you have multiple widgets on a page or if the user switches
|
||||
e.g. from an event list to an event detail view::
|
||||
|
||||
<script type="text/javascript">
|
||||
window.pretixWidgetCallback = function () {
|
||||
window.PretixWidget.addLoadListener(function () {
|
||||
console.log("Widget has loaded!");
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
Passing user data to the widget
|
||||
-------------------------------
|
||||
@@ -218,7 +267,8 @@ with that information::
|
||||
data-question-L9G8NG9M="Foobar">
|
||||
</pretix-widget>
|
||||
|
||||
This works for the pretix Button as well. Currently, the following attributes are understood by pretix itself:
|
||||
This works for the pretix Button as well, if you also specify a product.
|
||||
Currently, the following attributes are understood by pretix itself:
|
||||
|
||||
* ``data-email`` will pre-fill the order email field as well as the attendee email field (if enabled).
|
||||
|
||||
@@ -236,6 +286,11 @@ This works for the pretix Button as well. Currently, the following attributes ar
|
||||
naming scheme such as ``name-title`` or ``name-given-name`` (see above). ``country`` expects a two-character
|
||||
country code.
|
||||
|
||||
* If ``data-fix="true"`` is given, the user will not be able to change the other given values later. This currently
|
||||
only works for the order email address as well as the invoice address. Attendee-level fields and questions can
|
||||
always be modified. Note that this is not a security feature and can easily be overridden by users, so do not rely
|
||||
on this for authentication.
|
||||
|
||||
Any configured pretix plugins might understand more data fields. For example, if the appropriate plugins on pretix
|
||||
Hosted or pretix Enterprise are active, you can pass the following fields:
|
||||
|
||||
@@ -269,10 +324,17 @@ Hosted or pretix Enterprise are active, you can pass the following fields:
|
||||
};
|
||||
</script>
|
||||
|
||||
In some combinations with Google Tag Manager, the widget does not load this way. In this case, try replacing
|
||||
``tracker.get('clientId')`` with ``ga.getAll()[0].get('clientId')``.
|
||||
|
||||
|
||||
.. versionchanged:: 2.3
|
||||
|
||||
Data passing options have been added in pretix 2.3. If you use a self-hosted version of pretix, they only work
|
||||
fully if you configured a redis server.
|
||||
|
||||
.. versionchanged:: 3.6
|
||||
|
||||
Dynamically opening the widget has been added in pretix 3.6.
|
||||
|
||||
.. _Let's Encrypt: https://letsencrypt.org/
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -12,5 +12,6 @@ wanting to use pretix to sell tickets.
|
||||
events/settings
|
||||
events/structureguide
|
||||
events/widget
|
||||
events/giftcards
|
||||
faq
|
||||
markdown
|
||||
markdown
|
||||
|
||||
@@ -14,30 +14,23 @@ and with pretix, you can do this. On this page, you find out the necessary steps
|
||||
With the pretix.eu hosted service
|
||||
---------------------------------
|
||||
|
||||
Step 1: DNS Configuration
|
||||
#########################
|
||||
Go to "Organizers" in the backend and select your organizer account. Then, go to "Settings" and "Custom Domain".
|
||||
|
||||
This page will show you instructions on how to set up your own domain. Basically, it works like this:
|
||||
|
||||
Go to the website of the provider you registered your domain name with. Look for the "DNS" settings page in their
|
||||
interface. Unfortunately, we can't tell you exactly how that is named and how it looks, since it is different for every
|
||||
domain provider.
|
||||
|
||||
Use this interface to add a new subdomain record, e.g. ``tickets`` of the type ``CNAME`` (might also be called "alias").
|
||||
The value of the record should be ``www.pretix.eu``.
|
||||
|
||||
Step 2: Wait for the DNS entry to propagate
|
||||
###########################################
|
||||
The value of the record should be the one shown on the "Custom Domain" page in pretix' backend.
|
||||
|
||||
Submit your changes and wait a bit, it can regularly take up to three hours for DNS changes to propagate to the caches
|
||||
of all DNS servers. You can try checking by accessing your new subdomain, ``http://tickets.awesomepartycorp.com``.
|
||||
If DNS was changed successfully, you should see a SSL certificate error. If you ignore the error and access the page
|
||||
anyways, you should get a pretix-themed error page with the headline "Unknown domain".
|
||||
|
||||
Step 3: Tell us
|
||||
###############
|
||||
|
||||
Write an email to support@pretix.eu, naming your new domain and your organizer account. We will then generate a SSL
|
||||
certificate for you (for free!) and configure the domain.
|
||||
|
||||
Now, tell us about your domain on the "Custom Domain" page to get started.
|
||||
|
||||
With a custom pretix installation
|
||||
---------------------------------
|
||||
|
||||
@@ -22,3 +22,5 @@ recursive-include pretix/plugins/ticketoutputpdf/templates *
|
||||
recursive-include pretix/plugins/ticketoutputpdf/static *
|
||||
recursive-include pretix/plugins/badges/templates *
|
||||
recursive-include pretix/plugins/badges/static *
|
||||
recursive-include pretix/plugins/returnurl/templates *
|
||||
recursive-include pretix/plugins/returnurl/static *
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pretix.settings")
|
||||
|
||||
import django
|
||||
|
||||
django.setup()
|
||||
|
||||
from pretix.base.models import * # NOQA
|
||||
from django.utils.timezone import now
|
||||
|
||||
if Organizer.objects.exists():
|
||||
print("There already is data in your DB!")
|
||||
sys.exit(0)
|
||||
user = User.objects.get_or_create(
|
||||
email='admin@localhost',
|
||||
)[0]
|
||||
user.set_password('admin')
|
||||
user.save()
|
||||
organizer = Organizer.objects.create(
|
||||
name='BigEvents LLC', slug='bigevents'
|
||||
)
|
||||
year = now().year + 1
|
||||
event = Event.objects.create(
|
||||
organizer=organizer, name='Demo Conference {}'.format(year),
|
||||
slug=year, currency='EUR', live=True,
|
||||
date_from=datetime(year, 9, 4, 17, 0, 0),
|
||||
date_to=datetime(year, 9, 6, 17, 0, 0),
|
||||
)
|
||||
t = Team.objects.get_or_create(
|
||||
organizer=organizer, name='Admin Team',
|
||||
all_events=True, can_create_events=True, can_change_teams=True,
|
||||
can_change_organizer_settings=True, can_change_event_settings=True, can_change_items=True,
|
||||
can_view_orders=True, can_change_orders=True, can_view_vouchers=True, can_change_vouchers=True
|
||||
)
|
||||
t[0].members.add(user)
|
||||
cat_tickets = ItemCategory.objects.create(
|
||||
event=event, name='Tickets'
|
||||
)
|
||||
cat_merch = ItemCategory.objects.create(
|
||||
event=event, name='Merchandise'
|
||||
)
|
||||
question = Question.objects.create(
|
||||
event=event, question='Age',
|
||||
type=Question.TYPE_NUMBER, required=False
|
||||
)
|
||||
tr19 = event.tax_rules.create(rate=19)
|
||||
item_ticket = Item.objects.create(
|
||||
event=event, category=cat_tickets, name='Ticket',
|
||||
default_price=23, tax_rule=tr19, admission=True
|
||||
)
|
||||
item_ticket.questions.add(question)
|
||||
item_shirt = Item.objects.create(
|
||||
event=event, category=cat_merch, name='T-Shirt',
|
||||
default_price=15, tax_rule=tr19
|
||||
)
|
||||
var_s = ItemVariation.objects.create(item=item_shirt, value='S')
|
||||
var_m = ItemVariation.objects.create(item=item_shirt, value='M')
|
||||
var_l = ItemVariation.objects.create(item=item_shirt, value='L')
|
||||
ticket_quota = Quota.objects.create(
|
||||
event=event, name='Ticket quota', size=400,
|
||||
)
|
||||
ticket_quota.items.add(item_ticket)
|
||||
ticket_shirts = Quota.objects.create(
|
||||
event=event, name='Shirt quota', size=200,
|
||||
)
|
||||
ticket_quota.items.add(item_shirt)
|
||||
ticket_quota.variations.add(var_s, var_m, var_l)
|
||||
@@ -1 +1 @@
|
||||
__version__ = "2.8.0"
|
||||
__version__ = "3.9.0.dev0"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.authentication import TokenAuthentication
|
||||
|
||||
@@ -12,7 +13,8 @@ class DeviceTokenAuthentication(TokenAuthentication):
|
||||
def authenticate_credentials(self, key):
|
||||
model = self.get_model()
|
||||
try:
|
||||
device = model.objects.select_related('organizer').get(api_token=key)
|
||||
with scopes_disabled():
|
||||
device = model.objects.select_related('organizer').get(api_token=key)
|
||||
except model.DoesNotExist:
|
||||
raise exceptions.AuthenticationFailed('Invalid token.')
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from rest_framework.permissions import SAFE_METHODS, BasePermission
|
||||
from pretix.api.models import OAuthAccessToken
|
||||
from pretix.base.models import Device, Event, User
|
||||
from pretix.base.models.auth import SuperuserPermissionSet
|
||||
from pretix.base.models.organizer import Organizer, TeamAPIToken
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
from pretix.helpers.security import (
|
||||
SessionInvalid, SessionReauthRequired, assert_session_valid,
|
||||
)
|
||||
@@ -50,9 +50,6 @@ class EventPermission(BasePermission):
|
||||
return False
|
||||
|
||||
elif 'organizer' in request.resolver_match.kwargs:
|
||||
request.organizer = Organizer.objects.filter(
|
||||
slug=request.resolver_match.kwargs['organizer'],
|
||||
).first()
|
||||
if not request.organizer or not perm_holder.has_organizer_permission(request.organizer, request=request):
|
||||
return False
|
||||
if isinstance(perm_holder, User) and perm_holder.has_active_staff_session(request.session.session_key):
|
||||
|
||||
@@ -4,10 +4,13 @@ from hashlib import sha1
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.http import HttpRequest, HttpResponse, JsonResponse
|
||||
from django.urls import resolve
|
||||
from django.utils.timezone import now
|
||||
from django_scopes import scope
|
||||
from rest_framework import status
|
||||
|
||||
from pretix.api.models import ApiCall
|
||||
from pretix.base.models import Organizer
|
||||
|
||||
|
||||
class IdempotencyMiddleware:
|
||||
@@ -89,3 +92,21 @@ class IdempotencyMiddleware:
|
||||
for k, v in json.loads(call.response_headers).values():
|
||||
r[k] = v
|
||||
return r
|
||||
|
||||
|
||||
class ApiScopeMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request: HttpRequest):
|
||||
if not request.path.startswith('/api/'):
|
||||
return self.get_response(request)
|
||||
|
||||
url = resolve(request.path_info)
|
||||
if 'organizer' in url.kwargs:
|
||||
request.organizer = Organizer.objects.filter(
|
||||
slug=url.kwargs['organizer'],
|
||||
).first()
|
||||
|
||||
with scope(organizer=getattr(request, 'organizer', None)):
|
||||
return self.get_response(request)
|
||||
|
||||
18
src/pretix/api/migrations/0005_auto_20191028_1541.py
Normal file
18
src/pretix/api/migrations/0005_auto_20191028_1541.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.2.1 on 2019-10-28 15:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixapi', '0004_auto_20190405_1048'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='oauthgrant',
|
||||
name='redirect_uri',
|
||||
field=models.CharField(max_length=2500),
|
||||
),
|
||||
]
|
||||
@@ -3,7 +3,7 @@ from datetime import timedelta
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from oauth2_provider.generators import (
|
||||
generate_client_id, generate_client_secret,
|
||||
)
|
||||
@@ -43,6 +43,7 @@ class OAuthGrant(AbstractGrant):
|
||||
OAuthApplication, on_delete=models.CASCADE
|
||||
)
|
||||
organizers = models.ManyToManyField('pretixbase.Organizer')
|
||||
redirect_uri = models.CharField(max_length=2500) # Only 255 in AbstractGrant, which caused problems
|
||||
|
||||
|
||||
class OAuthAccessToken(AbstractAccessToken):
|
||||
|
||||
@@ -2,37 +2,40 @@ from datetime import timedelta
|
||||
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy
|
||||
from django.utils.translation import gettext_lazy
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.order import (
|
||||
AnswerCreateSerializer, AnswerSerializer,
|
||||
AnswerCreateSerializer, AnswerSerializer, InlineSeatSerializer,
|
||||
)
|
||||
from pretix.base.models import Quota
|
||||
from pretix.base.models import Quota, Seat
|
||||
from pretix.base.models.orders import CartPosition
|
||||
|
||||
|
||||
class CartPositionSerializer(I18nAwareModelSerializer):
|
||||
answers = AnswerSerializer(many=True)
|
||||
seat = InlineSeatSerializer()
|
||||
|
||||
class Meta:
|
||||
model = CartPosition
|
||||
fields = ('id', 'cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||
'attendee_email', 'voucher', 'addon_to', 'subevent', 'datetime', 'expires', 'includes_tax',
|
||||
'answers',)
|
||||
'answers', 'seat')
|
||||
|
||||
|
||||
class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
answers = AnswerCreateSerializer(many=True, required=False)
|
||||
expires = serializers.DateTimeField(required=False)
|
||||
attendee_name = serializers.CharField(required=False, allow_null=True)
|
||||
seat = serializers.CharField(required=False, allow_null=True)
|
||||
sales_channel = serializers.CharField(required=False, default='sales_channel')
|
||||
|
||||
class Meta:
|
||||
model = CartPosition
|
||||
fields = ('cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||
'subevent', 'expires', 'includes_tax', 'answers',)
|
||||
'subevent', 'expires', 'includes_tax', 'answers', 'seat', 'sales_channel')
|
||||
|
||||
def create(self, validated_data):
|
||||
answers_data = validated_data.pop('answers')
|
||||
@@ -53,7 +56,7 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
else validated_data.get('item').quotas.filter(subevent=validated_data.get('subevent')))
|
||||
if len(new_quotas) == 0:
|
||||
raise ValidationError(
|
||||
ugettext_lazy('The product "{}" is not assigned to a quota.').format(
|
||||
gettext_lazy('The product "{}" is not assigned to a quota.').format(
|
||||
str(validated_data.get('item'))
|
||||
)
|
||||
)
|
||||
@@ -61,8 +64,8 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
avail = quota.availability()
|
||||
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < 1):
|
||||
raise ValidationError(
|
||||
ugettext_lazy('There is not enough quota available on quota "{}" to perform '
|
||||
'the operation.').format(
|
||||
gettext_lazy('There is not enough quota available on quota "{}" to perform '
|
||||
'the operation.').format(
|
||||
quota.name
|
||||
)
|
||||
)
|
||||
@@ -71,6 +74,25 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
validated_data['attendee_name_parts'] = {
|
||||
'_legacy': attendee_name
|
||||
}
|
||||
|
||||
seated = validated_data.get('item').seat_category_mappings.filter(subevent=validated_data.get('subevent')).exists()
|
||||
if validated_data.get('seat'):
|
||||
if not seated:
|
||||
raise ValidationError('The specified product does not allow to choose a seat.')
|
||||
try:
|
||||
seat = self.context['event'].seats.get(seat_guid=validated_data['seat'], subevent=validated_data.get('subevent'))
|
||||
except Seat.DoesNotExist:
|
||||
raise ValidationError('The specified seat does not exist.')
|
||||
except Seat.MultipleObjectsReturned:
|
||||
raise ValidationError('The specified seat ID is not unique.')
|
||||
else:
|
||||
validated_data['seat'] = seat
|
||||
if not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web')):
|
||||
raise ValidationError(gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
|
||||
elif seated:
|
||||
raise ValidationError('The specified product requires to choose a seat.')
|
||||
|
||||
validated_data.pop('sales_channel')
|
||||
cp = CartPosition.objects.create(event=self.context['event'], **validated_data)
|
||||
|
||||
for answ_data in answers_data:
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.models import CheckinList
|
||||
|
||||
|
||||
@@ -13,7 +14,7 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = CheckinList
|
||||
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
|
||||
'include_pending')
|
||||
'include_pending', 'auto_checkin_sales_channels')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -35,4 +36,8 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
if full_data.get('subevent'):
|
||||
raise ValidationError(_('The subevent does not belong to this event.'))
|
||||
|
||||
for channel in full_data.get('auto_checkin_sales_channels') or []:
|
||||
if channel not in get_all_sales_channels():
|
||||
raise ValidationError(_('Unknown sales channel.'))
|
||||
|
||||
return data
|
||||
|
||||
@@ -2,15 +2,23 @@ from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from django_countries.serializers import CountryFieldMixin
|
||||
from rest_framework.fields import Field
|
||||
from hierarkey.proxy import HierarkeyProxy
|
||||
from pytz import common_timezones
|
||||
from rest_framework import serializers
|
||||
from rest_framework.fields import ChoiceField, Field
|
||||
from rest_framework.relations import SlugRelatedField
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.models import Event, TaxRule
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.items import SubEventItem, SubEventItemVariation
|
||||
from pretix.base.services.seating import (
|
||||
SeatProtected, generate_seats, validate_plan_change,
|
||||
)
|
||||
from pretix.base.settings import DEFAULTS, validate_settings
|
||||
from pretix.base.signals import api_event_settings_fields
|
||||
|
||||
|
||||
class MetaDataField(Field):
|
||||
@@ -26,6 +34,35 @@ class MetaDataField(Field):
|
||||
}
|
||||
|
||||
|
||||
class MetaPropertyField(Field):
|
||||
|
||||
def to_representation(self, value):
|
||||
return {
|
||||
v.name: v.default for v in value.item_meta_properties.all()
|
||||
}
|
||||
|
||||
def to_internal_value(self, data):
|
||||
return {
|
||||
'item_meta_properties': data
|
||||
}
|
||||
|
||||
|
||||
class SeatCategoryMappingField(Field):
|
||||
|
||||
def to_representation(self, value):
|
||||
qs = value.seat_category_mappings.all()
|
||||
if isinstance(value, Event):
|
||||
qs = qs.filter(subevent=None)
|
||||
return {
|
||||
v.layout_category: v.product_id for v in qs
|
||||
}
|
||||
|
||||
def to_internal_value(self, data):
|
||||
return {
|
||||
'seat_category_mapping': data or {}
|
||||
}
|
||||
|
||||
|
||||
class PluginsField(Field):
|
||||
|
||||
def to_representation(self, obj):
|
||||
@@ -42,15 +79,28 @@ class PluginsField(Field):
|
||||
}
|
||||
|
||||
|
||||
class TimeZoneField(ChoiceField):
|
||||
def get_attribute(self, instance):
|
||||
return instance.cache.get_or_set(
|
||||
'timezone_name',
|
||||
lambda: instance.settings.timezone,
|
||||
3600
|
||||
)
|
||||
|
||||
|
||||
class EventSerializer(I18nAwareModelSerializer):
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
item_meta_properties = MetaPropertyField(required=False, source='*')
|
||||
plugins = PluginsField(required=False, source='*')
|
||||
seat_category_mapping = SeatCategoryMappingField(source='*', required=False)
|
||||
timezone = TimeZoneField(required=False, choices=[(a, a) for a in common_timezones])
|
||||
|
||||
class Meta:
|
||||
model = Event
|
||||
fields = ('name', 'slug', 'live', 'testmode', 'currency', 'date_from',
|
||||
'date_to', 'date_admission', 'is_public', 'presale_start',
|
||||
'presale_end', 'location', 'has_subevents', 'meta_data', 'plugins')
|
||||
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
|
||||
'plugins', 'seat_category_mapping', 'timezone', 'item_meta_properties')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -61,6 +111,9 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
Event.clean_dates(data.get('date_from'), data.get('date_to'))
|
||||
Event.clean_presale(data.get('presale_start'), data.get('presale_end'))
|
||||
|
||||
if full_data.get('has_subevents') and full_data.get('seating_plan'):
|
||||
raise ValidationError('Event series should not directly be assigned a seating plan.')
|
||||
|
||||
return data
|
||||
|
||||
def validate_has_subevents(self, value):
|
||||
@@ -92,6 +145,36 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key))
|
||||
return value
|
||||
|
||||
@cached_property
|
||||
def item_meta_props(self):
|
||||
return {
|
||||
p.name: p for p in self.context['request'].event.item_meta_properties.all()
|
||||
}
|
||||
|
||||
def validate_seating_plan(self, value):
|
||||
if value and value.organizer != self.context['request'].organizer:
|
||||
raise ValidationError('Invalid seating plan.')
|
||||
if self.instance and self.instance.pk:
|
||||
try:
|
||||
validate_plan_change(self.instance, None, value)
|
||||
except SeatProtected as e:
|
||||
raise ValidationError(str(e))
|
||||
return value
|
||||
|
||||
def validate_seat_category_mapping(self, value):
|
||||
if not self.instance or not self.instance.pk:
|
||||
if value and value['seat_category_mapping']:
|
||||
raise ValidationError('You cannot specify seat category mappings on event creation.')
|
||||
else:
|
||||
return {'seat_category_mapping': {}}
|
||||
item_cache = {i.pk: i for i in self.instance.items.all()}
|
||||
result = {}
|
||||
for k, item in value['seat_category_mapping'].items():
|
||||
if item not in item_cache:
|
||||
raise ValidationError('Item \'{id}\' does not exist.'.format(id=item))
|
||||
result[k] = item_cache[item]
|
||||
return {'seat_category_mapping': result}
|
||||
|
||||
def validate_plugins(self, value):
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
|
||||
@@ -109,9 +192,15 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
@transaction.atomic
|
||||
def create(self, validated_data):
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
item_meta_properties = validated_data.pop('item_meta_properties', None)
|
||||
validated_data.pop('seat_category_mapping', None)
|
||||
plugins = validated_data.pop('plugins', settings.PRETIX_PLUGINS_DEFAULT.split(','))
|
||||
tz = validated_data.pop('timezone', None)
|
||||
event = super().create(validated_data)
|
||||
|
||||
if tz:
|
||||
event.settings.timezone = tz
|
||||
|
||||
# Meta data
|
||||
if meta_data is not None:
|
||||
for key, value in meta_data.items():
|
||||
@@ -120,6 +209,19 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
value=value
|
||||
)
|
||||
|
||||
# Item Meta properties
|
||||
if item_meta_properties is not None:
|
||||
for key, value in item_meta_properties.items():
|
||||
event.item_meta_properties.create(
|
||||
name=key,
|
||||
default=value,
|
||||
event=event
|
||||
)
|
||||
|
||||
# Seats
|
||||
if event.seating_plan:
|
||||
generate_seats(event, None, event.seating_plan, {})
|
||||
|
||||
# Plugins
|
||||
if plugins is not None:
|
||||
event.set_active_plugins(plugins)
|
||||
@@ -130,9 +232,15 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
@transaction.atomic
|
||||
def update(self, instance, validated_data):
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
item_meta_properties = validated_data.pop('item_meta_properties', None)
|
||||
plugins = validated_data.pop('plugins', None)
|
||||
seat_category_mapping = validated_data.pop('seat_category_mapping', None)
|
||||
tz = validated_data.pop('timezone', None)
|
||||
event = super().update(instance, validated_data)
|
||||
|
||||
if tz:
|
||||
event.settings.timezone = tz
|
||||
|
||||
# Meta data
|
||||
if meta_data is not None:
|
||||
current = {mv.property: mv for mv in event.meta_values.select_related('property')}
|
||||
@@ -151,6 +259,49 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
if prop.name not in meta_data:
|
||||
current_object.delete()
|
||||
|
||||
# Item Meta properties
|
||||
if item_meta_properties is not None:
|
||||
current = [imp for imp in event.item_meta_properties.all()]
|
||||
for key, value in item_meta_properties.items():
|
||||
prop = self.item_meta_props.get(key)
|
||||
if prop in current:
|
||||
prop.default = value
|
||||
prop.save()
|
||||
else:
|
||||
prop = event.item_meta_properties.create(
|
||||
name=key,
|
||||
default=value,
|
||||
event=event
|
||||
)
|
||||
current.append(prop)
|
||||
|
||||
for prop in current:
|
||||
if prop.name not in list(item_meta_properties.keys()):
|
||||
prop.delete()
|
||||
|
||||
# Seats
|
||||
if seat_category_mapping is not None or ('seating_plan' in validated_data and validated_data['seating_plan'] is None):
|
||||
current_mappings = {
|
||||
m.layout_category: m
|
||||
for m in event.seat_category_mappings.filter(subevent=None)
|
||||
}
|
||||
if not event.seating_plan:
|
||||
seat_category_mapping = {}
|
||||
for key, value in seat_category_mapping.items():
|
||||
if key in current_mappings:
|
||||
m = current_mappings.pop(key)
|
||||
m.product = value
|
||||
m.save()
|
||||
else:
|
||||
event.seat_category_mappings.create(product=value, layout_category=key)
|
||||
for m in current_mappings.values():
|
||||
m.delete()
|
||||
if 'seating_plan' in validated_data or seat_category_mapping is not None:
|
||||
generate_seats(event, None, event.seating_plan, {
|
||||
m.layout_category: m.product
|
||||
for m in event.seat_category_mappings.select_related('product').filter(subevent=None)
|
||||
})
|
||||
|
||||
# Plugins
|
||||
if plugins is not None:
|
||||
event.set_active_plugins(plugins)
|
||||
@@ -165,6 +316,8 @@ class CloneEventSerializer(EventSerializer):
|
||||
plugins = validated_data.pop('plugins', None)
|
||||
is_public = validated_data.pop('is_public', None)
|
||||
testmode = validated_data.pop('testmode', None)
|
||||
has_subevents = validated_data.pop('has_subevents', None)
|
||||
tz = validated_data.pop('timezone', None)
|
||||
new_event = super().create(validated_data)
|
||||
|
||||
event = Event.objects.filter(slug=self.context['event'], organizer=self.context['organizer'].pk).first()
|
||||
@@ -176,7 +329,11 @@ class CloneEventSerializer(EventSerializer):
|
||||
new_event.is_public = is_public
|
||||
if testmode is not None:
|
||||
new_event.testmode = testmode
|
||||
if has_subevents is not None:
|
||||
new_event.has_subevents = has_subevents
|
||||
new_event.save()
|
||||
if tz:
|
||||
new_event.settings.timezone = tz
|
||||
|
||||
return new_event
|
||||
|
||||
@@ -196,14 +353,16 @@ class SubEventItemVariationSerializer(I18nAwareModelSerializer):
|
||||
class SubEventSerializer(I18nAwareModelSerializer):
|
||||
item_price_overrides = SubEventItemSerializer(source='subeventitem_set', many=True, required=False)
|
||||
variation_price_overrides = SubEventItemVariationSerializer(source='subeventitemvariation_set', many=True, required=False)
|
||||
seat_category_mapping = SeatCategoryMappingField(source='*', required=False)
|
||||
event = SlugRelatedField(slug_field='slug', read_only=True)
|
||||
meta_data = MetaDataField(source='*')
|
||||
|
||||
class Meta:
|
||||
model = SubEvent
|
||||
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
|
||||
'presale_start', 'presale_end', 'location', 'event', 'is_public',
|
||||
'item_price_overrides', 'variation_price_overrides', 'meta_data')
|
||||
'presale_start', 'presale_end', 'location', 'geo_lat', 'geo_lon', 'event', 'is_public',
|
||||
'seating_plan', 'item_price_overrides', 'variation_price_overrides', 'meta_data',
|
||||
'seat_category_mapping')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -225,6 +384,25 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
def validate_variation_price_overrides(self, data):
|
||||
return list(filter(lambda i: 'variation' in i, data))
|
||||
|
||||
def validate_seating_plan(self, value):
|
||||
if value and value.organizer != self.context['request'].organizer:
|
||||
raise ValidationError('Invalid seating plan.')
|
||||
if self.instance and self.instance.pk:
|
||||
try:
|
||||
validate_plan_change(self.context['request'].event, self.instance, value)
|
||||
except SeatProtected as e:
|
||||
raise ValidationError(str(e))
|
||||
return value
|
||||
|
||||
def validate_seat_category_mapping(self, value):
|
||||
item_cache = {i.pk: i for i in self.context['request'].event.items.all()}
|
||||
result = {}
|
||||
for k, item in value['seat_category_mapping'].items():
|
||||
if item not in item_cache:
|
||||
raise ValidationError('Item \'{id}\' does not exist.'.format(id=item))
|
||||
result[k] = item_cache[item]
|
||||
return {'seat_category_mapping': result}
|
||||
|
||||
@cached_property
|
||||
def meta_properties(self):
|
||||
return {
|
||||
@@ -242,6 +420,7 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
item_price_overrides_data = validated_data.pop('subeventitem_set') if 'subeventitem_set' in validated_data else {}
|
||||
variation_price_overrides_data = validated_data.pop('subeventitemvariation_set') if 'subeventitemvariation_set' in validated_data else {}
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
seat_category_mapping = validated_data.pop('seat_category_mapping', None)
|
||||
subevent = super().create(validated_data)
|
||||
|
||||
for item_price_override_data in item_price_overrides_data:
|
||||
@@ -257,6 +436,18 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
value=value
|
||||
)
|
||||
|
||||
# Seats
|
||||
if subevent.seating_plan:
|
||||
if seat_category_mapping is not None:
|
||||
for key, value in seat_category_mapping.items():
|
||||
self.context['request'].event.seat_category_mappings.create(
|
||||
product=value, layout_category=key, subevent=subevent
|
||||
)
|
||||
generate_seats(self.context['request'].event, subevent, subevent.seating_plan, {
|
||||
m.layout_category: m.product
|
||||
for m in self.context['request'].event.seat_category_mappings.select_related('product').filter(subevent=subevent)
|
||||
})
|
||||
|
||||
return subevent
|
||||
|
||||
@transaction.atomic
|
||||
@@ -264,6 +455,7 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
item_price_overrides_data = validated_data.pop('subeventitem_set') if 'subeventitem_set' in validated_data else {}
|
||||
variation_price_overrides_data = validated_data.pop('subeventitemvariation_set') if 'subeventitemvariation_set' in validated_data else {}
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
seat_category_mapping = validated_data.pop('seat_category_mapping', None)
|
||||
subevent = super().update(instance, validated_data)
|
||||
|
||||
existing_item_overrides = {item.item: item.id for item in SubEventItem.objects.filter(subevent=subevent)}
|
||||
@@ -300,6 +492,31 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
if prop.name not in meta_data:
|
||||
current_object.delete()
|
||||
|
||||
# Seats
|
||||
if seat_category_mapping is not None or ('seating_plan' in validated_data and validated_data['seating_plan'] is None):
|
||||
current_mappings = {
|
||||
m.layout_category: m
|
||||
for m in self.context['request'].event.seat_category_mappings.filter(subevent=subevent)
|
||||
}
|
||||
if not subevent.seating_plan:
|
||||
seat_category_mapping = {}
|
||||
for key, value in seat_category_mapping.items():
|
||||
if key in current_mappings:
|
||||
m = current_mappings.pop(key)
|
||||
m.product = value
|
||||
m.save()
|
||||
else:
|
||||
self.context['request'].event.seat_category_mappings.create(
|
||||
product=value, layout_category=key, subevent=subevent
|
||||
)
|
||||
for m in current_mappings.values():
|
||||
m.delete()
|
||||
if 'seating_plan' in validated_data or seat_category_mapping is not None:
|
||||
generate_seats(self.context['request'].event, subevent, subevent.seating_plan, {
|
||||
m.layout_category: m.product
|
||||
for m in self.context['request'].event.seat_category_mappings.select_related('product').filter(subevent=subevent)
|
||||
})
|
||||
|
||||
return subevent
|
||||
|
||||
|
||||
@@ -307,3 +524,138 @@ class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = TaxRule
|
||||
fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country')
|
||||
|
||||
|
||||
class EventSettingsSerializer(serializers.Serializer):
|
||||
default_fields = [
|
||||
'imprint_url',
|
||||
'checkout_email_helptext',
|
||||
'presale_has_ended_text',
|
||||
'voucher_explanation_text',
|
||||
'banner_text',
|
||||
'banner_text_bottom',
|
||||
'show_dates_on_frontpage',
|
||||
'show_date_to',
|
||||
'show_times',
|
||||
'show_items_outside_presale_period',
|
||||
'display_net_prices',
|
||||
'presale_start_show_date',
|
||||
'locales',
|
||||
'locale',
|
||||
'last_order_modification_date',
|
||||
'show_quota_left',
|
||||
'waiting_list_enabled',
|
||||
'waiting_list_hours',
|
||||
'waiting_list_auto',
|
||||
'max_items_per_order',
|
||||
'reservation_time',
|
||||
'contact_mail',
|
||||
'show_variations_expanded',
|
||||
'hide_sold_out',
|
||||
'meta_noindex',
|
||||
'redirect_to_checkout_directly',
|
||||
'frontpage_subevent_ordering',
|
||||
'frontpage_text',
|
||||
'attendee_names_asked',
|
||||
'attendee_names_required',
|
||||
'attendee_emails_asked',
|
||||
'attendee_emails_required',
|
||||
'attendee_addresses_asked',
|
||||
'attendee_addresses_required',
|
||||
'attendee_company_asked',
|
||||
'attendee_company_required',
|
||||
'confirm_text',
|
||||
'order_email_asked_twice',
|
||||
'payment_term_days',
|
||||
'payment_term_last',
|
||||
'payment_term_weekdays',
|
||||
'payment_term_expire_automatically',
|
||||
'payment_term_accept_late',
|
||||
'payment_explanation',
|
||||
'ticket_download',
|
||||
'ticket_download_date',
|
||||
'ticket_download_addons',
|
||||
'ticket_download_nonadm',
|
||||
'ticket_download_pending',
|
||||
'mail_prefix',
|
||||
'mail_from',
|
||||
'mail_from_name',
|
||||
'mail_attach_ical',
|
||||
'invoice_address_asked',
|
||||
'invoice_address_required',
|
||||
'invoice_address_vatid',
|
||||
'invoice_address_company_required',
|
||||
'invoice_address_beneficiary',
|
||||
'invoice_address_custom_field',
|
||||
'invoice_name_required',
|
||||
'invoice_address_not_asked_free',
|
||||
'invoice_show_payments',
|
||||
'invoice_reissue_after_modify',
|
||||
'invoice_include_free',
|
||||
'invoice_generate',
|
||||
'invoice_numbers_consecutive',
|
||||
'invoice_numbers_prefix',
|
||||
'invoice_numbers_prefix_cancellations',
|
||||
'invoice_attendee_name',
|
||||
'invoice_include_expire_date',
|
||||
'invoice_address_explanation_text',
|
||||
'invoice_email_attachment',
|
||||
'invoice_address_from_name',
|
||||
'invoice_address_from',
|
||||
'invoice_address_from_zipcode',
|
||||
'invoice_address_from_city',
|
||||
'invoice_address_from_country',
|
||||
'invoice_address_from_tax_id',
|
||||
'invoice_address_from_vat_id',
|
||||
'invoice_introductory_text',
|
||||
'invoice_additional_text',
|
||||
'invoice_footer_text',
|
||||
'cancel_allow_user',
|
||||
'cancel_allow_user_until',
|
||||
'cancel_allow_user_paid',
|
||||
'cancel_allow_user_paid_until',
|
||||
'cancel_allow_user_paid_keep',
|
||||
'cancel_allow_user_paid_keep_fees',
|
||||
'cancel_allow_user_paid_keep_percentage',
|
||||
'cancel_allow_user_paid_adjust_fees',
|
||||
'cancel_allow_user_paid_adjust_fees_explanation',
|
||||
'cancel_allow_user_paid_refund_as_giftcard',
|
||||
'cancel_allow_user_paid_require_approval',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs.pop('event')
|
||||
super().__init__(*args, **kwargs)
|
||||
for fname in self.default_fields:
|
||||
kwargs = DEFAULTS[fname].get('serializer_kwargs', {})
|
||||
kwargs.setdefault('required', False)
|
||||
kwargs.setdefault('allow_null', True)
|
||||
form_kwargs = DEFAULTS[fname].get('form_kwargs', {})
|
||||
if 'serializer_class' not in DEFAULTS[fname]:
|
||||
raise ValidationError('{} has no serializer class'.format(fname))
|
||||
f = DEFAULTS[fname]['serializer_class'](
|
||||
**kwargs
|
||||
)
|
||||
f._label = form_kwargs.get('label', fname)
|
||||
f._help_text = form_kwargs.get('help_text')
|
||||
self.fields[fname] = f
|
||||
|
||||
for recv, resp in api_event_settings_fields.send(sender=self.event):
|
||||
for fname, field in resp.items():
|
||||
field.required = False
|
||||
self.fields[fname] = field
|
||||
|
||||
def update(self, instance: HierarkeyProxy, validated_data):
|
||||
for attr, value in validated_data.items():
|
||||
if value is None:
|
||||
instance.delete(attr)
|
||||
elif instance.get(attr, as_type=type(value)) != value:
|
||||
instance.set(attr, value)
|
||||
return instance
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
settings_dict = self.instance.freeze()
|
||||
settings_dict.update(data)
|
||||
validate_settings(self.event, settings_dict)
|
||||
return data
|
||||
|
||||
29
src/pretix/api/serializers/fields.py
Normal file
29
src/pretix/api/serializers/fields.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
def remove_duplicates_from_list(data):
|
||||
return list(OrderedDict.fromkeys(data))
|
||||
|
||||
|
||||
class ListMultipleChoiceField(serializers.MultipleChoiceField):
|
||||
def to_internal_value(self, data):
|
||||
if isinstance(data, str) or not hasattr(data, '__iter__'):
|
||||
self.fail('not_a_list', input_type=type(data).__name__)
|
||||
if not self.allow_empty and len(data) == 0:
|
||||
self.fail('empty')
|
||||
|
||||
internal_value_data = [
|
||||
super(serializers.MultipleChoiceField, self).to_internal_value(item)
|
||||
for item in data
|
||||
]
|
||||
|
||||
return remove_duplicates_from_list(internal_value_data)
|
||||
|
||||
def to_representation(self, value):
|
||||
representation_data = [
|
||||
self.choice_strings_to_values.get(str(item), item) for item in value
|
||||
]
|
||||
|
||||
return remove_duplicates_from_list(representation_data)
|
||||
@@ -2,13 +2,15 @@ from decimal import Decimal
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from pretix.api.serializers.event import MetaDataField
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.models import (
|
||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation, Question,
|
||||
QuestionOption, Quota,
|
||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation,
|
||||
Question, QuestionOption, Quota,
|
||||
)
|
||||
|
||||
|
||||
@@ -110,6 +112,7 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
bundles = InlineItemBundleSerializer(many=True, required=False)
|
||||
variations = InlineItemVariationSerializer(many=True, required=False)
|
||||
tax_rate = ItemTaxRateField(source='*', read_only=True)
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
@@ -118,12 +121,10 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
'position', 'picture', 'available_from', 'available_until',
|
||||
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
||||
'min_per_order', 'max_per_order', 'checkin_attention', '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', 'allow_waitinglist', 'issue_giftcard', 'meta_data')
|
||||
read_only_fields = ('has_variations', 'picture')
|
||||
|
||||
def get_serializer_context(self):
|
||||
return {"has_variations": self.kwargs['has_variations']}
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
if self.instance and ('addons' in data or 'variations' in data or 'bundles' in data):
|
||||
@@ -133,6 +134,17 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
Item.clean_per_order(data.get('min_per_order'), data.get('max_per_order'))
|
||||
Item.clean_available(data.get('available_from'), data.get('available_until'))
|
||||
|
||||
if data.get('issue_giftcard'):
|
||||
if data.get('tax_rule') and data.get('tax_rule').rate > 0:
|
||||
raise ValidationError(
|
||||
_("Gift card products should not be associated with non-zero tax rates since sales tax will be "
|
||||
"applied when the gift card is redeemed.")
|
||||
)
|
||||
if data.get('admission'):
|
||||
raise ValidationError(_(
|
||||
"Gift card products should not be admission products at the same time."
|
||||
))
|
||||
|
||||
return data
|
||||
|
||||
def validate_category(self, value):
|
||||
@@ -158,18 +170,65 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
ItemAddOn.clean_max_min_count(addon_data['max_count'], addon_data['min_count'])
|
||||
return value
|
||||
|
||||
@cached_property
|
||||
def item_meta_properties(self):
|
||||
return {
|
||||
p.name: p for p in self.context['request'].event.item_meta_properties.all()
|
||||
}
|
||||
|
||||
def validate_meta_data(self, value):
|
||||
for key in value['meta_data'].keys():
|
||||
if key not in self.item_meta_properties:
|
||||
raise ValidationError(_('Item meta data property \'{name}\' does not exist.').format(name=key))
|
||||
return value
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, validated_data):
|
||||
variations_data = validated_data.pop('variations') if 'variations' in validated_data else {}
|
||||
addons_data = validated_data.pop('addons') if 'addons' in validated_data else {}
|
||||
bundles_data = validated_data.pop('bundles') if 'bundles' in validated_data else {}
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
item = Item.objects.create(**validated_data)
|
||||
|
||||
for variation_data in variations_data:
|
||||
ItemVariation.objects.create(item=item, **variation_data)
|
||||
for addon_data in addons_data:
|
||||
ItemAddOn.objects.create(base_item=item, **addon_data)
|
||||
for bundle_data in bundles_data:
|
||||
ItemBundle.objects.create(base_item=item, **bundle_data)
|
||||
|
||||
# Meta data
|
||||
if meta_data is not None:
|
||||
for key, value in meta_data.items():
|
||||
ItemMetaValue.objects.create(
|
||||
property=self.item_meta_properties.get(key),
|
||||
value=value,
|
||||
item=item
|
||||
)
|
||||
return item
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
item = super().update(instance, validated_data)
|
||||
|
||||
# Meta data
|
||||
if meta_data is not None:
|
||||
current = {mv.property: mv for mv in item.meta_values.select_related('property')}
|
||||
for key, value in meta_data.items():
|
||||
prop = self.item_meta_properties.get(key)
|
||||
if prop in current:
|
||||
current[prop].value = value
|
||||
current[prop].save()
|
||||
else:
|
||||
item.meta_values.create(
|
||||
property=self.item_meta_properties.get(key),
|
||||
value=value
|
||||
)
|
||||
|
||||
for prop, current_object in current.items():
|
||||
if prop.name not in meta_data:
|
||||
current_object.delete()
|
||||
|
||||
return item
|
||||
|
||||
|
||||
@@ -200,15 +259,25 @@ class InlineQuestionOptionSerializer(I18nAwareModelSerializer):
|
||||
fields = ('id', 'identifier', 'answer', 'position')
|
||||
|
||||
|
||||
class LegacyDependencyValueField(serializers.CharField):
|
||||
|
||||
def to_representation(self, obj):
|
||||
return obj[0] if obj else None
|
||||
|
||||
def to_internal_value(self, data):
|
||||
return [data] if data else []
|
||||
|
||||
|
||||
class QuestionSerializer(I18nAwareModelSerializer):
|
||||
options = InlineQuestionOptionSerializer(many=True, required=False)
|
||||
identifier = serializers.CharField(allow_null=True)
|
||||
dependency_value = LegacyDependencyValueField(source='dependency_values', required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = Question
|
||||
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
|
||||
'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_value',
|
||||
'hidden')
|
||||
'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
|
||||
'hidden', 'dependency_value', 'print_on_invoice', 'help_text')
|
||||
|
||||
def validate_identifier(self, value):
|
||||
Question._clean_identifier(self.context['event'], value, self.instance)
|
||||
@@ -218,8 +287,8 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
if value:
|
||||
if value.type not in (Question.TYPE_CHOICE, Question.TYPE_BOOLEAN, Question.TYPE_CHOICE_MULTIPLE):
|
||||
raise ValidationError('Question dependencies can only be set to boolean or choice questions.')
|
||||
if value == self.instance:
|
||||
raise ValidationError('A question cannot depend on itself.')
|
||||
if value == self.instance:
|
||||
raise ValidationError('A question cannot depend on itself.')
|
||||
return value
|
||||
|
||||
def validate(self, data):
|
||||
@@ -238,6 +307,9 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
|
||||
dep = full_data.get('dependency_question')
|
||||
if dep:
|
||||
if dep.ask_during_checkin:
|
||||
raise ValidationError(_('Question cannot depend on a question asked during check-in.'))
|
||||
|
||||
seen_ids = {self.instance.pk} if self.instance else set()
|
||||
while dep:
|
||||
if dep.pk in seen_ids:
|
||||
@@ -245,6 +317,9 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
seen_ids.add(dep.pk)
|
||||
dep = dep.dependency_question
|
||||
|
||||
if full_data.get('ask_during_checkin') and full_data.get('type') in Question.ASK_DURING_CHECKIN_UNSUPPORTED:
|
||||
raise ValidationError(_('This type of question cannot be asked during check-in.'))
|
||||
|
||||
Question.clean_items(event, full_data.get('items'))
|
||||
return data
|
||||
|
||||
@@ -262,6 +337,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
def create(self, validated_data):
|
||||
options_data = validated_data.pop('options') if 'options' in validated_data else []
|
||||
items = validated_data.pop('items')
|
||||
|
||||
question = Question.objects.create(**validated_data)
|
||||
question.items.set(items)
|
||||
for opt_data in options_data:
|
||||
@@ -273,7 +349,7 @@ class QuotaSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Quota
|
||||
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent')
|
||||
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent', 'closed', 'close_when_sold_out')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import json
|
||||
from collections import Counter
|
||||
from collections import Counter, defaultdict
|
||||
from decimal import Decimal
|
||||
|
||||
import pycountry
|
||||
from django.db.models import F, Q
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy
|
||||
from django.utils.translation import gettext_lazy
|
||||
from django_countries.fields import Country
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
@@ -12,16 +14,22 @@ from rest_framework.reverse import reverse
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
Checkin, Invoice, InvoiceAddress, InvoiceLine, Item, ItemVariation, Order,
|
||||
OrderPosition, Question, QuestionAnswer, SubEvent,
|
||||
OrderPosition, Question, QuestionAnswer, Seat, SubEvent, TaxRule, Voucher,
|
||||
)
|
||||
from pretix.base.models.orders import (
|
||||
CartPosition, OrderFee, OrderPayment, OrderRefund,
|
||||
)
|
||||
from pretix.base.pdf import get_variables
|
||||
from pretix.base.services.cart import error_messages
|
||||
from pretix.base.services.locking import NoLockManager
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
||||
from pretix.base.signals import register_ticket_outputs
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
|
||||
|
||||
class CompatibleCountryField(serializers.Field):
|
||||
@@ -31,7 +39,7 @@ class CompatibleCountryField(serializers.Field):
|
||||
def to_representation(self, instance: InvoiceAddress):
|
||||
if instance.country:
|
||||
return str(instance.country)
|
||||
else:
|
||||
elif hasattr(instance, 'country_old'):
|
||||
return instance.country_old
|
||||
|
||||
|
||||
@@ -42,8 +50,8 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = InvoiceAddress
|
||||
fields = ('last_modified', 'is_business', 'company', 'name', 'name_parts', 'street', 'zipcode', 'city', 'country',
|
||||
'vat_id', 'vat_id_validated', 'internal_reference')
|
||||
read_only_fields = ('last_modified', 'vat_id_validated')
|
||||
'state', 'vat_id', 'vat_id_validated', 'internal_reference')
|
||||
read_only_fields = ('last_modified',)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -58,6 +66,24 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
||||
)
|
||||
if data.get('name_parts') and '_scheme' not in data.get('name_parts'):
|
||||
data['name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
|
||||
|
||||
if data.get('country'):
|
||||
if not pycountry.countries.get(alpha_2=data.get('country')):
|
||||
raise ValidationError(
|
||||
{'country': ['Invalid country code.']}
|
||||
)
|
||||
|
||||
if data.get('state'):
|
||||
cc = str(data.get('country') or self.instance.country or '')
|
||||
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||
raise ValidationError(
|
||||
{'state': ['States are not supported in country "{}".'.format(cc)]}
|
||||
)
|
||||
if not pycountry.subdivisions.get(code=cc + '-' + data.get('state')):
|
||||
raise ValidationError(
|
||||
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -71,9 +97,22 @@ class AnswerQuestionOptionsIdentifierField(serializers.Field):
|
||||
return [o.identifier for o in instance.options.all()]
|
||||
|
||||
|
||||
class AnswerQuestionOptionsField(serializers.Field):
|
||||
def to_representation(self, instance: QuestionAnswer):
|
||||
return [o.pk for o in instance.options.all()]
|
||||
|
||||
|
||||
class InlineSeatSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Seat
|
||||
fields = ('id', 'name', 'seat_guid')
|
||||
|
||||
|
||||
class AnswerSerializer(I18nAwareModelSerializer):
|
||||
question_identifier = AnswerQuestionIdentifierField(source='*', read_only=True)
|
||||
option_identifiers = AnswerQuestionOptionsIdentifierField(source='*', read_only=True)
|
||||
options = AnswerQuestionOptionsField(source='*', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = QuestionAnswer
|
||||
@@ -83,7 +122,7 @@ class AnswerSerializer(I18nAwareModelSerializer):
|
||||
class CheckinSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = Checkin
|
||||
fields = ('datetime', 'list')
|
||||
fields = ('datetime', 'list', 'auto_checked_in')
|
||||
|
||||
|
||||
class OrderDownloadsField(serializers.Field):
|
||||
@@ -157,6 +196,11 @@ class PdfDataSerializer(serializers.Field):
|
||||
for k, v in ev._cached_meta_data.items():
|
||||
res['meta:' + k] = v
|
||||
|
||||
if not hasattr(instance.item, '_cached_meta_data'):
|
||||
instance.item._cached_meta_data = instance.item.meta_data
|
||||
for k, v in instance.item._cached_meta_data.items():
|
||||
res['itemmeta:' + k] = v
|
||||
|
||||
return res
|
||||
|
||||
|
||||
@@ -166,12 +210,15 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
downloads = PositionDownloadsField(source='*')
|
||||
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
||||
pdf_data = PdfDataSerializer(source='*')
|
||||
seat = InlineSeatSerializer(read_only=True)
|
||||
country = CompatibleCountryField(source='*')
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data')
|
||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -251,13 +298,36 @@ class OrderPaymentDateField(serializers.DateField):
|
||||
class OrderFeeSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = OrderFee
|
||||
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule')
|
||||
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule', 'canceled')
|
||||
|
||||
|
||||
class PaymentURLField(serializers.URLField):
|
||||
def to_representation(self, instance: OrderPayment):
|
||||
if instance.state != OrderPayment.PAYMENT_STATE_CREATED:
|
||||
return None
|
||||
return build_absolute_uri(self.context['event'], 'presale:event.order.pay', kwargs={
|
||||
'order': instance.order.code,
|
||||
'secret': instance.order.secret,
|
||||
'payment': instance.pk,
|
||||
})
|
||||
|
||||
|
||||
class PaymentDetailsField(serializers.Field):
|
||||
def to_representation(self, value: OrderPayment):
|
||||
pp = value.payment_provider
|
||||
if not pp:
|
||||
return {}
|
||||
return pp.api_payment_details(value)
|
||||
|
||||
|
||||
class OrderPaymentSerializer(I18nAwareModelSerializer):
|
||||
payment_url = PaymentURLField(source='*', allow_null=True, read_only=True)
|
||||
details = PaymentDetailsField(source='*', allow_null=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = OrderPayment
|
||||
fields = ('local_id', 'state', 'amount', 'created', 'payment_date', 'provider')
|
||||
fields = ('local_id', 'state', 'amount', 'created', 'payment_date', 'provider', 'payment_url',
|
||||
'details')
|
||||
|
||||
|
||||
class OrderRefundSerializer(I18nAwareModelSerializer):
|
||||
@@ -268,6 +338,14 @@ class OrderRefundSerializer(I18nAwareModelSerializer):
|
||||
fields = ('local_id', 'state', 'source', 'amount', 'payment', 'created', 'execution_date', 'provider')
|
||||
|
||||
|
||||
class OrderURLField(serializers.URLField):
|
||||
def to_representation(self, instance: Order):
|
||||
return build_absolute_uri(self.context['event'], 'presale:event.order', kwargs={
|
||||
'order': instance.code,
|
||||
'secret': instance.secret,
|
||||
})
|
||||
|
||||
|
||||
class OrderSerializer(I18nAwareModelSerializer):
|
||||
invoice_address = InvoiceAddressSerializer(allow_null=True)
|
||||
positions = OrderPositionSerializer(many=True, read_only=True)
|
||||
@@ -277,13 +355,15 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
refunds = OrderRefundSerializer(many=True, read_only=True)
|
||||
payment_date = OrderPaymentDateField(source='*', read_only=True)
|
||||
payment_provider = OrderPaymentTypeField(source='*', read_only=True)
|
||||
url = OrderURLField(source='*', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = (
|
||||
'code', 'status', 'testmode', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
|
||||
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
|
||||
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel'
|
||||
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
|
||||
'url'
|
||||
)
|
||||
read_only_fields = (
|
||||
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
|
||||
@@ -305,7 +385,6 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
# Even though all fields that shouldn't be edited are marked as read_only in the serializer
|
||||
# (hopefully), we'll be extra careful here and be explicit about the model fields we update.
|
||||
update_fields = ['comment', 'checkin_attention', 'email', 'locale']
|
||||
print(validated_data)
|
||||
|
||||
if 'invoice_address' in validated_data:
|
||||
iadata = validated_data.pop('invoice_address')
|
||||
@@ -323,7 +402,7 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
}
|
||||
try:
|
||||
ia = instance.invoice_address
|
||||
if iadata.get('vat_id') != ia.vat_id:
|
||||
if iadata.get('vat_id') != ia.vat_id and 'vat_id_validated' not in iadata:
|
||||
ia.vat_id_validated = False
|
||||
self.fields['invoice_address'].update(ia, iadata)
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
@@ -413,9 +492,13 @@ class AnswerCreateSerializer(I18nAwareModelSerializer):
|
||||
|
||||
|
||||
class OrderFeeCreateSerializer(I18nAwareModelSerializer):
|
||||
_treat_value_as_percentage = serializers.BooleanField(default=False, required=False)
|
||||
_split_taxes_like_products = serializers.BooleanField(default=False, required=False)
|
||||
|
||||
class Meta:
|
||||
model = OrderFee
|
||||
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rule')
|
||||
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rule',
|
||||
'_treat_value_as_percentage', '_split_taxes_like_products')
|
||||
|
||||
def validate_tax_rule(self, tr):
|
||||
if tr and tr.event != self.context['event']:
|
||||
@@ -430,11 +513,26 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
addon_to = serializers.IntegerField(required=False, allow_null=True)
|
||||
secret = serializers.CharField(required=False)
|
||||
attendee_name = serializers.CharField(required=False, allow_null=True)
|
||||
seat = serializers.CharField(required=False, allow_null=True)
|
||||
price = serializers.DecimalField(required=False, allow_null=True, decimal_places=2,
|
||||
max_digits=10)
|
||||
voucher = serializers.SlugRelatedField(slug_field='code', queryset=Voucher.objects.none(),
|
||||
required=False, allow_null=True)
|
||||
country = CompatibleCountryField(source='*')
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||
'secret', 'addon_to', 'subevent', 'answers')
|
||||
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for k, v in self.fields.items():
|
||||
if k in ('company', 'street', 'zipcode', 'city', 'country', 'state'):
|
||||
v.required = False
|
||||
v.allow_blank = True
|
||||
v.allow_null = True
|
||||
|
||||
def validate_secret(self, secret):
|
||||
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
|
||||
@@ -490,6 +588,24 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
)
|
||||
if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'):
|
||||
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
|
||||
|
||||
if data.get('country'):
|
||||
if not pycountry.countries.get(alpha_2=data.get('country')):
|
||||
raise ValidationError(
|
||||
{'country': ['Invalid country code.']}
|
||||
)
|
||||
|
||||
if data.get('state'):
|
||||
cc = str(data.get('country') or self.instance.country or '')
|
||||
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||
raise ValidationError(
|
||||
{'state': ['States are not supported in country "{}".'.format(cc)]}
|
||||
)
|
||||
if not pycountry.subdivisions.get(code=cc + '-' + data.get('state')):
|
||||
raise ValidationError(
|
||||
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -506,9 +622,31 @@ class CompatibleJSONField(serializers.JSONField):
|
||||
return value
|
||||
|
||||
|
||||
class WrappedList:
|
||||
def __init__(self, data):
|
||||
self._data = data
|
||||
|
||||
def all(self):
|
||||
return self._data
|
||||
|
||||
|
||||
class WrappedModel:
|
||||
def __init__(self, model):
|
||||
self._wrapped = model
|
||||
|
||||
def __getattr__(self, item):
|
||||
return getattr(self._wrapped, item)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
invoice_address = InvoiceAddressSerializer(required=False)
|
||||
positions = OrderPositionCreateSerializer(many=True, required=False)
|
||||
positions = OrderPositionCreateSerializer(many=True, required=True)
|
||||
fees = OrderFeeCreateSerializer(many=True, required=False)
|
||||
status = serializers.ChoiceField(choices=(
|
||||
('n', Order.STATUS_PENDING),
|
||||
@@ -520,18 +658,27 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
min_length=5
|
||||
)
|
||||
comment = serializers.CharField(required=False, allow_blank=True)
|
||||
payment_provider = serializers.CharField(required=True)
|
||||
payment_provider = serializers.CharField(required=False, allow_null=True)
|
||||
payment_info = CompatibleJSONField(required=False)
|
||||
consume_carts = serializers.ListField(child=serializers.CharField(), required=False)
|
||||
force = serializers.BooleanField(default=False, required=False)
|
||||
payment_date = serializers.DateTimeField(required=False, allow_null=True)
|
||||
send_mail = serializers.BooleanField(default=False, required=False)
|
||||
simulate = serializers.BooleanField(default=False, required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['positions'].child.fields['voucher'].queryset = self.context['event'].vouchers.all()
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ('code', 'status', 'testmode', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
||||
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts', 'force')
|
||||
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts',
|
||||
'force', 'send_mail', 'simulate')
|
||||
|
||||
def validate_payment_provider(self, pp):
|
||||
if pp is None:
|
||||
return None
|
||||
if pp not in self.context['event'].get_payment_providers():
|
||||
raise ValidationError('The given payment provider is not known.')
|
||||
return pp
|
||||
@@ -590,18 +737,37 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
{'positionid': ["If you set addon_to on any position, you need to specify position IDs manually."]}
|
||||
for p in data
|
||||
]
|
||||
else:
|
||||
for i, p in enumerate(data):
|
||||
p['positionid'] = i + 1
|
||||
|
||||
if any(errs):
|
||||
raise ValidationError(errs)
|
||||
return data
|
||||
|
||||
def validate_testmode(self, testmode):
|
||||
if 'sales_channel' in self.initial_data:
|
||||
try:
|
||||
sales_channel = get_all_sales_channels()[self.initial_data['sales_channel']]
|
||||
|
||||
if testmode and not sales_channel.testmode_supported:
|
||||
raise ValidationError('This sales channel does not provide support for test mode.')
|
||||
except KeyError:
|
||||
# We do not need to raise a ValidationError here, since there is another check to validate the
|
||||
# sales_channel
|
||||
pass
|
||||
|
||||
return testmode
|
||||
|
||||
def create(self, validated_data):
|
||||
fees_data = validated_data.pop('fees') if 'fees' in validated_data else []
|
||||
positions_data = validated_data.pop('positions') if 'positions' in validated_data else []
|
||||
payment_provider = validated_data.pop('payment_provider')
|
||||
payment_provider = validated_data.pop('payment_provider', None)
|
||||
payment_info = validated_data.pop('payment_info', '{}')
|
||||
payment_date = validated_data.pop('payment_date', now())
|
||||
force = validated_data.pop('force', False)
|
||||
simulate = validated_data.pop('simulate', False)
|
||||
self._send_mail = validated_data.pop('send_mail', False)
|
||||
|
||||
if 'invoice_address' in validated_data:
|
||||
iadata = validated_data.pop('invoice_address')
|
||||
@@ -614,14 +780,21 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
else:
|
||||
ia = None
|
||||
|
||||
with self.context['event'].lock() as now_dt:
|
||||
quotadiff = Counter()
|
||||
|
||||
lockfn = self.context['event'].lock
|
||||
if simulate:
|
||||
lockfn = NoLockManager
|
||||
with lockfn() as now_dt:
|
||||
free_seats = set()
|
||||
seats_seen = set()
|
||||
consume_carts = validated_data.pop('consume_carts', [])
|
||||
delete_cps = []
|
||||
quota_avail_cache = {}
|
||||
v_budget = {}
|
||||
voucher_usage = Counter()
|
||||
if consume_carts:
|
||||
for cp in CartPosition.objects.filter(event=self.context['event'], cart_id__in=consume_carts):
|
||||
for cp in CartPosition.objects.filter(
|
||||
event=self.context['event'], cart_id__in=consume_carts, expires__gt=now()
|
||||
):
|
||||
quotas = (cp.variation.quotas.filter(subevent=cp.subevent)
|
||||
if cp.variation else cp.item.quotas.filter(subevent=cp.subevent))
|
||||
for quota in quotas:
|
||||
@@ -629,19 +802,112 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
quota_avail_cache[quota] = list(quota.availability())
|
||||
if quota_avail_cache[quota][1] is not None:
|
||||
quota_avail_cache[quota][1] += 1
|
||||
if cp.voucher:
|
||||
voucher_usage[cp.voucher] -= 1
|
||||
if cp.expires > now_dt:
|
||||
quotadiff.subtract(quotas)
|
||||
if cp.seat:
|
||||
free_seats.add(cp.seat)
|
||||
delete_cps.append(cp)
|
||||
|
||||
errs = [{} for p in positions_data]
|
||||
|
||||
for i, pos_data in enumerate(positions_data):
|
||||
|
||||
if pos_data.get('voucher'):
|
||||
v = pos_data['voucher']
|
||||
|
||||
if pos_data.get('addon_to'):
|
||||
errs[i]['voucher'] = ['Vouchers are currently not supported for add-on products.']
|
||||
continue
|
||||
|
||||
if not v.applies_to(pos_data['item'], pos_data.get('variation')):
|
||||
errs[i]['voucher'] = [error_messages['voucher_invalid_item']]
|
||||
continue
|
||||
|
||||
if v.subevent_id and pos_data.get('subevent').pk != v.subevent_id:
|
||||
errs[i]['voucher'] = [error_messages['voucher_invalid_subevent']]
|
||||
continue
|
||||
|
||||
if v.valid_until is not None and v.valid_until < now_dt:
|
||||
errs[i]['voucher'] = [error_messages['voucher_expired']]
|
||||
continue
|
||||
|
||||
voucher_usage[v] += 1
|
||||
if voucher_usage[v] > 0:
|
||||
redeemed_in_carts = CartPosition.objects.filter(
|
||||
Q(voucher=pos_data['voucher']) & Q(event=self.context['event']) & Q(expires__gte=now_dt)
|
||||
).exclude(pk__in=[cp.pk for cp in delete_cps])
|
||||
v_avail = v.max_usages - v.redeemed - redeemed_in_carts.count()
|
||||
if v_avail < voucher_usage[v]:
|
||||
errs[i]['voucher'] = [
|
||||
'The voucher has already been used the maximum number of times.'
|
||||
]
|
||||
|
||||
if v.budget is not None:
|
||||
price = pos_data.get('price')
|
||||
if price is None:
|
||||
price = get_price(
|
||||
item=pos_data.get('item'),
|
||||
variation=pos_data.get('variation'),
|
||||
voucher=v,
|
||||
custom_price=None,
|
||||
subevent=pos_data.get('subevent'),
|
||||
addon_to=pos_data.get('addon_to'),
|
||||
invoice_address=ia,
|
||||
).gross
|
||||
pbv = get_price(
|
||||
item=pos_data['item'],
|
||||
variation=pos_data.get('variation'),
|
||||
voucher=None,
|
||||
custom_price=None,
|
||||
subevent=pos_data.get('subevent'),
|
||||
addon_to=pos_data.get('addon_to'),
|
||||
invoice_address=ia,
|
||||
)
|
||||
|
||||
if v not in v_budget:
|
||||
v_budget[v] = v.budget - v.budget_used()
|
||||
disc = pbv.gross - price
|
||||
if disc > v_budget[v]:
|
||||
new_disc = v_budget[v]
|
||||
v_budget[v] -= new_disc
|
||||
if new_disc == Decimal('0.00') or pos_data.get('price') is not None:
|
||||
errs[i]['voucher'] = [
|
||||
'The voucher has a remaining budget of {}, therefore a discount of {} can not be '
|
||||
'given.'.format(v_budget[v] + new_disc, disc)
|
||||
]
|
||||
continue
|
||||
pos_data['price'] = price + (disc - new_disc)
|
||||
else:
|
||||
v_budget[v] -= disc
|
||||
|
||||
seated = pos_data.get('item').seat_category_mappings.filter(subevent=pos_data.get('subevent')).exists()
|
||||
if pos_data.get('seat'):
|
||||
if not seated:
|
||||
errs[i]['seat'] = ['The specified product does not allow to choose a seat.']
|
||||
try:
|
||||
seat = self.context['event'].seats.get(seat_guid=pos_data['seat'], subevent=pos_data.get('subevent'))
|
||||
except Seat.DoesNotExist:
|
||||
errs[i]['seat'] = ['The specified seat does not exist.']
|
||||
else:
|
||||
pos_data['seat'] = seat
|
||||
if (seat not in free_seats and not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web'))) or seat in seats_seen:
|
||||
errs[i]['seat'] = [gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
|
||||
seats_seen.add(seat)
|
||||
elif seated:
|
||||
errs[i]['seat'] = ['The specified product requires to choose a seat.']
|
||||
|
||||
if not force:
|
||||
for i, pos_data in enumerate(positions_data):
|
||||
if pos_data.get('voucher'):
|
||||
if pos_data['voucher'].allow_ignore_quota or pos_data['voucher'].block_quota:
|
||||
continue
|
||||
|
||||
new_quotas = (pos_data.get('variation').quotas.filter(subevent=pos_data.get('subevent'))
|
||||
if pos_data.get('variation')
|
||||
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
|
||||
if len(new_quotas) == 0:
|
||||
errs[i]['item'] = [ugettext_lazy('The product "{}" is not assigned to a quota.').format(
|
||||
errs[i]['item'] = [gettext_lazy('The product "{}" is not assigned to a quota.').format(
|
||||
str(pos_data.get('item'))
|
||||
)]
|
||||
else:
|
||||
@@ -653,13 +919,11 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
quota_avail_cache[quota][1] -= 1
|
||||
if quota_avail_cache[quota][1] < 0:
|
||||
errs[i]['item'] = [
|
||||
ugettext_lazy('There is not enough quota available on quota "{}" to perform the operation.').format(
|
||||
gettext_lazy('There is not enough quota available on quota "{}" to perform the operation.').format(
|
||||
quota.name
|
||||
)
|
||||
]
|
||||
|
||||
quotadiff.update(new_quotas)
|
||||
|
||||
if any(errs):
|
||||
raise ValidationError({'positions': errs})
|
||||
|
||||
@@ -667,38 +931,23 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
validated_data['locale'] = self.context['event'].settings.locale
|
||||
order = Order(event=self.context['event'], **validated_data)
|
||||
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
|
||||
order.total = sum([p['price'] for p in positions_data]) + sum([f['value'] for f in fees_data], Decimal('0.00'))
|
||||
order.meta_info = "{}"
|
||||
order.save()
|
||||
|
||||
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID:
|
||||
order.status = Order.STATUS_PAID
|
||||
order.total = Decimal('0.00')
|
||||
if simulate:
|
||||
order = WrappedModel(order)
|
||||
order.last_modified = now()
|
||||
order.code = 'PREVIEW'
|
||||
else:
|
||||
order.save()
|
||||
order.payments.create(
|
||||
amount=order.total, provider='free', state=OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||||
payment_date=now()
|
||||
)
|
||||
elif payment_provider == "free" and order.total != Decimal('0.00'):
|
||||
raise ValidationError('You cannot use the "free" payment provider for non-free orders.')
|
||||
elif validated_data.get('status') == Order.STATUS_PAID:
|
||||
order.payments.create(
|
||||
amount=order.total,
|
||||
provider=payment_provider,
|
||||
info=payment_info,
|
||||
payment_date=payment_date,
|
||||
state=OrderPayment.PAYMENT_STATE_CONFIRMED
|
||||
)
|
||||
elif payment_provider:
|
||||
order.payments.create(
|
||||
amount=order.total,
|
||||
provider=payment_provider,
|
||||
info=payment_info,
|
||||
state=OrderPayment.PAYMENT_STATE_CREATED
|
||||
)
|
||||
|
||||
if ia:
|
||||
ia.order = order
|
||||
ia.save()
|
||||
if not simulate:
|
||||
ia.order = order
|
||||
ia.save()
|
||||
else:
|
||||
order.invoice_address = ia
|
||||
ia.last_modified = now()
|
||||
|
||||
pos_map = {}
|
||||
for pos_data in positions_data:
|
||||
answers_data = pos_data.pop('answers', [])
|
||||
@@ -709,32 +958,171 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
'_legacy': attendee_name
|
||||
}
|
||||
pos = OrderPosition(**pos_data)
|
||||
pos.order = order
|
||||
pos._calculate_tax()
|
||||
if simulate:
|
||||
pos.order = order._wrapped
|
||||
else:
|
||||
pos.order = order
|
||||
if addon_to:
|
||||
pos.addon_to = pos_map[addon_to]
|
||||
pos.save()
|
||||
pos_map[pos.positionid] = pos
|
||||
for answ_data in answers_data:
|
||||
options = answ_data.pop('options', [])
|
||||
answ = pos.answers.create(**answ_data)
|
||||
answ.options.add(*options)
|
||||
|
||||
for cp in delete_cps:
|
||||
cp.delete()
|
||||
if pos.price is None:
|
||||
price = get_price(
|
||||
item=pos.item,
|
||||
variation=pos.variation,
|
||||
voucher=pos.voucher,
|
||||
custom_price=None,
|
||||
subevent=pos.subevent,
|
||||
addon_to=pos.addon_to,
|
||||
invoice_address=ia,
|
||||
)
|
||||
pos.price = price.gross
|
||||
pos.tax_rate = price.rate
|
||||
pos.tax_value = price.tax
|
||||
pos.tax_rule = pos.item.tax_rule
|
||||
else:
|
||||
pos._calculate_tax()
|
||||
|
||||
pos.price_before_voucher = get_price(
|
||||
item=pos.item,
|
||||
variation=pos.variation,
|
||||
voucher=None,
|
||||
custom_price=None,
|
||||
subevent=pos.subevent,
|
||||
addon_to=pos.addon_to,
|
||||
invoice_address=ia,
|
||||
).gross
|
||||
|
||||
if simulate:
|
||||
pos = WrappedModel(pos)
|
||||
pos.id = 0
|
||||
answers = []
|
||||
for answ_data in answers_data:
|
||||
options = answ_data.pop('options', [])
|
||||
answ = WrappedModel(QuestionAnswer(**answ_data))
|
||||
answ.options = WrappedList(options)
|
||||
answers.append(answ)
|
||||
pos.answers = answers
|
||||
pos.pseudonymization_id = "PREVIEW"
|
||||
else:
|
||||
if pos.voucher:
|
||||
Voucher.objects.filter(pk=pos.voucher.pk).update(redeemed=F('redeemed') + 1)
|
||||
pos.save()
|
||||
for answ_data in answers_data:
|
||||
options = answ_data.pop('options', [])
|
||||
answ = pos.answers.create(**answ_data)
|
||||
answ.options.add(*options)
|
||||
pos_map[pos.positionid] = pos
|
||||
|
||||
if not simulate:
|
||||
for cp in delete_cps:
|
||||
cp.delete()
|
||||
|
||||
order.total = sum([p.price for p in pos_map.values()])
|
||||
fees = []
|
||||
for fee_data in fees_data:
|
||||
f = OrderFee(**fee_data)
|
||||
f.order = order
|
||||
f._calculate_tax()
|
||||
f.save()
|
||||
is_percentage = fee_data.pop('_treat_value_as_percentage', False)
|
||||
if is_percentage:
|
||||
fee_data['value'] = round_decimal(order.total * (fee_data['value'] / Decimal('100.00')),
|
||||
self.context['event'].currency)
|
||||
is_split_taxes = fee_data.pop('_split_taxes_like_products', False)
|
||||
|
||||
if is_split_taxes:
|
||||
d = defaultdict(lambda: Decimal('0.00'))
|
||||
trz = TaxRule.zero()
|
||||
for p in pos_map.values():
|
||||
tr = p.tax_rule
|
||||
d[tr] += p.price - p.tax_value
|
||||
|
||||
base_values = sorted([tuple(t) for t in d.items()], key=lambda t: (t[0] or trz).rate)
|
||||
sum_base = sum(t[1] for t in base_values)
|
||||
fee_values = [(t[0], round_decimal(fee_data['value'] * t[1] / sum_base, self.context['event'].currency))
|
||||
for t in base_values]
|
||||
sum_fee = sum(t[1] for t in fee_values)
|
||||
|
||||
# If there are rounding differences, we fix them up, but always leaning to the benefit of the tax
|
||||
# authorities
|
||||
if sum_fee > fee_data['value']:
|
||||
fee_values[0] = (fee_values[0][0], fee_values[0][1] + (fee_data['value'] - sum_fee))
|
||||
elif sum_fee < fee_data['value']:
|
||||
fee_values[-1] = (fee_values[-1][0], fee_values[-1][1] + (fee_data['value'] - sum_fee))
|
||||
|
||||
for tr, val in fee_values:
|
||||
fee_data['tax_rule'] = tr
|
||||
fee_data['value'] = val
|
||||
f = OrderFee(**fee_data)
|
||||
f.order = order._wrapped if simulate else order
|
||||
f._calculate_tax()
|
||||
fees.append(f)
|
||||
if not simulate:
|
||||
f.save()
|
||||
else:
|
||||
f = OrderFee(**fee_data)
|
||||
f.order = order._wrapped if simulate else order
|
||||
f._calculate_tax()
|
||||
fees.append(f)
|
||||
if not simulate:
|
||||
f.save()
|
||||
|
||||
order.total += sum([f.value for f in fees])
|
||||
if simulate:
|
||||
order.fees = fees
|
||||
order.positions = pos_map.values()
|
||||
return order # ignore payments
|
||||
else:
|
||||
order.save(update_fields=['total'])
|
||||
|
||||
if order.total == Decimal('0.00') and validated_data.get('status') == Order.STATUS_PAID and not payment_provider:
|
||||
payment_provider = 'free'
|
||||
|
||||
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID:
|
||||
order.status = Order.STATUS_PAID
|
||||
order.save()
|
||||
order.payments.create(
|
||||
amount=order.total, provider='free', state=OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||||
payment_date=now()
|
||||
)
|
||||
elif payment_provider == "free" and order.total != Decimal('0.00'):
|
||||
raise ValidationError('You cannot use the "free" payment provider for non-free orders.')
|
||||
elif validated_data.get('status') == Order.STATUS_PAID:
|
||||
if not payment_provider:
|
||||
raise ValidationError('You cannot create a paid order without a payment provider.')
|
||||
order.payments.create(
|
||||
amount=order.total,
|
||||
provider=payment_provider,
|
||||
info=payment_info,
|
||||
payment_date=payment_date,
|
||||
state=OrderPayment.PAYMENT_STATE_CONFIRMED
|
||||
)
|
||||
elif payment_provider:
|
||||
order.payments.create(
|
||||
amount=order.total,
|
||||
provider=payment_provider,
|
||||
info=payment_info,
|
||||
state=OrderPayment.PAYMENT_STATE_CREATED
|
||||
)
|
||||
|
||||
return order
|
||||
|
||||
|
||||
class LinePositionField(serializers.IntegerField):
|
||||
"""
|
||||
Internally, the position field is stored starting at 0, but for the API, starting at 1 makes it
|
||||
more consistent with other models
|
||||
"""
|
||||
|
||||
def to_representation(self, value):
|
||||
return super().to_representation(value) + 1
|
||||
|
||||
def to_internal_value(self, data):
|
||||
return super().to_internal_value(data) - 1
|
||||
|
||||
|
||||
class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
||||
position = LinePositionField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = InvoiceLine
|
||||
fields = ('description', 'gross_value', 'tax_value', 'tax_rate', 'tax_name')
|
||||
fields = ('position', 'description', 'gross_value', 'tax_value', 'tax_rate', 'tax_name')
|
||||
|
||||
|
||||
class InvoiceSerializer(I18nAwareModelSerializer):
|
||||
@@ -750,6 +1138,20 @@ class InvoiceSerializer(I18nAwareModelSerializer):
|
||||
'internal_reference')
|
||||
|
||||
|
||||
class OrderPaymentCreateSerializer(I18nAwareModelSerializer):
|
||||
provider = serializers.CharField(required=True, allow_null=False, allow_blank=False)
|
||||
info = CompatibleJSONField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = OrderPayment
|
||||
fields = ('state', 'amount', 'payment_date', 'provider', 'info')
|
||||
|
||||
def create(self, validated_data):
|
||||
order = OrderPayment(order=self.context['order'], **validated_data)
|
||||
order.save()
|
||||
return order
|
||||
|
||||
|
||||
class OrderRefundCreateSerializer(I18nAwareModelSerializer):
|
||||
payment = serializers.IntegerField(required=False, allow_null=True)
|
||||
provider = serializers.CharField(required=True, allow_null=False, allow_blank=False)
|
||||
|
||||
@@ -1,8 +1,169 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import get_language, gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.models import Organizer
|
||||
from pretix.api.serializers.order import CompatibleJSONField
|
||||
from pretix.base.auth import get_auth_backends
|
||||
from pretix.base.models import (
|
||||
GiftCard, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
||||
)
|
||||
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
from pretix.helpers.urls import build_absolute_uri
|
||||
|
||||
|
||||
class OrganizerSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = Organizer
|
||||
fields = ('name', 'slug')
|
||||
|
||||
|
||||
class SeatingPlanSerializer(I18nAwareModelSerializer):
|
||||
layout = CompatibleJSONField(
|
||||
validators=[SeatingPlanLayoutValidator()]
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SeatingPlan
|
||||
fields = ('id', 'name', 'layout')
|
||||
|
||||
|
||||
class GiftCardSerializer(I18nAwareModelSerializer):
|
||||
value = serializers.DecimalField(max_digits=10, decimal_places=2, min_value=Decimal('0.00'))
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
s = data['secret']
|
||||
qs = GiftCard.objects.filter(
|
||||
secret=s
|
||||
).filter(
|
||||
Q(issuer=self.context["organizer"]) | Q(
|
||||
issuer__gift_card_collector_acceptance__collector=self.context["organizer"])
|
||||
)
|
||||
if self.instance:
|
||||
qs = qs.exclude(pk=self.instance.pk)
|
||||
if qs.exists():
|
||||
raise ValidationError(
|
||||
{'secret': _(
|
||||
'A gift card with the same secret already exists in your or an affiliated organizer account.')}
|
||||
)
|
||||
return data
|
||||
|
||||
class Meta:
|
||||
model = GiftCard
|
||||
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions')
|
||||
|
||||
|
||||
class EventSlugField(serializers.SlugRelatedField):
|
||||
def get_queryset(self):
|
||||
return self.context['organizer'].events.all()
|
||||
|
||||
|
||||
class TeamSerializer(serializers.ModelSerializer):
|
||||
limit_events = EventSlugField(slug_field='slug', many=True)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
model = Team
|
||||
fields = (
|
||||
'id', 'name', 'all_events', 'limit_events', 'can_create_events', 'can_change_teams',
|
||||
'can_change_organizer_settings', 'can_manage_gift_cards', 'can_change_event_settings',
|
||||
'can_change_items', 'can_view_orders', 'can_change_orders', 'can_view_vouchers',
|
||||
'can_change_vouchers'
|
||||
)
|
||||
|
||||
def validate(self, data):
|
||||
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||
full_data.update(data)
|
||||
if full_data.get('limit_events') and full_data.get('all_events'):
|
||||
raise ValidationError('Do not set both limit_events and all_events.')
|
||||
return data
|
||||
|
||||
|
||||
class TeamInviteSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = TeamInvite
|
||||
fields = (
|
||||
'id', 'email'
|
||||
)
|
||||
|
||||
def _send_invite(self, instance):
|
||||
try:
|
||||
mail(
|
||||
instance.email,
|
||||
_('pretix account invitation'),
|
||||
'pretixcontrol/email/invitation.txt',
|
||||
{
|
||||
'user': self,
|
||||
'organizer': self.context['organizer'].name,
|
||||
'team': instance.team.name,
|
||||
'url': build_absolute_uri('control:auth.invite', kwargs={
|
||||
'token': instance.token
|
||||
})
|
||||
},
|
||||
event=None,
|
||||
locale=get_language() # TODO: expose?
|
||||
)
|
||||
except SendMailException:
|
||||
pass # Already logged
|
||||
|
||||
def create(self, validated_data):
|
||||
if 'email' in validated_data:
|
||||
try:
|
||||
user = User.objects.get(email__iexact=validated_data['email'])
|
||||
except User.DoesNotExist:
|
||||
if self.context['team'].invites.filter(email__iexact=validated_data['email']).exists():
|
||||
raise ValidationError(_('This user already has been invited for this team.'))
|
||||
if 'native' not in get_auth_backends():
|
||||
raise ValidationError('Users need to have a pretix account before they can be invited.')
|
||||
|
||||
invite = self.context['team'].invites.create(email=validated_data['email'])
|
||||
self._send_invite(invite)
|
||||
invite.team.log_action(
|
||||
'pretix.team.invite.created',
|
||||
data={
|
||||
'email': validated_data['email']
|
||||
},
|
||||
**self.context['log_kwargs']
|
||||
)
|
||||
return invite
|
||||
else:
|
||||
if self.context['team'].members.filter(pk=user.pk).exists():
|
||||
raise ValidationError(_('This user already has permissions for this team.'))
|
||||
|
||||
self.context['team'].members.add(user)
|
||||
self.context['team'].log_action(
|
||||
'pretix.team.member.added',
|
||||
data={
|
||||
'email': user.email,
|
||||
'user': user.pk,
|
||||
},
|
||||
**self.context['log_kwargs']
|
||||
)
|
||||
return TeamInvite(email=user.email)
|
||||
else:
|
||||
raise ValidationError('No email address given.')
|
||||
|
||||
|
||||
class TeamAPITokenSerializer(serializers.ModelSerializer):
|
||||
active = serializers.BooleanField(default=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = TeamAPIToken
|
||||
fields = (
|
||||
'id', 'name', 'active'
|
||||
)
|
||||
|
||||
|
||||
class TeamMemberSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = (
|
||||
'id', 'email', 'fullname', 'require_2fa'
|
||||
)
|
||||
|
||||
@@ -2,15 +2,23 @@ from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.models import Voucher
|
||||
from pretix.base.models import Seat, Voucher
|
||||
|
||||
|
||||
class VoucherListSerializer(serializers.ListSerializer):
|
||||
def create(self, validated_data):
|
||||
codes = set()
|
||||
seats = set()
|
||||
errs = []
|
||||
err = False
|
||||
for voucher_data in validated_data:
|
||||
if voucher_data.get('seat') and (voucher_data.get('seat'), voucher_data.get('subevent')) in seats:
|
||||
err = True
|
||||
errs.append({'code': ['Duplicate seat ID in request.']})
|
||||
continue
|
||||
else:
|
||||
seats.add((voucher_data.get('seat'), voucher_data.get('subevent')))
|
||||
|
||||
if voucher_data['code'] in codes:
|
||||
err = True
|
||||
errs.append({'code': ['Duplicate voucher code in request.']})
|
||||
@@ -22,12 +30,19 @@ class VoucherListSerializer(serializers.ListSerializer):
|
||||
return super().create(validated_data)
|
||||
|
||||
|
||||
class SeatGuidField(serializers.CharField):
|
||||
def to_representation(self, val: Seat):
|
||||
return val.seat_guid
|
||||
|
||||
|
||||
class VoucherSerializer(I18nAwareModelSerializer):
|
||||
seat = SeatGuidField(allow_null=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = Voucher
|
||||
fields = ('id', 'code', 'max_usages', 'redeemed', 'valid_until', 'block_quota',
|
||||
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
|
||||
'tag', 'comment', 'subevent')
|
||||
'tag', 'comment', 'subevent', 'show_hidden_items', 'seat')
|
||||
read_only_fields = ('id', 'redeemed')
|
||||
list_serializer_class = VoucherListSerializer
|
||||
|
||||
@@ -39,7 +54,8 @@ class VoucherSerializer(I18nAwareModelSerializer):
|
||||
|
||||
Voucher.clean_item_properties(
|
||||
full_data, self.context.get('event'),
|
||||
full_data.get('quota'), full_data.get('item'), full_data.get('variation')
|
||||
full_data.get('quota'), full_data.get('item'), full_data.get('variation'),
|
||||
block_quota=full_data.get('block_quota')
|
||||
)
|
||||
Voucher.clean_subevent(
|
||||
full_data, self.context.get('event')
|
||||
@@ -61,4 +77,10 @@ class VoucherSerializer(I18nAwareModelSerializer):
|
||||
)
|
||||
Voucher.clean_voucher_code(full_data, self.context.get('event'), self.instance.pk if self.instance else None)
|
||||
|
||||
if full_data.get('seat'):
|
||||
data['seat'] = Voucher.clean_seat_id(
|
||||
full_data, full_data.get('item'), full_data.get('quota'), self.context.get('event'),
|
||||
self.instance.pk if self.instance else None
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
@@ -2,6 +2,7 @@ from datetime import timedelta
|
||||
|
||||
from django.dispatch import Signal, receiver
|
||||
from django.utils.timezone import now
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.api.models import ApiCall, WebHookCall
|
||||
from pretix.base.signals import periodic_task
|
||||
@@ -17,10 +18,12 @@ instances.
|
||||
|
||||
|
||||
@receiver(periodic_task)
|
||||
@scopes_disabled()
|
||||
def cleanup_webhook_logs(sender, **kwargs):
|
||||
WebHookCall.objects.filter(datetime__lte=now() - timedelta(days=30)).delete()
|
||||
|
||||
|
||||
@receiver(periodic_task)
|
||||
@scopes_disabled()
|
||||
def cleanup_api_logs(sender, **kwargs):
|
||||
ApiCall.objects.filter(created__lte=now() - timedelta(hours=24)).delete()
|
||||
|
||||
@@ -7,8 +7,8 @@ from rest_framework import routers
|
||||
from pretix.api.views import cart
|
||||
|
||||
from .views import (
|
||||
checkin, device, event, item, oauth, order, organizer, user, voucher,
|
||||
waitinglist, webhooks,
|
||||
checkin, device, event, item, oauth, order, organizer, user, version,
|
||||
voucher, waitinglist, webhooks,
|
||||
)
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
@@ -18,6 +18,14 @@ orga_router = routers.DefaultRouter()
|
||||
orga_router.register(r'events', event.EventViewSet)
|
||||
orga_router.register(r'subevents', event.SubEventViewSet)
|
||||
orga_router.register(r'webhooks', webhooks.WebHookViewSet)
|
||||
orga_router.register(r'seatingplans', organizer.SeatingPlanViewSet)
|
||||
orga_router.register(r'giftcards', organizer.GiftCardViewSet)
|
||||
orga_router.register(r'teams', organizer.TeamViewSet)
|
||||
|
||||
team_router = routers.DefaultRouter()
|
||||
team_router.register(r'members', organizer.TeamMemberViewSet)
|
||||
team_router.register(r'invites', organizer.TeamInviteViewSet)
|
||||
team_router.register(r'tokens', organizer.TeamAPITokenViewSet)
|
||||
|
||||
event_router = routers.DefaultRouter()
|
||||
event_router.register(r'subevents', event.SubEventViewSet)
|
||||
@@ -59,7 +67,10 @@ for app in apps.get_app_configs():
|
||||
urlpatterns = [
|
||||
url(r'^', include(router.urls)),
|
||||
url(r'^organizers/(?P<organizer>[^/]+)/', include(orga_router.urls)),
|
||||
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/settings/$', event.EventSettingsView.as_view(),
|
||||
name="event.settings"),
|
||||
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/', include(event_router.urls)),
|
||||
url(r'^organizers/(?P<organizer>[^/]+)/teams/(?P<team>[^/]+)/', include(team_router.urls)),
|
||||
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/items/(?P<item>[^/]+)/', include(item_router.urls)),
|
||||
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/questions/(?P<question>[^/]+)/',
|
||||
include(question_router.urls)),
|
||||
@@ -74,4 +85,5 @@ urlpatterns = [
|
||||
url(r"^device/roll$", device.RollKeyView.as_view(), name="device.roll"),
|
||||
url(r"^device/revoke$", device.RevokeKeyView.as_view(), name="device.revoke"),
|
||||
url(r"^me$", user.MeView.as_view(), name="user.me"),
|
||||
url(r"^version$", version.VersionView.as_view(), name="version"),
|
||||
]
|
||||
|
||||
@@ -24,7 +24,7 @@ class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
|
||||
return CartPosition.objects.filter(
|
||||
event=self.request.event,
|
||||
cart_id__endswith="@api"
|
||||
)
|
||||
).select_related('seat').prefetch_related('answers')
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.shortcuts import get_object_or_404
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import DateTimeField
|
||||
@@ -24,11 +25,11 @@ from pretix.base.services.checkin import (
|
||||
)
|
||||
from pretix.helpers.database import FixedOrderBy
|
||||
|
||||
|
||||
class CheckinListFilter(FilterSet):
|
||||
class Meta:
|
||||
model = CheckinList
|
||||
fields = ['subevent']
|
||||
with scopes_disabled():
|
||||
class CheckinListFilter(FilterSet):
|
||||
class Meta:
|
||||
model = CheckinList
|
||||
fields = ['subevent']
|
||||
|
||||
|
||||
class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
@@ -43,7 +44,6 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
qs = self.request.event.checkin_lists.prefetch_related(
|
||||
'limit_products',
|
||||
)
|
||||
qs = CheckinList.annotate_with_numbers(qs, self.request.event)
|
||||
return qs
|
||||
|
||||
def perform_create(self, serializer):
|
||||
@@ -92,6 +92,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
if not clist.all_products:
|
||||
pqs = pqs.filter(item__in=clist.limit_products.values_list('id', flat=True))
|
||||
cqs = cqs.filter(position__item__in=clist.limit_products.values_list('id', flat=True))
|
||||
|
||||
ev = clist.subevent or clist.event
|
||||
response = {
|
||||
@@ -146,15 +147,16 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
return Response(response)
|
||||
|
||||
|
||||
class CheckinOrderPositionFilter(OrderPositionFilter):
|
||||
with scopes_disabled():
|
||||
class CheckinOrderPositionFilter(OrderPositionFilter):
|
||||
|
||||
def has_checkin_qs(self, queryset, name, value):
|
||||
return queryset.filter(last_checked_in__isnull=not value)
|
||||
def has_checkin_qs(self, queryset, name, value):
|
||||
return queryset.filter(last_checked_in__isnull=not value)
|
||||
|
||||
|
||||
class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = CheckinListOrderPositionSerializer
|
||||
queryset = OrderPosition.objects.none()
|
||||
queryset = OrderPosition.all.none()
|
||||
filter_backends = (DjangoFilterBackend, RichOrderingFilter)
|
||||
ordering = ('attendee_name_cached', 'positionid')
|
||||
ordering_fields = (
|
||||
@@ -229,7 +231,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
)
|
||||
))
|
||||
).select_related(
|
||||
'item', 'variation', 'item__category', 'addon_to', 'order', 'order__invoice_address'
|
||||
'item', 'variation', 'item__category', 'addon_to', 'order', 'order__invoice_address', 'seat'
|
||||
)
|
||||
else:
|
||||
qs = qs.prefetch_related(
|
||||
@@ -239,7 +241,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
),
|
||||
'answers', 'answers__options', 'answers__question',
|
||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
|
||||
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order')
|
||||
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat')
|
||||
|
||||
if not self.checkinlist.all_products:
|
||||
qs = qs.filter(item__in=self.checkinlist.limit_products.values_list('id', flat=True))
|
||||
@@ -278,6 +280,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
nonce=nonce,
|
||||
datetime=dt,
|
||||
questions_supported=self.request.data.get('questions_supported', True),
|
||||
canceled_supported=self.request.data.get('canceled_supported', False),
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
)
|
||||
|
||||
@@ -3,13 +3,15 @@ from django.db import transaction
|
||||
from django.db.models import ProtectedError, Q
|
||||
from django.utils.timezone import now
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from rest_framework import filters, viewsets
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import filters, views, viewsets
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.response import Response
|
||||
|
||||
from pretix.api.auth.permission import EventCRUDPermission
|
||||
from pretix.api.serializers.event import (
|
||||
CloneEventSerializer, EventSerializer, SubEventSerializer,
|
||||
TaxRuleSerializer,
|
||||
CloneEventSerializer, EventSerializer, EventSettingsSerializer,
|
||||
SubEventSerializer, TaxRuleSerializer,
|
||||
)
|
||||
from pretix.api.views import ConditionalListView
|
||||
from pretix.base.models import (
|
||||
@@ -17,52 +19,53 @@ from pretix.base.models import (
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
from pretix.presale.views.organizer import filter_qs_by_attr
|
||||
|
||||
with scopes_disabled():
|
||||
class EventFilter(FilterSet):
|
||||
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
|
||||
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
|
||||
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
|
||||
|
||||
class EventFilter(FilterSet):
|
||||
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
|
||||
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
|
||||
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
|
||||
class Meta:
|
||||
model = Event
|
||||
fields = ['is_public', 'live', 'has_subevents']
|
||||
|
||||
class Meta:
|
||||
model = Event
|
||||
fields = ['is_public', 'live', 'has_subevents']
|
||||
|
||||
def ends_after_qs(self, queryset, name, value):
|
||||
expr = (
|
||||
Q(has_subevents=False) &
|
||||
Q(
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__gte=value))
|
||||
| Q(Q(date_to__isnull=False) & Q(date_to__gte=value))
|
||||
def ends_after_qs(self, queryset, name, value):
|
||||
expr = (
|
||||
Q(has_subevents=False) &
|
||||
Q(
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__gte=value))
|
||||
| Q(Q(date_to__isnull=False) & Q(date_to__gte=value))
|
||||
)
|
||||
)
|
||||
)
|
||||
return queryset.filter(expr)
|
||||
|
||||
def is_past_qs(self, queryset, name, value):
|
||||
expr = (
|
||||
Q(has_subevents=False) &
|
||||
Q(
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__lt=now()))
|
||||
| Q(Q(date_to__isnull=False) & Q(date_to__lt=now()))
|
||||
)
|
||||
)
|
||||
if value:
|
||||
return queryset.filter(expr)
|
||||
else:
|
||||
return queryset.exclude(expr)
|
||||
|
||||
def is_future_qs(self, queryset, name, value):
|
||||
expr = (
|
||||
Q(has_subevents=False) &
|
||||
Q(
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
|
||||
| Q(Q(date_to__isnull=False) & Q(date_to__gte=now()))
|
||||
def is_past_qs(self, queryset, name, value):
|
||||
expr = (
|
||||
Q(has_subevents=False) &
|
||||
Q(
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__lt=now()))
|
||||
| Q(Q(date_to__isnull=False) & Q(date_to__lt=now()))
|
||||
)
|
||||
)
|
||||
)
|
||||
if value:
|
||||
return queryset.filter(expr)
|
||||
else:
|
||||
return queryset.exclude(expr)
|
||||
if value:
|
||||
return queryset.filter(expr)
|
||||
else:
|
||||
return queryset.exclude(expr)
|
||||
|
||||
def is_future_qs(self, queryset, name, value):
|
||||
expr = (
|
||||
Q(has_subevents=False) &
|
||||
Q(
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
|
||||
| Q(Q(date_to__isnull=False) & Q(date_to__gte=now()))
|
||||
)
|
||||
)
|
||||
if value:
|
||||
return queryset.filter(expr)
|
||||
else:
|
||||
return queryset.exclude(expr)
|
||||
|
||||
|
||||
class EventViewSet(viewsets.ModelViewSet):
|
||||
@@ -84,8 +87,10 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
organizer=self.request.organizer
|
||||
)
|
||||
|
||||
qs = filter_qs_by_attr(qs, self.request)
|
||||
|
||||
return qs.prefetch_related(
|
||||
'meta_values', 'meta_values__property'
|
||||
'meta_values', 'meta_values__property', 'seat_category_mappings'
|
||||
)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
@@ -129,6 +134,7 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(organizer=self.request.organizer)
|
||||
serializer.instance.set_defaults()
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.added',
|
||||
user=self.request.user,
|
||||
@@ -182,41 +188,42 @@ class CloneEventViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
|
||||
|
||||
class SubEventFilter(FilterSet):
|
||||
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
|
||||
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
|
||||
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
|
||||
with scopes_disabled():
|
||||
class SubEventFilter(FilterSet):
|
||||
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
|
||||
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
|
||||
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
|
||||
|
||||
class Meta:
|
||||
model = SubEvent
|
||||
fields = ['active', 'event__live']
|
||||
class Meta:
|
||||
model = SubEvent
|
||||
fields = ['active', 'event__live']
|
||||
|
||||
def ends_after_qs(self, queryset, name, value):
|
||||
expr = Q(
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__gte=value))
|
||||
| Q(Q(date_to__isnull=False) & Q(date_to__gte=value))
|
||||
)
|
||||
return queryset.filter(expr)
|
||||
|
||||
def is_past_qs(self, queryset, name, value):
|
||||
expr = Q(
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__lt=now()))
|
||||
| Q(Q(date_to__isnull=False) & Q(date_to__lt=now()))
|
||||
)
|
||||
if value:
|
||||
def ends_after_qs(self, queryset, name, value):
|
||||
expr = Q(
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__gte=value))
|
||||
| Q(Q(date_to__isnull=False) & Q(date_to__gte=value))
|
||||
)
|
||||
return queryset.filter(expr)
|
||||
else:
|
||||
return queryset.exclude(expr)
|
||||
|
||||
def is_future_qs(self, queryset, name, value):
|
||||
expr = Q(
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
|
||||
| Q(Q(date_to__isnull=False) & Q(date_to__gte=now()))
|
||||
)
|
||||
if value:
|
||||
return queryset.filter(expr)
|
||||
else:
|
||||
return queryset.exclude(expr)
|
||||
def is_past_qs(self, queryset, name, value):
|
||||
expr = Q(
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__lt=now()))
|
||||
| Q(Q(date_to__isnull=False) & Q(date_to__lt=now()))
|
||||
)
|
||||
if value:
|
||||
return queryset.filter(expr)
|
||||
else:
|
||||
return queryset.exclude(expr)
|
||||
|
||||
def is_future_qs(self, queryset, name, value):
|
||||
expr = Q(
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
|
||||
| Q(Q(date_to__isnull=False) & Q(date_to__gte=now()))
|
||||
)
|
||||
if value:
|
||||
return queryset.filter(expr)
|
||||
else:
|
||||
return queryset.exclude(expr)
|
||||
|
||||
|
||||
class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
@@ -239,13 +246,22 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
event__organizer=self.request.organizer,
|
||||
event__in=self.request.user.get_events_with_any_permission()
|
||||
)
|
||||
|
||||
qs = filter_qs_by_attr(qs, self.request)
|
||||
|
||||
return qs.prefetch_related(
|
||||
'subeventitem_set', 'subeventitemvariation_set'
|
||||
'subeventitem_set', 'subeventitemvariation_set', 'seat_category_mappings'
|
||||
)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
original_data = self.get_serializer(instance=serializer.instance).data
|
||||
super().perform_update(serializer)
|
||||
|
||||
if serializer.data == original_data:
|
||||
# Performance optimization: If nothing was changed, we do not need to save or log anything.
|
||||
# This costs us a few cycles on save, but avoids thousands of lines in our log.
|
||||
return
|
||||
|
||||
serializer.instance.log_action(
|
||||
'pretix.subevent.changed',
|
||||
user=self.request.user,
|
||||
@@ -318,3 +334,33 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
auth=self.request.auth,
|
||||
)
|
||||
super().perform_destroy(instance)
|
||||
|
||||
|
||||
class EventSettingsView(views.APIView):
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
s = EventSettingsSerializer(instance=request.event.settings, event=request.event)
|
||||
if 'explain' in request.GET:
|
||||
return Response({
|
||||
fname: {
|
||||
'value': s.data[fname],
|
||||
'label': getattr(field, '_label', fname),
|
||||
'help_text': getattr(field, '_help_text', None)
|
||||
} for fname, field in s.fields.items()
|
||||
})
|
||||
return Response(s.data)
|
||||
|
||||
def patch(self, request, *wargs, **kwargs):
|
||||
s = EventSettingsSerializer(instance=request.event.settings, data=request.data, partial=True,
|
||||
event=request.event)
|
||||
s.is_valid(raise_exception=True)
|
||||
with transaction.atomic():
|
||||
s.save()
|
||||
self.request.event.log_action(
|
||||
'pretix.event.settings', user=self.request.user, auth=self.request.auth, data={
|
||||
k: v for k, v in s.validated_data.items()
|
||||
}
|
||||
)
|
||||
s = EventSettingsSerializer(instance=request.event.settings, event=request.event)
|
||||
return Response(s.data)
|
||||
|
||||
@@ -3,6 +3,7 @@ from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.functional import cached_property
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
@@ -21,19 +22,19 @@ from pretix.base.models import (
|
||||
)
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
|
||||
with scopes_disabled():
|
||||
class ItemFilter(FilterSet):
|
||||
tax_rate = django_filters.CharFilter(method='tax_rate_qs')
|
||||
|
||||
class ItemFilter(FilterSet):
|
||||
tax_rate = django_filters.CharFilter(method='tax_rate_qs')
|
||||
def tax_rate_qs(self, queryset, name, value):
|
||||
if value in ("0", "None", "0.00"):
|
||||
return queryset.filter(Q(tax_rule__isnull=True) | Q(tax_rule__rate=0))
|
||||
else:
|
||||
return queryset.filter(tax_rule__rate=value)
|
||||
|
||||
def tax_rate_qs(self, queryset, name, value):
|
||||
if value in ("0", "None", "0.00"):
|
||||
return queryset.filter(Q(tax_rule__isnull=True) | Q(tax_rule__rate=0))
|
||||
else:
|
||||
return queryset.filter(tax_rule__rate=value)
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
|
||||
class Meta:
|
||||
model = Item
|
||||
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
|
||||
|
||||
|
||||
class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
@@ -61,11 +62,17 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['event'] = self.request.event
|
||||
ctx['has_variations'] = self.request.data.get('has_variations')
|
||||
return ctx
|
||||
|
||||
def perform_update(self, serializer):
|
||||
original_data = self.get_serializer(instance=serializer.instance).data
|
||||
|
||||
serializer.save(event=self.request.event)
|
||||
|
||||
if serializer.data == original_data:
|
||||
# Performance optimization: If nothing was changed, we do not need to save or log anything.
|
||||
# This costs us a few cycles on save, but avoids thousands of lines in our log.
|
||||
return
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.item.changed',
|
||||
user=self.request.user,
|
||||
@@ -312,10 +319,11 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
super().perform_destroy(instance)
|
||||
|
||||
|
||||
class QuestionFilter(FilterSet):
|
||||
class Meta:
|
||||
model = Question
|
||||
fields = ['ask_during_checkin', 'required', 'identifier']
|
||||
with scopes_disabled():
|
||||
class QuestionFilter(FilterSet):
|
||||
class Meta:
|
||||
model = Question
|
||||
fields = ['ask_during_checkin', 'required', 'identifier']
|
||||
|
||||
|
||||
class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
@@ -411,10 +419,11 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
||||
super().perform_destroy(instance)
|
||||
|
||||
|
||||
class QuotaFilter(FilterSet):
|
||||
class Meta:
|
||||
model = Quota
|
||||
fields = ['subevent']
|
||||
with scopes_disabled():
|
||||
class QuotaFilter(FilterSet):
|
||||
class Meta:
|
||||
model = Quota
|
||||
fields = ['subevent']
|
||||
|
||||
|
||||
class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
@@ -452,9 +461,30 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
return ctx
|
||||
|
||||
def perform_update(self, serializer):
|
||||
original_data = self.get_serializer(instance=serializer.instance).data
|
||||
|
||||
current_subevent = serializer.instance.subevent
|
||||
serializer.save(event=self.request.event)
|
||||
request_subevent = serializer.instance.subevent
|
||||
|
||||
if serializer.data == original_data:
|
||||
# Performance optimization: If nothing was changed, we do not need to save or log anything.
|
||||
# This costs us a few cycles on save, but avoids thousands of lines in our log.
|
||||
return
|
||||
|
||||
if original_data['closed'] is True and serializer.instance.closed is False:
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.quota.opened',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
)
|
||||
elif original_data['closed'] is False and serializer.instance.closed is True:
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.quota.closed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
)
|
||||
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.quota.changed',
|
||||
user=self.request.user,
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from oauth2_provider.exceptions import OAuthToolkitError
|
||||
from oauth2_provider.forms import AllowForm
|
||||
from oauth2_provider.views import (
|
||||
|
||||
@@ -6,11 +6,12 @@ import pytz
|
||||
from django.db import transaction
|
||||
from django.db.models import F, Prefetch, Q
|
||||
from django.db.models.functions import Coalesce, Concat
|
||||
from django.http import FileResponse
|
||||
from django.http import FileResponse, HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import mixins, serializers, status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import (
|
||||
@@ -22,15 +23,16 @@ from rest_framework.response import Response
|
||||
|
||||
from pretix.api.models import OAuthAccessToken
|
||||
from pretix.api.serializers.order import (
|
||||
InvoiceSerializer, OrderCreateSerializer, OrderPaymentSerializer,
|
||||
OrderPositionSerializer, OrderRefundCreateSerializer,
|
||||
OrderRefundSerializer, OrderSerializer, PriceCalcSerializer,
|
||||
InvoiceSerializer, OrderCreateSerializer, OrderPaymentCreateSerializer,
|
||||
OrderPaymentSerializer, OrderPositionSerializer,
|
||||
OrderRefundCreateSerializer, OrderRefundSerializer, OrderSerializer,
|
||||
PriceCalcSerializer,
|
||||
)
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedCombinedTicket, CachedTicket, Device, Event, Invoice, InvoiceAddress,
|
||||
Order, OrderPayment, OrderPosition, OrderRefund, Quota, TeamAPIToken,
|
||||
generate_position_secret, generate_secret,
|
||||
Order, OrderFee, OrderPayment, OrderPosition, OrderRefund, Quota,
|
||||
TeamAPIToken, generate_position_secret, generate_secret,
|
||||
)
|
||||
from pretix.base.payment import PaymentException
|
||||
from pretix.base.services import tickets
|
||||
@@ -40,27 +42,28 @@ from pretix.base.services.invoices import (
|
||||
)
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.services.orders import (
|
||||
OrderChangeManager, OrderError, approve_order, cancel_order, deny_order,
|
||||
extend_order, mark_order_expired, mark_order_refunded,
|
||||
OrderChangeManager, OrderError, _order_placed_email,
|
||||
_order_placed_email_attendee, approve_order, cancel_order, deny_order,
|
||||
extend_order, mark_order_expired, mark_order_refunded, reactivate_order,
|
||||
)
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.base.services.tickets import generate
|
||||
from pretix.base.signals import (
|
||||
order_modified, order_placed, register_ticket_outputs,
|
||||
order_modified, order_paid, order_placed, register_ticket_outputs,
|
||||
)
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
|
||||
with scopes_disabled():
|
||||
class OrderFilter(FilterSet):
|
||||
email = django_filters.CharFilter(field_name='email', lookup_expr='iexact')
|
||||
code = django_filters.CharFilter(field_name='code', lookup_expr='iexact')
|
||||
status = django_filters.CharFilter(field_name='status', lookup_expr='iexact')
|
||||
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
|
||||
created_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte')
|
||||
|
||||
class OrderFilter(FilterSet):
|
||||
email = django_filters.CharFilter(field_name='email', lookup_expr='iexact')
|
||||
code = django_filters.CharFilter(field_name='code', lookup_expr='iexact')
|
||||
status = django_filters.CharFilter(field_name='status', lookup_expr='iexact')
|
||||
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
|
||||
created_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte')
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ['code', 'status', 'email', 'locale', 'testmode', 'require_approval']
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ['code', 'status', 'email', 'locale', 'testmode', 'require_approval']
|
||||
|
||||
|
||||
class OrderViewSet(viewsets.ModelViewSet):
|
||||
@@ -80,20 +83,29 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
return ctx
|
||||
|
||||
def get_queryset(self):
|
||||
if self.request.query_params.get('include_canceled_fees', 'false') == 'true':
|
||||
fqs = OrderFee.all
|
||||
else:
|
||||
fqs = OrderFee.objects
|
||||
qs = self.request.event.orders.prefetch_related(
|
||||
'fees', 'payments', 'refunds', 'refunds__payment'
|
||||
Prefetch('fees', queryset=fqs.all()),
|
||||
'payments', 'refunds', 'refunds__payment'
|
||||
).select_related(
|
||||
'invoice_address'
|
||||
)
|
||||
|
||||
if self.request.query_params.get('include_canceled_positions', 'false') == 'true':
|
||||
opq = OrderPosition.all
|
||||
else:
|
||||
opq = OrderPosition.objects
|
||||
if self.request.query_params.get('pdf_data', 'false') == 'true':
|
||||
qs = qs.prefetch_related(
|
||||
Prefetch(
|
||||
'positions',
|
||||
OrderPosition.objects.all().prefetch_related(
|
||||
opq.all().prefetch_related(
|
||||
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||
'item__category', 'addon_to',
|
||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
|
||||
'item__category', 'addon_to', 'seat',
|
||||
Prefetch('addons', opq.select_related('item', 'variation', 'seat'))
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -101,8 +113,8 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
qs = qs.prefetch_related(
|
||||
Prefetch(
|
||||
'positions',
|
||||
OrderPosition.objects.all().prefetch_related(
|
||||
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||
opq.all().prefetch_related(
|
||||
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question', 'seat',
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -146,12 +158,16 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
generate.apply_async(args=('order', order.pk, provider.identifier))
|
||||
raise RetryException()
|
||||
else:
|
||||
resp = FileResponse(ct.file.file, content_type=ct.type)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
|
||||
self.request.event.slug.upper(), order.code,
|
||||
provider.identifier, ct.extension
|
||||
)
|
||||
return resp
|
||||
if ct.type == 'text/uri-list':
|
||||
resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list')
|
||||
return resp
|
||||
else:
|
||||
resp = FileResponse(ct.file.file, content_type=ct.type)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
|
||||
self.request.event.slug.upper(), order.code,
|
||||
provider.identifier, ct.extension
|
||||
)
|
||||
return resp
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
def mark_paid(self, request, **kwargs):
|
||||
@@ -167,9 +183,26 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
amount=ps
|
||||
)
|
||||
except OrderPayment.DoesNotExist:
|
||||
order.payments.filter(state__in=(OrderPayment.PAYMENT_STATE_PENDING,
|
||||
OrderPayment.PAYMENT_STATE_CREATED)) \
|
||||
.update(state=OrderPayment.PAYMENT_STATE_CANCELED)
|
||||
for p in order.payments.filter(state__in=(OrderPayment.PAYMENT_STATE_PENDING,
|
||||
OrderPayment.PAYMENT_STATE_CREATED)):
|
||||
try:
|
||||
with transaction.atomic():
|
||||
p.payment_provider.cancel_payment(p)
|
||||
order.log_action('pretix.event.order.payment.canceled', {
|
||||
'local_id': p.local_id,
|
||||
'provider': p.provider,
|
||||
}, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
|
||||
except PaymentException as e:
|
||||
order.log_action(
|
||||
'pretix.event.order.payment.canceled.failed',
|
||||
{
|
||||
'local_id': p.local_id,
|
||||
'provider': p.provider,
|
||||
'error': str(e)
|
||||
},
|
||||
user=self.request.user if self.request.user.is_authenticated else None,
|
||||
auth=self.request.auth
|
||||
)
|
||||
p = order.payments.create(
|
||||
state=OrderPayment.PAYMENT_STATE_CREATED,
|
||||
provider='manual',
|
||||
@@ -228,6 +261,29 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
def reactivate(self, request, **kwargs):
|
||||
|
||||
order = self.get_object()
|
||||
if order.status != Order.STATUS_CANCELED:
|
||||
return Response(
|
||||
{'detail': 'The order is not allowed to be reactivated.'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
try:
|
||||
reactivate_order(
|
||||
order,
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None,
|
||||
)
|
||||
except OrderError as e:
|
||||
return Response(
|
||||
{'detail': str(e)},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
def approve(self, request, **kwargs):
|
||||
send_mail = request.data.get('send_email', True)
|
||||
@@ -430,22 +486,64 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
with transaction.atomic():
|
||||
self.perform_create(serializer)
|
||||
send_mail = serializer._send_mail
|
||||
order = serializer.instance
|
||||
serializer = OrderSerializer(order, context=serializer.context)
|
||||
if not order.pk:
|
||||
# Simulation
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
order.log_action(
|
||||
'pretix.event.order.placed',
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
auth=request.auth,
|
||||
)
|
||||
order_placed.send(self.request.event, order=order)
|
||||
|
||||
gen_invoice = invoice_qualified(order) and (
|
||||
(order.event.settings.get('invoice_generate') == 'True') or
|
||||
(order.event.settings.get('invoice_generate') == 'paid' and order.status == Order.STATUS_PAID)
|
||||
) and not order.invoices.last()
|
||||
if gen_invoice:
|
||||
generate_invoice(order, trigger_pdf=True)
|
||||
with language(order.locale):
|
||||
order_placed.send(self.request.event, order=order)
|
||||
if order.status == Order.STATUS_PAID:
|
||||
order_paid.send(self.request.event, order=order)
|
||||
|
||||
gen_invoice = invoice_qualified(order) and (
|
||||
(order.event.settings.get('invoice_generate') == 'True') or
|
||||
(order.event.settings.get('invoice_generate') == 'paid' and order.status == Order.STATUS_PAID)
|
||||
) and not order.invoices.last()
|
||||
invoice = None
|
||||
if gen_invoice:
|
||||
invoice = generate_invoice(order, trigger_pdf=True)
|
||||
|
||||
if send_mail:
|
||||
payment = order.payments.last()
|
||||
free_flow = (
|
||||
payment and order.total == Decimal('0.00') and order.status == Order.STATUS_PAID and
|
||||
not order.require_approval and payment.provider == "free"
|
||||
)
|
||||
if free_flow:
|
||||
email_template = request.event.settings.mail_text_order_free
|
||||
log_entry = 'pretix.event.order.email.order_free'
|
||||
email_attendees = request.event.settings.mail_send_order_free_attendee
|
||||
email_attendees_template = request.event.settings.mail_text_order_free_attendee
|
||||
else:
|
||||
email_template = request.event.settings.mail_text_order_placed
|
||||
log_entry = 'pretix.event.order.email.order_placed'
|
||||
email_attendees = request.event.settings.mail_send_order_placed_attendee
|
||||
email_attendees_template = request.event.settings.mail_text_order_placed_attendee
|
||||
|
||||
_order_placed_email(
|
||||
request.event, order, payment.payment_provider if payment else None, email_template,
|
||||
log_entry, invoice, payment
|
||||
)
|
||||
if email_attendees:
|
||||
for p in order.positions.all():
|
||||
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
|
||||
_order_placed_email_attendee(request.event, order, p, email_attendees_template, log_entry)
|
||||
|
||||
if not free_flow and order.status == Order.STATUS_PAID and payment:
|
||||
payment._send_paid_mail(invoice, None, '')
|
||||
if self.request.event.settings.mail_send_order_paid_attendee:
|
||||
for p in order.positions.all():
|
||||
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
|
||||
payment._send_paid_mail_attendee(p, None)
|
||||
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
@@ -531,48 +629,49 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
self.get_object().gracefully_delete(user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
|
||||
|
||||
|
||||
class OrderPositionFilter(FilterSet):
|
||||
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
|
||||
has_checkin = django_filters.rest_framework.BooleanFilter(method='has_checkin_qs')
|
||||
attendee_name = django_filters.CharFilter(method='attendee_name_qs')
|
||||
search = django_filters.CharFilter(method='search_qs')
|
||||
with scopes_disabled():
|
||||
class OrderPositionFilter(FilterSet):
|
||||
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
|
||||
has_checkin = django_filters.rest_framework.BooleanFilter(method='has_checkin_qs')
|
||||
attendee_name = django_filters.CharFilter(method='attendee_name_qs')
|
||||
search = django_filters.CharFilter(method='search_qs')
|
||||
|
||||
def search_qs(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
Q(secret__istartswith=value)
|
||||
| Q(attendee_name_cached__icontains=value)
|
||||
| Q(addon_to__attendee_name_cached__icontains=value)
|
||||
| Q(attendee_email__icontains=value)
|
||||
| Q(addon_to__attendee_email__icontains=value)
|
||||
| Q(order__code__istartswith=value)
|
||||
| Q(order__invoice_address__name_cached__icontains=value)
|
||||
| Q(order__email__icontains=value)
|
||||
)
|
||||
def search_qs(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
Q(secret__istartswith=value)
|
||||
| Q(attendee_name_cached__icontains=value)
|
||||
| Q(addon_to__attendee_name_cached__icontains=value)
|
||||
| Q(attendee_email__icontains=value)
|
||||
| Q(addon_to__attendee_email__icontains=value)
|
||||
| Q(order__code__istartswith=value)
|
||||
| Q(order__invoice_address__name_cached__icontains=value)
|
||||
| Q(order__email__icontains=value)
|
||||
)
|
||||
|
||||
def has_checkin_qs(self, queryset, name, value):
|
||||
return queryset.filter(checkins__isnull=not value)
|
||||
def has_checkin_qs(self, queryset, name, value):
|
||||
return queryset.filter(checkins__isnull=not value)
|
||||
|
||||
def attendee_name_qs(self, queryset, name, value):
|
||||
return queryset.filter(Q(attendee_name_cached__iexact=value) | Q(addon_to__attendee_name_cached__iexact=value))
|
||||
def attendee_name_qs(self, queryset, name, value):
|
||||
return queryset.filter(Q(attendee_name_cached__iexact=value) | Q(addon_to__attendee_name_cached__iexact=value))
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = {
|
||||
'item': ['exact', 'in'],
|
||||
'variation': ['exact', 'in'],
|
||||
'secret': ['exact'],
|
||||
'order__status': ['exact', 'in'],
|
||||
'addon_to': ['exact', 'in'],
|
||||
'subevent': ['exact', 'in'],
|
||||
'pseudonymization_id': ['exact'],
|
||||
'voucher__code': ['exact'],
|
||||
'voucher': ['exact'],
|
||||
}
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = {
|
||||
'item': ['exact', 'in'],
|
||||
'variation': ['exact', 'in'],
|
||||
'secret': ['exact'],
|
||||
'order__status': ['exact', 'in'],
|
||||
'addon_to': ['exact', 'in'],
|
||||
'subevent': ['exact', 'in'],
|
||||
'pseudonymization_id': ['exact'],
|
||||
'voucher__code': ['exact'],
|
||||
'voucher': ['exact'],
|
||||
}
|
||||
|
||||
|
||||
class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = OrderPositionSerializer
|
||||
queryset = OrderPosition.objects.none()
|
||||
queryset = OrderPosition.all.none()
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||
ordering = ('order__datetime', 'positionid')
|
||||
ordering_fields = ('order__code', 'order__datetime', 'positionid', 'attendee_name', 'order__status',)
|
||||
@@ -591,11 +690,16 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
qs = OrderPosition.objects.filter(order__event=self.request.event)
|
||||
if self.request.query_params.get('include_canceled_positions', 'false') == 'true':
|
||||
qs = OrderPosition.all
|
||||
else:
|
||||
qs = OrderPosition.objects
|
||||
|
||||
qs = qs.filter(order__event=self.request.event)
|
||||
if self.request.query_params.get('pdf_data', 'false') == 'true':
|
||||
qs = qs.prefetch_related(
|
||||
'checkins', 'answers', 'answers__options', 'answers__question',
|
||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
|
||||
Prefetch('addons', qs.select_related('item', 'variation')),
|
||||
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
|
||||
Prefetch(
|
||||
'event',
|
||||
@@ -603,19 +707,19 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
|
||||
),
|
||||
Prefetch(
|
||||
'positions',
|
||||
OrderPosition.objects.prefetch_related(
|
||||
qs.prefetch_related(
|
||||
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||
)
|
||||
)
|
||||
))
|
||||
).select_related(
|
||||
'item', 'variation', 'item__category', 'addon_to'
|
||||
'item', 'variation', 'item__category', 'addon_to', 'seat'
|
||||
)
|
||||
else:
|
||||
qs = qs.prefetch_related(
|
||||
'checkins', 'answers', 'answers__options', 'answers__question'
|
||||
'checkins', 'answers', 'answers__options', 'answers__question',
|
||||
).select_related(
|
||||
'item', 'order', 'order__event', 'order__event__organizer'
|
||||
'item', 'order', 'order__event', 'order__event__organizer', 'seat'
|
||||
)
|
||||
return qs
|
||||
|
||||
@@ -721,12 +825,16 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
|
||||
generate.apply_async(args=('orderposition', pos.pk, provider.identifier))
|
||||
raise RetryException()
|
||||
else:
|
||||
resp = FileResponse(ct.file.file, content_type=ct.type)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
|
||||
self.request.event.slug.upper(), pos.order.code, pos.positionid,
|
||||
provider.identifier, ct.extension
|
||||
)
|
||||
return resp
|
||||
if ct.type == 'text/uri-list':
|
||||
resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list')
|
||||
return resp
|
||||
else:
|
||||
resp = FileResponse(ct.file.file, content_type=ct.type)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
|
||||
self.request.event.slug.upper(), pos.order.code, pos.positionid,
|
||||
provider.identifier, ct.extension
|
||||
)
|
||||
return resp
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
try:
|
||||
@@ -744,17 +852,62 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
|
||||
raise ValidationError(str(e))
|
||||
|
||||
|
||||
class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = OrderPaymentSerializer
|
||||
queryset = OrderPayment.objects.none()
|
||||
permission = 'can_view_orders'
|
||||
write_permission = 'can_change_orders'
|
||||
lookup_field = 'local_id'
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['order'] = get_object_or_404(Order, code=self.kwargs['order'], event=self.request.event)
|
||||
return ctx
|
||||
|
||||
def get_queryset(self):
|
||||
order = get_object_or_404(Order, code=self.kwargs['order'], event=self.request.event)
|
||||
return order.payments.all()
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = OrderPaymentCreateSerializer(data=request.data, context=self.get_serializer_context())
|
||||
serializer.is_valid(raise_exception=True)
|
||||
with transaction.atomic():
|
||||
mark_confirmed = False
|
||||
if serializer.validated_data['state'] == OrderPayment.PAYMENT_STATE_CONFIRMED:
|
||||
serializer.validated_data['state'] = OrderPayment.PAYMENT_STATE_PENDING
|
||||
mark_confirmed = True
|
||||
self.perform_create(serializer)
|
||||
r = serializer.instance
|
||||
if mark_confirmed:
|
||||
try:
|
||||
r.confirm(
|
||||
user=self.request.user if self.request.user.is_authenticated else None,
|
||||
auth=self.request.auth,
|
||||
count_waitinglist=False,
|
||||
force=request.data.get('force', False)
|
||||
)
|
||||
except Quota.QuotaExceededException:
|
||||
pass
|
||||
except SendMailException:
|
||||
pass
|
||||
|
||||
serializer = OrderPaymentSerializer(r, context=serializer.context)
|
||||
|
||||
r.order.log_action(
|
||||
'pretix.event.order.payment.started', {
|
||||
'local_id': r.local_id,
|
||||
'provider': r.provider,
|
||||
},
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
auth=request.auth
|
||||
)
|
||||
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save()
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
def confirm(self, request, **kwargs):
|
||||
payment = self.get_object()
|
||||
@@ -848,13 +1001,16 @@ class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
if payment.state not in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED):
|
||||
return Response({'detail': 'Invalid state of payment'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
with transaction.atomic():
|
||||
payment.state = OrderPayment.PAYMENT_STATE_CANCELED
|
||||
payment.save()
|
||||
payment.order.log_action('pretix.event.order.payment.canceled', {
|
||||
'local_id': payment.local_id,
|
||||
'provider': payment.provider,
|
||||
}, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
|
||||
try:
|
||||
with transaction.atomic():
|
||||
payment.payment_provider.cancel_payment(payment)
|
||||
payment.order.log_action('pretix.event.order.payment.canceled', {
|
||||
'local_id': payment.local_id,
|
||||
'provider': payment.provider,
|
||||
}, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
|
||||
except PaymentException as e:
|
||||
return Response({'detail': 'External error: {}'.format(str(e))},
|
||||
status=status.HTTP_400_BAD_REQUEST)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
|
||||
@@ -931,6 +1087,7 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
mark_refunded = request.data.pop('mark_refunded', False)
|
||||
else:
|
||||
mark_refunded = request.data.pop('mark_canceled', False)
|
||||
mark_pending = request.data.pop('mark_pending', False)
|
||||
serializer = OrderRefundCreateSerializer(data=request.data, context=self.get_serializer_context())
|
||||
serializer.is_valid(raise_exception=True)
|
||||
with transaction.atomic():
|
||||
@@ -947,11 +1104,23 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
auth=request.auth
|
||||
)
|
||||
if mark_refunded:
|
||||
mark_order_refunded(
|
||||
r.order,
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
auth=(request.auth if request.auth else None),
|
||||
)
|
||||
try:
|
||||
mark_order_refunded(
|
||||
r.order,
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
auth=(request.auth if request.auth else None),
|
||||
)
|
||||
except OrderError as e:
|
||||
raise ValidationError(str(e))
|
||||
elif mark_pending:
|
||||
if r.order.status == Order.STATUS_PAID and r.order.pending_sum > 0:
|
||||
r.order.status = Order.STATUS_PENDING
|
||||
r.order.set_expires(
|
||||
now(),
|
||||
r.order.event.subevents.filter(
|
||||
id__in=r.order.positions.values_list('subevent_id', flat=True))
|
||||
)
|
||||
r.order.save(update_fields=['status', 'expires'])
|
||||
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
@@ -960,22 +1129,23 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
serializer.save()
|
||||
|
||||
|
||||
class InvoiceFilter(FilterSet):
|
||||
refers = django_filters.CharFilter(method='refers_qs')
|
||||
number = django_filters.CharFilter(method='nr_qs')
|
||||
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
|
||||
with scopes_disabled():
|
||||
class InvoiceFilter(FilterSet):
|
||||
refers = django_filters.CharFilter(method='refers_qs')
|
||||
number = django_filters.CharFilter(method='nr_qs')
|
||||
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
|
||||
|
||||
def refers_qs(self, queryset, name, value):
|
||||
return queryset.annotate(
|
||||
refers_nr=Concat('refers__prefix', 'refers__invoice_no')
|
||||
).filter(refers_nr__iexact=value)
|
||||
def refers_qs(self, queryset, name, value):
|
||||
return queryset.annotate(
|
||||
refers_nr=Concat('refers__prefix', 'refers__invoice_no')
|
||||
).filter(refers_nr__iexact=value)
|
||||
|
||||
def nr_qs(self, queryset, name, value):
|
||||
return queryset.filter(nr__iexact=value)
|
||||
def nr_qs(self, queryset, name, value):
|
||||
return queryset.filter(nr__iexact=value)
|
||||
|
||||
class Meta:
|
||||
model = Invoice
|
||||
fields = ['order', 'number', 'is_cancellation', 'refers', 'locale']
|
||||
class Meta:
|
||||
model = Invoice
|
||||
fields = ['order', 'number', 'is_cancellation', 'refers', 'locale']
|
||||
|
||||
|
||||
class RetryException(APIException):
|
||||
|
||||
@@ -1,8 +1,27 @@
|
||||
from rest_framework import filters, viewsets
|
||||
from decimal import Decimal
|
||||
|
||||
import django_filters
|
||||
from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.functional import cached_property
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import filters, serializers, status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import MethodNotAllowed, PermissionDenied
|
||||
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin
|
||||
from rest_framework.response import Response
|
||||
|
||||
from pretix.api.models import OAuthAccessToken
|
||||
from pretix.api.serializers.organizer import OrganizerSerializer
|
||||
from pretix.base.models import Organizer
|
||||
from pretix.api.serializers.organizer import (
|
||||
GiftCardSerializer, OrganizerSerializer, SeatingPlanSerializer,
|
||||
TeamAPITokenSerializer, TeamInviteSerializer, TeamMemberSerializer,
|
||||
TeamSerializer,
|
||||
)
|
||||
from pretix.base.models import (
|
||||
GiftCard, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
||||
)
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
|
||||
|
||||
class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
@@ -30,3 +49,306 @@ class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
return Organizer.objects.filter(pk=self.request.auth.organizer_id)
|
||||
else:
|
||||
return Organizer.objects.filter(pk=self.request.auth.team.organizer_id)
|
||||
|
||||
|
||||
class SeatingPlanViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = SeatingPlanSerializer
|
||||
queryset = SeatingPlan.objects.none()
|
||||
permission = 'can_change_organizer_settings'
|
||||
write_permission = 'can_change_organizer_settings'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.organizer.seating_plans.order_by('name')
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['organizer'] = self.request.organizer
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
inst = serializer.save(organizer=self.request.organizer)
|
||||
self.request.organizer.log_action(
|
||||
'pretix.seatingplan.added',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=merge_dicts(self.request.data, {'id': inst.pk})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
if serializer.instance.events.exists() or serializer.instance.subevents.exists():
|
||||
raise PermissionDenied('This plan can not be changed while it is in use for an event.')
|
||||
inst = serializer.save(organizer=self.request.organizer)
|
||||
self.request.organizer.log_action(
|
||||
'pretix.seatingplan.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
|
||||
)
|
||||
return inst
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
if instance.events.exists() or instance.subevents.exists():
|
||||
raise PermissionDenied('This plan can not be deleted while it is in use for an event.')
|
||||
instance.log_action(
|
||||
'pretix.seatingplan.deleted',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={'id': instance.pk}
|
||||
)
|
||||
instance.delete()
|
||||
|
||||
|
||||
with scopes_disabled():
|
||||
class GiftCardFilter(FilterSet):
|
||||
secret = django_filters.CharFilter(field_name='secret', lookup_expr='iexact')
|
||||
|
||||
class Meta:
|
||||
model = GiftCard
|
||||
fields = ['secret', 'testmode']
|
||||
|
||||
|
||||
class GiftCardViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = GiftCardSerializer
|
||||
queryset = GiftCard.objects.none()
|
||||
permission = 'can_manage_gift_cards'
|
||||
write_permission = 'can_manage_gift_cards'
|
||||
filter_backends = (DjangoFilterBackend,)
|
||||
filterset_class = GiftCardFilter
|
||||
|
||||
def get_queryset(self):
|
||||
if self.request.GET.get('include_accepted') == 'true':
|
||||
qs = self.request.organizer.accepted_gift_cards
|
||||
else:
|
||||
qs = self.request.organizer.issued_gift_cards.all()
|
||||
return qs
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['organizer'] = self.request.organizer
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
value = serializer.validated_data.pop('value')
|
||||
inst = serializer.save(issuer=self.request.organizer)
|
||||
inst.transactions.create(value=value)
|
||||
inst.log_action(
|
||||
'pretix.giftcards.transaction.manual',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=merge_dicts(self.request.data, {'id': inst.pk})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
if 'include_accepted' in self.request.GET:
|
||||
raise PermissionDenied("Accepted gift cards cannot be updated, use transact instead.")
|
||||
GiftCard.objects.select_for_update().get(pk=self.get_object().pk)
|
||||
old_value = serializer.instance.value
|
||||
value = serializer.validated_data.pop('value')
|
||||
inst = serializer.save(secret=serializer.instance.secret, currency=serializer.instance.currency,
|
||||
testmode=serializer.instance.testmode)
|
||||
diff = value - old_value
|
||||
inst.transactions.create(value=diff)
|
||||
inst.log_action(
|
||||
'pretix.giftcards.transaction.manual',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={'value': diff}
|
||||
)
|
||||
return inst
|
||||
|
||||
@action(detail=True, methods=["POST"])
|
||||
@transaction.atomic()
|
||||
def transact(self, request, **kwargs):
|
||||
gc = GiftCard.objects.select_for_update().get(pk=self.get_object().pk)
|
||||
value = serializers.DecimalField(max_digits=10, decimal_places=2).to_internal_value(
|
||||
request.data.get('value')
|
||||
)
|
||||
text = serializers.CharField(allow_blank=True, allow_null=True).to_internal_value(
|
||||
request.data.get('text', '')
|
||||
)
|
||||
if gc.value + value < Decimal('0.00'):
|
||||
return Response({
|
||||
'value': ['The gift card does not have sufficient credit for this operation.']
|
||||
}, status=status.HTTP_409_CONFLICT)
|
||||
gc.transactions.create(value=value, text=text)
|
||||
gc.log_action(
|
||||
'pretix.giftcards.transaction.manual',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={'value': value, 'text': text}
|
||||
)
|
||||
return Response(GiftCardSerializer(gc).data, status=status.HTTP_200_OK)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
raise MethodNotAllowed("Gift cards cannot be deleted.")
|
||||
|
||||
|
||||
class TeamViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = TeamSerializer
|
||||
queryset = Team.objects.none()
|
||||
permission = 'can_change_teams'
|
||||
write_permission = 'can_change_teams'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.organizer.teams.order_by('pk')
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['organizer'] = self.request.organizer
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
inst = serializer.save(organizer=self.request.organizer)
|
||||
inst.log_action(
|
||||
'pretix.team.created',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=merge_dicts(self.request.data, {'id': inst.pk})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
inst = serializer.save()
|
||||
inst.log_action(
|
||||
'pretix.team.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data
|
||||
)
|
||||
return inst
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
instance.log_action('pretix.team.deleted', user=self.request.user, auth=self.request.auth)
|
||||
instance.delete()
|
||||
|
||||
|
||||
class TeamMemberViewSet(DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = TeamMemberSerializer
|
||||
queryset = User.objects.none()
|
||||
permission = 'can_change_teams'
|
||||
write_permission = 'can_change_teams'
|
||||
|
||||
@cached_property
|
||||
def team(self):
|
||||
return get_object_or_404(self.request.organizer.teams, pk=self.kwargs.get('team'))
|
||||
|
||||
def get_queryset(self):
|
||||
return self.team.members.all()
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['organizer'] = self.request.organizer
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
self.team.members.remove(instance)
|
||||
self.team.log_action(
|
||||
'pretix.team.member.removed', user=self.request.user, auth=self.request.auth, data={
|
||||
'email': instance.email,
|
||||
'user': instance.pk
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class TeamInviteViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = TeamInviteSerializer
|
||||
queryset = TeamInvite.objects.none()
|
||||
permission = 'can_change_teams'
|
||||
write_permission = 'can_change_teams'
|
||||
|
||||
@cached_property
|
||||
def team(self):
|
||||
return get_object_or_404(self.request.organizer.teams, pk=self.kwargs.get('team'))
|
||||
|
||||
def get_queryset(self):
|
||||
return self.team.invites.order_by('email')
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['organizer'] = self.request.organizer
|
||||
ctx['team'] = self.team
|
||||
ctx['log_kwargs'] = {
|
||||
'user': self.request.user,
|
||||
'auth': self.request.auth,
|
||||
}
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
self.team.log_action(
|
||||
'pretix.team.invite.deleted', user=self.request.user, auth=self.request.auth, data={
|
||||
'email': instance.email,
|
||||
}
|
||||
)
|
||||
instance.delete()
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(team=self.team)
|
||||
|
||||
|
||||
class TeamAPITokenViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = TeamAPITokenSerializer
|
||||
queryset = TeamAPIToken.objects.none()
|
||||
permission = 'can_change_teams'
|
||||
write_permission = 'can_change_teams'
|
||||
|
||||
@cached_property
|
||||
def team(self):
|
||||
return get_object_or_404(self.request.organizer.teams, pk=self.kwargs.get('team'))
|
||||
|
||||
def get_queryset(self):
|
||||
return self.team.tokens.order_by('name')
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['organizer'] = self.request.organizer
|
||||
ctx['team'] = self.team
|
||||
ctx['log_kwargs'] = {
|
||||
'user': self.request.user,
|
||||
'auth': self.request.auth,
|
||||
}
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
instance.active = False
|
||||
instance.save()
|
||||
self.team.log_action(
|
||||
'pretix.team.token.deleted', user=self.request.user, auth=self.request.auth, data={
|
||||
'name': instance.name,
|
||||
}
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
instance = serializer.save(team=self.team)
|
||||
self.team.log_action(
|
||||
'pretix.team.token.created', auth=self.request.auth, user=self.request.user, data={
|
||||
'name': instance.name,
|
||||
'id': instance.pk
|
||||
}
|
||||
)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_create(serializer)
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
d = serializer.data
|
||||
d['token'] = serializer.instance.token
|
||||
return Response(d, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
self.perform_destroy(instance)
|
||||
serializer = self.get_serializer_class()(instance)
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK, headers=headers)
|
||||
|
||||
56
src/pretix/api/views/version.py
Normal file
56
src/pretix/api/views/version.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from oauth2_provider.contrib.rest_framework import OAuth2Authentication
|
||||
from packaging import version
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from pretix import __version__
|
||||
from pretix.api.auth.device import DeviceTokenAuthentication
|
||||
from pretix.api.auth.token import TeamTokenAuthentication
|
||||
|
||||
|
||||
def numeric_version(v):
|
||||
# Converts a pretix version to a large int
|
||||
# e.g. 30060001000
|
||||
# |--------------------- Major version
|
||||
# |-|------------------ Minor version
|
||||
# |-|--------------- Patch version
|
||||
# ||------------- Stage (10 dev, 20 alpha, 30 beta, 40 rc, 50 release, 60 post)
|
||||
# ||----------- Stage version (number of dev/alpha/beta/rc/post release)
|
||||
v = version.parse(v)
|
||||
phases = {
|
||||
'dev': 10,
|
||||
'a': 20,
|
||||
'b': 30,
|
||||
'rc': 40,
|
||||
'release': 50,
|
||||
'post': 60
|
||||
}
|
||||
vnum = 0
|
||||
|
||||
if v.is_postrelease:
|
||||
vnum += v.post
|
||||
vnum += phases['post'] * 100
|
||||
elif v.dev is not None:
|
||||
vnum += v.dev
|
||||
vnum += phases['dev'] * 100
|
||||
elif v.is_prerelease and v.pre:
|
||||
vnum += v.pre[0]
|
||||
vnum += phases[v.pre[1]] * 100
|
||||
else:
|
||||
vnum += phases['release'] * 100
|
||||
for i, part in enumerate(reversed(v.release)):
|
||||
vnum += part * (1000 ** i) * 10000
|
||||
return vnum
|
||||
|
||||
|
||||
class VersionView(APIView):
|
||||
authentication_classes = (
|
||||
SessionAuthentication, OAuth2Authentication, DeviceTokenAuthentication, TeamTokenAuthentication
|
||||
)
|
||||
|
||||
def get(self, request, format=None):
|
||||
return Response({
|
||||
'pretix': __version__,
|
||||
'pretix_numeric': numeric_version(__version__),
|
||||
})
|
||||
@@ -6,6 +6,7 @@ from django.utils.timezone import now
|
||||
from django_filters.rest_framework import (
|
||||
BooleanFilter, DjangoFilterBackend, FilterSet,
|
||||
)
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
@@ -15,22 +16,22 @@ from rest_framework.response import Response
|
||||
from pretix.api.serializers.voucher import VoucherSerializer
|
||||
from pretix.base.models import Voucher
|
||||
|
||||
with scopes_disabled():
|
||||
class VoucherFilter(FilterSet):
|
||||
active = BooleanFilter(method='filter_active')
|
||||
|
||||
class VoucherFilter(FilterSet):
|
||||
active = BooleanFilter(method='filter_active')
|
||||
class Meta:
|
||||
model = Voucher
|
||||
fields = ['code', 'max_usages', 'redeemed', 'block_quota', 'allow_ignore_quota',
|
||||
'price_mode', 'value', 'item', 'variation', 'quota', 'tag', 'subevent']
|
||||
|
||||
class Meta:
|
||||
model = Voucher
|
||||
fields = ['code', 'max_usages', 'redeemed', 'block_quota', 'allow_ignore_quota',
|
||||
'price_mode', 'value', 'item', 'variation', 'quota', 'tag', 'subevent']
|
||||
|
||||
def filter_active(self, queryset, name, value):
|
||||
if value:
|
||||
return queryset.filter(Q(redeemed__lt=F('max_usages')) &
|
||||
(Q(valid_until__isnull=True) | Q(valid_until__gt=now())))
|
||||
else:
|
||||
return queryset.filter(Q(redeemed__gte=F('max_usages')) |
|
||||
(Q(valid_until__isnull=False) & Q(valid_until__lte=now())))
|
||||
def filter_active(self, queryset, name, value):
|
||||
if value:
|
||||
return queryset.filter(Q(redeemed__lt=F('max_usages')) &
|
||||
(Q(valid_until__isnull=True) | Q(valid_until__gt=now())))
|
||||
else:
|
||||
return queryset.filter(Q(redeemed__gte=F('max_usages')) |
|
||||
(Q(valid_until__isnull=False) & Q(valid_until__lte=now())))
|
||||
|
||||
|
||||
class VoucherViewSet(viewsets.ModelViewSet):
|
||||
@@ -44,7 +45,7 @@ class VoucherViewSet(viewsets.ModelViewSet):
|
||||
write_permission = 'can_change_vouchers'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.vouchers.all()
|
||||
return self.request.event.vouchers.select_related('seat').all()
|
||||
|
||||
def _predict_quota_check(self, data, instance):
|
||||
# This method predicts if Voucher.clean_quota_needs_checking
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import django_filters
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
||||
@@ -10,16 +11,16 @@ from pretix.api.serializers.waitinglist import WaitingListSerializer
|
||||
from pretix.base.models import WaitingListEntry
|
||||
from pretix.base.models.waitinglist import WaitingListException
|
||||
|
||||
with scopes_disabled():
|
||||
class WaitingListFilter(FilterSet):
|
||||
has_voucher = django_filters.rest_framework.BooleanFilter(method='has_voucher_qs')
|
||||
|
||||
class WaitingListFilter(FilterSet):
|
||||
has_voucher = django_filters.rest_framework.BooleanFilter(method='has_voucher_qs')
|
||||
def has_voucher_qs(self, queryset, name, value):
|
||||
return queryset.filter(voucher__isnull=not value)
|
||||
|
||||
def has_voucher_qs(self, queryset, name, value):
|
||||
return queryset.filter(voucher__isnull=not value)
|
||||
|
||||
class Meta:
|
||||
model = WaitingListEntry
|
||||
fields = ['item', 'variation', 'email', 'locale', 'has_voucher', 'subevent']
|
||||
class Meta:
|
||||
model = WaitingListEntry
|
||||
fields = ['item', 'variation', 'email', 'locale', 'has_voucher', 'subevent']
|
||||
|
||||
|
||||
class WaitingListViewSet(viewsets.ModelViewSet):
|
||||
|
||||
@@ -7,7 +7,8 @@ import requests
|
||||
from celery.exceptions import MaxRetriesExceededError
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import scope, scopes_disabled
|
||||
from requests import RequestException
|
||||
|
||||
from pretix.api.models import WebHook, WebHookCall, WebHookEventListener
|
||||
@@ -124,6 +125,10 @@ def register_default_webhook_events(sender, **kwargs):
|
||||
'pretix.event.order.canceled',
|
||||
_('Order canceled'),
|
||||
),
|
||||
ParametrizedOrderWebhookEvent(
|
||||
'pretix.event.order.reactivated',
|
||||
_('Order reactivated'),
|
||||
),
|
||||
ParametrizedOrderWebhookEvent(
|
||||
'pretix.event.order.expired',
|
||||
_('Order expired'),
|
||||
@@ -203,51 +208,52 @@ def notify_webhooks(logentry_id: int):
|
||||
@app.task(base=ProfiledTask, bind=True, max_retries=9)
|
||||
def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int):
|
||||
# 9 retries with 2**(2*x) timing is roughly 72 hours
|
||||
logentry = LogEntry.all.get(id=logentry_id)
|
||||
webhook = WebHook.objects.get(id=webhook_id)
|
||||
with scopes_disabled():
|
||||
webhook = WebHook.objects.get(id=webhook_id)
|
||||
with scope(organizer=webhook.organizer):
|
||||
logentry = LogEntry.all.get(id=logentry_id)
|
||||
types = get_all_webhook_events()
|
||||
event_type = types.get(action_type)
|
||||
if not event_type or not webhook.enabled:
|
||||
return # Ignore, e.g. plugin not installed
|
||||
|
||||
types = get_all_webhook_events()
|
||||
event_type = types.get(action_type)
|
||||
if not event_type or not webhook.enabled:
|
||||
return # Ignore, e.g. plugin not installed
|
||||
payload = event_type.build_payload(logentry)
|
||||
t = time.time()
|
||||
|
||||
payload = event_type.build_payload(logentry)
|
||||
t = time.time()
|
||||
|
||||
try:
|
||||
try:
|
||||
resp = requests.post(
|
||||
webhook.target_url,
|
||||
json=payload,
|
||||
allow_redirects=False
|
||||
)
|
||||
WebHookCall.objects.create(
|
||||
webhook=webhook,
|
||||
action_type=logentry.action_type,
|
||||
target_url=webhook.target_url,
|
||||
is_retry=self.request.retries > 0,
|
||||
execution_time=time.time() - t,
|
||||
return_code=resp.status_code,
|
||||
payload=json.dumps(payload),
|
||||
response_body=resp.text[:1024 * 1024],
|
||||
success=200 <= resp.status_code <= 299
|
||||
)
|
||||
if resp.status_code == 410:
|
||||
webhook.enabled = False
|
||||
webhook.save()
|
||||
elif resp.status_code > 299:
|
||||
try:
|
||||
resp = requests.post(
|
||||
webhook.target_url,
|
||||
json=payload,
|
||||
allow_redirects=False
|
||||
)
|
||||
WebHookCall.objects.create(
|
||||
webhook=webhook,
|
||||
action_type=logentry.action_type,
|
||||
target_url=webhook.target_url,
|
||||
is_retry=self.request.retries > 0,
|
||||
execution_time=time.time() - t,
|
||||
return_code=resp.status_code,
|
||||
payload=json.dumps(payload),
|
||||
response_body=resp.text[:1024 * 1024],
|
||||
success=200 <= resp.status_code <= 299
|
||||
)
|
||||
if resp.status_code == 410:
|
||||
webhook.enabled = False
|
||||
webhook.save()
|
||||
elif resp.status_code > 299:
|
||||
raise self.retry(countdown=2 ** (self.request.retries * 2))
|
||||
except RequestException as e:
|
||||
WebHookCall.objects.create(
|
||||
webhook=webhook,
|
||||
action_type=logentry.action_type,
|
||||
target_url=webhook.target_url,
|
||||
is_retry=self.request.retries > 0,
|
||||
execution_time=time.time() - t,
|
||||
return_code=0,
|
||||
payload=json.dumps(payload),
|
||||
response_body=str(e)[:1024 * 1024]
|
||||
)
|
||||
raise self.retry(countdown=2 ** (self.request.retries * 2))
|
||||
except RequestException as e:
|
||||
WebHookCall.objects.create(
|
||||
webhook=webhook,
|
||||
action_type=logentry.action_type,
|
||||
target_url=webhook.target_url,
|
||||
is_retry=self.request.retries > 0,
|
||||
execution_time=time.time() - t,
|
||||
return_code=0,
|
||||
payload=json.dumps(payload),
|
||||
response_body=str(e)[:1024 * 1024]
|
||||
)
|
||||
raise self.retry(countdown=2 ** (self.request.retries * 2))
|
||||
except MaxRetriesExceededError:
|
||||
pass
|
||||
except MaxRetriesExceededError:
|
||||
pass
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class PretixBaseConfig(AppConfig):
|
||||
@@ -13,7 +12,8 @@ class PretixBaseConfig(AppConfig):
|
||||
from . import invoice # NOQA
|
||||
from . import notifications # NOQA
|
||||
from . import email # NOQA
|
||||
from .services import auth, export, mail, tickets, cart, orders, invoices, cleanup, update_check, quotas, notifications # NOQA
|
||||
from .services import auth, checkin, export, mail, tickets, cart, orderimport, orders, invoices, cleanup, update_check, quotas, notifications, vouchers # NOQA
|
||||
from django.conf import settings
|
||||
|
||||
try:
|
||||
from .celery_app import app as celery_app # NOQA
|
||||
|
||||
119
src/pretix/base/auth.py
Normal file
119
src/pretix/base/auth.py
Normal file
@@ -0,0 +1,119 @@
|
||||
from collections import OrderedDict
|
||||
from importlib import import_module
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import authenticate
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
def get_auth_backends():
|
||||
backends = {}
|
||||
for b in settings.PRETIX_AUTH_BACKENDS:
|
||||
mod, name = b.rsplit('.', 1)
|
||||
b = getattr(import_module(mod), name)()
|
||||
backends[b.identifier] = b
|
||||
return backends
|
||||
|
||||
|
||||
class BaseAuthBackend:
|
||||
"""
|
||||
This base class defines the interface that needs to be implemented by every class that supplies
|
||||
an authentication method to pretix. Please note that pretix authentication backends are different
|
||||
from plain Django authentication backends! Be sure to read the documentation chapter on authentication
|
||||
backends before you implement one.
|
||||
"""
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
"""
|
||||
A short and unique identifier for this authentication backend.
|
||||
This should only contain lowercase letters and in most cases will
|
||||
be the same as your package name.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
"""
|
||||
A human-readable name of this authentication backend.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def visible(self):
|
||||
"""
|
||||
Whether or not this backend can be selected by users actively. Set this to ``False``
|
||||
if you only implement ``request_authenticate``.
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def login_form_fields(self) -> dict:
|
||||
"""
|
||||
This property may return form fields that the user needs to fill in to log in.
|
||||
"""
|
||||
return {}
|
||||
|
||||
def form_authenticate(self, request, form_data):
|
||||
"""
|
||||
This method will be called after the user filled in the login form. ``request`` will contain
|
||||
the current request and ``form_data`` the input for the form fields defined in ``login_form_fields``.
|
||||
You are expected to either return a ``User`` object (if login was successful) or ``None``.
|
||||
"""
|
||||
return
|
||||
|
||||
def request_authenticate(self, request):
|
||||
"""
|
||||
This method will be called when the user opens the login form. If the user already has a valid session
|
||||
according to your login mechanism, for example a cookie set by a different system or HTTP header set by a
|
||||
reverse proxy, you can directly return a ``User`` object that will be logged in.
|
||||
|
||||
``request`` will contain the current request.
|
||||
You are expected to either return a ``User`` object (if login was successful) or ``None``.
|
||||
"""
|
||||
return
|
||||
|
||||
def authentication_url(self, request):
|
||||
"""
|
||||
This method will be called to populate the URL for your authentication method's tab on the login page.
|
||||
For example, if your method works through OAuth, you could return the URL of the OAuth authorization URL the
|
||||
user needs to visit.
|
||||
|
||||
If you return ``None`` (the default), the link will point to a page that shows the form defined by
|
||||
``login_form_fields``.
|
||||
"""
|
||||
return
|
||||
|
||||
def get_next_url(self, request):
|
||||
"""
|
||||
This method will be called after a successful login to determine the next URL. Pretix in general uses the
|
||||
``'next'`` query parameter. However, external authentication methods could use custom attributes with hardcoded
|
||||
names for security purposes. For example, OAuth uses ``'state'`` for keeping track of application state.
|
||||
"""
|
||||
if "next" in request.GET:
|
||||
return request.GET.get("next")
|
||||
return None
|
||||
|
||||
|
||||
class NativeAuthBackend(BaseAuthBackend):
|
||||
identifier = 'native'
|
||||
verbose_name = _('pretix User')
|
||||
|
||||
@property
|
||||
def login_form_fields(self) -> dict:
|
||||
"""
|
||||
This property may return form fields that the user needs to fill in
|
||||
to log in.
|
||||
"""
|
||||
d = OrderedDict([
|
||||
('email', forms.EmailField(label=_("E-mail"), max_length=254,
|
||||
widget=forms.EmailInput(attrs={'autofocus': 'autofocus'}))),
|
||||
('password', forms.CharField(label=_("Password"), widget=forms.PasswordInput)),
|
||||
])
|
||||
return d
|
||||
|
||||
def form_authenticate(self, request, form_data):
|
||||
u = authenticate(request=request, email=form_data['email'].lower(), password=form_data['password'])
|
||||
if u and u.auth_backend == self.identifier:
|
||||
return u
|
||||
80
src/pretix/base/banlist.py
Normal file
80
src/pretix/base/banlist.py
Normal file
@@ -0,0 +1,80 @@
|
||||
import re
|
||||
|
||||
# banlist based on http://www.bannedwordlist.com/lists/swearWords.txt
|
||||
banlist = [
|
||||
"anal",
|
||||
"anus",
|
||||
"arse",
|
||||
"ass",
|
||||
"balls",
|
||||
"bastard",
|
||||
"bitch",
|
||||
"biatch",
|
||||
"bloody",
|
||||
"blowjob",
|
||||
"bollock",
|
||||
"bollok",
|
||||
"boner",
|
||||
"boob",
|
||||
"bugger",
|
||||
"bum",
|
||||
"butt",
|
||||
"clitoris",
|
||||
"cock",
|
||||
"coon",
|
||||
"crap",
|
||||
"cunt",
|
||||
"damn",
|
||||
"dick",
|
||||
"dildo",
|
||||
"dyke",
|
||||
"fag",
|
||||
"feck",
|
||||
"fellate",
|
||||
"fellatio",
|
||||
"felching",
|
||||
"fuck",
|
||||
"fudgepacker",
|
||||
"flange",
|
||||
"goddamn",
|
||||
"hell",
|
||||
"homo",
|
||||
"jerk",
|
||||
"jizz",
|
||||
"knobend",
|
||||
"labia",
|
||||
"lmao",
|
||||
"lmfao",
|
||||
"muff",
|
||||
"nigger",
|
||||
"nigga",
|
||||
"omg",
|
||||
"penis",
|
||||
"piss",
|
||||
"poop",
|
||||
"prick",
|
||||
"pube",
|
||||
"pussy",
|
||||
"queer",
|
||||
"scrotum",
|
||||
"sex",
|
||||
"shit",
|
||||
"sh1t",
|
||||
"slut",
|
||||
"smegma",
|
||||
"spunk",
|
||||
"tit",
|
||||
"tosser",
|
||||
"turd",
|
||||
"twat",
|
||||
"vagina",
|
||||
"wank",
|
||||
"whore",
|
||||
"wtf"
|
||||
]
|
||||
|
||||
blacklist_regex = re.compile('(' + '|'.join(banlist) + ')')
|
||||
|
||||
|
||||
def banned(string):
|
||||
return bool(blacklist_regex.search(string.lower()))
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.signals import register_sales_channels
|
||||
|
||||
@@ -35,6 +35,32 @@ class SalesChannel:
|
||||
"""
|
||||
return "circle"
|
||||
|
||||
@property
|
||||
def testmode_supported(self) -> bool:
|
||||
"""
|
||||
Indication, if a saleschannels supports test mode orders
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def payment_restrictions_supported(self) -> bool:
|
||||
"""
|
||||
If this property is ``True``, organizers can restrict the usage of payment providers to this sales channel.
|
||||
|
||||
Example: pretixPOS provides its own sales channel, ignores the configured payment providers completely and
|
||||
handles payments locally. Therefor, this property should be set to ``False`` for the pretixPOS sales channel as
|
||||
the event organizer cannot restrict the usage of any payment provider through the backend.
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def unlimited_items_per_order(self) -> bool:
|
||||
"""
|
||||
If this property is ``True``, purchases made using this sales channel are not limited to the maximum amount of
|
||||
items defined in the event settings.
|
||||
"""
|
||||
return False
|
||||
|
||||
|
||||
def get_all_sales_channels():
|
||||
global _ALL_CHANNELS
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user