forked from CGM_Public/pretix_original
Compare commits
905 Commits
v3.1.0
...
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 |
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
|
||||
45
.travis.yml
45
.travis.yml
@@ -1,45 +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=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``.
|
||||
|
||||
@@ -87,6 +90,11 @@ Example::
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -194,6 +194,7 @@ Cart position endpoints
|
||||
* ``subevent`` (optional)
|
||||
* ``expires`` (optional)
|
||||
* ``includes_tax`` (optional)
|
||||
* ``sales_channel`` (optional)
|
||||
* ``answers``
|
||||
|
||||
* ``question``
|
||||
|
||||
@@ -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
|
||||
@@ -215,7 +228,10 @@ Endpoints
|
||||
"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
|
||||
@@ -400,7 +427,8 @@ Order position endpoints
|
||||
"checkins": [
|
||||
{
|
||||
"list": 1,
|
||||
"datetime": "2017-12-25T12:45:23Z"
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": true
|
||||
}
|
||||
],
|
||||
"answers": [
|
||||
@@ -510,7 +538,8 @@ Order position endpoints
|
||||
"checkins": [
|
||||
{
|
||||
"list": 1,
|
||||
"datetime": "2017-12-25T12:45:23Z"
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": true
|
||||
}
|
||||
],
|
||||
"answers": [
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
.. spelling::
|
||||
|
||||
geo
|
||||
lat
|
||||
lon
|
||||
|
||||
Events
|
||||
======
|
||||
|
||||
@@ -25,6 +31,8 @@ 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 object Values set for organizer-specific meta data parameters.
|
||||
@@ -34,6 +42,8 @@ seating_plan integer If reserved sea
|
||||
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.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
@@ -62,9 +72,25 @@ seat_category_mapping object An object mappi
|
||||
|
||||
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.
|
||||
@@ -105,10 +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"
|
||||
@@ -129,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
|
||||
@@ -169,10 +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"
|
||||
@@ -220,8 +258,12 @@ Endpoints
|
||||
"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"
|
||||
@@ -249,10 +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"
|
||||
@@ -268,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.
|
||||
|
||||
@@ -300,10 +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"
|
||||
@@ -331,10 +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"
|
||||
@@ -394,10 +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",
|
||||
@@ -440,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,7 +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",
|
||||
|
||||
@@ -77,6 +77,7 @@ generate_tickets boolean If ``false``, t
|
||||
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.
|
||||
@@ -113,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
|
||||
@@ -153,6 +155,10 @@ bundles list of objects Definition of b
|
||||
|
||||
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
|
||||
-----
|
||||
|
||||
@@ -206,6 +212,8 @@ Endpoints
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
"issue_giftcard": false,
|
||||
"meta_data": {},
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
"available_from": null,
|
||||
@@ -300,6 +308,8 @@ Endpoints
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
"issue_giftcard": false,
|
||||
"meta_data": {},
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
"available_from": null,
|
||||
@@ -375,6 +385,8 @@ Endpoints
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
"issue_giftcard": false,
|
||||
"meta_data": {},
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
"available_from": null,
|
||||
@@ -437,6 +449,8 @@ Endpoints
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
"issue_giftcard": false,
|
||||
"meta_data": {},
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
"available_from": null,
|
||||
@@ -531,6 +545,8 @@ Endpoints
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
"issue_giftcard": false,
|
||||
"meta_data": {},
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
"available_from": null,
|
||||
|
||||
@@ -61,9 +61,10 @@ invoice_address object Invoice address
|
||||
└ 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
|
||||
@@ -72,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
|
||||
@@ -131,20 +133,28 @@ 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:
|
||||
.. 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:
|
||||
|
||||
@@ -159,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
|
||||
@@ -175,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
|
||||
@@ -214,6 +234,23 @@ pdf_data object Data object req
|
||||
|
||||
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
|
||||
@@ -280,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.
|
||||
@@ -345,6 +386,7 @@ List of all orders
|
||||
"id": 23442,
|
||||
"order": "ABC12",
|
||||
"positionid": 1,
|
||||
"canceled": false,
|
||||
"item": 1345,
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
@@ -353,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",
|
||||
@@ -365,7 +413,8 @@ List of all orders
|
||||
"checkins": [
|
||||
{
|
||||
"list": 44,
|
||||
"datetime": "2017-12-25T12:45:23Z"
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": false
|
||||
}
|
||||
],
|
||||
"answers": [
|
||||
@@ -416,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
|
||||
@@ -433,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.
|
||||
@@ -492,6 +548,7 @@ Fetching individual orders
|
||||
"id": 23442,
|
||||
"order": "ABC12",
|
||||
"positionid": 1,
|
||||
"canceled": false,
|
||||
"item": 1345,
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
@@ -500,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,
|
||||
@@ -512,7 +575,8 @@ Fetching individual orders
|
||||
"checkins": [
|
||||
{
|
||||
"list": 44,
|
||||
"datetime": "2017-12-25T12:45:23Z"
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": false
|
||||
}
|
||||
],
|
||||
"answers": [
|
||||
@@ -556,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.
|
||||
@@ -762,6 +829,8 @@ Creating orders
|
||||
|
||||
* does not support file upload questions
|
||||
|
||||
* does not support redeeming gift cards
|
||||
|
||||
You can supply the following fields of the resource:
|
||||
|
||||
* ``code`` (optional)
|
||||
@@ -774,12 +843,13 @@ Creating orders
|
||||
* ``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 or seats that become free by this operation will be credited to your order
|
||||
creation.
|
||||
* ``email``
|
||||
* ``email`` (optional)
|
||||
* ``locale``
|
||||
* ``sales_channel``
|
||||
* ``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.
|
||||
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
|
||||
@@ -808,15 +878,21 @@ Creating orders
|
||||
|
||||
* ``positionid`` (optional, see below)
|
||||
* ``item``
|
||||
* ``variation``
|
||||
* ``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``
|
||||
* ``attendee_name`` **or** ``attendee_name_parts`` (optional)
|
||||
* ``voucher`` (optional, the ``code`` attribute of a valid voucher)
|
||||
* ``attendee_email``
|
||||
* ``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``
|
||||
@@ -830,6 +906,14 @@ 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
|
||||
@@ -840,6 +924,13 @@ Creating orders
|
||||
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
|
||||
@@ -999,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.
|
||||
@@ -1286,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/
|
||||
|
||||
@@ -1317,6 +1450,7 @@ List of all order positions
|
||||
"id": 23442,
|
||||
"order": "ABC12",
|
||||
"positionid": 1,
|
||||
"canceled": false,
|
||||
"item": 1345,
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
@@ -1337,7 +1471,8 @@ List of all order positions
|
||||
"checkins": [
|
||||
{
|
||||
"list": 44,
|
||||
"datetime": "2017-12-25T12:45:23Z"
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": false
|
||||
}
|
||||
],
|
||||
"answers": [
|
||||
@@ -1385,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
|
||||
@@ -1418,6 +1555,7 @@ Fetching individual positions
|
||||
"id": 23442,
|
||||
"order": "ABC12",
|
||||
"positionid": 1,
|
||||
"canceled": false,
|
||||
"item": 1345,
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
@@ -1438,7 +1576,8 @@ Fetching individual positions
|
||||
"checkins": [
|
||||
{
|
||||
"list": 44,
|
||||
"datetime": "2017-12-25T12:45:23Z"
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": false
|
||||
}
|
||||
],
|
||||
"answers": [
|
||||
@@ -1461,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
|
||||
------------------------------
|
||||
|
||||
@@ -1475,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
|
||||
@@ -1550,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.
|
||||
@@ -1758,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
|
||||
----------------------
|
||||
@@ -1876,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.
|
||||
@@ -86,6 +88,10 @@ dependency_value string An old version
|
||||
|
||||
The attribute ``print_on_invoice`` has been added.
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
The attribute ``help_text`` has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -122,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],
|
||||
@@ -192,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],
|
||||
@@ -247,6 +255,7 @@ Endpoints
|
||||
|
||||
{
|
||||
"question": {"en": "T-Shirt size"},
|
||||
"help_text": {"en": "Choose your preferred t-shirt-size"},
|
||||
"type": "C",
|
||||
"required": false,
|
||||
"items": [1, 2],
|
||||
@@ -281,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],
|
||||
@@ -355,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],
|
||||
|
||||
@@ -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
|
||||
@@ -62,9 +70,17 @@ seat_category_mapping object An object mappi
|
||||
|
||||
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.
|
||||
@@ -104,6 +120,8 @@ Endpoints
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": 2,
|
||||
@@ -123,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.
|
||||
@@ -152,6 +175,8 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"item_price_overrides": [
|
||||
@@ -184,6 +209,8 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"item_price_overrides": [
|
||||
@@ -237,6 +264,8 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"item_price_overrides": [
|
||||
@@ -303,6 +332,8 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"item_price_overrides": [
|
||||
@@ -389,6 +420,8 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"item_price_overrides": [
|
||||
|
||||
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,6 +38,7 @@ 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``).
|
||||
@@ -53,6 +54,10 @@ show_hidden_items boolean Only if set to
|
||||
|
||||
The attribute ``show_hidden_items`` has been added.
|
||||
|
||||
.. versionchanged:: 3.4
|
||||
|
||||
The attribute ``seat`` has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -96,7 +101,8 @@ Endpoints
|
||||
"quota": null,
|
||||
"tag": "testvoucher",
|
||||
"comment": "",
|
||||
"subevent": null
|
||||
"seat": null,
|
||||
"subevent": null,
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -162,6 +168,7 @@ Endpoints
|
||||
"quota": null,
|
||||
"tag": "testvoucher",
|
||||
"comment": "",
|
||||
"seat": null,
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
@@ -225,6 +232,7 @@ Endpoints
|
||||
"quota": null,
|
||||
"tag": "testvoucher",
|
||||
"comment": "",
|
||||
"seat": null,
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
@@ -352,6 +360,7 @@ Endpoints
|
||||
"quota": null,
|
||||
"tag": "testvoucher",
|
||||
"comment": "",
|
||||
"seat": null,
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -20,17 +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_cart_addons, 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, order_split
|
||||
: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, render_seating_plan, checkout_flow_steps, 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_meta_from_request
|
||||
:members: order_info, order_info_top, order_meta_from_request
|
||||
|
||||
Request flow
|
||||
""""""""""""
|
||||
@@ -49,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, subevent_forms, item_formsets
|
||||
|
||||
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
|
||||
@@ -78,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,10 +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)
|
||||
|
||||
@@ -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,6 +31,7 @@ deprovision
|
||||
discoverable
|
||||
django
|
||||
dockerfile
|
||||
downloadable
|
||||
durations
|
||||
eu
|
||||
filename
|
||||
@@ -41,6 +42,7 @@ formsets
|
||||
frontend
|
||||
frontpage
|
||||
gettext
|
||||
giftcard
|
||||
gunicorn
|
||||
guid
|
||||
hardcoded
|
||||
@@ -55,6 +57,7 @@ Jimdo
|
||||
libpretixprint
|
||||
libsass
|
||||
linters
|
||||
login
|
||||
memcached
|
||||
metadata
|
||||
middleware
|
||||
@@ -76,6 +79,7 @@ overpayment
|
||||
param
|
||||
passphrase
|
||||
percental
|
||||
pluggable
|
||||
positionid
|
||||
pre
|
||||
prepend
|
||||
@@ -99,6 +103,7 @@ regex
|
||||
renderer
|
||||
renderers
|
||||
reportlab
|
||||
reseller
|
||||
SaaS
|
||||
scalability
|
||||
screenshot
|
||||
@@ -106,9 +111,10 @@ scss
|
||||
searchable
|
||||
selectable
|
||||
serializable
|
||||
serializers
|
||||
serializer
|
||||
serializers
|
||||
sexualized
|
||||
SQL
|
||||
startup
|
||||
stdout
|
||||
stylesheet
|
||||
@@ -135,8 +141,10 @@ untrusted
|
||||
uptime
|
||||
username
|
||||
url
|
||||
validator
|
||||
versa
|
||||
versioning
|
||||
viewable
|
||||
viewset
|
||||
viewsets
|
||||
waitinglist
|
||||
|
||||
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.
|
||||
@@ -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
|
||||
---------------------
|
||||
|
||||
@@ -183,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
|
||||
------------------------------
|
||||
|
||||
@@ -206,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
|
||||
-------------------------------
|
||||
@@ -223,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).
|
||||
|
||||
@@ -241,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:
|
||||
|
||||
@@ -283,4 +333,8 @@ Hosted or pretix Enterprise are active, you can pass the following fields:
|
||||
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
|
||||
---------------------------------
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "3.1.0"
|
||||
__version__ = "3.9.0.dev0"
|
||||
|
||||
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,7 +2,7 @@ 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
|
||||
|
||||
@@ -30,11 +30,12 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
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', 'seat')
|
||||
'subevent', 'expires', 'includes_tax', 'answers', 'seat', 'sales_channel')
|
||||
|
||||
def create(self, validated_data):
|
||||
answers_data = validated_data.pop('answers')
|
||||
@@ -55,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'))
|
||||
)
|
||||
)
|
||||
@@ -63,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
|
||||
)
|
||||
)
|
||||
@@ -86,11 +87,12 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
raise ValidationError('The specified seat ID is not unique.')
|
||||
else:
|
||||
validated_data['seat'] = seat
|
||||
if not seat.is_available():
|
||||
raise ValidationError(ugettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
|
||||
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,9 +2,12 @@ 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
|
||||
@@ -14,6 +17,8 @@ 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):
|
||||
@@ -29,6 +34,19 @@ 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):
|
||||
@@ -61,17 +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', 'seating_plan',
|
||||
'plugins', 'seat_category_mapping')
|
||||
'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)
|
||||
@@ -116,6 +145,12 @@ 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.')
|
||||
@@ -127,8 +162,11 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
return value
|
||||
|
||||
def validate_seat_category_mapping(self, value):
|
||||
if value and value['seat_category_mapping'] and (not self.instance or not self.instance.pk):
|
||||
raise ValidationError('You cannot specify seat category mappings on event creation.')
|
||||
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():
|
||||
@@ -154,10 +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():
|
||||
@@ -166,6 +209,15 @@ 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, {})
|
||||
@@ -180,10 +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')}
|
||||
@@ -202,6 +259,26 @@ 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 = {
|
||||
@@ -239,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()
|
||||
@@ -250,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
|
||||
|
||||
@@ -277,8 +360,9 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = SubEvent
|
||||
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
|
||||
'presale_start', 'presale_end', 'location', 'event', 'is_public', 'seating_plan',
|
||||
'item_price_overrides', 'variation_price_overrides', 'meta_data', 'seat_category_mapping')
|
||||
'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)
|
||||
@@ -440,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
|
||||
@@ -119,12 +122,9 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
'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',
|
||||
'show_quota_left', 'hidden_if_available', 'allow_waitinglist')
|
||||
'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):
|
||||
@@ -134,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):
|
||||
@@ -159,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
|
||||
|
||||
|
||||
@@ -219,7 +277,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
model = Question
|
||||
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
|
||||
'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
|
||||
'hidden', 'dependency_value', 'print_on_invoice')
|
||||
'hidden', 'dependency_value', 'print_on_invoice', 'help_text')
|
||||
|
||||
def validate_identifier(self, value):
|
||||
Question._clean_identifier(self.context['event'], value, self.instance)
|
||||
@@ -229,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):
|
||||
@@ -249,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:
|
||||
@@ -256,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
|
||||
|
||||
|
||||
@@ -1,11 +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
|
||||
@@ -14,16 +14,18 @@ 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, Seat, SubEvent, Voucher,
|
||||
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
|
||||
@@ -37,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
|
||||
|
||||
|
||||
@@ -95,6 +97,11 @@ 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:
|
||||
@@ -105,6 +112,7 @@ class InlineSeatSerializer(I18nAwareModelSerializer):
|
||||
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
|
||||
@@ -114,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):
|
||||
@@ -188,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
|
||||
|
||||
|
||||
@@ -198,12 +211,14 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
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', 'seat')
|
||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -283,7 +298,7 @@ class OrderPaymentDateField(serializers.DateField):
|
||||
class OrderFeeSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = OrderFee
|
||||
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule')
|
||||
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule', 'canceled')
|
||||
|
||||
|
||||
class PaymentURLField(serializers.URLField):
|
||||
@@ -477,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']:
|
||||
@@ -499,12 +518,22 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
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',
|
||||
'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():
|
||||
raise ValidationError(
|
||||
@@ -559,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
|
||||
|
||||
|
||||
@@ -575,6 +622,28 @@ 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=True)
|
||||
@@ -595,6 +664,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
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)
|
||||
@@ -604,7 +674,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
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', 'send_mail')
|
||||
'force', 'send_mail', 'simulate')
|
||||
|
||||
def validate_payment_provider(self, pp):
|
||||
if pp is None:
|
||||
@@ -675,6 +745,20 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
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 []
|
||||
@@ -682,6 +766,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
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:
|
||||
@@ -695,12 +780,16 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
else:
|
||||
ia = None
|
||||
|
||||
with self.context['event'].lock() as now_dt:
|
||||
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(
|
||||
@@ -723,9 +812,14 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
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
|
||||
@@ -749,6 +843,44 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
'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:
|
||||
@@ -759,8 +891,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
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()) or seat in seats_seen:
|
||||
errs[i]['seat'] = [ugettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
|
||||
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.']
|
||||
@@ -775,7 +907,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
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:
|
||||
@@ -787,7 +919,7 @@ 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
|
||||
)
|
||||
]
|
||||
@@ -801,11 +933,20 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
|
||||
order.meta_info = "{}"
|
||||
order.total = Decimal('0.00')
|
||||
order.save()
|
||||
if simulate:
|
||||
order = WrappedModel(order)
|
||||
order.last_modified = now()
|
||||
order.code = 'PREVIEW'
|
||||
else:
|
||||
order.save()
|
||||
|
||||
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:
|
||||
@@ -817,7 +958,10 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
'_legacy': attendee_name
|
||||
}
|
||||
pos = OrderPosition(**pos_data)
|
||||
pos.order = order
|
||||
if simulate:
|
||||
pos.order = order._wrapped
|
||||
else:
|
||||
pos.order = order
|
||||
if addon_to:
|
||||
pos.addon_to = pos_map[addon_to]
|
||||
|
||||
@@ -837,26 +981,98 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
pos.tax_rule = pos.item.tax_rule
|
||||
else:
|
||||
pos._calculate_tax()
|
||||
if pos.voucher:
|
||||
Voucher.objects.filter(pk=pos.voucher.pk).update(redeemed=F('redeemed') + 1)
|
||||
pos.save()
|
||||
|
||||
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
|
||||
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 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)
|
||||
|
||||
order.total = sum([p.price for p in order.positions.all()]) + sum([f.value for f in order.fees.all()])
|
||||
order.save(update_fields=['total'])
|
||||
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
|
||||
@@ -888,10 +1104,25 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
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):
|
||||
@@ -907,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,7 +1,19 @@
|
||||
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.api.serializers.order import CompatibleJSONField
|
||||
from pretix.base.models import Organizer, SeatingPlan
|
||||
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):
|
||||
@@ -18,3 +30,140 @@ class SeatingPlanSerializer(I18nAwareModelSerializer):
|
||||
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', 'show_hidden_items')
|
||||
'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
|
||||
|
||||
@@ -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()
|
||||
@@ -19,6 +19,13 @@ 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)
|
||||
@@ -60,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)),
|
||||
@@ -75,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"),
|
||||
]
|
||||
|
||||
@@ -44,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):
|
||||
|
||||
@@ -4,13 +4,14 @@ from django.db.models import ProtectedError, Q
|
||||
from django.utils.timezone import now
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import filters, viewsets
|
||||
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 (
|
||||
@@ -18,6 +19,7 @@ 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):
|
||||
@@ -85,6 +87,8 @@ 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', 'seat_category_mappings'
|
||||
)
|
||||
@@ -130,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,
|
||||
@@ -241,6 +246,9 @@ 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', 'seat_category_mappings'
|
||||
)
|
||||
@@ -326,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)
|
||||
|
||||
@@ -62,7 +62,6 @@ 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):
|
||||
|
||||
@@ -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,10 +6,10 @@ 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
|
||||
@@ -23,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
|
||||
@@ -43,12 +44,12 @@ from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.services.orders import (
|
||||
OrderChangeManager, OrderError, _order_placed_email,
|
||||
_order_placed_email_attendee, approve_order, cancel_order, deny_order,
|
||||
extend_order, mark_order_expired, mark_order_refunded,
|
||||
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
|
||||
|
||||
@@ -82,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', 'seat',
|
||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation', 'seat'))
|
||||
Prefetch('addons', opq.select_related('item', 'variation', 'seat'))
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -103,7 +113,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
qs = qs.prefetch_related(
|
||||
Prefetch(
|
||||
'positions',
|
||||
OrderPosition.objects.all().prefetch_related(
|
||||
opq.all().prefetch_related(
|
||||
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question', 'seat',
|
||||
)
|
||||
)
|
||||
@@ -148,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):
|
||||
@@ -169,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',
|
||||
@@ -230,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)
|
||||
@@ -435,54 +489,61 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
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()
|
||||
invoice = None
|
||||
if gen_invoice:
|
||||
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)
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
_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 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
|
||||
|
||||
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:
|
||||
_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:
|
||||
payment._send_paid_mail_attendee(p, None)
|
||||
_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)
|
||||
@@ -629,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',
|
||||
@@ -641,7 +707,7 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
|
||||
),
|
||||
Prefetch(
|
||||
'positions',
|
||||
OrderPosition.objects.prefetch_related(
|
||||
qs.prefetch_related(
|
||||
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||
)
|
||||
)
|
||||
@@ -651,7 +717,7 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
|
||||
)
|
||||
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', 'seat'
|
||||
)
|
||||
@@ -759,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:
|
||||
@@ -782,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()
|
||||
@@ -886,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)
|
||||
|
||||
|
||||
@@ -969,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():
|
||||
@@ -985,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)
|
||||
|
||||
@@ -1,11 +1,26 @@
|
||||
from rest_framework import filters, viewsets
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
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, SeatingPlanSerializer,
|
||||
GiftCardSerializer, OrganizerSerializer, SeatingPlanSerializer,
|
||||
TeamAPITokenSerializer, TeamInviteSerializer, TeamMemberSerializer,
|
||||
TeamSerializer,
|
||||
)
|
||||
from pretix.base.models import (
|
||||
GiftCard, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
||||
)
|
||||
from pretix.base.models import Organizer, SeatingPlan
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
|
||||
|
||||
@@ -43,13 +58,14 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
|
||||
write_permission = 'can_change_organizer_settings'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.organizer.seating_plans.all()
|
||||
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(
|
||||
@@ -59,6 +75,7 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
|
||||
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.')
|
||||
@@ -71,6 +88,7 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
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.')
|
||||
@@ -81,3 +99,256 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
|
||||
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__),
|
||||
})
|
||||
@@ -45,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
|
||||
|
||||
@@ -7,7 +7,7 @@ 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
|
||||
|
||||
@@ -125,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'),
|
||||
|
||||
@@ -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
|
||||
|
||||
15
src/pretix/base/context.py
Normal file
15
src/pretix/base/context.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import sys
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def contextprocessor(request):
|
||||
ctx = {
|
||||
'rtl': getattr(request, 'LANGUAGE_CODE', 'en') in settings.LANGUAGES_RTL,
|
||||
}
|
||||
if settings.DEBUG and 'runserver' not in sys.argv:
|
||||
ctx['debug_warning'] = True
|
||||
elif 'runserver' in sys.argv:
|
||||
ctx['development_warning'] = True
|
||||
|
||||
return ctx
|
||||
@@ -1,15 +1,23 @@
|
||||
import inspect
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
from smtplib import SMTPResponseException
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.mail.backends.smtp import EmailBackend
|
||||
from django.dispatch import receiver
|
||||
from django.template.loader import get_template
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import get_language, gettext_lazy as _
|
||||
from inlinestyler.utils import inline_css
|
||||
|
||||
from pretix.base.models import Event, Order, OrderPosition
|
||||
from pretix.base.signals import register_html_mail_renderers
|
||||
from pretix.base.i18n import LazyCurrencyNumber, LazyDate, LazyNumber
|
||||
from pretix.base.models import Event
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.base.signals import (
|
||||
register_html_mail_renderers, register_mail_placeholders,
|
||||
)
|
||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||
|
||||
logger = logging.getLogger('pretix.base.email')
|
||||
@@ -44,8 +52,8 @@ class BaseHTMLMailRenderer:
|
||||
def __str__(self):
|
||||
return self.identifier
|
||||
|
||||
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order=None,
|
||||
position: OrderPosition=None) -> str:
|
||||
def render(self, plain_body: str, plain_signature: str, subject: str, order=None,
|
||||
position=None) -> str:
|
||||
"""
|
||||
This method should generate the HTML part of the email.
|
||||
|
||||
@@ -97,14 +105,15 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
||||
def template_name(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order, position: OrderPosition) -> str:
|
||||
def render(self, plain_body: str, plain_signature: str, subject: str, order, position) -> str:
|
||||
body_md = markdown_compile_email(plain_body)
|
||||
htmlctx = {
|
||||
'site': settings.PRETIX_INSTANCE_NAME,
|
||||
'site_url': settings.SITE_URL,
|
||||
'body': body_md,
|
||||
'subject': str(subject),
|
||||
'color': '#8E44B3'
|
||||
'color': '#8E44B3',
|
||||
'rtl': get_language() in settings.LANGUAGES_RTL
|
||||
}
|
||||
if self.event:
|
||||
htmlctx['event'] = self.event
|
||||
@@ -127,12 +136,330 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
||||
|
||||
|
||||
class ClassicMailRenderer(TemplateBasedMailRenderer):
|
||||
verbose_name = _('pretix default')
|
||||
verbose_name = _('Default')
|
||||
identifier = 'classic'
|
||||
thumbnail_filename = 'pretixbase/email/thumb.png'
|
||||
template_name = 'pretixbase/email/plainwrapper.html'
|
||||
|
||||
|
||||
class UnembellishedMailRenderer(TemplateBasedMailRenderer):
|
||||
verbose_name = _('Simple with logo')
|
||||
identifier = 'simple_logo'
|
||||
thumbnail_filename = 'pretixbase/email/thumb_simple_logo.png'
|
||||
template_name = 'pretixbase/email/simple_logo.html'
|
||||
|
||||
|
||||
@receiver(register_html_mail_renderers, dispatch_uid="pretixbase_email_renderers")
|
||||
def base_renderers(sender, **kwargs):
|
||||
return [ClassicMailRenderer]
|
||||
return [ClassicMailRenderer, UnembellishedMailRenderer]
|
||||
|
||||
|
||||
class BaseMailTextPlaceholder:
|
||||
"""
|
||||
This is the base class for for all email text placeholders.
|
||||
"""
|
||||
|
||||
@property
|
||||
def required_context(self):
|
||||
"""
|
||||
This property should return a list of all attribute names that need to be
|
||||
contained in the base context so that this placeholder is available. By default,
|
||||
it returns a list containing the string "event".
|
||||
"""
|
||||
return ["event"]
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
"""
|
||||
This should return the identifier of this placeholder in the email.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def render(self, context):
|
||||
"""
|
||||
This method is called to generate the actual text that is being
|
||||
used in the email. You will be passed a context dictionary with the
|
||||
base context attributes specified in ``required_context``. You are
|
||||
expected to return a plain-text string.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def render_sample(self, event):
|
||||
"""
|
||||
This method is called to generate a text to be used in email previews.
|
||||
This may only depend on the event.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class SimpleFunctionalMailTextPlaceholder(BaseMailTextPlaceholder):
|
||||
def __init__(self, identifier, args, func, sample):
|
||||
self._identifier = identifier
|
||||
self._args = args
|
||||
self._func = func
|
||||
self._sample = sample
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
return self._identifier
|
||||
|
||||
@property
|
||||
def required_context(self):
|
||||
return self._args
|
||||
|
||||
def render(self, context):
|
||||
return self._func(**{k: context[k] for k in self._args})
|
||||
|
||||
def render_sample(self, event):
|
||||
if callable(self._sample):
|
||||
return self._sample(event)
|
||||
else:
|
||||
return self._sample
|
||||
|
||||
|
||||
def get_available_placeholders(event, base_parameters):
|
||||
if 'order' in base_parameters:
|
||||
base_parameters.append('invoice_address')
|
||||
params = {}
|
||||
for r, val in register_mail_placeholders.send(sender=event):
|
||||
if not isinstance(val, (list, tuple)):
|
||||
val = [val]
|
||||
for v in val:
|
||||
if all(rp in base_parameters for rp in v.required_context):
|
||||
params[v.identifier] = v
|
||||
return params
|
||||
|
||||
|
||||
def get_email_context(**kwargs):
|
||||
from pretix.base.models import InvoiceAddress
|
||||
|
||||
event = kwargs['event']
|
||||
if 'order' in kwargs:
|
||||
try:
|
||||
kwargs['invoice_address'] = kwargs['order'].invoice_address
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
kwargs['invoice_address'] = InvoiceAddress()
|
||||
ctx = {}
|
||||
for r, val in register_mail_placeholders.send(sender=event):
|
||||
if not isinstance(val, (list, tuple)):
|
||||
val = [val]
|
||||
for v in val:
|
||||
if all(rp in kwargs for rp in v.required_context):
|
||||
ctx[v.identifier] = v.render(kwargs)
|
||||
return ctx
|
||||
|
||||
|
||||
def _placeholder_payment(order, payment):
|
||||
if not payment:
|
||||
return None
|
||||
if 'payment' in inspect.signature(payment.payment_provider.order_pending_mail_render).parameters:
|
||||
return str(payment.payment_provider.order_pending_mail_render(order, payment))
|
||||
else:
|
||||
return str(payment.payment_provider.order_pending_mail_render(order))
|
||||
|
||||
|
||||
@receiver(register_mail_placeholders, dispatch_uid="pretixbase_register_mail_placeholders")
|
||||
def base_placeholders(sender, **kwargs):
|
||||
from pretix.base.models import InvoiceAddress
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
|
||||
ph = [
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'event', ['event'], lambda event: event.name, lambda event: event.name
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'event', ['event_or_subevent'], lambda event_or_subevent: event_or_subevent.name,
|
||||
lambda event_or_subevent: event_or_subevent.name
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'event_slug', ['event'], lambda event: event.slug, lambda event: event.slug
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'code', ['order'], lambda order: order.code, 'F8VVL'
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'total', ['order'], lambda order: LazyNumber(order.total), lambda event: LazyNumber(Decimal('42.23'))
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'currency', ['event'], lambda event: event.currency, lambda event: event.currency
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'refund_amount', ['event_or_subevent', 'refund_amount'],
|
||||
lambda event_or_subevent, refund_amount: LazyCurrencyNumber(refund_amount, event_or_subevent.currency),
|
||||
lambda event_or_subevent: LazyCurrencyNumber(Decimal('42.23'), event_or_subevent.currency)
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'total_with_currency', ['event', 'order'], lambda event, order: LazyCurrencyNumber(order.total,
|
||||
event.currency),
|
||||
lambda event: LazyCurrencyNumber(Decimal('42.23'), event.currency)
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'expire_date', ['event', 'order'], lambda event, order: LazyDate(order.expires.astimezone(event.timezone)),
|
||||
lambda event: LazyDate(now() + timedelta(days=15))
|
||||
# TODO: This used to be "date" in some placeholders, add a migration!
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'url', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.open', kwargs={
|
||||
'order': order.code,
|
||||
'secret': order.secret,
|
||||
'hash': order.email_confirm_hash()
|
||||
}
|
||||
), lambda event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.open', kwargs={
|
||||
'order': 'F8VVL',
|
||||
'secret': '6zzjnumtsx136ddy',
|
||||
'hash': '98kusd8ofsj8dnkd'
|
||||
}
|
||||
),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'url', ['event', 'position'], lambda event, position: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.position',
|
||||
kwargs={
|
||||
'order': position.order.code,
|
||||
'secret': position.web_secret,
|
||||
'position': position.positionid
|
||||
}
|
||||
),
|
||||
lambda event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.position', kwargs={
|
||||
'order': 'F8VVL',
|
||||
'secret': '6zzjnumtsx136ddy',
|
||||
'position': '123'
|
||||
}
|
||||
),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'url', ['waiting_list_entry', 'event'],
|
||||
lambda waiting_list_entry, event: build_absolute_uri(
|
||||
event, 'presale:event.redeem'
|
||||
) + '?voucher=' + waiting_list_entry.voucher.code,
|
||||
lambda event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.redeem',
|
||||
) + '?voucher=68CYU2H6ZTP3WLK5',
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'invoice_name', ['invoice_address'], lambda invoice_address: invoice_address.name or '',
|
||||
_('John Doe')
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'invoice_company', ['invoice_address'], lambda invoice_address: invoice_address.company or '',
|
||||
_('Sample Corporation')
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
|
||||
'* {} - {}'.format(
|
||||
order.full_code,
|
||||
build_absolute_uri(event, 'presale:event.order', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'order': order.code,
|
||||
'secret': order.secret
|
||||
}),
|
||||
)
|
||||
for order in orders
|
||||
), lambda event: '\n' + '\n\n'.join(
|
||||
'* {} - {}'.format(
|
||||
'{}-{}'.format(event.slug.upper(), order['code']),
|
||||
build_absolute_uri(event, 'presale:event.order', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'order': order['code'],
|
||||
'secret': order['secret']
|
||||
}),
|
||||
)
|
||||
for order in [
|
||||
{'code': 'F8VVL', 'secret': '6zzjnumtsx136ddy'},
|
||||
{'code': 'HIDHK', 'secret': '98kusd8ofsj8dnkd'},
|
||||
{'code': 'OPKSB', 'secret': '09pjdksflosk3njd'}
|
||||
]
|
||||
),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'hours', ['event', 'waiting_list_entry'], lambda event, waiting_list_entry:
|
||||
event.settings.waiting_list_hours,
|
||||
lambda event: event.settings.waiting_list_hours
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'product', ['waiting_list_entry'], lambda waiting_list_entry: waiting_list_entry.item.name,
|
||||
_('Sample Admission Ticket')
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'code', ['waiting_list_entry'], lambda waiting_list_entry: waiting_list_entry.voucher.code,
|
||||
'68CYU2H6ZTP3WLK5'
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'voucher_list', ['voucher_list'], lambda voucher_list: '\n'.join(voucher_list),
|
||||
' 68CYU2H6ZTP3WLK5\n 7MB94KKPVEPSMVF2'
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'url', ['event', 'voucher_list'], lambda event, voucher_list: build_absolute_uri(event, 'presale:event.index', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
}), lambda event: build_absolute_uri(event, 'presale:event.index', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
})
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'name', ['name'], lambda name: name,
|
||||
_('John Doe')
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'comment', ['comment'], lambda comment: comment,
|
||||
_('An individual text with a reason can be inserted here.'),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'payment_info', ['order', 'payment'], _placeholder_payment,
|
||||
_('The amount has been charged to your card.'),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'payment_info', ['payment_info'], lambda payment_info: payment_info,
|
||||
_('Please transfer money to this bank account: 9999-9999-9999-9999'),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'attendee_name', ['position'], lambda position: position.attendee_name,
|
||||
_('John Doe'),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'name', ['position_or_address'],
|
||||
lambda position_or_address: (
|
||||
position_or_address.name
|
||||
if isinstance(position_or_address, InvoiceAddress)
|
||||
else position_or_address.attendee_name
|
||||
),
|
||||
_('John Doe'),
|
||||
),
|
||||
]
|
||||
|
||||
name_scheme = PERSON_NAME_SCHEMES[sender.settings.name_scheme]
|
||||
for f, l, w in name_scheme['fields']:
|
||||
if f == 'full_name':
|
||||
continue
|
||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
||||
'attendee_name_%s' % f, ['position'], lambda position, f=f: position.attendee_name_parts.get(f, ''),
|
||||
name_scheme['sample'][f]
|
||||
))
|
||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
||||
'name_%s' % f, ['position_or_address'],
|
||||
lambda position_or_address, f=f: (
|
||||
position_or_address.name_parts.get(f, '')
|
||||
if isinstance(position_or_address, InvoiceAddress)
|
||||
else position_or_address.attendee_name_parts.get(f, '')
|
||||
),
|
||||
name_scheme['sample'][f]
|
||||
))
|
||||
|
||||
for k, v in sender.meta_data.items():
|
||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
||||
'meta_%s' % k, ['event'], lambda event, k=k: event.meta_data[k],
|
||||
v
|
||||
))
|
||||
|
||||
return ph
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import io
|
||||
import tempfile
|
||||
from collections import OrderedDict
|
||||
from decimal import Decimal
|
||||
from typing import Tuple
|
||||
|
||||
from defusedcsv import csv
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext, ugettext_lazy as _
|
||||
from django.utils.formats import localize
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.cell.cell import KNOWN_TYPES
|
||||
|
||||
@@ -111,24 +113,32 @@ class ListExporter(BaseExporter):
|
||||
raise NotImplementedError() # noqa
|
||||
|
||||
def get_filename(self):
|
||||
return 'export.csv'
|
||||
return 'export'
|
||||
|
||||
def _render_csv(self, form_data, output_file=None, **kwargs):
|
||||
if output_file:
|
||||
writer = csv.writer(output_file, **kwargs)
|
||||
for line in self.iterate_list(form_data):
|
||||
line = [
|
||||
localize(f) if isinstance(f, Decimal) else f
|
||||
for f in line
|
||||
]
|
||||
writer.writerow(line)
|
||||
return self.get_filename() + '.csv', 'text/csv', None
|
||||
else:
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output, **kwargs)
|
||||
for line in self.iterate_list(form_data):
|
||||
line = [
|
||||
localize(f) if isinstance(f, Decimal) else f
|
||||
for f in line
|
||||
]
|
||||
writer.writerow(line)
|
||||
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
|
||||
|
||||
def _render_xlsx(self, form_data, output_file=None):
|
||||
wb = Workbook()
|
||||
ws = wb.get_active_sheet()
|
||||
ws = wb.active
|
||||
try:
|
||||
ws.title = str(self.verbose_name)
|
||||
except:
|
||||
@@ -170,9 +180,9 @@ class MultiSheetListExporter(ListExporter):
|
||||
]
|
||||
for s, l in self.sheets:
|
||||
choices += [
|
||||
(s + ':default', str(l) + ' – ' + ugettext('CSV (with commas)')),
|
||||
(s + ':excel', str(l) + ' – ' + ugettext('CSV (Excel-style)')),
|
||||
(s + ':semicolon', str(l) + ' – ' + ugettext('CSV (with semicolons)')),
|
||||
(s + ':default', str(l) + ' – ' + gettext('CSV (with commas)')),
|
||||
(s + ':excel', str(l) + ' – ' + gettext('CSV (Excel-style)')),
|
||||
(s + ':semicolon', str(l) + ' – ' + gettext('CSV (with semicolons)')),
|
||||
]
|
||||
ff = OrderedDict(
|
||||
[
|
||||
@@ -196,18 +206,26 @@ class MultiSheetListExporter(ListExporter):
|
||||
if output_file:
|
||||
writer = csv.writer(output_file, **kwargs)
|
||||
for line in self.iterate_sheet(form_data, sheet):
|
||||
line = [
|
||||
localize(f) if isinstance(f, Decimal) else f
|
||||
for f in line
|
||||
]
|
||||
writer.writerow(line)
|
||||
return self.get_filename() + '.csv', 'text/csv', None
|
||||
else:
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output, **kwargs)
|
||||
for line in self.iterate_sheet(form_data, sheet):
|
||||
line = [
|
||||
localize(f) if isinstance(f, Decimal) else f
|
||||
for f in line
|
||||
]
|
||||
writer.writerow(line)
|
||||
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
|
||||
|
||||
def _render_xlsx(self, form_data, output_file=None):
|
||||
wb = Workbook()
|
||||
ws = wb.get_active_sheet()
|
||||
ws = wb.active
|
||||
wb.remove(ws)
|
||||
for s, l in self.sheets:
|
||||
ws = wb.create_sheet(str(l))
|
||||
|
||||
@@ -5,7 +5,7 @@ from zipfile import ZipFile
|
||||
|
||||
from django import forms
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.models import QuestionAnswer
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import dateutil
|
||||
from django import forms
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext, ugettext_lazy
|
||||
from django.utils.translation import gettext, gettext_lazy
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import Invoice, OrderPayment
|
||||
@@ -79,7 +79,7 @@ class DekodiNREIExporter(BaseExporter):
|
||||
payments.append({
|
||||
'PTID': '5',
|
||||
'PTN': 'Lastschrift',
|
||||
'PTNo4': ugettext('Event ticket {event}-{code}').format(
|
||||
'PTNo4': gettext('Event ticket {event}-{code}').format(
|
||||
event=self.event.slug.upper(),
|
||||
code=invoice.order.code
|
||||
),
|
||||
@@ -129,8 +129,11 @@ class DekodiNREIExporter(BaseExporter):
|
||||
'DIDt': invoice.order.datetime.isoformat().replace('Z', '+00:00'),
|
||||
'DT': '30' if invoice.is_cancellation else '10',
|
||||
'EM': invoice.order.email,
|
||||
'FamN': invoice.invoice_to_name.rsplit(' ', 1)[-1],
|
||||
'FN': invoice.invoice_to_name.rsplit(' ', 1)[0] if ' ' in invoice.invoice_to_name else '',
|
||||
'FamN': invoice.invoice_to_name.rsplit(' ', 1)[-1] if invoice.invoice_to_name else '',
|
||||
'FN': (
|
||||
invoice.invoice_to_name.rsplit(' ', 1)[0]
|
||||
if invoice.invoice_to_name and ' ' in invoice.invoice_to_name else ''
|
||||
),
|
||||
'IDt': invoice.date.isoformat() + 'T08:00:00+01:00',
|
||||
'INo': invoice.full_invoice_no,
|
||||
'IsNet': invoice.reverse_charge,
|
||||
@@ -196,19 +199,19 @@ class DekodiNREIExporter(BaseExporter):
|
||||
[
|
||||
('date_from',
|
||||
forms.DateField(
|
||||
label=ugettext_lazy('Start date'),
|
||||
label=gettext_lazy('Start date'),
|
||||
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
|
||||
required=False,
|
||||
help_text=ugettext_lazy('Only include invoices issued on or after this date. Note that the invoice date does '
|
||||
'not always correspond to the order or payment date.')
|
||||
help_text=gettext_lazy('Only include invoices issued on or after this date. Note that the invoice date does '
|
||||
'not always correspond to the order or payment date.')
|
||||
)),
|
||||
('date_to',
|
||||
forms.DateField(
|
||||
label=ugettext_lazy('End date'),
|
||||
label=gettext_lazy('End date'),
|
||||
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
|
||||
required=False,
|
||||
help_text=ugettext_lazy('Only include invoices issued on or before this date. Note that the invoice date '
|
||||
'does not always correspond to the order or payment date.')
|
||||
help_text=gettext_lazy('Only include invoices issued on or before this date. Note that the invoice date '
|
||||
'does not always correspond to the order or payment date.')
|
||||
)),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ import dateutil.parser
|
||||
from django import forms
|
||||
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 pretix.base.models import OrderPayment
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from collections import OrderedDict
|
||||
|
||||
from django import forms
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.models import OrderPosition
|
||||
|
||||
|
||||
@@ -3,13 +3,15 @@ from decimal import Decimal
|
||||
|
||||
import pytz
|
||||
from django import forms
|
||||
from django.db.models import DateTimeField, F, Max, OuterRef, Subquery, Sum
|
||||
from django.db.models import (
|
||||
Count, DateTimeField, F, IntegerField, Max, OuterRef, Subquery, Sum,
|
||||
)
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import date_format, localize
|
||||
from django.utils.translation import pgettext, ugettext as _, ugettext_lazy
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.translation import gettext as _, gettext_lazy, pgettext
|
||||
|
||||
from pretix.base.models import (
|
||||
InvoiceAddress, InvoiceLine, Order, OrderPosition, Question,
|
||||
GiftCard, InvoiceAddress, InvoiceLine, Order, OrderPosition, Question,
|
||||
)
|
||||
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
@@ -20,7 +22,7 @@ from ..signals import register_data_exporters
|
||||
|
||||
class OrderListExporter(MultiSheetListExporter):
|
||||
identifier = 'orderlist'
|
||||
verbose_name = ugettext_lazy('Order data')
|
||||
verbose_name = gettext_lazy('Order data')
|
||||
|
||||
@property
|
||||
def sheets(self):
|
||||
@@ -80,8 +82,12 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
'm'
|
||||
).order_by()
|
||||
|
||||
s = OrderPosition.objects.filter(
|
||||
order=OuterRef('pk')
|
||||
).order_by().values('order').annotate(k=Count('id')).values('k')
|
||||
qs = self.event.orders.annotate(
|
||||
payment_date=Subquery(p_date, output_field=DateTimeField())
|
||||
payment_date=Subquery(p_date, output_field=DateTimeField()),
|
||||
pcnt=Subquery(s, output_field=IntegerField())
|
||||
).select_related('invoice_address').prefetch_related('invoices')
|
||||
if form_data['paid_only']:
|
||||
qs = qs.filter(status=Order.STATUS_PAID)
|
||||
@@ -111,6 +117,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
headers.append(_('Sales channel'))
|
||||
headers.append(_('Requires special attention'))
|
||||
headers.append(_('Comment'))
|
||||
headers.append(_('Positions'))
|
||||
|
||||
yield headers
|
||||
|
||||
@@ -134,7 +141,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
for order in qs.order_by('datetime'):
|
||||
row = [
|
||||
order.code,
|
||||
localize(order.total),
|
||||
order.total,
|
||||
order.get_status_display(),
|
||||
order.email,
|
||||
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
|
||||
@@ -163,7 +170,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
|
||||
row += [
|
||||
order.payment_date.astimezone(tz).strftime('%Y-%m-%d') if order.payment_date else '',
|
||||
localize(full_fee_sum_cache.get(order.id) or Decimal('0.00')),
|
||||
full_fee_sum_cache.get(order.id) or Decimal('0.00'),
|
||||
order.locale,
|
||||
]
|
||||
|
||||
@@ -173,16 +180,19 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
{'grosssum': Decimal('0.00'), 'taxsum': Decimal('0.00')})
|
||||
|
||||
row += [
|
||||
localize(taxrate_values['grosssum'] + fee_taxrate_values['grosssum']),
|
||||
localize(taxrate_values['grosssum'] - taxrate_values['taxsum']
|
||||
+ fee_taxrate_values['grosssum'] - fee_taxrate_values['taxsum']),
|
||||
localize(taxrate_values['taxsum'] + fee_taxrate_values['taxsum']),
|
||||
taxrate_values['grosssum'] + fee_taxrate_values['grosssum'],
|
||||
(
|
||||
taxrate_values['grosssum'] - taxrate_values['taxsum'] +
|
||||
fee_taxrate_values['grosssum'] - fee_taxrate_values['taxsum']
|
||||
),
|
||||
taxrate_values['taxsum'] + fee_taxrate_values['taxsum'],
|
||||
]
|
||||
|
||||
row.append(', '.join([i.number for i in order.invoices.all()]))
|
||||
row.append(order.sales_channel)
|
||||
row.append(_('Yes') if order.checkin_attention else _('No'))
|
||||
row.append(order.comment or "")
|
||||
row.append(order.pcnt)
|
||||
yield row
|
||||
|
||||
def iterate_fees(self, form_data: dict):
|
||||
@@ -264,7 +274,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
'order', 'order__invoice_address', 'item', 'variation',
|
||||
'voucher', 'tax_rule'
|
||||
).prefetch_related(
|
||||
'answers', 'answers__question'
|
||||
'answers', 'answers__question', 'answers__options'
|
||||
)
|
||||
if form_data['paid_only']:
|
||||
qs = qs.filter(order__status=Order.STATUS_PAID)
|
||||
@@ -278,6 +288,8 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
]
|
||||
if self.event.has_subevents:
|
||||
headers.append(pgettext('subevent', 'Date'))
|
||||
headers.append(_('Start date'))
|
||||
headers.append(_('End date'))
|
||||
headers += [
|
||||
_('Product'),
|
||||
_('Variation'),
|
||||
@@ -293,12 +305,25 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
headers.append(_('Attendee name') + ': ' + str(label))
|
||||
headers += [
|
||||
_('Attendee email'),
|
||||
_('Company'),
|
||||
_('Address'),
|
||||
_('ZIP code'),
|
||||
_('City'),
|
||||
_('Country'),
|
||||
pgettext('address', 'State'),
|
||||
_('Voucher'),
|
||||
_('Pseudonymization ID'),
|
||||
]
|
||||
questions = list(self.event.questions.all())
|
||||
options = {}
|
||||
for q in questions:
|
||||
headers.append(str(q.question))
|
||||
if q.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||
options[q.pk] = []
|
||||
for o in q.options.all():
|
||||
headers.append(str(q.question) + ' – ' + str(o.answer))
|
||||
options[q.pk].append(o)
|
||||
else:
|
||||
headers.append(str(q.question))
|
||||
headers += [
|
||||
_('Company'),
|
||||
_('Invoice address name'),
|
||||
@@ -323,7 +348,12 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
|
||||
]
|
||||
if self.event.has_subevents:
|
||||
row.append(op.subevent)
|
||||
row.append(op.subevent.name)
|
||||
row.append(op.subevent.date_from.astimezone(self.event.timezone).strftime('%Y-%m-%d %H:%M:%S'))
|
||||
if op.subevent.date_to:
|
||||
row.append(op.subevent.date_to.astimezone(self.event.timezone).strftime('%Y-%m-%d %H:%M:%S'))
|
||||
else:
|
||||
row.append('')
|
||||
row += [
|
||||
str(op.item),
|
||||
str(op.variation) if op.variation else '',
|
||||
@@ -340,6 +370,12 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
)
|
||||
row += [
|
||||
op.attendee_email,
|
||||
op.company or '',
|
||||
op.street or '',
|
||||
op.zipcode or '',
|
||||
op.city or '',
|
||||
op.country if op.country else '',
|
||||
op.state or '',
|
||||
op.voucher.code if op.voucher else '',
|
||||
op.pseudonymization_id,
|
||||
]
|
||||
@@ -347,12 +383,19 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
for a in op.answers.all():
|
||||
# We do not want to localize Date, Time and Datetime question answers, as those can lead
|
||||
# to difficulties parsing the data (for example 2019-02-01 may become Février, 2019 01 in French).
|
||||
if a.question.type in Question.UNLOCALIZED_TYPES:
|
||||
if a.question.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||
acache[a.question_id] = set(o.pk for o in a.options.all())
|
||||
elif a.question.type in Question.UNLOCALIZED_TYPES:
|
||||
acache[a.question_id] = a.answer
|
||||
else:
|
||||
acache[a.question_id] = str(a)
|
||||
for q in questions:
|
||||
row.append(acache.get(q.pk, ''))
|
||||
if q.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||
for o in options[q.pk]:
|
||||
row.append(_('Yes') if o.pk in acache.get(q.pk, set()) else _('No'))
|
||||
else:
|
||||
row.append(acache.get(q.pk, ''))
|
||||
|
||||
try:
|
||||
row += [
|
||||
order.invoice_address.company,
|
||||
@@ -383,7 +426,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
|
||||
class PaymentListExporter(ListExporter):
|
||||
identifier = 'paymentlist'
|
||||
verbose_name = ugettext_lazy('Order payments and refunds')
|
||||
verbose_name = gettext_lazy('Order payments and refunds')
|
||||
|
||||
@property
|
||||
def additional_form_fields(self):
|
||||
@@ -443,7 +486,7 @@ class PaymentListExporter(ListExporter):
|
||||
d2,
|
||||
obj.get_state_display(),
|
||||
obj.state,
|
||||
localize(obj.amount * (-1 if isinstance(obj, OrderRefund) else 1)),
|
||||
obj.amount * (-1 if isinstance(obj, OrderRefund) else 1),
|
||||
provider_names.get(obj.provider, obj.provider)
|
||||
]
|
||||
yield row
|
||||
@@ -454,7 +497,7 @@ class PaymentListExporter(ListExporter):
|
||||
|
||||
class QuotaListExporter(ListExporter):
|
||||
identifier = 'quotalist'
|
||||
verbose_name = ugettext_lazy('Quota availabilities')
|
||||
verbose_name = gettext_lazy('Quota availabilities')
|
||||
|
||||
def iterate_list(self, form_data):
|
||||
headers = [
|
||||
@@ -483,7 +526,7 @@ class QuotaListExporter(ListExporter):
|
||||
|
||||
class InvoiceDataExporter(MultiSheetListExporter):
|
||||
identifier = 'invoicedata'
|
||||
verbose_name = ugettext_lazy('Invoice data')
|
||||
verbose_name = gettext_lazy('Invoice data')
|
||||
|
||||
@property
|
||||
def sheets(self):
|
||||
@@ -524,10 +567,11 @@ class InvoiceDataExporter(MultiSheetListExporter):
|
||||
_('Foreign currency rate'),
|
||||
_('Total value (with taxes)'),
|
||||
_('Total value (without taxes)'),
|
||||
_('Payment matching IDs'),
|
||||
]
|
||||
qs = self.event.invoices.order_by('full_invoice_no').select_related(
|
||||
'order', 'refers'
|
||||
).annotate(
|
||||
).prefetch_related('order__payments').annotate(
|
||||
total_gross=Subquery(
|
||||
InvoiceLine.objects.filter(
|
||||
invoice=OuterRef('pk')
|
||||
@@ -544,6 +588,16 @@ class InvoiceDataExporter(MultiSheetListExporter):
|
||||
)
|
||||
)
|
||||
for i in qs:
|
||||
pmis = []
|
||||
for p in i.order.payments.all():
|
||||
if p.state in (OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_CREATED,
|
||||
OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_REFUNDED):
|
||||
pprov = p.payment_provider
|
||||
if pprov:
|
||||
mid = pprov.matching_id(p)
|
||||
if mid:
|
||||
pmis.append(mid)
|
||||
pmi = '\n'.join(pmis)
|
||||
yield [
|
||||
i.full_invoice_no,
|
||||
date_format(i.date, "SHORT_DATE_FORMAT"),
|
||||
@@ -574,6 +628,7 @@ class InvoiceDataExporter(MultiSheetListExporter):
|
||||
i.foreign_currency_rate,
|
||||
i.total_gross if i.total_gross else Decimal('0.00'),
|
||||
Decimal(i.total_net if i.total_net else '0.00').quantize(Decimal('0.01')),
|
||||
pmi
|
||||
]
|
||||
elif sheet == 'lines':
|
||||
yield [
|
||||
@@ -655,6 +710,45 @@ class InvoiceDataExporter(MultiSheetListExporter):
|
||||
return '{}_invoices'.format(self.event.slug)
|
||||
|
||||
|
||||
class GiftcardRedemptionListExporter(ListExporter):
|
||||
identifier = 'giftcardredemptionlist'
|
||||
verbose_name = gettext_lazy('Gift card redemptions')
|
||||
|
||||
def iterate_list(self, form_data):
|
||||
tz = pytz.timezone(self.event.settings.timezone)
|
||||
|
||||
payments = OrderPayment.objects.filter(
|
||||
order__event=self.event,
|
||||
provider='giftcard'
|
||||
).order_by('created')
|
||||
refunds = OrderRefund.objects.filter(
|
||||
order__event=self.event,
|
||||
provider='giftcard'
|
||||
).order_by('created')
|
||||
|
||||
objs = sorted(list(payments) + list(refunds), key=lambda o: (o.order.code, o.created))
|
||||
|
||||
headers = [
|
||||
_('Order'), _('Payment ID'), _('Date'), _('Gift card code'), _('Amount'), _('Issuer')
|
||||
]
|
||||
yield headers
|
||||
|
||||
for obj in objs:
|
||||
gc = GiftCard.objects.get(pk=obj.info_data.get('gift_card'))
|
||||
row = [
|
||||
obj.order.code,
|
||||
obj.full_id,
|
||||
obj.created.astimezone(tz).date().strftime('%Y-%m-%d'),
|
||||
gc.secret,
|
||||
obj.amount * (-1 if isinstance(obj, OrderRefund) else 1),
|
||||
gc.issuer
|
||||
]
|
||||
yield row
|
||||
|
||||
def get_filename(self):
|
||||
return '{}_giftcardredemptions'.format(self.event.slug)
|
||||
|
||||
|
||||
@receiver(register_data_exporters, dispatch_uid="exporter_orderlist")
|
||||
def register_orderlist_exporter(sender, **kwargs):
|
||||
return OrderListExporter
|
||||
@@ -673,3 +767,8 @@ def register_quotalist_exporter(sender, **kwargs):
|
||||
@receiver(register_data_exporters, dispatch_uid="exporter_invoicedata")
|
||||
def register_invoicedata_exporter(sender, **kwargs):
|
||||
return InvoiceDataExporter
|
||||
|
||||
|
||||
@receiver(register_data_exporters, dispatch_uid="exporter_giftcardredemptionlist")
|
||||
def register_giftcardredemptionlist_exporter(sender, **kwargs):
|
||||
return GiftcardRedemptionListExporter
|
||||
|
||||
@@ -3,12 +3,10 @@ import logging
|
||||
import i18nfield.forms
|
||||
from django import forms
|
||||
from django.forms.models import ModelFormMetaclass
|
||||
from django.utils import six
|
||||
from django.utils.crypto import get_random_string
|
||||
from formtools.wizard.views import SessionWizardView
|
||||
from hierarkey.forms import HierarkeyForm
|
||||
|
||||
from pretix.base.models import Event
|
||||
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
||||
|
||||
from .validators import PlaceholderValidator # NOQA
|
||||
@@ -26,7 +24,7 @@ class BaseI18nModelForm(i18nfield.forms.BaseI18nModelForm):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class I18nModelForm(six.with_metaclass(ModelFormMetaclass, BaseI18nModelForm)):
|
||||
class I18nModelForm(BaseI18nModelForm, metaclass=ModelFormMetaclass):
|
||||
pass
|
||||
|
||||
|
||||
@@ -51,19 +49,33 @@ class I18nInlineFormSet(i18nfield.forms.I18nInlineFormSet):
|
||||
|
||||
|
||||
class SettingsForm(i18nfield.forms.I18nFormMixin, HierarkeyForm):
|
||||
auto_fields = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
from pretix.base.settings import DEFAULTS
|
||||
|
||||
self.obj = kwargs.get('obj', None)
|
||||
self.locales = self.obj.settings.get('locales') if self.obj else kwargs.pop('locales', None)
|
||||
kwargs['attribute_name'] = 'settings'
|
||||
kwargs['locales'] = self.locales
|
||||
kwargs['initial'] = self.obj.settings.freeze()
|
||||
super().__init__(*args, **kwargs)
|
||||
for fname in self.auto_fields:
|
||||
kwargs = DEFAULTS[fname].get('form_kwargs', {})
|
||||
kwargs.setdefault('required', False)
|
||||
field = DEFAULTS[fname]['form_class'](
|
||||
**kwargs
|
||||
)
|
||||
if isinstance(field, i18nfield.forms.I18nFormField):
|
||||
field.widget.enabled_locales = self.locales
|
||||
self.fields[fname] = field
|
||||
for k, f in self.fields.items():
|
||||
if isinstance(f, (RelativeDateTimeField, RelativeDateField)):
|
||||
f.set_event(self.obj)
|
||||
|
||||
def get_new_filename(self, name: str) -> str:
|
||||
from pretix.base.models import Event
|
||||
|
||||
nonce = get_random_string(length=8)
|
||||
if isinstance(self.obj, Event):
|
||||
fname = '%s/%s/%s.%s.%s' % (
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth.password_validation import (
|
||||
password_validators_help_texts, validate_password,
|
||||
)
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.models import User
|
||||
from pretix.helpers.dicts import move_to_end
|
||||
|
||||
|
||||
class LoginForm(forms.Form):
|
||||
@@ -14,32 +14,34 @@ class LoginForm(forms.Form):
|
||||
Base class for authenticating users. Extend this to get a form that accepts
|
||||
username/password logins.
|
||||
"""
|
||||
email = forms.EmailField(label=_("E-mail"), max_length=254, widget=forms.EmailInput(attrs={'autofocus': 'autofocus'}))
|
||||
password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
|
||||
keep_logged_in = forms.BooleanField(label=_("Keep me logged in"), required=False)
|
||||
|
||||
error_messages = {
|
||||
'invalid_login': _("Please enter a correct email address and password."),
|
||||
'invalid_login': _("This combination of credentials is not known to our system."),
|
||||
'inactive': _("This account is inactive.")
|
||||
}
|
||||
|
||||
def __init__(self, request=None, *args, **kwargs):
|
||||
def __init__(self, backend, request=None, *args, **kwargs):
|
||||
"""
|
||||
The 'request' parameter is set for custom auth use by subclasses.
|
||||
The form data comes in via the standard 'data' kwarg.
|
||||
"""
|
||||
self.request = request
|
||||
self.user_cache = None
|
||||
self.backend = backend
|
||||
super().__init__(*args, **kwargs)
|
||||
if not settings.PRETIX_LONG_SESSIONS:
|
||||
for k, f in backend.login_form_fields.items():
|
||||
self.fields[k] = f
|
||||
|
||||
# Authentication backends which use urls cannot have long sessions.
|
||||
if not settings.PRETIX_LONG_SESSIONS or backend.url:
|
||||
del self.fields['keep_logged_in']
|
||||
else:
|
||||
move_to_end(self.fields, 'keep_logged_in')
|
||||
|
||||
def clean(self):
|
||||
email = self.cleaned_data.get('email')
|
||||
password = self.cleaned_data.get('password')
|
||||
|
||||
if email and password:
|
||||
self.user_cache = authenticate(request=self.request, email=email.lower(), password=password)
|
||||
if all(k in self.cleaned_data for k, f in self.fields.items() if f.required):
|
||||
self.user_cache = self.backend.form_authenticate(self.request, self.cleaned_data)
|
||||
if self.user_cache is None:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['invalid_login'],
|
||||
@@ -181,3 +183,45 @@ class PasswordForgotForm(forms.Form):
|
||||
|
||||
def clean_email(self):
|
||||
return self.cleaned_data['email']
|
||||
|
||||
|
||||
class ReauthForm(forms.Form):
|
||||
error_messages = {
|
||||
'invalid_login': _("This combination of credentials is not known to our system."),
|
||||
'inactive': _("This account is inactive.")
|
||||
}
|
||||
|
||||
def __init__(self, backend, user, request=None, *args, **kwargs):
|
||||
"""
|
||||
The 'request' parameter is set for custom auth use by subclasses.
|
||||
The form data comes in via the standard 'data' kwarg.
|
||||
"""
|
||||
self.request = request
|
||||
self.user = user
|
||||
self.backend = backend
|
||||
self.backend.url = backend.authentication_url(self.request)
|
||||
super().__init__(*args, **kwargs)
|
||||
for k, f in backend.login_form_fields.items():
|
||||
self.fields[k] = f
|
||||
if 'email' in self.fields:
|
||||
self.fields['email'].disabled = True
|
||||
|
||||
def clean(self):
|
||||
self.cleaned_data['email'] = self.user.email
|
||||
user_cache = self.backend.form_authenticate(self.request, self.cleaned_data)
|
||||
if user_cache != self.user:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['invalid_login'],
|
||||
code='invalid_login'
|
||||
)
|
||||
else:
|
||||
self.confirm_login_allowed(user_cache)
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
def confirm_login_allowed(self, user: User):
|
||||
if not user.is_active:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['inactive'],
|
||||
code='inactive',
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ import pycountry
|
||||
import pytz
|
||||
import vat_moss.errors
|
||||
import vat_moss.id
|
||||
from babel import localedata
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import ValidationError
|
||||
@@ -17,15 +18,21 @@ from django.forms import Select
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import (
|
||||
get_language, pgettext_lazy, ugettext_lazy as _,
|
||||
get_language, gettext_lazy as _, pgettext_lazy,
|
||||
)
|
||||
from django_countries import countries
|
||||
from django_countries.fields import Country, CountryField
|
||||
from phonenumber_field.formfields import PhoneNumberField
|
||||
from phonenumber_field.phonenumber import PhoneNumber
|
||||
from phonenumber_field.widgets import PhoneNumberPrefixWidget
|
||||
from phonenumbers import NumberParseException
|
||||
from phonenumbers.data import _COUNTRY_CODE_TO_REGION_CODE
|
||||
|
||||
from pretix.base.forms.widgets import (
|
||||
BusinessBooleanRadio, DatePickerWidget, SplitDateTimePickerWidget,
|
||||
TimePickerWidget, UploadedFileWidget,
|
||||
)
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import InvoiceAddress, Question, QuestionOption
|
||||
from pretix.base.models.tax import EU_COUNTRIES, cc_to_vat_prefix
|
||||
from pretix.base.settings import (
|
||||
@@ -34,6 +41,7 @@ from pretix.base.settings import (
|
||||
)
|
||||
from pretix.base.templatetags.rich_text import rich_text
|
||||
from pretix.control.forms import SplitDateTimeField
|
||||
from pretix.helpers.countries import CachedCountries
|
||||
from pretix.helpers.escapejson import escapejson_attr
|
||||
from pretix.helpers.i18n import get_format_without_seconds
|
||||
from pretix.presale.signals import question_form_fields
|
||||
@@ -41,8 +49,19 @@ from pretix.presale.signals import question_form_fields
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
REQUIRED_NAME_PARTS = ['given_name', 'family_name', 'full_name']
|
||||
|
||||
|
||||
class NamePartsWidget(forms.MultiWidget):
|
||||
widget = forms.TextInput
|
||||
autofill_map = {
|
||||
'given_name': 'given-name',
|
||||
'family_name': 'family-name',
|
||||
'middle_name': 'additional-name',
|
||||
'title': 'honorific-prefix',
|
||||
'full_name': 'name',
|
||||
'calling_name': 'nickname',
|
||||
}
|
||||
|
||||
def __init__(self, scheme: dict, field: forms.Field, attrs=None, titles: list=None):
|
||||
widgets = []
|
||||
@@ -83,14 +102,21 @@ class NamePartsWidget(forms.MultiWidget):
|
||||
except (IndexError, TypeError):
|
||||
widget_value = None
|
||||
if id_:
|
||||
final_attrs = dict(
|
||||
these_attrs = dict(
|
||||
final_attrs,
|
||||
id='%s_%s' % (id_, i),
|
||||
title=self.scheme['fields'][i][1],
|
||||
placeholder=self.scheme['fields'][i][1],
|
||||
)
|
||||
final_attrs['data-size'] = self.scheme['fields'][i][2]
|
||||
output.append(widget.render(name + '_%s' % i, widget_value, final_attrs, renderer=renderer))
|
||||
if self.scheme['fields'][i][0] in REQUIRED_NAME_PARTS:
|
||||
if self.field.required:
|
||||
these_attrs['required'] = 'required'
|
||||
these_attrs.pop('data-no-required-attr', None)
|
||||
these_attrs['autocomplete'] = (self.attrs.get('autocomplete', '') + ' ' + self.autofill_map.get(self.scheme['fields'][i][0], 'off')).strip()
|
||||
these_attrs['data-size'] = self.scheme['fields'][i][2]
|
||||
else:
|
||||
these_attrs = final_attrs
|
||||
output.append(widget.render(name + '_%s' % i, widget_value, these_attrs, renderer=renderer))
|
||||
return mark_safe(self.format_output(output))
|
||||
|
||||
def format_output(self, rendered_widgets) -> str:
|
||||
@@ -150,13 +176,49 @@ class NamePartsFormField(forms.MultiValueField):
|
||||
|
||||
def clean(self, value) -> dict:
|
||||
value = super().clean(value)
|
||||
if self.one_required and (not value or not any(v for v in value)):
|
||||
if self.one_required and (not value or not any(v for v in value.values())):
|
||||
raise forms.ValidationError(self.error_messages['required'], code='required')
|
||||
if self.one_required:
|
||||
for k, v in value.items():
|
||||
if k in REQUIRED_NAME_PARTS and not v:
|
||||
raise forms.ValidationError(self.error_messages['required'], code='required')
|
||||
if self.require_all_fields and not all(v for v in value):
|
||||
raise forms.ValidationError(self.error_messages['incomplete'], code='required')
|
||||
return value
|
||||
|
||||
|
||||
class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
|
||||
def render(self, name, value, attrs=None, renderer=None):
|
||||
output = super().render(name, value, attrs, renderer)
|
||||
return mark_safe(self.format_output(output))
|
||||
|
||||
def format_output(self, rendered_widgets) -> str:
|
||||
return '<div class="nameparts-form-group">%s</div>' % ''.join(rendered_widgets)
|
||||
|
||||
|
||||
def guess_country(event):
|
||||
# Try to guess the initial country from either the country of the merchant
|
||||
# or the locale. This will hopefully save at least some users some scrolling :)
|
||||
locale = get_language()
|
||||
country = event.settings.invoice_address_from_country
|
||||
if not country:
|
||||
valid_countries = countries.countries
|
||||
if '-' in locale:
|
||||
parts = locale.split('-')
|
||||
if parts[1].upper() in valid_countries:
|
||||
country = Country(parts[1].upper())
|
||||
elif parts[0].upper() in valid_countries:
|
||||
country = Country(parts[0].upper())
|
||||
else:
|
||||
if locale in valid_countries:
|
||||
country = Country(locale.upper())
|
||||
return country
|
||||
|
||||
|
||||
class QuestionCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
|
||||
option_template_name = 'pretixbase/forms/widgets/checkbox_option_with_links.html'
|
||||
|
||||
|
||||
class BaseQuestionsForm(forms.Form):
|
||||
"""
|
||||
This form class is responsible for asking order-related questions. This includes
|
||||
@@ -184,7 +246,7 @@ class BaseQuestionsForm(forms.Form):
|
||||
if item.admission and event.settings.attendee_names_asked:
|
||||
self.fields['attendee_name_parts'] = NamePartsFormField(
|
||||
max_length=255,
|
||||
required=event.settings.attendee_names_required,
|
||||
required=event.settings.attendee_names_required and not self.all_optional,
|
||||
scheme=event.settings.name_scheme,
|
||||
titles=event.settings.name_scheme_titles,
|
||||
label=_('Attendee name'),
|
||||
@@ -192,10 +254,84 @@ class BaseQuestionsForm(forms.Form):
|
||||
)
|
||||
if item.admission and event.settings.attendee_emails_asked:
|
||||
self.fields['attendee_email'] = forms.EmailField(
|
||||
required=event.settings.attendee_emails_required,
|
||||
required=event.settings.attendee_emails_required and not self.all_optional,
|
||||
label=_('Attendee email'),
|
||||
initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email)
|
||||
initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email),
|
||||
widget=forms.EmailInput(
|
||||
attrs={
|
||||
'autocomplete': 'email'
|
||||
}
|
||||
)
|
||||
)
|
||||
if item.admission and event.settings.attendee_company_asked:
|
||||
self.fields['company'] = forms.CharField(
|
||||
required=event.settings.attendee_company_required and not self.all_optional,
|
||||
label=_('Company'),
|
||||
initial=(cartpos.company if cartpos else orderpos.company),
|
||||
)
|
||||
|
||||
if item.admission and event.settings.attendee_addresses_asked:
|
||||
self.fields['street'] = forms.CharField(
|
||||
required=event.settings.attendee_addresses_required and not self.all_optional,
|
||||
label=_('Address'),
|
||||
widget=forms.Textarea(attrs={
|
||||
'rows': 2,
|
||||
'placeholder': _('Street and Number'),
|
||||
'autocomplete': 'street-address'
|
||||
}),
|
||||
initial=(cartpos.street if cartpos else orderpos.street),
|
||||
)
|
||||
self.fields['zipcode'] = forms.CharField(
|
||||
required=event.settings.attendee_addresses_required and not self.all_optional,
|
||||
label=_('ZIP code'),
|
||||
initial=(cartpos.zipcode if cartpos else orderpos.zipcode),
|
||||
widget=forms.TextInput(attrs={
|
||||
'autocomplete': 'postal-code',
|
||||
}),
|
||||
)
|
||||
self.fields['city'] = forms.CharField(
|
||||
required=event.settings.attendee_addresses_required and not self.all_optional,
|
||||
label=_('City'),
|
||||
initial=(cartpos.city if cartpos else orderpos.city),
|
||||
widget=forms.TextInput(attrs={
|
||||
'autocomplete': 'address-level2',
|
||||
}),
|
||||
)
|
||||
country = (cartpos.country if cartpos else orderpos.country) or guess_country(event)
|
||||
self.fields['country'] = CountryField(
|
||||
countries=CachedCountries
|
||||
).formfield(
|
||||
required=event.settings.attendee_addresses_required and not self.all_optional,
|
||||
label=_('Country'),
|
||||
initial=country,
|
||||
widget=forms.Select(attrs={
|
||||
'autocomplete': 'country',
|
||||
}),
|
||||
)
|
||||
c = [('', pgettext_lazy('address', 'Select state'))]
|
||||
fprefix = str(self.prefix) + '-' if self.prefix is not None and self.prefix != '-' else ''
|
||||
cc = None
|
||||
if fprefix + 'country' in self.data:
|
||||
cc = str(self.data[fprefix + 'country'])
|
||||
elif country:
|
||||
cc = str(country)
|
||||
if cc and cc in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc]
|
||||
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
|
||||
c += sorted([(s.code[3:], s.name) for s in statelist], key=lambda s: s[1])
|
||||
elif fprefix + 'state' in self.data:
|
||||
self.data = self.data.copy()
|
||||
del self.data[fprefix + 'state']
|
||||
|
||||
self.fields['state'] = forms.ChoiceField(
|
||||
label=pgettext_lazy('address', 'State'),
|
||||
required=False,
|
||||
choices=c,
|
||||
widget=forms.Select(attrs={
|
||||
'autocomplete': 'address-level1',
|
||||
}),
|
||||
)
|
||||
self.fields['state'].widget.is_required = True
|
||||
|
||||
for q in questions:
|
||||
# Do we already have an answer? Provide it as the initial value
|
||||
@@ -247,12 +383,14 @@ class BaseQuestionsForm(forms.Form):
|
||||
initial=initial.answer if initial else None,
|
||||
)
|
||||
elif q.type == Question.TYPE_COUNTRYCODE:
|
||||
field = CountryField().formfield(
|
||||
field = CountryField(
|
||||
countries=CachedCountries
|
||||
).formfield(
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
widget=forms.Select,
|
||||
empty_label='',
|
||||
initial=initial.answer if initial else None,
|
||||
initial=initial.answer if initial else guess_country(event),
|
||||
)
|
||||
elif q.type == Question.TYPE_CHOICE:
|
||||
field = forms.ModelChoiceField(
|
||||
@@ -270,7 +408,7 @@ class BaseQuestionsForm(forms.Form):
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
to_field_name='identifier',
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
widget=QuestionCheckboxSelectMultiple,
|
||||
initial=initial.options.all() if initial else None,
|
||||
)
|
||||
elif q.type == Question.TYPE_FILE:
|
||||
@@ -301,6 +439,32 @@ class BaseQuestionsForm(forms.Form):
|
||||
initial=dateutil.parser.parse(initial.answer).astimezone(tz) if initial and initial.answer else None,
|
||||
widget=SplitDateTimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
|
||||
)
|
||||
elif q.type == Question.TYPE_PHONENUMBER:
|
||||
babel_locale = 'en'
|
||||
# Babel, and therefore django-phonenumberfield, do not support our custom locales such das de_Informal
|
||||
if localedata.exists(get_language()):
|
||||
babel_locale = get_language()
|
||||
elif localedata.exists(get_language()[:2]):
|
||||
babel_locale = get_language()[:2]
|
||||
with language(babel_locale):
|
||||
default_country = guess_country(event)
|
||||
default_prefix = None
|
||||
for prefix, values in _COUNTRY_CODE_TO_REGION_CODE.items():
|
||||
if str(default_country) in values:
|
||||
default_prefix = prefix
|
||||
try:
|
||||
initial = PhoneNumber().from_string(initial.answer) if initial else "+{}.".format(default_prefix)
|
||||
except NumberParseException:
|
||||
initial = None
|
||||
field = PhoneNumberField(
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
# We now exploit an implementation detail in PhoneNumberPrefixWidget to allow us to pass just
|
||||
# a country code but no number as an initial value. It's a bit hacky, but should be stable for
|
||||
# the future.
|
||||
initial=initial,
|
||||
widget=WrappedPhoneNumberPrefixWidget()
|
||||
)
|
||||
field.question = q
|
||||
if answers:
|
||||
# Cache the answer object for later use
|
||||
@@ -324,12 +488,22 @@ class BaseQuestionsForm(forms.Form):
|
||||
self.fields[key] = value
|
||||
value.initial = data.get('question_form_data', {}).get(key)
|
||||
|
||||
for k, v in self.fields.items():
|
||||
if v.widget.attrs.get('autocomplete') or k == 'attendee_name_parts':
|
||||
v.widget.attrs['autocomplete'] = 'section-{} '.format(self.prefix) + v.widget.attrs.get('autocomplete', '')
|
||||
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
|
||||
if d.get('city') and d.get('country') and str(d['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||
if not d.get('state'):
|
||||
self.add_error('state', _('This field is required.'))
|
||||
|
||||
question_cache = {f.question.pk: f.question for f in self.fields.values() if getattr(f, 'question', None)}
|
||||
|
||||
def question_is_visible(parentid, qvals):
|
||||
if parentid not in question_cache:
|
||||
return False
|
||||
parentq = question_cache[parentid]
|
||||
if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id, parentq.dependency_values):
|
||||
return False
|
||||
@@ -363,12 +537,28 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = InvoiceAddress
|
||||
fields = ('is_business', 'company', 'name_parts', 'street', 'zipcode', 'city', 'country', 'state',
|
||||
'vat_id', 'internal_reference', 'beneficiary')
|
||||
'vat_id', 'internal_reference', 'beneficiary', 'custom_field')
|
||||
widgets = {
|
||||
'is_business': BusinessBooleanRadio,
|
||||
'street': forms.Textarea(attrs={'rows': 2, 'placeholder': _('Street and Number')}),
|
||||
'street': forms.Textarea(attrs={
|
||||
'rows': 2,
|
||||
'placeholder': _('Street and Number'),
|
||||
'autocomplete': 'street-address'
|
||||
}),
|
||||
'beneficiary': forms.Textarea(attrs={'rows': 3}),
|
||||
'company': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
|
||||
'country': forms.Select(attrs={
|
||||
'autocomplete': 'country',
|
||||
}),
|
||||
'zipcode': forms.TextInput(attrs={
|
||||
'autocomplete': 'postal-code',
|
||||
}),
|
||||
'city': forms.TextInput(attrs={
|
||||
'autocomplete': 'address-level2',
|
||||
}),
|
||||
'company': forms.TextInput(attrs={
|
||||
'data-display-dependency': '#id_is_business_1',
|
||||
'autocomplete': 'organization',
|
||||
}),
|
||||
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
|
||||
'internal_reference': forms.TextInput,
|
||||
}
|
||||
@@ -384,28 +574,14 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
|
||||
kwargs.setdefault('initial', {})
|
||||
if not kwargs.get('instance') or not kwargs['instance'].country:
|
||||
# Try to guess the initial country from either the country of the merchant
|
||||
# or the locale. This will hopefully save at least some users some scrolling :)
|
||||
locale = get_language()
|
||||
country = event.settings.invoice_address_from_country
|
||||
if not country:
|
||||
valid_countries = countries.countries
|
||||
if '-' in locale:
|
||||
parts = locale.split('-')
|
||||
if parts[1].upper() in valid_countries:
|
||||
country = Country(parts[1].upper())
|
||||
elif parts[0].upper() in valid_countries:
|
||||
country = Country(parts[0].upper())
|
||||
else:
|
||||
if locale in valid_countries:
|
||||
country = Country(locale.upper())
|
||||
|
||||
kwargs['initial']['country'] = country
|
||||
kwargs['initial']['country'] = guess_country(self.event)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
if not event.settings.invoice_address_vatid:
|
||||
del self.fields['vat_id']
|
||||
|
||||
self.fields['country'].choices = CachedCountries()
|
||||
|
||||
c = [('', pgettext_lazy('address', 'Select state'))]
|
||||
fprefix = self.prefix + '-' if self.prefix else ''
|
||||
cc = None
|
||||
@@ -426,7 +602,10 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
self.fields['state'] = forms.ChoiceField(
|
||||
label=pgettext_lazy('address', 'State'),
|
||||
required=False,
|
||||
choices=c
|
||||
choices=c,
|
||||
widget=forms.Select(attrs={
|
||||
'autocomplete': 'address-level1',
|
||||
}),
|
||||
)
|
||||
self.fields['state'].widget.is_required = True
|
||||
|
||||
@@ -456,13 +635,23 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
initial=(self.instance.name_parts if self.instance else self.instance.name_parts),
|
||||
)
|
||||
if event.settings.invoice_address_required and not event.settings.invoice_address_company_required and not self.all_optional:
|
||||
self.fields['name_parts'].widget.attrs['data-required-if'] = '#id_is_business_0'
|
||||
if not event.settings.invoice_name_required:
|
||||
self.fields['name_parts'].widget.attrs['data-required-if'] = '#id_is_business_0'
|
||||
self.fields['name_parts'].widget.attrs['data-no-required-attr'] = '1'
|
||||
self.fields['company'].widget.attrs['data-required-if'] = '#id_is_business_1'
|
||||
|
||||
if not event.settings.invoice_address_beneficiary:
|
||||
del self.fields['beneficiary']
|
||||
|
||||
if event.settings.invoice_address_custom_field:
|
||||
self.fields['custom_field'].label = event.settings.invoice_address_custom_field
|
||||
else:
|
||||
del self.fields['custom_field']
|
||||
|
||||
for k, v in self.fields.items():
|
||||
if v.widget.attrs.get('autocomplete') or k == 'name_parts':
|
||||
v.widget.attrs['autocomplete'] = 'section-invoice billing ' + v.widget.attrs.get('autocomplete', '')
|
||||
|
||||
def clean(self):
|
||||
data = self.cleaned_data
|
||||
if not data.get('is_business'):
|
||||
@@ -522,9 +711,8 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
|
||||
|
||||
class BaseInvoiceNameForm(BaseInvoiceAddressForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for f in list(self.fields.keys()):
|
||||
if f != 'name':
|
||||
if f != 'name_parts':
|
||||
del self.fields[f]
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.contrib.auth.password_validation import (
|
||||
password_validators_help_texts, validate_password,
|
||||
)
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from pytz import common_timezones
|
||||
|
||||
from pretix.base.models import User
|
||||
@@ -56,6 +56,11 @@ class UserSettingsForm(forms.ModelForm):
|
||||
self.user = kwargs.pop('user')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['email'].required = True
|
||||
if self.user.auth_backend != 'native':
|
||||
del self.fields['old_pw']
|
||||
del self.fields['new_pw']
|
||||
del self.fields['new_pw_repeat']
|
||||
self.fields['email'].disabled = True
|
||||
|
||||
def clean_old_pw(self):
|
||||
old_pw = self.cleaned_data.get('old_pw')
|
||||
|
||||
@@ -2,7 +2,7 @@ import re
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import BaseValidator
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ class PlaceholderValidator(BaseValidator):
|
||||
if value.count('{') != value.count('}'):
|
||||
raise ValidationError(
|
||||
_('Invalid placeholder syntax: You used a different number of "{" than of "}".'),
|
||||
code='invalid',
|
||||
code='invalid_placeholder_syntax',
|
||||
)
|
||||
|
||||
data_placeholders = list(re.findall(r'({[^}]*})', value, re.X))
|
||||
@@ -37,7 +37,7 @@ class PlaceholderValidator(BaseValidator):
|
||||
if invalid_placeholders:
|
||||
raise ValidationError(
|
||||
_('Invalid placeholder(s): %(value)s'),
|
||||
code='invalid',
|
||||
code='invalid_placeholders',
|
||||
params={'value': ", ".join(invalid_placeholders,)})
|
||||
|
||||
def clean(self, x):
|
||||
|
||||
@@ -4,10 +4,7 @@ from django import forms
|
||||
from django.utils.formats import get_format
|
||||
from django.utils.functional import lazy
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import OrderPosition
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class DatePickerWidget(forms.DateInput):
|
||||
@@ -46,6 +43,14 @@ class TimePickerWidget(forms.TimeInput):
|
||||
|
||||
class UploadedFileWidget(forms.ClearableFileInput):
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Browsers can't recognize that the server already has a file uploaded
|
||||
# Don't mark this input as being required if we already have an answer
|
||||
# (this needs to be done via the attrs, otherwise we wouldn't get the "required" star on the field label)
|
||||
attrs = kwargs.get('attrs', {})
|
||||
if kwargs.get('required') and kwargs.get('initial'):
|
||||
attrs.update({'required': None})
|
||||
kwargs.update({'attrs': attrs})
|
||||
|
||||
self.position = kwargs.pop('position')
|
||||
self.event = kwargs.pop('event')
|
||||
self.answer = kwargs.pop('answer')
|
||||
@@ -63,6 +68,9 @@ class UploadedFileWidget(forms.ClearableFileInput):
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
from pretix.base.models import OrderPosition
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
|
||||
if isinstance(self.position, OrderPosition):
|
||||
return eventreverse(self.event, 'presale:event.order.download.answer', kwargs={
|
||||
'order': self.position.order.code,
|
||||
|
||||
@@ -3,7 +3,7 @@ from contextlib import contextmanager
|
||||
from django.conf import settings
|
||||
from django.utils import translation
|
||||
from django.utils.formats import date_format, number_format
|
||||
from django.utils.translation import ugettext
|
||||
from django.utils.translation import gettext
|
||||
from i18nfield.fields import ( # noqa
|
||||
I18nCharField, I18nTextarea, I18nTextField, I18nTextInput,
|
||||
)
|
||||
@@ -69,6 +69,6 @@ class LazyLocaleException(Exception):
|
||||
|
||||
def __str__(self):
|
||||
if self.msgargs:
|
||||
return ugettext(self.msg) % self.msgargs
|
||||
return gettext(self.msg) % self.msgargs
|
||||
else:
|
||||
return ugettext(self.msg)
|
||||
return gettext(self.msg)
|
||||
|
||||
@@ -10,7 +10,7 @@ from django.contrib.staticfiles import finders
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import date_format, localize
|
||||
from django.utils.translation import (
|
||||
get_language, pgettext, ugettext, ugettext_lazy,
|
||||
get_language, gettext, gettext_lazy, pgettext,
|
||||
)
|
||||
from PIL.Image import BICUBIC
|
||||
from reportlab.lib import pagesizes
|
||||
@@ -28,7 +28,7 @@ from reportlab.platypus import (
|
||||
)
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models import Event, Invoice
|
||||
from pretix.base.models import Event, Invoice, Order
|
||||
from pretix.base.signals import register_invoice_renderers
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
|
||||
@@ -264,7 +264,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
invoice_to_top = 52 * mm
|
||||
|
||||
def _draw_invoice_to(self, canvas):
|
||||
p = Paragraph(self.invoice.address_invoice_to.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
||||
p = Paragraph(bleach.clean(self.invoice.address_invoice_to, tags=[]).strip().replace('\n', '<br />\n'),
|
||||
style=self.stylesheet['Normal'])
|
||||
p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height)
|
||||
p_size = p.wrap(self.invoice_to_width, self.invoice_to_height)
|
||||
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - p_size[1] - self.invoice_to_top)
|
||||
@@ -422,7 +423,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
canvas.saveState()
|
||||
canvas.setFont('OpenSansBd', 30)
|
||||
canvas.setFillColorRGB(32, 0, 0)
|
||||
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, ugettext('TEST MODE'))
|
||||
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, gettext('TEST MODE'))
|
||||
canvas.restoreState()
|
||||
|
||||
def _on_first_page(self, canvas: Canvas, doc):
|
||||
@@ -457,18 +458,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
id='normal')
|
||||
]
|
||||
|
||||
def _get_story(self, doc):
|
||||
has_taxes = any(il.tax_value for il in self.invoice.lines.all())
|
||||
|
||||
story = [
|
||||
NextPageTemplate('FirstPage'),
|
||||
Paragraph(pgettext('invoice', 'Invoice')
|
||||
if not self.invoice.is_cancellation
|
||||
else pgettext('invoice', 'Cancellation'),
|
||||
self.stylesheet['Heading1']),
|
||||
Spacer(1, 5 * mm),
|
||||
NextPageTemplate('OtherPages'),
|
||||
]
|
||||
def _get_intro(self):
|
||||
story = []
|
||||
if self.invoice.custom_field:
|
||||
story.append(Paragraph(
|
||||
'{}: {}'.format(self.invoice.event.settings.invoice_address_custom_field, self.invoice.custom_field),
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
|
||||
if self.invoice.internal_reference:
|
||||
story.append(Paragraph(
|
||||
@@ -478,7 +474,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
|
||||
if self.invoice.invoice_to_vat_id:
|
||||
story.append(Paragraph(
|
||||
pgettext('invoice', 'Customer VAT ID') + ':<br />' +
|
||||
pgettext('invoice', 'Customer VAT ID') + ': ' +
|
||||
bleach.clean(self.invoice.invoice_to_vat_id, tags=[]).replace("\n", "<br />\n"),
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
@@ -494,6 +490,25 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
story.append(Paragraph(self.invoice.introductory_text, self.stylesheet['Normal']))
|
||||
story.append(Spacer(1, 10 * mm))
|
||||
|
||||
return story
|
||||
|
||||
def _get_story(self, doc):
|
||||
has_taxes = any(il.tax_value for il in self.invoice.lines.all())
|
||||
|
||||
story = [
|
||||
NextPageTemplate('FirstPage'),
|
||||
Paragraph(
|
||||
(
|
||||
pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU'
|
||||
else pgettext('invoice', 'Invoice')
|
||||
) if not self.invoice.is_cancellation else pgettext('invoice', 'Cancellation'),
|
||||
self.stylesheet['Heading1']
|
||||
),
|
||||
Spacer(1, 5 * mm),
|
||||
NextPageTemplate('OtherPages'),
|
||||
]
|
||||
story += self._get_intro()
|
||||
|
||||
taxvalue_map = defaultdict(Decimal)
|
||||
grossvalue_map = defaultdict(Decimal)
|
||||
|
||||
@@ -551,6 +566,20 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
])
|
||||
colwidths = [a * doc.width for a in (.65, .05, .30)]
|
||||
|
||||
if self.invoice.event.settings.invoice_show_payments and not self.invoice.is_cancellation and \
|
||||
self.invoice.order.status == Order.STATUS_PENDING:
|
||||
pending_sum = self.invoice.order.pending_sum
|
||||
if pending_sum != total:
|
||||
tdata.append([pgettext('invoice', 'Received payments')] + (['', '', ''] if has_taxes else ['']) + [
|
||||
money_filter(pending_sum - total, self.invoice.event.currency)
|
||||
])
|
||||
tdata.append([pgettext('invoice', 'Outstanding payments')] + (['', '', ''] if has_taxes else ['']) + [
|
||||
money_filter(pending_sum, self.invoice.event.currency)
|
||||
])
|
||||
tstyledata += [
|
||||
('FONTNAME', (0, len(tdata) - 3), (-1, len(tdata) - 3), self.font_bold),
|
||||
]
|
||||
|
||||
table = Table(tdata, colWidths=colwidths, repeatRows=1)
|
||||
table.setStyle(TableStyle(tstyledata))
|
||||
story.append(table)
|
||||
@@ -659,7 +688,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
|
||||
class Modern1Renderer(ClassicInvoiceRenderer):
|
||||
identifier = 'modern1'
|
||||
verbose_name = ugettext_lazy('Modern Invoice Renderer (pretix 2.7)')
|
||||
verbose_name = gettext_lazy('Modern Invoice Renderer (pretix 2.7)')
|
||||
bottom_margin = 16.9 * mm
|
||||
top_margin = 16.9 * mm
|
||||
right_margin = 20 * mm
|
||||
|
||||
@@ -23,7 +23,7 @@ modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("default_manager_name")
|
||||
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("permissions")
|
||||
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("default_permissions")
|
||||
IGNORED_ATTRS = [
|
||||
# (field type, attribute name, blacklist of field sub-types)
|
||||
# (field type, attribute name, banlist of field sub-types)
|
||||
(models.Field, 'verbose_name', []),
|
||||
(models.Field, 'help_text', []),
|
||||
(models.Field, 'validators', []),
|
||||
@@ -38,8 +38,8 @@ original_deconstruct = models.Field.deconstruct
|
||||
|
||||
def new_deconstruct(self):
|
||||
name, path, args, kwargs = original_deconstruct(self)
|
||||
for ftype, attr, blacklist in IGNORED_ATTRS:
|
||||
if isinstance(self, ftype) and not any(isinstance(self, ft) for ft in blacklist):
|
||||
for ftype, attr, banlist in IGNORED_ATTRS:
|
||||
if isinstance(self, ftype) and not any(isinstance(self, ft) for ft in banlist):
|
||||
kwargs.pop(attr, None)
|
||||
return name, path, args, kwargs
|
||||
|
||||
|
||||
@@ -11,13 +11,13 @@ from django.core.management.commands.migrate import Command as Parent
|
||||
|
||||
|
||||
class OutputFilter(OutputWrapper):
|
||||
blacklist = (
|
||||
banlist = (
|
||||
"Your models have changes that are not yet reflected",
|
||||
"Run 'manage.py makemigrations' to make new "
|
||||
)
|
||||
|
||||
def write(self, msg, style_func=None, ending=None):
|
||||
if any(b in msg for b in self.blacklist):
|
||||
if any(b in msg for b in self.banlist):
|
||||
return
|
||||
super().write(msg, style_func, ending)
|
||||
|
||||
|
||||
@@ -13,11 +13,17 @@ class Command(BaseCommand):
|
||||
return parser
|
||||
|
||||
def handle(self, *args, **options):
|
||||
try:
|
||||
from django_extensions.management.commands import shell_plus # noqa
|
||||
cmd = 'shell_plus'
|
||||
except ImportError:
|
||||
cmd = 'shell'
|
||||
|
||||
parser = self.create_parser(sys.argv[0], sys.argv[1])
|
||||
flags = parser.parse_known_args(sys.argv[2:])[1]
|
||||
if "--override" in flags:
|
||||
with scopes_disabled():
|
||||
return call_command("shell_plus", *args, **options)
|
||||
return call_command(cmd, *args, **options)
|
||||
|
||||
lookups = {}
|
||||
for flag in flags:
|
||||
@@ -36,4 +42,4 @@ class Command(BaseCommand):
|
||||
for app_name, app_value in lookups.items()
|
||||
}
|
||||
with scope(**scope_options):
|
||||
return call_command("shell_plus", *args, **options)
|
||||
return call_command(cmd, *args, **options)
|
||||
|
||||
@@ -14,7 +14,10 @@ from django.utils.translation.trans_real import (
|
||||
parse_accept_lang_header,
|
||||
)
|
||||
|
||||
from pretix.multidomain.urlreverse import get_domain
|
||||
from pretix.base.settings import GlobalSettingsObject
|
||||
from pretix.multidomain.urlreverse import (
|
||||
get_event_domain, get_organizer_domain,
|
||||
)
|
||||
|
||||
_supported = None
|
||||
|
||||
@@ -187,6 +190,11 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
# https://github.com/pretix/pretix/issues/765
|
||||
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
|
||||
|
||||
img_src = []
|
||||
gs = GlobalSettingsObject()
|
||||
if gs.settings.leaflet_tiles:
|
||||
img_src.append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*"))
|
||||
|
||||
h = {
|
||||
'default-src': ["{static}"],
|
||||
'script-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
|
||||
@@ -196,7 +204,7 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
'child-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
|
||||
'style-src': ["{static}", "{media}"],
|
||||
'connect-src': ["{dynamic}", "{media}", "https://checkout.stripe.com"],
|
||||
'img-src': ["{static}", "{media}", "data:", "https://*.stripe.com"],
|
||||
'img-src': ["{static}", "{media}", "data:", "https://*.stripe.com"] + img_src,
|
||||
'font-src': ["{static}"],
|
||||
'media-src': ["{static}", "data:"],
|
||||
# form-action is not only used to match on form actions, but also on URLs
|
||||
@@ -225,7 +233,10 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
dynamicdomain += " " + settings.SITE_URL
|
||||
|
||||
if hasattr(request, 'organizer') and request.organizer:
|
||||
domain = get_domain(request.organizer)
|
||||
if hasattr(request, 'event') and request.event:
|
||||
domain = get_event_domain(request.event, fallback=True)
|
||||
else:
|
||||
domain = get_organizer_domain(request.organizer)
|
||||
if domain:
|
||||
siteurlsplit = urlsplit(settings.SITE_URL)
|
||||
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
|
||||
|
||||
@@ -182,12 +182,12 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='slug',
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Slug'),
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Slug'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='organizer',
|
||||
name='slug',
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Slug'),
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Slug'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voucher',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user