forked from CGM_Public/pretix_original
Compare commits
1146 Commits
release/1.
...
loadtest
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05e9d09e1f | ||
|
|
93947cace0 | ||
|
|
d4eac76a8d | ||
|
|
8889607d1c | ||
|
|
5e9e00acec | ||
|
|
0e89d4c0f7 | ||
|
|
8b3ce69425 | ||
|
|
b20d1e8373 | ||
|
|
c278687487 | ||
|
|
0c45e73456 | ||
|
|
104f84b7a8 | ||
|
|
ac4ecfbe69 | ||
|
|
61c6cd2937 | ||
|
|
38066ca5ab | ||
|
|
373ab29701 | ||
|
|
7302bba602 | ||
|
|
5096121ac7 | ||
|
|
ca4c21a843 | ||
|
|
407ecdf6c5 | ||
|
|
2faeee8e9c | ||
|
|
e1bbf7139f | ||
|
|
64fc38a06e | ||
|
|
6bcf884b7a | ||
|
|
d319293da8 | ||
|
|
832c58d288 | ||
|
|
c251e0e7d3 | ||
|
|
27437e065a | ||
|
|
86534aa7cc | ||
|
|
379a2140c8 | ||
|
|
67059fe323 | ||
|
|
8ffc96bf31 | ||
|
|
58b688628e | ||
|
|
3f7348717b | ||
|
|
90c8e0c172 | ||
|
|
d35ad345d7 | ||
|
|
21634369a8 | ||
|
|
a2b075c0d7 | ||
|
|
0617abe6e3 | ||
|
|
040466353c | ||
|
|
46b7e9467b | ||
|
|
283ff3b5e5 | ||
|
|
b0bb22ea38 | ||
|
|
334ee98318 | ||
|
|
c4d342029b | ||
|
|
bc86f9c059 | ||
|
|
51107fe4fd | ||
|
|
3d65c2fd51 | ||
|
|
9b394b3833 | ||
|
|
d5747084ec | ||
|
|
777772b89e | ||
|
|
c202286470 | ||
|
|
0c1738b9bb | ||
|
|
af607083cb | ||
|
|
def7918b29 | ||
|
|
0933fc848d | ||
|
|
166f8b8a2a | ||
|
|
70fcba96a5 | ||
|
|
2d2d62045a | ||
|
|
3988f1e2f6 | ||
|
|
d3ecb92108 | ||
|
|
b3debdfb55 | ||
|
|
abb770a8e7 | ||
|
|
72a2d0da35 | ||
|
|
937cec53f7 | ||
|
|
6e4af5da64 | ||
|
|
7ed35e06ba | ||
|
|
55841ea660 | ||
|
|
78544cdb30 | ||
|
|
37183aced7 | ||
|
|
a7d3cb134c | ||
|
|
da8f7f163f | ||
|
|
89d612beed | ||
|
|
f23de7e2c0 | ||
|
|
d073007fd7 | ||
|
|
d9d1c83218 | ||
|
|
ae9b8bafb8 | ||
|
|
cbf5c2ec1d | ||
|
|
17392f3ef4 | ||
|
|
bf36ad009f | ||
|
|
ca9e4823e2 | ||
|
|
d505422e0f | ||
|
|
33c43ce482 | ||
|
|
f273cf4960 | ||
|
|
afdf09eeb4 | ||
|
|
01e5872f61 | ||
|
|
14cc31c810 | ||
|
|
2972129547 | ||
|
|
ec4227651a | ||
|
|
77950de588 | ||
|
|
187576eee5 | ||
|
|
0e513a0985 | ||
|
|
1cde728ffe | ||
|
|
76893caffc | ||
|
|
a539999c04 | ||
|
|
b9c570b3d8 | ||
|
|
48b399424a | ||
|
|
1c73f000a9 | ||
|
|
d0721165c1 | ||
|
|
bed0a0ceeb | ||
|
|
b53ee1dc1d | ||
|
|
41b56c00e5 | ||
|
|
cb17febf7c | ||
|
|
07d42a4d77 | ||
|
|
e3ebf887a4 | ||
|
|
0440187e59 | ||
|
|
dfcda0fa2c | ||
|
|
560c0a8729 | ||
|
|
bc80b60b04 | ||
|
|
08bf3648ea | ||
|
|
f8ee7acad6 | ||
|
|
10c86869ea | ||
|
|
9034a98df9 | ||
|
|
a7142fdf55 | ||
|
|
ee97c46aec | ||
|
|
7063f32f24 | ||
|
|
2ec926b7c7 | ||
|
|
834b5a26a5 | ||
|
|
90f08d0aca | ||
|
|
d5c2637198 | ||
|
|
f517ba51bd | ||
|
|
d738198ec5 | ||
|
|
b1ce58d06c | ||
|
|
b26ef74128 | ||
|
|
4f8c8ea917 | ||
|
|
0803b049af | ||
|
|
97f3fbdb80 | ||
|
|
434b6e4729 | ||
|
|
f56bceb55f | ||
|
|
2aa246b3d5 | ||
|
|
f77b551aa6 | ||
|
|
c9415cba2b | ||
|
|
4dae224d73 | ||
|
|
13cc57e98b | ||
|
|
6f980b82ac | ||
|
|
f32c581a9e | ||
|
|
fcadfffb92 | ||
|
|
9e43459879 | ||
|
|
87424c25de | ||
|
|
acdf7d62b5 | ||
|
|
944138f7a9 | ||
|
|
5da2eab1fb | ||
|
|
d680937a6c | ||
|
|
f35c2544b6 | ||
|
|
0285cd12f7 | ||
|
|
03cacace57 | ||
|
|
6ed016e49e | ||
|
|
da8da01614 | ||
|
|
9a2ea6699a | ||
|
|
51a8bac9e6 | ||
|
|
303ed07504 | ||
|
|
c7627f631f | ||
|
|
604c31c6e2 | ||
|
|
c3da6731a1 | ||
|
|
6e556ab09b | ||
|
|
16622883f6 | ||
|
|
cce4379d3e | ||
|
|
5af99f4f1a | ||
|
|
9ed49888b8 | ||
|
|
5bfb00db73 | ||
|
|
a031d72ca9 | ||
|
|
15a190cdf3 | ||
|
|
d181375479 | ||
|
|
d8a57b0baa | ||
|
|
d482bc9de0 | ||
|
|
5c030796d7 | ||
|
|
f6eb3bfb80 | ||
|
|
3703fbcacf | ||
|
|
cdea6eb55e | ||
|
|
bf1e9d47d0 | ||
|
|
350df2a3cc | ||
|
|
bc6915b251 | ||
|
|
f9c7eeff9a | ||
|
|
247bcf0a20 | ||
|
|
455c961fc7 | ||
|
|
9052d4a7a9 | ||
|
|
589401e8d2 | ||
|
|
0c366a8473 | ||
|
|
c9ddbd0e88 | ||
|
|
31bf0c24f1 | ||
|
|
c74386346b | ||
|
|
725e1f019e | ||
|
|
06eddb2c6d | ||
|
|
80b5750756 | ||
|
|
f37d265534 | ||
|
|
7c4a1e5fb8 | ||
|
|
9a045c76ec | ||
|
|
447b36fdd3 | ||
|
|
5dbd984178 | ||
|
|
95f96f8321 | ||
|
|
3933032778 | ||
|
|
d0b18d9f64 | ||
|
|
71de71ed37 | ||
|
|
3438d079d5 | ||
|
|
e7730333c2 | ||
|
|
e8b9f0a3ae | ||
|
|
77ebd18404 | ||
|
|
2d48198c83 | ||
|
|
d103b0bb84 | ||
|
|
01411b84e4 | ||
|
|
b7e154d8c9 | ||
|
|
f39ac96322 | ||
|
|
74db808978 | ||
|
|
ab72b93706 | ||
|
|
af5aece639 | ||
|
|
228ab15900 | ||
|
|
66164d8202 | ||
|
|
d5ac155914 | ||
|
|
75a966529e | ||
|
|
28a6a6185d | ||
|
|
07cdaa9ca9 | ||
|
|
1c6935ebd9 | ||
|
|
60c1ea8aad | ||
|
|
0b8798a65c | ||
|
|
a8836cbeec | ||
|
|
336a34b10b | ||
|
|
c5862cc0a0 | ||
|
|
89cdcd3781 | ||
|
|
2837cac554 | ||
|
|
3b54556739 | ||
|
|
4d6d6ff737 | ||
|
|
ffee31e415 | ||
|
|
8abfbba9d0 | ||
|
|
588955901c | ||
|
|
4b7bf2f27f | ||
|
|
664957e886 | ||
|
|
f15a6d39c3 | ||
|
|
3fd80a9a46 | ||
|
|
2fd2716303 | ||
|
|
37315fc380 | ||
|
|
f96fc0744e | ||
|
|
5bb7883020 | ||
|
|
3f95434922 | ||
|
|
08da5a8b91 | ||
|
|
97dc4421ea | ||
|
|
26ca2ff006 | ||
|
|
980c359f57 | ||
|
|
ff1198dec6 | ||
|
|
7275de94af | ||
|
|
ed46f41f8c | ||
|
|
1078e38890 | ||
|
|
2e9bbff308 | ||
|
|
13a48701fa | ||
|
|
ddc9c850c0 | ||
|
|
fa0dae6ed6 | ||
|
|
da6176a51e | ||
|
|
4ef6659551 | ||
|
|
82624a1dc0 | ||
|
|
b50add260a | ||
|
|
f72f97d366 | ||
|
|
ad46e9e541 | ||
|
|
343dbc00be | ||
|
|
3cb94f702d | ||
|
|
ddeae224fb | ||
|
|
3c57895101 | ||
|
|
687c85eb58 | ||
|
|
90ffdbdfa3 | ||
|
|
654be0db34 | ||
|
|
82e3359b40 | ||
|
|
01a6861453 | ||
|
|
7f6cdd6241 | ||
|
|
aad1fda31f | ||
|
|
ad462921f0 | ||
|
|
dc433f6420 | ||
|
|
2d8b3d1c79 | ||
|
|
eb85fa956e | ||
|
|
215514fca7 | ||
|
|
3fe2dfe810 | ||
|
|
041d05eb66 | ||
|
|
d05530ddfc | ||
|
|
734e77d1a3 | ||
|
|
633061e203 | ||
|
|
e11ee4a427 | ||
|
|
1edcd47703 | ||
|
|
cf4b2544f2 | ||
|
|
04c3cffd43 | ||
|
|
483d41c7a6 | ||
|
|
b0c4c88d01 | ||
|
|
518298f71c | ||
|
|
62c2e7765b | ||
|
|
2bb2a40509 | ||
|
|
49828186b0 | ||
|
|
c07a6cb4aa | ||
|
|
67ad9a0dcb | ||
|
|
d267dfc682 | ||
|
|
eed220f14a | ||
|
|
85289fe0d1 | ||
|
|
6293ad34d4 | ||
|
|
0dc4f61cf0 | ||
|
|
6849e682d7 | ||
|
|
6b725a9db9 | ||
|
|
989ebbb444 | ||
|
|
0a6efc1e0f | ||
|
|
d577a0d286 | ||
|
|
6b9b379ce2 | ||
|
|
13234b6fd5 | ||
|
|
2fa0067663 | ||
|
|
4e37fa5778 | ||
|
|
bfb74448b1 | ||
|
|
a255082b07 | ||
|
|
14df35bd90 | ||
|
|
bd0ba7baa5 | ||
|
|
9aa220b95b | ||
|
|
3ed4be63fe | ||
|
|
23f4b0b62f | ||
|
|
4b9acb64da | ||
|
|
ebba0ee0cb | ||
|
|
335ce48d7e | ||
|
|
d9a0c8c523 | ||
|
|
a297bd1944 | ||
|
|
953ea26984 | ||
|
|
e4f80f7660 | ||
|
|
128a185957 | ||
|
|
0bdd14b47a | ||
|
|
3b84b181ad | ||
|
|
c9b0626324 | ||
|
|
dc9a82cade | ||
|
|
8266733e34 | ||
|
|
246987955b | ||
|
|
b93e7fcb60 | ||
|
|
b1cebdbd99 | ||
|
|
d04047abd5 | ||
|
|
efca46945a | ||
|
|
0f9755e36f | ||
|
|
478d8e4116 | ||
|
|
81693e042c | ||
|
|
47b7d7b36c | ||
|
|
ba15c34ce1 | ||
|
|
94f2ad9325 | ||
|
|
d8070ba8a3 | ||
|
|
b1019672b0 | ||
|
|
631307a4d5 | ||
|
|
180a26ee1d | ||
|
|
7eab1982fe | ||
|
|
ca59237ebf | ||
|
|
cc92210dc2 | ||
|
|
6602afdd6c | ||
|
|
c7a04bc08a | ||
|
|
2cc5b7f4e8 | ||
|
|
453f16af03 | ||
|
|
0f3398ae13 | ||
|
|
f1b65c8695 | ||
|
|
2c4c89c8c2 | ||
|
|
4042b603b7 | ||
|
|
63b0288383 | ||
|
|
7c01fee70b | ||
|
|
8127c32ef5 | ||
|
|
563decdfba | ||
|
|
a205b01d70 | ||
|
|
b4290384e1 | ||
|
|
0f76779fb1 | ||
|
|
f34c528cba | ||
|
|
cf01e04101 | ||
|
|
a3a63def55 | ||
|
|
a3489eea04 | ||
|
|
c6cb98c30a | ||
|
|
332c58c82f | ||
|
|
beb0ded6dc | ||
|
|
b49b2035bd | ||
|
|
106c8d373d | ||
|
|
aee44a3284 | ||
|
|
d4c1fcf838 | ||
|
|
832f57c9d7 | ||
|
|
ac2a9b207d | ||
|
|
f1e5d60a14 | ||
|
|
7b1a1dc754 | ||
|
|
c93f804992 | ||
|
|
1cba4b1d45 | ||
|
|
22369a5559 | ||
|
|
a8223ad354 | ||
|
|
c9d3cf7cac | ||
|
|
bbdbc94f6e | ||
|
|
5c8d9c4dca | ||
|
|
546ff6e42f | ||
|
|
7b7d45ce2e | ||
|
|
be3ca7c561 | ||
|
|
abdb6e2d52 | ||
|
|
138ddcdcd7 | ||
|
|
8ffc6550da | ||
|
|
0734715bab | ||
|
|
7528bfb10b | ||
|
|
2798fb3468 | ||
|
|
4e6f4716ec | ||
|
|
e523a4e610 | ||
|
|
31cec76809 | ||
|
|
fdfd9f9275 | ||
|
|
b658c73c19 | ||
|
|
ebd3e6f31a | ||
|
|
ccec114653 | ||
|
|
f0716dc482 | ||
|
|
513778b2c4 | ||
|
|
742e403ae2 | ||
|
|
09a9d610f8 | ||
|
|
b9534f23f5 | ||
|
|
b053f61001 | ||
|
|
21042f2111 | ||
|
|
e953474138 | ||
|
|
0d438ad07c | ||
|
|
e285b7cff0 | ||
|
|
2bb2f30e66 | ||
|
|
9a8d23f582 | ||
|
|
f37d12e056 | ||
|
|
334ffc0be7 | ||
|
|
03f0da4ee6 | ||
|
|
fbbd6eebc0 | ||
|
|
584ced87db | ||
|
|
901953d988 | ||
|
|
8c34a47138 | ||
|
|
0fe3db634c | ||
|
|
d8d838fc4f | ||
|
|
9b94a1b3b2 | ||
|
|
479abc1a65 | ||
|
|
1a17ba13ca | ||
|
|
371c42b738 | ||
|
|
ed85394845 | ||
|
|
a9a684a456 | ||
|
|
d7d7792a4a | ||
|
|
c09587f5d3 | ||
|
|
23f719381c | ||
|
|
d74d39d6e9 | ||
|
|
5f2cf8d3ef | ||
|
|
1843799345 | ||
|
|
bd838b3b7c | ||
|
|
c2d03f5e6b | ||
|
|
74e8e73877 | ||
|
|
8830dc8f78 | ||
|
|
ac877a7c0d | ||
|
|
0a442e712b | ||
|
|
4477f8001e | ||
|
|
152b94428f | ||
|
|
5390b0b191 | ||
|
|
97de8cea08 | ||
|
|
cd465c1aad | ||
|
|
449dea41a8 | ||
|
|
0b1a6e4745 | ||
|
|
e49061e28c | ||
|
|
18cb29b425 | ||
|
|
994ff23719 | ||
|
|
15d077df6e | ||
|
|
b490aa7f5d | ||
|
|
ca6b3badde | ||
|
|
1f200271af | ||
|
|
894a60d016 | ||
|
|
4a2219134b | ||
|
|
7d38fc5c03 | ||
|
|
ef5de187b9 | ||
|
|
a1c424266b | ||
|
|
557b4b7b6f | ||
|
|
98be21253d | ||
|
|
e5a04ada94 | ||
|
|
9b8b3090e6 | ||
|
|
e622c3948d | ||
|
|
94be46ffdb | ||
|
|
7039374588 | ||
|
|
0a5347c08b | ||
|
|
87f3318431 | ||
|
|
2557a8e4ec | ||
|
|
aff7094cb0 | ||
|
|
5a29b4bf70 | ||
|
|
e618183b49 | ||
|
|
a18236b12d | ||
|
|
b5da4e89a6 | ||
|
|
1da2737427 | ||
|
|
032fdadc3c | ||
|
|
8ae3ff3fe6 | ||
|
|
b8669503fa | ||
|
|
863165caaa | ||
|
|
b885f30789 | ||
|
|
461b62bd51 | ||
|
|
23776db3b6 | ||
|
|
19e91a6c7c | ||
|
|
6f40325d3f | ||
|
|
1987bff4b1 | ||
|
|
5aa0d55d47 | ||
|
|
a28196e930 | ||
|
|
c55387819d | ||
|
|
c8cc527aee | ||
|
|
a39b207ad5 | ||
|
|
ea63b50f2e | ||
|
|
b101251aa4 | ||
|
|
c9ba72ebc5 | ||
|
|
4a1c3088a9 | ||
|
|
a480ca1142 | ||
|
|
a928fbfafe | ||
|
|
3bf3ff1ee2 | ||
|
|
9647cc6cf2 | ||
|
|
df2d8925ed | ||
|
|
7a945daefc | ||
|
|
409e77cf2f | ||
|
|
552f99a63b | ||
|
|
0842311451 | ||
|
|
4d4b498636 | ||
|
|
d08cc12240 | ||
|
|
237442872e | ||
|
|
16983826fb | ||
|
|
e60ff6b777 | ||
|
|
3a0ef3760c | ||
|
|
bc0bc78219 | ||
|
|
d3137505a1 | ||
|
|
a2acd336eb | ||
|
|
6e4750336b | ||
|
|
ddefeeaf02 | ||
|
|
250e0a930d | ||
|
|
51c6d60760 | ||
|
|
db513b21f8 | ||
|
|
ab336678ce | ||
|
|
3eea4d6945 | ||
|
|
d091d3fd17 | ||
|
|
fc71f484ad | ||
|
|
bd772bf900 | ||
|
|
14db654681 | ||
|
|
a85b96ea89 | ||
|
|
c2b5e876bc | ||
|
|
91c02dc0b3 | ||
|
|
f78ec830b5 | ||
|
|
9f0e508ab3 | ||
|
|
4ca50d750b | ||
|
|
07c1b1b7f3 | ||
|
|
3e95dd52cf | ||
|
|
80ef2f6b0e | ||
|
|
53a8cda310 | ||
|
|
63de49104c | ||
|
|
8aa80bcb84 | ||
|
|
95115a7c5e | ||
|
|
ce2967fd02 | ||
|
|
399fb87d20 | ||
|
|
c4bd5ac5df | ||
|
|
123c2d6c02 | ||
|
|
6954e9c984 | ||
|
|
fc573e4e48 | ||
|
|
0dbcfdc5ac | ||
|
|
4b8d4b4792 | ||
|
|
d798da33ef | ||
|
|
d99517c8d1 | ||
|
|
0787adcb8e | ||
|
|
f848561d25 | ||
|
|
efbddc2486 | ||
|
|
e6a138d8f2 | ||
|
|
5b7a578307 | ||
|
|
737738de93 | ||
|
|
eb3951ce13 | ||
|
|
c2b7d9a257 | ||
|
|
4738aa2771 | ||
|
|
29ac0af55e | ||
|
|
96bc64c456 | ||
|
|
0369deb72d | ||
|
|
6e53990845 | ||
|
|
feb262644e | ||
|
|
abd679820f | ||
|
|
cd3ce848d1 | ||
|
|
63ba393c12 | ||
|
|
23fdf8c457 | ||
|
|
304ad4e3db | ||
|
|
ec58ab07b6 | ||
|
|
1ba4047b1b | ||
|
|
0bab8adc41 | ||
|
|
17e09c601e | ||
|
|
1aca5fb6ff | ||
|
|
7860d690fa | ||
|
|
6d01c99d38 | ||
|
|
ddb645aeea | ||
|
|
f08e4b41c4 | ||
|
|
1e23624955 | ||
|
|
ee951a7448 | ||
|
|
9935ba370d | ||
|
|
e815cce143 | ||
|
|
cea1032180 | ||
|
|
5695e1d9c8 | ||
|
|
fd317afd01 | ||
|
|
ccddd2a96f | ||
|
|
513d3034d8 | ||
|
|
51495187fa | ||
|
|
2bd53f7b9f | ||
|
|
06d9c48ed4 | ||
|
|
1155d18b7f | ||
|
|
6e14592c78 | ||
|
|
55feaf2d2c | ||
|
|
c487036c8b | ||
|
|
853ebf8c70 | ||
|
|
1c695c1cf9 | ||
|
|
bd5687d169 | ||
|
|
b384f71b64 | ||
|
|
10dd5278e7 | ||
|
|
befa6527e4 | ||
|
|
00497630cb | ||
|
|
95cd457de1 | ||
|
|
7518c9e3e0 | ||
|
|
6a999835e2 | ||
|
|
41d099c1be | ||
|
|
ff306ce2c5 | ||
|
|
c7abc82055 | ||
|
|
041d91dd3c | ||
|
|
387f56ed9b | ||
|
|
3181323c1f | ||
|
|
ecf84150c1 | ||
|
|
5b5025c776 | ||
|
|
e47dd3058b | ||
|
|
71f1dcd475 | ||
|
|
941856932c | ||
|
|
c51fde52e7 | ||
|
|
c5362e3bde | ||
|
|
a113703451 | ||
|
|
55ecb918e9 | ||
|
|
3a870e2f8b | ||
|
|
734231a4f1 | ||
|
|
223d6b29f4 | ||
|
|
4f41ec0a97 | ||
|
|
347a53297d | ||
|
|
820766abcb | ||
|
|
4974fa1fed | ||
|
|
7e829fa204 | ||
|
|
f6c7caa48d | ||
|
|
0dd9d252fd | ||
|
|
39f67a241c | ||
|
|
5706b08366 | ||
|
|
81de9695e2 | ||
|
|
589fb25fe3 | ||
|
|
61e5c6b468 | ||
|
|
087ceb3687 | ||
|
|
0a2cd208b2 | ||
|
|
678a936897 | ||
|
|
7c72ca089b | ||
|
|
21530f315f | ||
|
|
7274905a92 | ||
|
|
6c5cff6162 | ||
|
|
cf6b6c129a | ||
|
|
74491d16ae | ||
|
|
c1ab6e4eb4 | ||
|
|
18c9ae235a | ||
|
|
5c69d5fb88 | ||
|
|
90f0bda879 | ||
|
|
1b5c4a21bb | ||
|
|
08ee37112f | ||
|
|
cfbc88d3d6 | ||
|
|
79f5529a5a | ||
|
|
11ed0abd18 | ||
|
|
01830d9910 | ||
|
|
0f573805f2 | ||
|
|
93b1d81a48 | ||
|
|
e28d13b910 | ||
|
|
8731e343c4 | ||
|
|
605eca8cd7 | ||
|
|
5a8ddf5e4a | ||
|
|
f6d5d575fc | ||
|
|
d5c344e3ac | ||
|
|
18ba326cea | ||
|
|
1a1473d3ba | ||
|
|
72804a09ec | ||
|
|
c1ce0a514c | ||
|
|
bd479312b5 | ||
|
|
469da540d2 | ||
|
|
69edaa974f | ||
|
|
ff56963040 | ||
|
|
266aeaef50 | ||
|
|
fc660cfb1f | ||
|
|
27d343bdea | ||
|
|
a04b0da54a | ||
|
|
b15a6bfa98 | ||
|
|
dcc638c12f | ||
|
|
84ea96a5ad | ||
|
|
ae1bf85740 | ||
|
|
1612d713c9 | ||
|
|
6a4a8af731 | ||
|
|
e18375ca6d | ||
|
|
e537e4538a | ||
|
|
1ae97f5477 | ||
|
|
cc0083c6e5 | ||
|
|
43e6ed2da9 | ||
|
|
27bb3a948b | ||
|
|
7c155d307b | ||
|
|
d789beddd0 | ||
|
|
f790148ad3 | ||
|
|
a643abe293 | ||
|
|
099b08f009 | ||
|
|
35ddf6790e | ||
|
|
6502fdb1f5 | ||
|
|
b5cd3bf0af | ||
|
|
8183648902 | ||
|
|
0e1159b01e | ||
|
|
625ef3da8a | ||
|
|
10c7d9a6e1 | ||
|
|
85952ce6b7 | ||
|
|
bf9ce68d8b | ||
|
|
08c5992447 | ||
|
|
dfc7f7c827 | ||
|
|
efdbbc6098 | ||
|
|
185cf90d4c | ||
|
|
4db4790270 | ||
|
|
be3b890e2f | ||
|
|
4536f96493 | ||
|
|
a598c3e7a8 | ||
|
|
d9f5ee9d76 | ||
|
|
a4ced609cd | ||
|
|
673a4e6805 | ||
|
|
d017ccfbd4 | ||
|
|
1f52ed2e83 | ||
|
|
08e83f616c | ||
|
|
51edc4652e | ||
|
|
a3c6f38642 | ||
|
|
a1db53f50b | ||
|
|
9e1046fde3 | ||
|
|
17173f72e0 | ||
|
|
f60a99c357 | ||
|
|
1d763f1bc9 | ||
|
|
248b94c296 | ||
|
|
f52447ff58 | ||
|
|
0cbacbb959 | ||
|
|
a01edecaef | ||
|
|
779756f1ab | ||
|
|
723fedc066 | ||
|
|
a83bb23540 | ||
|
|
5d68a5133e | ||
|
|
8ca629151d | ||
|
|
693965af28 | ||
|
|
e645a350f2 | ||
|
|
85e9808550 | ||
|
|
0ce1c4565e | ||
|
|
478964ad30 | ||
|
|
74a04e3b35 | ||
|
|
a48992ed9d | ||
|
|
9a6ea8c9bb | ||
|
|
51b05cb128 | ||
|
|
de33d6d44c | ||
|
|
3d5cc98df5 | ||
|
|
13f3b54393 | ||
|
|
f17f7b2272 | ||
|
|
f61dc7197a | ||
|
|
0534508bc3 | ||
|
|
446c7ffd6a | ||
|
|
79e6216669 | ||
|
|
5047e48de5 | ||
|
|
bd48112bf9 | ||
|
|
5dc100d900 | ||
|
|
9f2ecb67d4 | ||
|
|
5e4f45826e | ||
|
|
be6ff21184 | ||
|
|
5c660fbe7f | ||
|
|
108718f275 | ||
|
|
ab53a0b403 | ||
|
|
49b815bc98 | ||
|
|
c702814203 | ||
|
|
0c0172a0b6 | ||
|
|
a8266c22f6 | ||
|
|
532c7fbc8f | ||
|
|
23ed381859 | ||
|
|
1ad11b0c58 | ||
|
|
18cca916a0 | ||
|
|
97012082de | ||
|
|
423810cf61 | ||
|
|
a5159ce8e1 | ||
|
|
4dd3952c19 | ||
|
|
1e26b5c5f1 | ||
|
|
67897dfcc0 | ||
|
|
0100604798 | ||
|
|
47afe01721 | ||
|
|
a2e12b795f | ||
|
|
806ab3438e | ||
|
|
f4be90fdd0 | ||
|
|
dd46767ee3 | ||
|
|
a2c712e5b3 | ||
|
|
35f3a0077a | ||
|
|
bc4195942a | ||
|
|
03baca2ed7 | ||
|
|
54a9c31a1a | ||
|
|
db5073223d | ||
|
|
afd766999c | ||
|
|
0637490216 | ||
|
|
6a3ba87b22 | ||
|
|
20b287da52 | ||
|
|
18a378976b | ||
|
|
8e7af49206 | ||
|
|
edeab082d4 | ||
|
|
7b76baaacf | ||
|
|
053365cb67 | ||
|
|
8301120a95 | ||
|
|
f15f0a6226 | ||
|
|
0cfcadf5fa | ||
|
|
435c4acba6 | ||
|
|
edb913855d | ||
|
|
24739e1638 | ||
|
|
54b906addb | ||
|
|
4a7a8df8a4 | ||
|
|
f1dd62c936 | ||
|
|
80cc7b0d64 | ||
|
|
eb4fbf3c0b | ||
|
|
c1cf1206fc | ||
|
|
efebc02d24 | ||
|
|
21dca8c17f | ||
|
|
4eb9839f77 | ||
|
|
3b7906ea04 | ||
|
|
9d17858500 | ||
|
|
d5ceb5f465 | ||
|
|
7dd2a0bbb4 | ||
|
|
13284fb3b9 | ||
|
|
f42c5ec0ce | ||
|
|
6b269839cb | ||
|
|
2eb3e0a278 | ||
|
|
183a437678 | ||
|
|
116b8171f8 | ||
|
|
c8c723bf4a | ||
|
|
d01cf018ce | ||
|
|
c701ab0776 | ||
|
|
180269d6b0 | ||
|
|
645c604fd4 | ||
|
|
de210db90d | ||
|
|
beddf1c772 | ||
|
|
75e618ee4a | ||
|
|
d2a3ba182b | ||
|
|
427f78b14d | ||
|
|
febcf237ca | ||
|
|
5e158c3bd7 | ||
|
|
b4c9c86ba6 | ||
|
|
7c00853f5d | ||
|
|
a0fcb116f5 | ||
|
|
e46b33544d | ||
|
|
6b9c3ad4e7 | ||
|
|
dc12b9a197 | ||
|
|
d473f56c3a | ||
|
|
4138ab3d7d | ||
|
|
e18d1a451d | ||
|
|
a3048cd393 | ||
|
|
dd8fdc6c0a | ||
|
|
9099e4b709 | ||
|
|
52b176b9eb | ||
|
|
69fd70787c | ||
|
|
ff37aea9c8 | ||
|
|
85f73977bf | ||
|
|
2c04ed48c2 | ||
|
|
1228754280 | ||
|
|
a43ee054ad | ||
|
|
83bc714739 | ||
|
|
a08390c84a | ||
|
|
8b6eacecfe | ||
|
|
fb96787697 | ||
|
|
9cff77be62 | ||
|
|
0d1643da66 | ||
|
|
5e7027647a | ||
|
|
28f6f09e8f | ||
|
|
332af5d21b | ||
|
|
e187005130 | ||
|
|
0357386f7c | ||
|
|
47f8e5b8c6 | ||
|
|
e95c9d73a1 | ||
|
|
b7174070fe | ||
|
|
dd06a7b62c | ||
|
|
ff9d480b6e | ||
|
|
229ad9108b | ||
|
|
0e332d291a | ||
|
|
180904cdc2 | ||
|
|
0e83f7d807 | ||
|
|
5d7931fcaf | ||
|
|
2e906b0bf5 | ||
|
|
33ae6f12de | ||
|
|
f302c2e154 | ||
|
|
3ee2492382 | ||
|
|
4caed50018 | ||
|
|
aadb19a792 | ||
|
|
9f8211a873 | ||
|
|
d45fc05e5d | ||
|
|
955a3a054e | ||
|
|
60f265a5fa | ||
|
|
a2d82a1a7b | ||
|
|
0875d728e8 | ||
|
|
f3cf6b8b38 | ||
|
|
e4465cffb0 | ||
|
|
ca35d714dc | ||
|
|
c06e7348c4 | ||
|
|
60ac8a6ebd | ||
|
|
e3450baeb3 | ||
|
|
72661623f3 | ||
|
|
b4d97d9432 | ||
|
|
b40100f78b | ||
|
|
a343d2b42c | ||
|
|
d3d7e54cff | ||
|
|
6535bc3d5e | ||
|
|
f966fc8d84 | ||
|
|
8a20bbd943 | ||
|
|
cd0f6d85ba | ||
|
|
d51edbb3bb | ||
|
|
553e475cfb | ||
|
|
b9367446d9 | ||
|
|
82d9fccec8 | ||
|
|
cbbcfb7a3a | ||
|
|
1f862b27c1 | ||
|
|
883b03349e | ||
|
|
f740a6ba61 | ||
|
|
fb3e761a37 | ||
|
|
3c7411328d | ||
|
|
9c2bfdfead | ||
|
|
4f3bd1ff4a | ||
|
|
69d10489b8 | ||
|
|
df031b2222 | ||
|
|
850b9e5e3d | ||
|
|
a95a208e1b | ||
|
|
50ff3628f7 | ||
|
|
14d203055b | ||
|
|
4628e28592 | ||
|
|
7fb3d13733 | ||
|
|
11ff81f852 | ||
|
|
0f5af4b990 | ||
|
|
85420602e8 | ||
|
|
6ccf55b601 | ||
|
|
42c9e21d04 | ||
|
|
3030c300f2 | ||
|
|
48b969f3c3 | ||
|
|
bbb78aa5e6 | ||
|
|
31380bbef2 | ||
|
|
479a7d9162 | ||
|
|
6fe02f156a | ||
|
|
c4ed210fed | ||
|
|
ae686fab38 | ||
|
|
8edca9ed5d | ||
|
|
05bafd0db5 | ||
|
|
341d699240 | ||
|
|
552093d962 | ||
|
|
eb6063cc2d | ||
|
|
550ff4ff18 | ||
|
|
5383a8b77c | ||
|
|
86117091fe | ||
|
|
b113028a5f | ||
|
|
60a3f21857 | ||
|
|
65a2ea3935 | ||
|
|
6ecddfc6c0 | ||
|
|
d65d48db48 | ||
|
|
f509b26800 | ||
|
|
43fb6fe6e5 | ||
|
|
9d2d8684b6 | ||
|
|
1689925508 | ||
|
|
4d249553bf | ||
|
|
43ea1044cd | ||
|
|
cc4a301dc1 | ||
|
|
ab67eea36e | ||
|
|
fa326eba6f | ||
|
|
c30ebdf287 | ||
|
|
835bcb7207 | ||
|
|
777424ad18 | ||
|
|
4985e7e96d | ||
|
|
ca1e64ec10 | ||
|
|
26029508c6 | ||
|
|
118259a96b | ||
|
|
35e8dcf2bc | ||
|
|
359a5d01e6 | ||
|
|
1c2acbb57f | ||
|
|
01a702c529 | ||
|
|
1ee584c5a1 | ||
|
|
fc10bd7749 | ||
|
|
f2568092a7 | ||
|
|
6b5d5a6334 | ||
|
|
195ed57025 | ||
|
|
008b4a134b | ||
|
|
1b9bfb5b62 | ||
|
|
edeaa1333b | ||
|
|
e678b52a7e | ||
|
|
b549db58e4 | ||
|
|
c14059f66a | ||
|
|
11f69daaec | ||
|
|
c0120c0f17 | ||
|
|
c1a5f9adf1 | ||
|
|
5087f27546 | ||
|
|
efbff9e217 | ||
|
|
20ea83ae93 | ||
|
|
05daeb561c | ||
|
|
bfff001752 | ||
|
|
c3a45a1584 | ||
|
|
b09a92a264 | ||
|
|
44a792583c | ||
|
|
71c8267dea | ||
|
|
b6688f56b5 | ||
|
|
f703164098 | ||
|
|
6a6b27e905 | ||
|
|
731a46c612 | ||
|
|
92a8078322 | ||
|
|
ba2d77f0bb | ||
|
|
3d21c15281 | ||
|
|
cb4b20c057 | ||
|
|
2af2767699 | ||
|
|
e4bb19b98a | ||
|
|
7e784c9509 | ||
|
|
3dd27797dc | ||
|
|
5e059272dc | ||
|
|
0a9aeca3bc | ||
|
|
11d42e0f93 | ||
|
|
85d8658037 | ||
|
|
dfa29950ef | ||
|
|
b7366a8704 | ||
|
|
57416103c3 | ||
|
|
72bd3731de | ||
|
|
9fab20ca6c | ||
|
|
8b4453f32d | ||
|
|
f4b77e6b03 | ||
|
|
c3da2fca9b | ||
|
|
c0d68c5740 | ||
|
|
5398564aec | ||
|
|
904dc80aab | ||
|
|
516de20148 | ||
|
|
be088709af | ||
|
|
fd4f5057b3 | ||
|
|
686d5e8b03 | ||
|
|
c371ff5504 | ||
|
|
9862dca4aa | ||
|
|
716321b37b | ||
|
|
b3ed8bad9c | ||
|
|
0a170f5c29 | ||
|
|
ec0fba7913 | ||
|
|
2630c2baf1 | ||
|
|
a01865b19b | ||
|
|
00f6115a93 | ||
|
|
a466202bac | ||
|
|
bb12ef24f8 | ||
|
|
c862c6de0f | ||
|
|
a1cb3ec8d5 | ||
|
|
187d4cd02d | ||
|
|
20a0f9b026 | ||
|
|
f9c0ed6ad4 | ||
|
|
27652e7191 | ||
|
|
828665cb29 | ||
|
|
fa784c83bf | ||
|
|
2d83176892 | ||
|
|
776758c7e8 | ||
|
|
d72afe9b92 | ||
|
|
dee61b5499 | ||
|
|
9f73d0a7fb | ||
|
|
bc804c9e56 | ||
|
|
35f450aee7 | ||
|
|
5803b4ca27 | ||
|
|
7bccd62a4f | ||
|
|
335838f2b2 | ||
|
|
204d8cc7eb | ||
|
|
61f5d4b172 | ||
|
|
3d829c6ce8 | ||
|
|
5d9852b72c | ||
|
|
f561ece9d1 | ||
|
|
66eabd3bd6 | ||
|
|
b2f92acbf6 | ||
|
|
6f30ecb365 | ||
|
|
32a89d3895 | ||
|
|
97bf958b74 | ||
|
|
30f8afca85 | ||
|
|
ed88a8e3e3 | ||
|
|
421f690f42 | ||
|
|
a330e8afb2 | ||
|
|
d8e5c9f033 | ||
|
|
209646e012 | ||
|
|
7d518df13c | ||
|
|
ca603f41db | ||
|
|
7bb18f6fad | ||
|
|
1a0e2031d2 | ||
|
|
4f83d69205 | ||
|
|
cfafd90f15 | ||
|
|
a94f416b3c | ||
|
|
fd47e2de29 | ||
|
|
abbc403f73 | ||
|
|
b41c536865 | ||
|
|
bee7314dd7 | ||
|
|
d25407e3b4 | ||
|
|
ad697369ef | ||
|
|
edbdb17a2f | ||
|
|
9d2e2a1ea2 | ||
|
|
6df0597c5e | ||
|
|
093eb28463 | ||
|
|
7d0c279f5b | ||
|
|
d98a6a09bb | ||
|
|
02cf7b9d66 | ||
|
|
9253b783dd | ||
|
|
2ed82be809 | ||
|
|
1c1499dec8 | ||
|
|
f7f151d2a9 | ||
|
|
13f29ee3ce | ||
|
|
ce68f52ca0 | ||
|
|
33172767a6 | ||
|
|
649b3839d2 | ||
|
|
666fb4c194 | ||
|
|
9301497a4a | ||
|
|
6956e21caf | ||
|
|
71dec5746e | ||
|
|
0ea8f4c259 | ||
|
|
8602814dc3 | ||
|
|
20e60edbc6 | ||
|
|
88f59ad1eb | ||
|
|
668a899260 | ||
|
|
75ae85a5d4 | ||
|
|
abc1b4e1b2 | ||
|
|
fa194f0cef | ||
|
|
b3fbd89456 | ||
|
|
5334a4cbe0 | ||
|
|
8f2adf0a50 | ||
|
|
62dfd7cef0 | ||
|
|
a8321e8cd3 | ||
|
|
0119552336 | ||
|
|
033abc64c8 | ||
|
|
ef8014bc6d | ||
|
|
96a880b5ae | ||
|
|
bfedcde978 | ||
|
|
badad70984 | ||
|
|
7611188535 | ||
|
|
31f2cc1fdc | ||
|
|
187e646fa0 | ||
|
|
b2721db8e0 | ||
|
|
fd9f521c60 | ||
|
|
edd6fbe35f | ||
|
|
839c9c9884 | ||
|
|
3a1fe992d6 | ||
|
|
9899f6d1f8 | ||
|
|
446a464b3d | ||
|
|
6a347799c7 | ||
|
|
2dae89e41c | ||
|
|
e8119ba80d | ||
|
|
a5ecad8fae | ||
|
|
1708a4c831 | ||
|
|
a237078b68 | ||
|
|
4ef63d026e | ||
|
|
b8ae3cdd3f | ||
|
|
46d855ce0f | ||
|
|
a3306bbb5a | ||
|
|
1428a5e7e2 | ||
|
|
427940b3be | ||
|
|
7a3e7dc631 | ||
|
|
b38bb40a5d | ||
|
|
b2e1e2e89a | ||
|
|
02fcc42395 | ||
|
|
3accc406a7 | ||
|
|
1c238b7ce4 | ||
|
|
45770173c4 | ||
|
|
3e5f6abdad | ||
|
|
4117c6127e | ||
|
|
aae1fad7ab | ||
|
|
ada65b5ce2 | ||
|
|
14c0c65e17 | ||
|
|
0201aa9bd1 | ||
|
|
dca530f2f2 | ||
|
|
c9f9668e52 | ||
|
|
4f636b7cfb | ||
|
|
34a04c0059 | ||
|
|
00ee58d3fd | ||
|
|
ecb3c4f4f3 | ||
|
|
9dace592c0 | ||
|
|
5d73221b06 | ||
|
|
d50958c9ee | ||
|
|
52bb005792 | ||
|
|
3121aa7164 | ||
|
|
87c54f07c6 | ||
|
|
f1d4a686b1 | ||
|
|
56ac037128 | ||
|
|
e977045d5f | ||
|
|
3301b106ab | ||
|
|
e645f55191 | ||
|
|
278d25c803 |
28
.gitattributes
vendored
28
.gitattributes
vendored
@@ -1,17 +1,17 @@
|
|||||||
src/static/fontawesome/* linguist-vendored
|
src/pretix/static/fontawesome/* linguist-vendored
|
||||||
src/static/lightbox/* linguist-vendored
|
src/pretix/static/lightbox/* linguist-vendored
|
||||||
src/static/typeahead/* linguist-vendored
|
src/pretix/static/typeahead/* linguist-vendored
|
||||||
src/static/moment/* linguist-vendored
|
src/pretix/static/moment/* linguist-vendored
|
||||||
src/static/datetimepicker/* linguist-vendored
|
src/pretix/static/datetimepicker/* linguist-vendored
|
||||||
src/static/colorpicker/* linguist-vendored
|
src/pretix/static/colorpicker/* linguist-vendored
|
||||||
src/static/fileupload/* linguist-vendored
|
src/pretix/static/fileupload/* linguist-vendored
|
||||||
src/static/vuejs/* linguist-vendored
|
src/pretix/static/vuejs/* linguist-vendored
|
||||||
src/static/select2/* linguist-vendored
|
src/pretix/static/select2/* linguist-vendored
|
||||||
src/static/charts/* linguist-vendored
|
src/pretix/static/charts/* linguist-vendored
|
||||||
src/static/rrule/* linguist-vendored
|
src/pretix/static/rrule/* linguist-vendored
|
||||||
src/static/iframeresizer/* linguist-vendored
|
src/pretix/static/iframeresizer/* linguist-vendored
|
||||||
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/fabric.* linguist-vendored
|
src/pretix/static/pdfjs/* linguist-vendored
|
||||||
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/pdf.* linguist-vendored
|
src/pretix/static/fabric/* linguist-vendored
|
||||||
|
|
||||||
# Denote all files that are truly binary and should not be modified.
|
# Denote all files that are truly binary and should not be modified.
|
||||||
*.eot binary
|
*.eot binary
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ pypi:
|
|||||||
- virtualenv env
|
- virtualenv env
|
||||||
- source env/bin/activate
|
- source env/bin/activate
|
||||||
- pip install -U pip wheel setuptools
|
- pip install -U pip wheel setuptools
|
||||||
- XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements.txt -r src/requirements/dev.txt -r src/requirements/py34.txt
|
- XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements.txt -r src/requirements/dev.txt
|
||||||
- cd src
|
- cd src
|
||||||
- python setup.py sdist
|
- python setup.py sdist
|
||||||
- pip install dist/pretix-*.tar.gz
|
- pip install dist/pretix-*.tar.gz
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
-r src/requirements/py34.txt
|
|
||||||
-r doc/requirements.txt
|
-r doc/requirements.txt
|
||||||
|
|||||||
20
.travis.sh
20
.travis.sh
@@ -11,21 +11,20 @@ fi
|
|||||||
|
|
||||||
if [ "$PRETIX_CONFIG_FILE" == "tests/travis_postgres.cfg" ]; then
|
if [ "$PRETIX_CONFIG_FILE" == "tests/travis_postgres.cfg" ]; then
|
||||||
psql -c 'create database travis_ci_test;' -U postgres
|
psql -c 'create database travis_ci_test;' -U postgres
|
||||||
pip3 install -Ur src/requirements/postgres.txt
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$1" == "style" ]; then
|
if [ "$1" == "style" ]; then
|
||||||
XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements.txt -r src/requirements/dev.txt -r src/requirements/py34.txt
|
XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements.txt -r src/requirements/dev.txt
|
||||||
cd src
|
cd src
|
||||||
flake8 .
|
flake8 .
|
||||||
isort -c -rc -df .
|
isort -c -rc -df .
|
||||||
fi
|
fi
|
||||||
if [ "$1" == "doctests" ]; then
|
if [ "$1" == "doctests" ]; then
|
||||||
XDG_CACHE_HOME=/cache pip3 install -Ur doc/requirements.txt -r src/requirements/py34.txt
|
XDG_CACHE_HOME=/cache pip3 install -Ur doc/requirements.txt
|
||||||
cd doc
|
cd doc
|
||||||
make doctest
|
make doctest
|
||||||
fi
|
fi
|
||||||
if [ "$1" == "spelling" ]; then
|
if [ "$1" == "doc-spelling" ]; then
|
||||||
XDG_CACHE_HOME=/cache pip3 install -Ur doc/requirements.txt
|
XDG_CACHE_HOME=/cache pip3 install -Ur doc/requirements.txt
|
||||||
cd doc
|
cd doc
|
||||||
make spelling
|
make spelling
|
||||||
@@ -33,22 +32,27 @@ if [ "$1" == "spelling" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
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
|
if [ "$1" == "tests" ]; then
|
||||||
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt -r src/requirements/py34.txt
|
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt
|
||||||
cd src
|
cd src
|
||||||
python manage.py check
|
python manage.py check
|
||||||
make all compress
|
make all compress
|
||||||
py.test --reruns 5 tests
|
py.test --reruns 5 -n 3 tests
|
||||||
fi
|
fi
|
||||||
if [ "$1" == "tests-cov" ]; then
|
if [ "$1" == "tests-cov" ]; then
|
||||||
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt -r src/requirements/py34.txt
|
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt
|
||||||
cd src
|
cd src
|
||||||
python manage.py check
|
python manage.py check
|
||||||
make all compress
|
make all compress
|
||||||
coverage run -m py.test --reruns 5 tests && codecov
|
coverage run -m py.test --reruns 5 tests && codecov
|
||||||
fi
|
fi
|
||||||
if [ "$1" == "plugins" ]; then
|
if [ "$1" == "plugins" ]; then
|
||||||
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt -r src/requirements/py34.txt
|
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt
|
||||||
cd src
|
cd src
|
||||||
python setup.py develop
|
python setup.py develop
|
||||||
make all compress
|
make all compress
|
||||||
|
|||||||
26
.travis.yml
26
.travis.yml
@@ -1,7 +1,7 @@
|
|||||||
language: python
|
language: python
|
||||||
sudo: false
|
sudo: false
|
||||||
install:
|
install:
|
||||||
- pip install -U pip wheel setuptools==28.6.1
|
- pip install -U pip wheel setuptools
|
||||||
script:
|
script:
|
||||||
- bash .travis.sh $JOB
|
- bash .travis.sh $JOB
|
||||||
cache:
|
cache:
|
||||||
@@ -15,34 +15,32 @@ matrix:
|
|||||||
- python: 3.6
|
- python: 3.6
|
||||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
|
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
|
||||||
- python: 3.6
|
- python: 3.6
|
||||||
env: JOB=tests-cov
|
env: JOB=tests-cov PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||||
- python: 3.6
|
- python: 3.6
|
||||||
env: JOB=style
|
env: JOB=style
|
||||||
- python: 3.4
|
|
||||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
|
|
||||||
- python: 3.5
|
|
||||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
|
|
||||||
- python: 3.4
|
|
||||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
|
|
||||||
- python: 3.5
|
|
||||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
|
|
||||||
- python: 3.6
|
- python: 3.6
|
||||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
|
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
|
||||||
- python: 3.4
|
- python: 3.6
|
||||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||||
- python: 3.5
|
- python: 3.5
|
||||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||||
- python: 3.6
|
|
||||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
|
||||||
- python: 3.6
|
- python: 3.6
|
||||||
env: JOB=plugins
|
env: JOB=plugins
|
||||||
- python: 3.6
|
- python: 3.6
|
||||||
env: JOB=spelling
|
env: JOB=doc-spelling
|
||||||
|
- python: 3.6
|
||||||
|
env: JOB=translation-spelling
|
||||||
addons:
|
addons:
|
||||||
postgresql: "9.4"
|
postgresql: "9.4"
|
||||||
|
mariadb: '10.3'
|
||||||
apt:
|
apt:
|
||||||
packages:
|
packages:
|
||||||
- enchant
|
- enchant
|
||||||
|
- myspell-de-de
|
||||||
|
- aspell-en
|
||||||
|
- sqlite3
|
||||||
|
sources:
|
||||||
|
- travis-ci/sqlite3
|
||||||
branches:
|
branches:
|
||||||
except:
|
except:
|
||||||
- /^weblate-.*/
|
- /^weblate-.*/
|
||||||
|
|||||||
43
Dockerfile
43
Dockerfile
@@ -1,10 +1,26 @@
|
|||||||
FROM python:3.6
|
FROM python:3.6
|
||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y git libxml2-dev libxslt1-dev python-dev python-virtualenv locales \
|
apt-get install -y --no-install-recommends \
|
||||||
libffi-dev build-essential python3-dev zlib1g-dev libssl-dev gettext libpq-dev \
|
build-essential \
|
||||||
libmysqlclient-dev libmemcached-dev libjpeg-dev supervisor nginx sudo \
|
default-libmysqlclient-dev \
|
||||||
--no-install-recommends && \
|
gettext \
|
||||||
|
git \
|
||||||
|
libffi-dev \
|
||||||
|
libjpeg-dev \
|
||||||
|
libmemcached-dev \
|
||||||
|
libpq-dev \
|
||||||
|
libssl-dev \
|
||||||
|
libxml2-dev \
|
||||||
|
libxslt1-dev \
|
||||||
|
locales \
|
||||||
|
nginx \
|
||||||
|
python-dev \
|
||||||
|
python-virtualenv \
|
||||||
|
python3-dev \
|
||||||
|
sudo \
|
||||||
|
supervisor \
|
||||||
|
zlib1g-dev && \
|
||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
rm -rf /var/lib/apt/lists/* && \
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
dpkg-reconfigure locales && \
|
dpkg-reconfigure locales && \
|
||||||
@@ -19,6 +35,22 @@ RUN apt-get update && \
|
|||||||
ENV LC_ALL=C.UTF-8 \
|
ENV LC_ALL=C.UTF-8 \
|
||||||
DJANGO_SETTINGS_MODULE=production_settings
|
DJANGO_SETTINGS_MODULE=production_settings
|
||||||
|
|
||||||
|
# To copy only the requirements files needed to install from PIP
|
||||||
|
COPY src/requirements /pretix/src/requirements
|
||||||
|
COPY src/requirements.txt /pretix/src
|
||||||
|
RUN pip3 install -U \
|
||||||
|
pip \
|
||||||
|
setuptools \
|
||||||
|
wheel && \
|
||||||
|
cd /pretix/src && \
|
||||||
|
pip3 install \
|
||||||
|
-r requirements.txt \
|
||||||
|
-r requirements/memcached.txt \
|
||||||
|
-r requirements/mysql.txt \
|
||||||
|
-r requirements/redis.txt \
|
||||||
|
gunicorn && \
|
||||||
|
rm -rf ~/.cache/pip
|
||||||
|
|
||||||
COPY deployment/docker/pretix.bash /usr/local/bin/pretix
|
COPY deployment/docker/pretix.bash /usr/local/bin/pretix
|
||||||
COPY deployment/docker/supervisord.conf /etc/supervisord.conf
|
COPY deployment/docker/supervisord.conf /etc/supervisord.conf
|
||||||
COPY deployment/docker/nginx.conf /etc/nginx/nginx.conf
|
COPY deployment/docker/nginx.conf /etc/nginx/nginx.conf
|
||||||
@@ -27,11 +59,8 @@ COPY src /pretix/src
|
|||||||
|
|
||||||
RUN chmod +x /usr/local/bin/pretix && \
|
RUN chmod +x /usr/local/bin/pretix && \
|
||||||
rm /etc/nginx/sites-enabled/default && \
|
rm /etc/nginx/sites-enabled/default && \
|
||||||
pip3 install -U pip wheel setuptools && \
|
|
||||||
cd /pretix/src && \
|
cd /pretix/src && \
|
||||||
rm -f pretix.cfg && \
|
rm -f pretix.cfg && \
|
||||||
pip3 install -r requirements.txt -r requirements/mysql.txt -r requirements/postgres.txt \
|
|
||||||
-r requirements/memcached.txt -r requirements/redis.txt gunicorn && \
|
|
||||||
mkdir -p data && \
|
mkdir -p data && \
|
||||||
chown -R pretixuser:pretixuser /pretix /data data && \
|
chown -R pretixuser:pretixuser /pretix /data data && \
|
||||||
sudo -u pretixuser make production
|
sudo -u pretixuser make production
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ cd /pretix/src
|
|||||||
export DJANGO_SETTINGS_MODULE=production_settings
|
export DJANGO_SETTINGS_MODULE=production_settings
|
||||||
export DATA_DIR=/data/
|
export DATA_DIR=/data/
|
||||||
export HOME=/pretix
|
export HOME=/pretix
|
||||||
NUM_WORKERS=10
|
export NUM_WORKERS=$((2 * $(nproc --all)))
|
||||||
|
|
||||||
if [ ! -d /data/logs ]; then
|
if [ ! -d /data/logs ]; then
|
||||||
mkdir /data/logs;
|
mkdir /data/logs;
|
||||||
|
|||||||
@@ -53,6 +53,10 @@ Example::
|
|||||||
A comma-separated list of plugins that are enabled by default for all new events.
|
A comma-separated list of plugins that are enabled by default for all new events.
|
||||||
Defaults to ``pretix.plugins.sendmail,pretix.plugins.statistics``.
|
Defaults to ``pretix.plugins.sendmail,pretix.plugins.statistics``.
|
||||||
|
|
||||||
|
``plugins_exclude``
|
||||||
|
A comma-separated list of plugins that are not available even though they are installed.
|
||||||
|
Defaults to an empty string.
|
||||||
|
|
||||||
``cookie_domain``
|
``cookie_domain``
|
||||||
The cookie domain to be set. Defaults to ``None``.
|
The cookie domain to be set. Defaults to ``None``.
|
||||||
|
|
||||||
@@ -121,6 +125,23 @@ Example::
|
|||||||
Indicates if the database backend is a MySQL/MariaDB Galera cluster and
|
Indicates if the database backend is a MySQL/MariaDB Galera cluster and
|
||||||
turns on some optimizations/special case handlers. Default: ``False``
|
turns on some optimizations/special case handlers. Default: ``False``
|
||||||
|
|
||||||
|
Database replica settings
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
If you use a replicated database setup, pretix expects that the default database connection always points to the primary database node.
|
||||||
|
Routing read queries to a replica on database layer is **strongly** discouraged since this can lead to inaccurate such as more tickets
|
||||||
|
being sold than are actually available.
|
||||||
|
|
||||||
|
However, pretix can still make use of a database replica to keep some expensive queries with that can tolerate some latency from your
|
||||||
|
primary database, such as backend search queries. The ``replica`` configuration section can have the same settings as the ``database``
|
||||||
|
section (except for the ``backend`` setting) and will default back to the ``database`` settings for all values that are not given. This
|
||||||
|
way, you just need to specify the settings that are different for the replica.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
[replica]
|
||||||
|
host=192.168.0.2
|
||||||
|
|
||||||
URLs
|
URLs
|
||||||
----
|
----
|
||||||
|
|
||||||
@@ -291,5 +312,13 @@ various places like order codes, secrets in the ticket QR codes, etc. Example::
|
|||||||
; Voucher code needs to be < 255 characters, default is 16
|
; Voucher code needs to be < 255 characters, default is 16
|
||||||
voucher_code=16
|
voucher_code=16
|
||||||
|
|
||||||
|
External tools
|
||||||
|
--------------
|
||||||
|
|
||||||
|
pretix can make use of some external tools if they are installed. Currently, they are all optional. Example::
|
||||||
|
|
||||||
|
[tools]
|
||||||
|
pdftk=/usr/bin/pdftk
|
||||||
|
|
||||||
.. _Python documentation: https://docs.python.org/3/library/configparser.html?highlight=configparser#supported-ini-file-structure
|
.. _Python documentation: https://docs.python.org/3/library/configparser.html?highlight=configparser#supported-ini-file-structure
|
||||||
.. _Celery documentation: http://docs.celeryproject.org/en/latest/userguide/configuration.html
|
.. _Celery documentation: http://docs.celeryproject.org/en/latest/userguide/configuration.html
|
||||||
|
|||||||
37
doc/admin/installation/dev_version.rst
Normal file
37
doc/admin/installation/dev_version.rst
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
.. highlight:: none
|
||||||
|
|
||||||
|
Installing a development version
|
||||||
|
================================
|
||||||
|
|
||||||
|
If you want to use a feature of pretix that is not yet contained in the last monthly release, you can also
|
||||||
|
install a development version with pretix.
|
||||||
|
|
||||||
|
.. warning:: When in production, we strongly recommend only installing released versions. Development versions might
|
||||||
|
be broken, incompatible to plugins, or in rare cases incompatible to upgrade later on.
|
||||||
|
|
||||||
|
|
||||||
|
Manual installation
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
You can use ``pip`` to update pretix directly to the development branch. Then, upgrade as usual::
|
||||||
|
|
||||||
|
$ source /var/pretix/venv/bin/activate
|
||||||
|
(venv)$ pip3 install -U "git+https://github.com/pretix/pretix.git#egg=pretix&subdirectory=src"
|
||||||
|
(venv)$ python -m pretix migrate
|
||||||
|
(venv)$ python -m pretix rebuild
|
||||||
|
(venv)$ python -m pretix updatestyles
|
||||||
|
# systemctl restart pretix-web pretix-worker
|
||||||
|
|
||||||
|
Docker installation
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
To use the latest development version with Docker, first pull it from Docker Hub::
|
||||||
|
|
||||||
|
$ docker pull pretix/standalone:latest
|
||||||
|
|
||||||
|
|
||||||
|
Then change your ``/etc/systemd/system/pretix.service`` file to use the ``:latest`` tag instead of ``:stable`` as well
|
||||||
|
and upgrade as usual::
|
||||||
|
|
||||||
|
$ systemctl restart pretix.service
|
||||||
|
$ docker exec -it pretix.service pretix upgrade
|
||||||
@@ -26,7 +26,7 @@ installation guides):
|
|||||||
* `Docker`_
|
* `Docker`_
|
||||||
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for
|
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for
|
||||||
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
|
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
|
||||||
* A `MySQL`_ or `PostgreSQL`_ database server
|
* A `PostgreSQL`_, `MySQL`_ 5.7+, or MariaDB 10.2.7+ database server
|
||||||
* A `redis`_ server
|
* A `redis`_ server
|
||||||
|
|
||||||
We also recommend that you use a firewall, although this is not a pretix-specific recommendation. If you're new to
|
We also recommend that you use a firewall, although this is not a pretix-specific recommendation. If you're new to
|
||||||
@@ -36,6 +36,9 @@ Linux and firewalls, we recommend that you start with `ufw`_.
|
|||||||
SSL certificates can be obtained for free these days. We also *do not* provide support for HTTP-only
|
SSL certificates can be obtained for free these days. We also *do not* provide support for HTTP-only
|
||||||
installations except for evaluation purposes.
|
installations except for evaluation purposes.
|
||||||
|
|
||||||
|
.. warning:: We recommend **PostgreSQL**. If you go for MySQL, make sure you run **MySQL 5.7 or newer** or
|
||||||
|
**MariaDB 10.2.7 or newer**.
|
||||||
|
|
||||||
On this guide
|
On this guide
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
@@ -58,7 +61,7 @@ Next, we need a database and a database user. We can create these with any kind
|
|||||||
our database's shell, e.g. for MySQL::
|
our database's shell, e.g. for MySQL::
|
||||||
|
|
||||||
$ mysql -u root -p
|
$ mysql -u root -p
|
||||||
mysql> CREATE DATABASE pretix DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
|
mysql> CREATE DATABASE pretix DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||||
mysql> GRANT ALL PRIVILEGES ON pretix.* TO pretix@'localhost' IDENTIFIED BY '*********';
|
mysql> GRANT ALL PRIVILEGES ON pretix.* TO pretix@'localhost' IDENTIFIED BY '*********';
|
||||||
mysql> FLUSH PRIVILEGES;
|
mysql> FLUSH PRIVILEGES;
|
||||||
|
|
||||||
|
|||||||
84
doc/admin/installation/enterprise.rst
Normal file
84
doc/admin/installation/enterprise.rst
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
.. highlight:: none
|
||||||
|
|
||||||
|
Installing pretix Enterprise plugins
|
||||||
|
====================================
|
||||||
|
|
||||||
|
If you want to use a feature of pretix that is part of our commercial offering pretix Enterprise, you need to follow
|
||||||
|
some extra steps. Installation works similar to normal pretix plugins, but involves a few extra steps.
|
||||||
|
|
||||||
|
Buying the license
|
||||||
|
------------------
|
||||||
|
|
||||||
|
To obtain a license, please get in touch at sales@pretix.eu. Please let us know how many tickets you roughly intend
|
||||||
|
to sell per year and how many servers you want to use the plugin on. We recommend having a look at our `price list`_
|
||||||
|
first.
|
||||||
|
|
||||||
|
|
||||||
|
Manual installation
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
First, generate an SSH key for the system user that you install pretix as. In our tutorial, that would be the user
|
||||||
|
``pretix``. Choose an empty passphrase::
|
||||||
|
|
||||||
|
# su pretix
|
||||||
|
$ ssh-keygen
|
||||||
|
Generating public/private rsa key pair.
|
||||||
|
Enter file in which to save the key (/var/pretix/.ssh/id_rsa):
|
||||||
|
Enter passphrase (empty for no passphrase):
|
||||||
|
Enter same passphrase again:
|
||||||
|
Your identification has been saved in /var/pretix/.ssh/id_rsa.
|
||||||
|
Your public key has been saved in /var/pretix/.ssh/id_rsa.pub.
|
||||||
|
|
||||||
|
Next, send the content of the *public* key to your sales representative at pretix::
|
||||||
|
|
||||||
|
$ cat /var/pretix/.ssh/id_rsa.pub
|
||||||
|
ssh-rsa AAAAB3N...744HZawHlD pretix@foo
|
||||||
|
|
||||||
|
After we configured your key in our system, you can install the plugin directly using ``pip`` from the URL we told
|
||||||
|
you, for example::
|
||||||
|
|
||||||
|
$ source /var/pretix/venv/bin/activate
|
||||||
|
(venv)$ pip3 install -U "git+ssh://git@code.rami.io:10022/pretix/pretix-slack.git@stable#egg=pretix-slack"
|
||||||
|
(venv)$ python -m pretix migrate
|
||||||
|
(venv)$ python -m pretix rebuild
|
||||||
|
# systemctl restart pretix-web pretix-worker
|
||||||
|
|
||||||
|
Docker installation
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
To install a plugin, you need to build your own docker image. To do so, create a new directory to work in. As a first
|
||||||
|
step, generate a new SSH key in that directory to use for authentication with us::
|
||||||
|
|
||||||
|
$ cd /home/me/mypretixdocker
|
||||||
|
$ ssh-keygen -N "" -f id_pretix_enterprise
|
||||||
|
|
||||||
|
Next, send the content of the *public* key to your sales representative at pretix::
|
||||||
|
|
||||||
|
$ cat id_pretix_enterprise.pub
|
||||||
|
ssh-rsa AAAAB3N...744HZawHlD pretix@foo
|
||||||
|
|
||||||
|
After we configured your key in our system, you can add a ``Dockerfile`` in your directory that includes the newly
|
||||||
|
generated key and installs the plugin from the URL we told you::
|
||||||
|
|
||||||
|
FROM pretix/standalone:stable
|
||||||
|
USER root
|
||||||
|
COPY id_pretix_enterprise /root/.ssh/id_rsa
|
||||||
|
COPY id_pretix_enterprise.pub /root/.ssh/id_rsa.pub
|
||||||
|
RUN chmod -R 0600 /root/.ssh && \
|
||||||
|
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" && \
|
||||||
|
cd /pretix/src && \
|
||||||
|
sudo -u pretixuser make production
|
||||||
|
USER pretixuser
|
||||||
|
|
||||||
|
Then, build the image for docker::
|
||||||
|
|
||||||
|
$ docker build -t mypretix
|
||||||
|
|
||||||
|
You can now use that image ``mypretix`` instead of ``pretix/standalone:stable`` in your ``/etc/systemd/system/pretix.service``
|
||||||
|
service file. Be sure to re-build your custom image after you pulled ``pretix/standalone`` if you want to perform an
|
||||||
|
update to a new version of pretix.
|
||||||
|
|
||||||
|
.. _price list: https://pretix.eu/about/en/pricing
|
||||||
@@ -21,6 +21,9 @@ To use pretix, you will need the following things:
|
|||||||
|
|
||||||
.. warning:: Do not ever use SQLite in production. It will break.
|
.. warning:: Do not ever use SQLite in production. It will break.
|
||||||
|
|
||||||
|
.. warning:: We recommend **PostgreSQL**. If you go for MySQL, make sure you run **MySQL 5.7 or newer** or
|
||||||
|
**MariaDB 10.2.7 or newer**.
|
||||||
|
|
||||||
* A **reverse proxy**. pretix needs to deliver some static content to your users (e.g. CSS, images, ...). While pretix
|
* A **reverse proxy**. pretix needs to deliver some static content to your users (e.g. CSS, images, ...). While pretix
|
||||||
is capable of doing this, having this handled by a proper web server like **nginx** or **Apache** will be much
|
is capable of doing this, having this handled by a proper web server like **nginx** or **Apache** will be much
|
||||||
faster. Also, you need a proxying web server in front to provide SSL encryption.
|
faster. Also, you need a proxying web server in front to provide SSL encryption.
|
||||||
|
|||||||
@@ -10,3 +10,5 @@ for your needs.
|
|||||||
general
|
general
|
||||||
docker_smallscale
|
docker_smallscale
|
||||||
manual_smallscale
|
manual_smallscale
|
||||||
|
dev_version
|
||||||
|
enterprise
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ installation guides):
|
|||||||
|
|
||||||
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for
|
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for
|
||||||
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
|
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
|
||||||
* A `MySQL`_ or `PostgreSQL`_ database server
|
* A `PostgreSQL`_, `MySQL`_ 5.7+, or MariaDB 10.2.7+ database server
|
||||||
* A `redis`_ server
|
* A `redis`_ server
|
||||||
|
|
||||||
We also recommend that you use a firewall, although this is not a pretix-specific recommendation. If you're new to
|
We also recommend that you use a firewall, although this is not a pretix-specific recommendation. If you're new to
|
||||||
@@ -33,6 +33,9 @@ Linux and firewalls, we recommend that you start with `ufw`_.
|
|||||||
SSL certificates can be obtained for free these days. We also *do not* provide support for HTTP-only
|
SSL certificates can be obtained for free these days. We also *do not* provide support for HTTP-only
|
||||||
installations except for evaluation purposes.
|
installations except for evaluation purposes.
|
||||||
|
|
||||||
|
.. warning:: We recommend **PostgreSQL**. If you go for MySQL, make sure you run **MySQL 5.7 or newer** or
|
||||||
|
**MariaDB 10.2.7 or newer**.
|
||||||
|
|
||||||
Unix user
|
Unix user
|
||||||
---------
|
---------
|
||||||
|
|
||||||
@@ -50,7 +53,7 @@ Having the database server installed, we still need a database and a database us
|
|||||||
of database managing tool or directly on our database's shell, e.g. for MySQL::
|
of database managing tool or directly on our database's shell, e.g. for MySQL::
|
||||||
|
|
||||||
$ mysql -u root -p
|
$ mysql -u root -p
|
||||||
mysql> CREATE DATABASE pretix DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
|
mysql> CREATE DATABASE pretix DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||||
mysql> GRANT ALL PRIVILEGES ON pretix.* TO pretix@'localhost' IDENTIFIED BY '*********';
|
mysql> GRANT ALL PRIVILEGES ON pretix.* TO pretix@'localhost' IDENTIFIED BY '*********';
|
||||||
mysql> FLUSH PRIVILEGES;
|
mysql> FLUSH PRIVILEGES;
|
||||||
|
|
||||||
@@ -61,7 +64,7 @@ To build and run pretix, you will need the following debian packages::
|
|||||||
|
|
||||||
# apt-get install git build-essential python-dev python-virtualenv python3 python3-pip \
|
# apt-get install git build-essential python-dev python-virtualenv python3 python3-pip \
|
||||||
python3-dev libxml2-dev libxslt1-dev libffi-dev zlib1g-dev libssl-dev \
|
python3-dev libxml2-dev libxslt1-dev libffi-dev zlib1g-dev libssl-dev \
|
||||||
gettext libpq-dev libmysqlclient-dev libjpeg-dev
|
gettext libpq-dev libmysqlclient-dev libjpeg-dev libopenjp2-7-dev
|
||||||
|
|
||||||
Config file
|
Config file
|
||||||
-----------
|
-----------
|
||||||
@@ -121,8 +124,7 @@ command if you're running PostgreSQL::
|
|||||||
|
|
||||||
(venv)$ pip3 install "pretix[mysql]" gunicorn
|
(venv)$ pip3 install "pretix[mysql]" gunicorn
|
||||||
|
|
||||||
If you are running Python 3.4, you also need to ``pip3 install typing``. This is not required on 3.5 or newer.
|
Note that you need Python 3.5 or newer. You can find out your Python version using ``python -V``.
|
||||||
You can find out your Python version using ``python -V``.
|
|
||||||
|
|
||||||
We also need to create a data directory::
|
We also need to create a data directory::
|
||||||
|
|
||||||
|
|||||||
9
doc/api/auth.rst
Normal file
9
doc/api/auth.rst
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Authentication
|
||||||
|
==============
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
tokenauth
|
||||||
|
oauth
|
||||||
|
deviceauth
|
||||||
137
doc/api/deviceauth.rst
Normal file
137
doc/api/deviceauth.rst
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
.. _`rest-deviceauth`:
|
||||||
|
|
||||||
|
Device authentication
|
||||||
|
=====================
|
||||||
|
|
||||||
|
Initializing a new device
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
Users can create new devices in the "Device" section of their organizer settings. When creating
|
||||||
|
a new device, users can specify a list of events the device is allowed to access. After a new
|
||||||
|
device is created, users will be presented initialization instructions, consisting of an URL
|
||||||
|
and an initialization token. They will also be shown as a QR code with the following contents::
|
||||||
|
|
||||||
|
{"handshake_version": 1, "url": "https://pretix.eu", "token": "kpp4jn8g2ynzonp6"}
|
||||||
|
|
||||||
|
Your application should be able to scan a QR code of this type, or allow to enter the URL and the
|
||||||
|
initialization token manually. The handshake version is not used for manual initialization. When a
|
||||||
|
QR code is scanned with a higher handshake version than you support, you should reject the request
|
||||||
|
and prompt the user to update the client application.
|
||||||
|
|
||||||
|
After your application received the token, you need to call the initialization endpoint to obtain
|
||||||
|
a proper API token. At this point, you need to identify the name and version of your application,
|
||||||
|
as well as the type of underlying hardware. Example:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/device/initialize HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"token": "kpp4jn8g2ynzonp6",
|
||||||
|
"hardware_brand": "Samsung",
|
||||||
|
"hardware_model": "Galaxy S",
|
||||||
|
"software_brand": "pretixdroid",
|
||||||
|
"software_version": "4.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
Every initialization token can only be used once. On success, you will receive a response containing
|
||||||
|
information on your device as well as your API token:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"organizer": "foo",
|
||||||
|
"device_id": 5,
|
||||||
|
"unique_serial": "HHZ9LW9JWP390VFZ",
|
||||||
|
"api_token": "1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd",
|
||||||
|
"name": "Bar"
|
||||||
|
}
|
||||||
|
|
||||||
|
Please make sure that you store this ``api_token`` value. We also recommend storing your device ID, your assigned
|
||||||
|
``unique_serial``, and the ``organizer`` you have access to, but that's up to you.
|
||||||
|
|
||||||
|
In case of an error, the response will look like this:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 400 Bad Request
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{"token":["This initialization token has already been used."]}
|
||||||
|
|
||||||
|
|
||||||
|
Performing API requests
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
You need to include the API token with every request to pretix' API in the ``Authorization`` header
|
||||||
|
like the following:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
:emphasize-lines: 3
|
||||||
|
|
||||||
|
GET /api/v1/organizers/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Authorization: Device 1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd
|
||||||
|
|
||||||
|
Updating the software version
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
If your application is updated, we ask you to tell the server about the new version in use. You can do this at the
|
||||||
|
following endpoint:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/device/update HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Device 1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd
|
||||||
|
|
||||||
|
{
|
||||||
|
"hardware_brand": "Samsung",
|
||||||
|
"hardware_model": "Galaxy S",
|
||||||
|
"software_brand": "pretixdroid",
|
||||||
|
"software_version": "4.1.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
Creating a new API key
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
If you think your API key might have leaked or just want to be extra cautious, the API allows you to create a new key.
|
||||||
|
The old API key will be invalid immediately. A request for a new key looks like this:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/device/roll HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Authorization: Device 1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd
|
||||||
|
|
||||||
|
The response will look like the response to the initialization request.
|
||||||
|
|
||||||
|
Removing a device
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
If you want implement a way to to deprovision a device in your software, you can call the ``revoke`` endpoint to
|
||||||
|
invalidate your API key. There is no way to reverse this operation.
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/device/revoke HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Authorization: Device 1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd
|
||||||
|
|
||||||
|
This can also be done by the user through the web interface.
|
||||||
|
|
||||||
|
Permissions
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Device authentication is currently hardcoded to grant the following permissions:
|
||||||
|
|
||||||
|
* View event meta data and products etc.
|
||||||
|
* View and change orders
|
||||||
|
|
||||||
|
Devices cannot change events or products and cannot access vouchers.
|
||||||
@@ -6,43 +6,42 @@ with pretix' REST API, such as authentication, pagination and similar definition
|
|||||||
|
|
||||||
.. _`rest-auth`:
|
.. _`rest-auth`:
|
||||||
|
|
||||||
Obtaining an API token
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
To authenticate your API requests, you need to obtain an API token. You can create a
|
|
||||||
token in the pretix web interface on the level of organizer teams. Create a new team
|
|
||||||
or choose an existing team that has the level of permissions the token should have and
|
|
||||||
create a new token using the form below the list of team members:
|
|
||||||
|
|
||||||
.. image:: img/token_form.png
|
|
||||||
:class: screenshot
|
|
||||||
|
|
||||||
You can enter a description for the token to distinguish from other tokens later on.
|
|
||||||
Once you click "Add", you will be provided with an API token in the success message.
|
|
||||||
Copy this token, as you won't be able to retrieve it again.
|
|
||||||
|
|
||||||
.. image:: img/token_success.png
|
|
||||||
:class: screenshot
|
|
||||||
|
|
||||||
Authentication
|
Authentication
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
You need to include the API token with every request to pretix' API in the ``Authorization`` header
|
To access the API, you need to present valid authentication credentials. pretix currently
|
||||||
like the following:
|
supports the following authorization schemes:
|
||||||
|
|
||||||
.. sourcecode:: http
|
* :ref:`rest-tokenauth`: This is the simplest way and recommended for server-side applications
|
||||||
:emphasize-lines: 3
|
that interact with pretix without user interaction.
|
||||||
|
* :ref:`rest-oauth`: This is the recommended way to use if you write a third-party application
|
||||||
|
that users can connect with their pretix account. It provides the best user experience, but
|
||||||
|
requires user interaction and slightly more implementation effort.
|
||||||
|
* :ref:`rest-deviceauth`: This is the recommended way if you build apps or hardware devices that can
|
||||||
|
connect to pretix, e.g. for processing check-ins or to sell tickets offline. It provides a way
|
||||||
|
to uniquely identify devices and allows for a quick configuration flow inside your software.
|
||||||
|
* Authentication using browser sessions: This is used by the pretix web interface and it is *not*
|
||||||
|
officially supported for use by third-party applications. It might change or be removed at any
|
||||||
|
time without prior notice. If you use it, you need to comply with Django's `CSRF policies`_.
|
||||||
|
|
||||||
GET /api/v1/organizers/ HTTP/1.1
|
Permissions
|
||||||
Host: pretix.eu
|
-----------
|
||||||
Authorization: Token e1l6gq2ye72thbwkacj7jbri7a7tvxe614ojv8ybureain92ocub46t5gab5966k
|
|
||||||
|
|
||||||
.. note:: The API currently also supports authentication via browser sessions, i.e. the
|
The API follows pretix team based permissions model. Each organizer can have several teams
|
||||||
same way that you authenticate with pretix when using the browser interface.
|
each with it's own set of permissions. Each team can have any number of API keys attached.
|
||||||
Using this type of authentication is *not* officially supported for use by
|
|
||||||
third-party clients and might change or be removed at any time. We plan on
|
To access a given endpoint the team the API key belongs to needs to have the corresponding
|
||||||
adding OAuth2 support in the future for user-level authentication. If you want
|
permission for the organizer/event being accessed.
|
||||||
to use session authentication, be sure to comply with Django's `CSRF policies`_.
|
|
||||||
|
Possible permissions are:
|
||||||
|
|
||||||
|
* Can create events
|
||||||
|
* Can change event settings
|
||||||
|
* Can change product settings
|
||||||
|
* Can view orders
|
||||||
|
* Can change orders
|
||||||
|
* Can view vouchers
|
||||||
|
* Can change vouchers
|
||||||
|
|
||||||
Compatibility
|
Compatibility
|
||||||
-------------
|
-------------
|
||||||
@@ -90,6 +89,41 @@ respective page.
|
|||||||
The field ``results`` contains a list of objects representing the first results. For most
|
The field ``results`` contains a list of objects representing the first results. For most
|
||||||
objects, every page contains 50 results.
|
objects, every page contains 50 results.
|
||||||
|
|
||||||
|
Conditional fetching
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
If you pull object lists from pretix' APIs regularly, we ask you to implement conditional fetching
|
||||||
|
to avoid unnecessary data traffic. This is not supported on all resources and we currently implement
|
||||||
|
two different mechanisms for different resources, which is necessary because we can only obtain best
|
||||||
|
efficiency for resources that do not support deletion operations.
|
||||||
|
|
||||||
|
Object-level conditional fetching
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
The :ref:`rest-orders` resource list contains an HTTP header called ``X-Page-Generated`` containing the
|
||||||
|
current time on the server in ISO 8601 format. On your next request, you can pass this header
|
||||||
|
(as is, without any modifications necessary) as the ``modified_since`` query parameter and you will receive
|
||||||
|
a list containing only objects that have changed in the time since your last request.
|
||||||
|
|
||||||
|
List-level conditional fetching
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
If modification checks are not possible with this granularity, you can instead check for the full list.
|
||||||
|
In this case, the list of objects may contain a regular HTTP header ``Last-Modified`` with the date of the
|
||||||
|
last modification to any item of that resource. You can then pass this date back in your next request in the
|
||||||
|
``If-Modified-Since`` header. If the any object has changed in the meantime, you will receive back a full list
|
||||||
|
(if something it missing, this means the object has been deleted). If nothing happened, we'll send back a
|
||||||
|
``304 Not Modified`` return code.
|
||||||
|
|
||||||
|
This is currently implemented on the following resources:
|
||||||
|
|
||||||
|
* :ref:`rest-categories`
|
||||||
|
* :ref:`rest-items`
|
||||||
|
* :ref:`rest-questions`
|
||||||
|
* :ref:`rest-quotas`
|
||||||
|
* :ref:`rest-subevents`
|
||||||
|
* :ref:`rest-taxrules`
|
||||||
|
|
||||||
Errors
|
Errors
|
||||||
------
|
------
|
||||||
|
|
||||||
@@ -114,6 +148,7 @@ Field specific input errors include the name of the offending fields as keys in
|
|||||||
|
|
||||||
{"amount": ["A valid integer is required."], "description": ["This field may not be blank."]}
|
{"amount": ["A valid integer is required."], "description": ["This field may not be blank."]}
|
||||||
|
|
||||||
|
If you see errors of type ``429 Too Many Requests``, you should read our documentation on :ref:`rest-ratelimit`.
|
||||||
|
|
||||||
Data types
|
Data types
|
||||||
----------
|
----------
|
||||||
@@ -146,4 +181,4 @@ as the string values ``true`` and ``false``.
|
|||||||
If the ``ordering`` parameter is documented for a resource, you can use it to sort the result set by one of the allowed
|
If the ``ordering`` parameter is documented for a resource, you can use it to sort the result set by one of the allowed
|
||||||
fields. Prepend a ``-`` to the field name to reverse the sort order.
|
fields. Prepend a ``-`` to the field name to reverse the sort order.
|
||||||
|
|
||||||
.. _CSRF policies: https://docs.djangoproject.com/en/1.11/ref/csrf/#ajax
|
.. _CSRF policies: https://docs.djangoproject.com/en/1.11/ref/csrf/#ajax
|
||||||
|
|||||||
@@ -14,4 +14,7 @@ in functionality over time.
|
|||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
fundamentals
|
fundamentals
|
||||||
|
auth
|
||||||
resources/index
|
resources/index
|
||||||
|
ratelimit
|
||||||
|
webhooks
|
||||||
|
|||||||
207
doc/api/oauth.rst
Normal file
207
doc/api/oauth.rst
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
.. _`rest-oauth`:
|
||||||
|
|
||||||
|
OAuth authentication / "Connect with pretix"
|
||||||
|
============================================
|
||||||
|
|
||||||
|
In addition to static tokens, pretix supports `OAuth2`_-based authentication starting with
|
||||||
|
pretix 1.16. This allows you to put a "Connect with pretix" button into your website or tool
|
||||||
|
that allows the user to easily set up a connection between the two systems.
|
||||||
|
|
||||||
|
If you haven't worked with OAuth before, have a look at the `OAuth2 Simplified`_ tutorial.
|
||||||
|
|
||||||
|
Registering an application
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
To use OAuth, you need to register your application with the pretix instance you want to connect to.
|
||||||
|
In order to do this, log in to your pretix account and go to your user settings. Click on "Authorized applications"
|
||||||
|
first and then on "Manage your own apps". From there, you can "Create a new application".
|
||||||
|
|
||||||
|
You should fill in a descriptive name of your application that allows users to recognize who you are. You also need to
|
||||||
|
give a list of fully-qualified URLs that users will be redirected to after a successful authorization. After you pressed
|
||||||
|
"Save", you will be presented with a client ID and a client secret. Please note them down and treat the client secret
|
||||||
|
like a password; it should not become available to your users.
|
||||||
|
|
||||||
|
Obtaining an authorization grant
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
To authorize a new user, link or redirect them to the ``authorize`` endpoint, passing your client ID as a query
|
||||||
|
parameter. Additionally, you can pass a scope (currently either ``read``, ``write``, or ``read write``)
|
||||||
|
and an URL the user should be redirected to after successful or failed authorization. You also need to pass the
|
||||||
|
``response_type`` parameter with a value of ``code``. Example::
|
||||||
|
|
||||||
|
https://pretix.eu/api/v1/oauth/authorize?client_id=lsLi0hNL0vk53mEdYjNJxHUn1PcO1R6wVg81dLNT&response_type=code&scope=read+write&redirect_uri=https://pretalx.com
|
||||||
|
|
||||||
|
To prevent CSRF attacks, you can also optionally pass a ``state`` parameter with a random string. Later, when
|
||||||
|
redirecting back to your application, we will pass the same ``state`` parameter back to you, so you can compare if they
|
||||||
|
match.
|
||||||
|
|
||||||
|
After the user granted or denied access, they will be redirected back either to the ``redirect_url`` you passed in the
|
||||||
|
query or to the first redirect URL configured in your application settings.
|
||||||
|
|
||||||
|
On successful registration, we will append the query parameter ``code`` to the URL containing an authorization code.
|
||||||
|
For example, we might redirect the user to this URL::
|
||||||
|
|
||||||
|
https://pretalx.com/?code=eYBBf8gmeD4E01HLoj0XflqO4Lg3Cw&state=e3KCh9mfx07qxU4bRpXk
|
||||||
|
|
||||||
|
You will need this ``code`` parameter to perform the next step.
|
||||||
|
|
||||||
|
On a failed registration, a query string like ``?error=access_denied`` will be appended to the redirection URL.
|
||||||
|
|
||||||
|
.. note:: In this step, the user is allowed to restrict your access to certain organizer accounts. If you try to
|
||||||
|
re-authenticate the user later, the user might be instantly redirected back to you if authorization is already
|
||||||
|
given and would therefore be unable to review their organizer restriction settings. You can append the
|
||||||
|
``approval_prompt=force`` query parameter if you want to make sure the user actively needs to confirm the
|
||||||
|
authorization.
|
||||||
|
|
||||||
|
Getting an access token
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Using the ``code`` value you obtained above and your client ID, you can now request an access token that actually gives
|
||||||
|
access to the API. The ``token`` endpoint expects you to authenticate using `HTTP Basic authentication`_ using your client
|
||||||
|
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
|
||||||
|
|
||||||
|
Request a new access token
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/oauth/token HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Authorization: Basic bHNMaTBoTkwwdms1M21FZFlqTkp4SFVuMVBjTzFSNndWZzgxZExOVDplSmpzZVA0UjJMN0hMcjBiS0p1b3BmbnJtT2cyY3NDeTdYaFVVZ0FoalhUU0NhZHhRTjk3cVNvMkpPaXlWTFpQOEozaTVQd1FVdFIwNUNycG5ac2Z0bXJjdmNTbkZ1SkFmb2ZsUTdZUDRpSjZNTWFYTHIwQ0FpNlhIRFJjV1Awcg==
|
||||||
|
|
||||||
|
grant_type=authorization_code&code=eYBBf8gmeD4E01HLoj0XflqO4Lg3Cw&redirect_uri=https://pretalx.com
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"access_token": "i3ytqTSRWsKp16fqjekHXa4tdM4qNC",
|
||||||
|
"expires_in": 86400,
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"scope": "read write",
|
||||||
|
"refresh_token": "XBK0r8z4A4TTeR9LyMUyU2AM5rqpXp"
|
||||||
|
}
|
||||||
|
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
|
||||||
|
|
||||||
|
As you can see, you receive two types of tokens: One "access token", and one "refresh token". The access token is valid
|
||||||
|
for a day and can be used to actually access the API. The refresh token does not have an expiration date and can be used
|
||||||
|
to obtain a new access_token after a day, so you should make sure to store the access token safely if you need long-term
|
||||||
|
access.
|
||||||
|
|
||||||
|
Using the API with an access token
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
You can supply a valid access token as a ``Bearer``-type token in the ``Authorization`` header to get API access.
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
:emphasize-lines: 3
|
||||||
|
|
||||||
|
GET /api/v1/organizers/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Authorization: Bearer i3ytqTSRWsKp16fqjekHXa4tdM4qNC
|
||||||
|
|
||||||
|
Refreshing an access token
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
You can obtain a new access token using your refresh token any time. This can be done using the same ``token`` endpoint
|
||||||
|
used to obtain the first access token above, but with a different set of parameters:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/oauth/token HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Authorization: Basic bHNMaTBoTkwwdms1M21FZFlqTkp4SFVuMVBjTzFSNndWZzgxZExOVDplSmpzZVA0UjJMN0hMcjBiS0p1b3BmbnJtT2cyY3NDeTdYaFVVZ0FoalhUU0NhZHhRTjk3cVNvMkpPaXlWTFpQOEozaTVQd1FVdFIwNUNycG5ac2Z0bXJjdmNTbkZ1SkFmb2ZsUTdZUDRpSjZNTWFYTHIwQ0FpNlhIRFJjV1Awcg==
|
||||||
|
|
||||||
|
grant_type=refresh_token&refresh_token=XBK0r8z4A4TTeR9LyMUyU2AM5rqpXp
|
||||||
|
|
||||||
|
The previous access token will instantly become invalid.
|
||||||
|
|
||||||
|
Revoking a token
|
||||||
|
----------------
|
||||||
|
|
||||||
|
If you don't need a token any more or if you believe it may have been compromised, you can use the ``revoke_token``
|
||||||
|
endpoint to revoke it.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/oauth/revoke_token
|
||||||
|
|
||||||
|
Revoke an access or refresh token. If you revoke an access token, you can still create a new one using the refresh token. If you
|
||||||
|
revoke a refresh token, the connected access token will also be revoked.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/oauth/revoke_token HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Authorization: Basic bHNMaTBoTkwwdms1M21FZFlqTkp4SFVuMVBjTzFSNndWZzgxZExOVDplSmpzZVA0UjJMN0hMcjBiS0p1b3BmbnJtT2cyY3NDeTdYaFVVZ0FoalhUU0NhZHhRTjk3cVNvMkpPaXlWTFpQOEozaTVQd1FVdFIwNUNycG5ac2Z0bXJjdmNTbkZ1SkFmb2ZsUTdZUDRpSjZNTWFYTHIwQ0FpNlhIRFJjV1Awcg==
|
||||||
|
|
||||||
|
token=XBK0r8z4A4TTeR9LyMUyU2AM5rqpXp
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
|
||||||
|
If you want to revoke your client secret, you can generate a new one in the list of your managed applications in the
|
||||||
|
pretix user interface.
|
||||||
|
|
||||||
|
Fetching the user profile
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
If you need the user's meta data, you can fetch it here:
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/me
|
||||||
|
|
||||||
|
Returns the profile of the authenticated user
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/me HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Authorization: Bearer i3ytqTSRWsKp16fqjekHXa4tdM4qNC
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
email: "admin@localhost",
|
||||||
|
fullname: "John Doe",
|
||||||
|
locale: "de",
|
||||||
|
timezone: "Europe/Berlin"
|
||||||
|
}
|
||||||
|
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
|
||||||
|
.. _OAuth2: https://en.wikipedia.org/wiki/OAuth
|
||||||
|
.. _OAuth2 Simplified: https://aaronparecki.com/oauth-2-simplified/
|
||||||
|
.. _HTTP Basic authentication: https://en.wikipedia.org/wiki/Basic_access_authentication
|
||||||
31
doc/api/ratelimit.rst
Normal file
31
doc/api/ratelimit.rst
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
.. _`rest-ratelimit`:
|
||||||
|
|
||||||
|
Rate limiting
|
||||||
|
=============
|
||||||
|
|
||||||
|
.. note:: This page only applies to the pretix Hosted service at pretix.eu. APIs of custom pretix installations do not
|
||||||
|
enforce any rate limiting by default.
|
||||||
|
|
||||||
|
All authenticated requests to pretix' API are rate limited. If you exceed the limits, you will receive a response
|
||||||
|
with HTTP status code ``429 Too Many Requests``. This response will have a ``Retry-After`` header, containing the number
|
||||||
|
of seconds you are supposed to wait until you try again. We expect that all API clients respect this. If you continue
|
||||||
|
to burst requests after a ``429`` status code, we might get in touch with you or, in extreme cases, disable your API
|
||||||
|
access.
|
||||||
|
|
||||||
|
Currently, the following rate limits apply:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== =================================================================================
|
||||||
|
Authentication method Rate limit
|
||||||
|
===================================== =================================================================================
|
||||||
|
:ref:`rest-deviceauth` 360 requests per minute per device
|
||||||
|
:ref:`rest-tokenauth` 360 requests per minute per organizer account
|
||||||
|
:ref:`rest-oauth` 360 requests per minute per combination of accessed organizer and OAuth application
|
||||||
|
Session authentication *Not an officially supported authentication method for external access*
|
||||||
|
===================================== =================================================================================
|
||||||
|
|
||||||
|
If you require a higher rate limit, please get in touch at support@pretix.eu and tell us about your use case, we are
|
||||||
|
sure we can work something out.
|
||||||
264
doc/api/resources/carts.rst
Normal file
264
doc/api/resources/carts.rst
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
.. _rest-carts:
|
||||||
|
|
||||||
|
Cart positions
|
||||||
|
==============
|
||||||
|
|
||||||
|
The API provides limited access to the cart position data model. This API currently only allows creating and deleting
|
||||||
|
cart positions to reserve quota.
|
||||||
|
|
||||||
|
Cart position resource
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
The cart position resource contains the following public fields:
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
id integer Internal ID of the cart position
|
||||||
|
cart_id string Identifier of the cart this belongs to. Needs to end
|
||||||
|
in "@api" for API-created positions.
|
||||||
|
datetime datetime Time of creation
|
||||||
|
expires datetime The cart position will expire at this time and no longer block quota
|
||||||
|
item integer ID of the item
|
||||||
|
variation integer ID of the 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 Composition of attendee name (i.e. first name, last name, …)
|
||||||
|
attendee_email string Specified attendee email address for this position (or ``null``)
|
||||||
|
voucher integer Internal ID of the voucher used for this position (or ``null``)
|
||||||
|
addon_to integer Internal ID of the position this position is an add-on for (or ``null``)
|
||||||
|
subevent integer ID of the date inside an event series this position belongs to (or ``null``).
|
||||||
|
answers list of objects Answers to user-defined questions
|
||||||
|
├ question integer Internal ID of the answered question
|
||||||
|
├ answer string Text representation of the answer
|
||||||
|
├ question_identifier string The question's ``identifier`` field
|
||||||
|
├ options list of integers Internal IDs of selected option(s)s (only for choice types)
|
||||||
|
└ option_identifiers list of strings The ``identifier`` fields of the selected option(s)s
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
.. versionchanged:: 1.17
|
||||||
|
|
||||||
|
This resource has been added.
|
||||||
|
|
||||||
|
|
||||||
|
Cart position endpoints
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/cartpositions/
|
||||||
|
|
||||||
|
Returns a list of API-created cart positions.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/cartpositions/ 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
|
||||||
|
X-Page-Generated: 2017-12-01T10:00:00Z
|
||||||
|
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"cart_id": "XwokV8FojQviD9jhtDzKvHFdlLRNMhlfo3cNjGbuK6MUTQDT@api",
|
||||||
|
"item": 1,
|
||||||
|
"variation": null,
|
||||||
|
"price": "23.00",
|
||||||
|
"attendee_name": null,
|
||||||
|
"attendee_name_parts": {},
|
||||||
|
"attendee_email": null,
|
||||||
|
"voucher": null,
|
||||||
|
"addon_to": null,
|
||||||
|
"subevent": null,
|
||||||
|
"datetime": "2018-06-11T10:00:00Z",
|
||||||
|
"expires": "2018-06-11T10:00:00Z",
|
||||||
|
"includes_tax": true,
|
||||||
|
"answers": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
: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
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/cartpositions/(id)/
|
||||||
|
|
||||||
|
Returns information on one cart position, identified by its internal ID.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/cartpositions/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,
|
||||||
|
"cart_id": "XwokV8FojQviD9jhtDzKvHFdlLRNMhlfo3cNjGbuK6MUTQDT@api",
|
||||||
|
"item": 1,
|
||||||
|
"variation": null,
|
||||||
|
"price": "23.00",
|
||||||
|
"attendee_name": null,
|
||||||
|
"attendee_name_parts": {},
|
||||||
|
"attendee_email": null,
|
||||||
|
"voucher": null,
|
||||||
|
"addon_to": null,
|
||||||
|
"subevent": null,
|
||||||
|
"datetime": "2018-06-11T10:00:00Z",
|
||||||
|
"expires": "2018-06-11T10:00:00Z",
|
||||||
|
"includes_tax": true,
|
||||||
|
"answers": []
|
||||||
|
}
|
||||||
|
|
||||||
|
: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 position to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||||
|
:statuscode 404: The requested cart position does not exist.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/cartpositions/
|
||||||
|
|
||||||
|
Creates a new cart position.
|
||||||
|
|
||||||
|
.. warning:: This endpoint is considered **experimental**. It might change at any time without prior notice.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
This endpoint is intended for advanced users. It is not designed to be used to build your own shop frontend.
|
||||||
|
There is a lot that it does not or can not do, and you will need to be careful using it.
|
||||||
|
It allows to bypass many of the restrictions imposed when creating a cart through the
|
||||||
|
regular shop.
|
||||||
|
|
||||||
|
Specifically, this endpoint currently
|
||||||
|
|
||||||
|
* does not validate if products are only to be sold in a specific time frame
|
||||||
|
|
||||||
|
* does not validate if the event's ticket sales are already over or haven't started
|
||||||
|
|
||||||
|
* does not support add-on products at the moment
|
||||||
|
|
||||||
|
* does not check or calculate prices but believes any prices you send
|
||||||
|
|
||||||
|
* does not support the redemption of vouchers
|
||||||
|
|
||||||
|
* does not prevent you from buying items that can only be bought with a voucher
|
||||||
|
|
||||||
|
* does not support file upload questions
|
||||||
|
|
||||||
|
You can supply the following fields of the resource:
|
||||||
|
|
||||||
|
* ``cart_id`` (optional, needs to end in ``@api``)
|
||||||
|
* ``item``
|
||||||
|
* ``variation`` (optional)
|
||||||
|
* ``price``
|
||||||
|
* ``attendee_name`` **or** ``attendee_name_parts`` (optional)
|
||||||
|
* ``attendee_email`` (optional)
|
||||||
|
* ``subevent`` (optional)
|
||||||
|
* ``expires`` (optional)
|
||||||
|
* ``includes_tax`` (optional)
|
||||||
|
* ``answers``
|
||||||
|
|
||||||
|
* ``question``
|
||||||
|
* ``answer``
|
||||||
|
* ``options``
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/cartpositions/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"item": 1,
|
||||||
|
"variation": null,
|
||||||
|
"price": "23.00",
|
||||||
|
"attendee_name_parts": {
|
||||||
|
"given_name": "Peter",
|
||||||
|
"family_name": "Miller"
|
||||||
|
},
|
||||||
|
"attendee_email": null,
|
||||||
|
"answers": [
|
||||||
|
{
|
||||||
|
"question": 1,
|
||||||
|
"answer": "23",
|
||||||
|
"options": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subevent": null
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
(Full cart position resource, see above.)
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer of the event to create a position for
|
||||||
|
:param event: The ``slug`` field of the event to create a position for
|
||||||
|
:statuscode 201: no error
|
||||||
|
:statuscode 400: The item could not be created due to invalid submitted data or lack of quota.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this
|
||||||
|
order.
|
||||||
|
|
||||||
|
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/cartpositions/(id)/
|
||||||
|
|
||||||
|
Deletes a cart position, identified by its internal ID.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
DELETE /api/v1/organizers/bigevents/events/sampleconf/cartpositions/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
: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 position to delete
|
||||||
|
: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 cart position does not exist.
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
.. _`rest-categories`:
|
||||||
|
|
||||||
Item categories
|
Item categories
|
||||||
===============
|
===============
|
||||||
|
|
||||||
@@ -14,6 +16,7 @@ Field Type Description
|
|||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
id integer Internal ID of the category
|
id integer Internal ID of the category
|
||||||
name multi-lingual string The category's visible name
|
name multi-lingual string The category's visible name
|
||||||
|
internal_name string An optional name that is only used in the backend
|
||||||
description multi-lingual string A public description (might include markdown, can
|
description multi-lingual string A public description (might include markdown, can
|
||||||
be ``null``)
|
be ``null``)
|
||||||
position integer An integer, used for sorting the categories
|
position integer An integer, used for sorting the categories
|
||||||
@@ -26,6 +29,10 @@ is_addon boolean If ``True``, it
|
|||||||
|
|
||||||
The operations POST, PATCH, PUT and DELETE have been added.
|
The operations POST, PATCH, PUT and DELETE have been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.16
|
||||||
|
|
||||||
|
The field ``internal_name`` has been added.
|
||||||
|
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
@@ -58,6 +65,7 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Tickets"},
|
"name": {"en": "Tickets"},
|
||||||
|
"internal_name": "",
|
||||||
"description": {"en": "Tickets are what you need to get in."},
|
"description": {"en": "Tickets are what you need to get in."},
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"is_addon": false
|
"is_addon": false
|
||||||
@@ -99,6 +107,7 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Tickets"},
|
"name": {"en": "Tickets"},
|
||||||
|
"internal_name": "",
|
||||||
"description": {"en": "Tickets are what you need to get in."},
|
"description": {"en": "Tickets are what you need to get in."},
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"is_addon": false
|
"is_addon": false
|
||||||
@@ -126,6 +135,7 @@ Endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"name": {"en": "Tickets"},
|
"name": {"en": "Tickets"},
|
||||||
|
"internal_name": "",
|
||||||
"description": {"en": "Tickets are what you need to get in."},
|
"description": {"en": "Tickets are what you need to get in."},
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"is_addon": false
|
"is_addon": false
|
||||||
@@ -142,6 +152,7 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Tickets"},
|
"name": {"en": "Tickets"},
|
||||||
|
"internal_name": "",
|
||||||
"description": {"en": "Tickets are what you need to get in."},
|
"description": {"en": "Tickets are what you need to get in."},
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"is_addon": false
|
"is_addon": false
|
||||||
@@ -187,6 +198,7 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Tickets"},
|
"name": {"en": "Tickets"},
|
||||||
|
"internal_name": "",
|
||||||
"description": {"en": "Tickets are what you need to get in."},
|
"description": {"en": "Tickets are what you need to get in."},
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"is_addon": true
|
"is_addon": true
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ include_pending boolean If ``true``, th
|
|||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
|
|
||||||
|
.. versionchanged:: 1.15
|
||||||
|
|
||||||
|
The ``../status/`` detail endpoint has been added.
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/
|
||||||
|
|
||||||
Returns a list of all check-in lists within a given event.
|
Returns a list of all check-in lists within a given event.
|
||||||
@@ -128,6 +132,72 @@ Endpoints
|
|||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(id)/status/
|
||||||
|
|
||||||
|
Returns detailed status information on a check-in list, identified by its ID.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/checkinlists/1/status/ 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
|
||||||
|
|
||||||
|
{
|
||||||
|
"checkin_count": 17,
|
||||||
|
"position_count": 42,
|
||||||
|
"event": {
|
||||||
|
"name": "Demo Converence",
|
||||||
|
},
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"name": "T-Shirt",
|
||||||
|
"id": 1,
|
||||||
|
"checkin_count": 1,
|
||||||
|
"admission": False,
|
||||||
|
"position_count": 1,
|
||||||
|
"variations": [
|
||||||
|
{
|
||||||
|
"value": "Red",
|
||||||
|
"id": 1,
|
||||||
|
"checkin_count": 1,
|
||||||
|
"position_count": 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "Blue",
|
||||||
|
"id": 2,
|
||||||
|
"checkin_count": 4,
|
||||||
|
"position_count": 8
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ticket",
|
||||||
|
"id": 2,
|
||||||
|
"checkin_count": 15,
|
||||||
|
"admission": True,
|
||||||
|
"position_count": 22,
|
||||||
|
"variations": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
: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 check-in list to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/
|
||||||
|
|
||||||
Creates a new check-in list.
|
Creates a new check-in list.
|
||||||
@@ -254,6 +324,18 @@ Endpoints
|
|||||||
Order position endpoints
|
Order position endpoints
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
|
.. versionchanged:: 1.15
|
||||||
|
|
||||||
|
The order positions endpoint has been extended by the filter queries ``item__in``, ``variation__in``,
|
||||||
|
``order__status__in``, ``subevent__in``, ``addon_to__in``, and ``search``. The search for attendee names and order
|
||||||
|
codes is now case-insensitive.
|
||||||
|
|
||||||
|
The ``.../redeem/`` endpoint has been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0
|
||||||
|
|
||||||
|
The order positions endpoint has been extended by the filter queries ``voucher`` and ``voucher__code``.
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(list)/positions/
|
.. 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
|
Returns a list of all order positions within a given event. The result is the same as
|
||||||
@@ -289,6 +371,9 @@ Order position endpoints
|
|||||||
"variation": null,
|
"variation": null,
|
||||||
"price": "23.00",
|
"price": "23.00",
|
||||||
"attendee_name": "Peter",
|
"attendee_name": "Peter",
|
||||||
|
"attendee_name_parts": {
|
||||||
|
"full_name": "Peter",
|
||||||
|
},
|
||||||
"attendee_email": null,
|
"attendee_email": null,
|
||||||
"voucher": null,
|
"voucher": null,
|
||||||
"tax_rate": "0.00",
|
"tax_rate": "0.00",
|
||||||
@@ -297,6 +382,7 @@ Order position endpoints
|
|||||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||||
"addon_to": null,
|
"addon_to": null,
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
|
"pseudonymization_id": "MQLJvANO3B",
|
||||||
"checkins": [
|
"checkins": [
|
||||||
{
|
{
|
||||||
"list": 1,
|
"list": 1,
|
||||||
@@ -325,15 +411,26 @@ Order position endpoints
|
|||||||
``order__datetime``, ``positionid``, ``attendee_name``, ``last_checked_in`` and ``order__email``. Default:
|
``order__datetime``, ``positionid``, ``attendee_name``, ``last_checked_in`` and ``order__email``. Default:
|
||||||
``attendee_name,positionid``
|
``attendee_name,positionid``
|
||||||
:query string order: Only return positions of the order with the given order code
|
:query string order: Only return positions of the order with the given order code
|
||||||
|
:query string search: Fuzzy search matching the attendee name, order code, invoice address name as well as to the beginning of the secret.
|
||||||
:query integer item: Only return positions with the purchased item matching the given ID.
|
:query integer item: Only return positions with the purchased item matching the given ID.
|
||||||
|
:query integer item__in: Only return positions with the purchased item matching one of the given comma-separated IDs.
|
||||||
:query integer variation: Only return positions with the purchased item variation matching the given ID.
|
:query integer variation: Only return positions with the purchased item variation matching the given ID.
|
||||||
|
:query integer variation__in: Only return positions with one of the purchased item variation matching the given
|
||||||
|
comma-separated IDs.
|
||||||
:query string attendee_name: Only return positions with the given value in the attendee_name field. Also, add-on
|
:query string attendee_name: Only return positions with the given value in the attendee_name field. Also, add-on
|
||||||
products positions are shown if they refer to an attendee with the given name.
|
products positions are shown if they refer to an attendee with the given name.
|
||||||
:query string secret: Only return positions with the given ticket secret.
|
:query string secret: Only return positions with the given ticket secret.
|
||||||
:query bollean has_checkin: If set to ``true`` or ``false``, only return positions that have or have not been
|
:query string order__status: Only return positions with the given order status.
|
||||||
checked in already on this list.
|
:query string order__status__in: Only return positions with one the given comma-separated order status.
|
||||||
|
:query boolean has_checkin: If set to ``true`` or ``false``, only return positions that have or have not been
|
||||||
|
checked in already.
|
||||||
:query integer subevent: Only return positions of the sub-event with the given ID
|
:query integer subevent: Only return positions of the sub-event with the given ID
|
||||||
|
:query integer subevent__in: Only return positions of one of the sub-events with the given comma-separated IDs
|
||||||
:query integer addon_to: Only return positions that are add-ons to the position with the given ID.
|
:query integer addon_to: Only return positions that are add-ons to the position with the given ID.
|
||||||
|
:query integer addon_to__in: Only return positions that are add-ons to one of the positions with the given
|
||||||
|
comma-separated IDs.
|
||||||
|
:query string voucher: Only return positions with a specific voucher.
|
||||||
|
:query string voucher__code: Only return positions with a specific voucher code.
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:param event: The ``slug`` field of the event to fetch
|
:param event: The ``slug`` field of the event to fetch
|
||||||
:param list: The ID of the check-in list to look for
|
:param list: The ID of the check-in list to look for
|
||||||
@@ -342,17 +439,19 @@ Order position endpoints
|
|||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||||
:statuscode 404: The requested check-in list does not exist.
|
:statuscode 404: The requested check-in list does not exist.
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(list)/positions/(id)
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(list)/positions/(id)/
|
||||||
|
|
||||||
Returns information on one order position, identified by its internal ID.
|
Returns information on one order position, identified by its internal ID.
|
||||||
The result format is the same as the :ref:`order-position-resource`, with one important difference: the
|
The result format is the same as the :ref:`order-position-resource`, with one important difference: the
|
||||||
``checkins`` value will only include check-ins for the selected list.
|
``checkins`` value will only include check-ins for the selected list.
|
||||||
|
|
||||||
|
**Instead of an ID, you can also use the ``secret`` field as the lookup parameter.**
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
|
|
||||||
GET /api/v1/organizers/bigevents/events/sampleconf/checkinlists/1/positions/ HTTP/1.1
|
GET /api/v1/organizers/bigevents/events/sampleconf/checkinlists/1/positions/23442/ HTTP/1.1
|
||||||
Host: pretix.eu
|
Host: pretix.eu
|
||||||
Accept: application/json, text/javascript
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
@@ -372,6 +471,9 @@ Order position endpoints
|
|||||||
"variation": null,
|
"variation": null,
|
||||||
"price": "23.00",
|
"price": "23.00",
|
||||||
"attendee_name": "Peter",
|
"attendee_name": "Peter",
|
||||||
|
"attendee_name_parts": {
|
||||||
|
"full_name": "Peter",
|
||||||
|
},
|
||||||
"attendee_email": null,
|
"attendee_email": null,
|
||||||
"voucher": null,
|
"voucher": null,
|
||||||
"tax_rate": "0.00",
|
"tax_rate": "0.00",
|
||||||
@@ -380,6 +482,7 @@ Order position endpoints
|
|||||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||||
"addon_to": null,
|
"addon_to": null,
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
|
"pseudonymization_id": "MQLJvANO3B",
|
||||||
"checkins": [
|
"checkins": [
|
||||||
{
|
{
|
||||||
"list": 1,
|
"list": 1,
|
||||||
@@ -409,3 +512,138 @@ Order position endpoints
|
|||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||||
:statuscode 404: The requested order position or check-in list does not exist.
|
:statuscode 404: The requested order position or check-in list does not exist.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(list)/positions/(id)/redeem/
|
||||||
|
|
||||||
|
Tries to redeem an order position, identified by its internal ID, i.e. checks the attendee in. This endpoint
|
||||||
|
accepts a number of optional requests in the body.
|
||||||
|
|
||||||
|
**Instead of an ID, you can also use the ``secret`` field as the lookup parameter.**
|
||||||
|
|
||||||
|
:<json boolean questions_supported: When this parameter is set to ``true``, handling of questions is supported. If
|
||||||
|
you do not implement question handling in your user interface, you **must**
|
||||||
|
set this to ``false``. In that case, questions will just be ignored. Defaults
|
||||||
|
to ``true``.
|
||||||
|
:<json datetime datetime: Specifies the datetime of the check-in. If not supplied, the current time will be used.
|
||||||
|
:<json boolean force: Specifies that the check-in should succeed regardless of previous check-ins or required
|
||||||
|
questions that have not been filled. Defaults to ``false``.
|
||||||
|
:<json boolean ignore_unpaid: Specifies that the check-in should succeed even if the order is in pending state.
|
||||||
|
Defaults to ``false``.
|
||||||
|
:<json string nonce: You can set this parameter to a unique random value to identify this check-in. If you're sending
|
||||||
|
this request twice with the same nonce, the second request will also succeed but will always
|
||||||
|
create only one check-in object even when the previous request was successful as well. This
|
||||||
|
allows for a certain level of idempotency and enables you to re-try after a connection failure.
|
||||||
|
:<json object answers: If questions are supported/required, you may/must supply a mapping of question IDs to their
|
||||||
|
respective answers. The answers should always be strings. In case of (multiple-)choice-type
|
||||||
|
answers, the string should contain the (comma-separated) IDs of the selected options.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/checkinlists/1/positions/234/redeem/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"force": false,
|
||||||
|
"ignore_unpaid": false,
|
||||||
|
"nonce": "Pvrk50vUzQd0DhdpNRL4I4OcXsvg70uA",
|
||||||
|
"datetime": null,
|
||||||
|
"questions_supported": true,
|
||||||
|
"answers": {
|
||||||
|
"4": "XS"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example successful response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"position": {
|
||||||
|
…
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response with required questions**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 400 Bad Request
|
||||||
|
Content-Type: text/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"status": "incomplete",
|
||||||
|
"position": {
|
||||||
|
…
|
||||||
|
},
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"question": {"en": "T-Shirt size"},
|
||||||
|
"type": "C",
|
||||||
|
"required": false,
|
||||||
|
"items": [1, 2],
|
||||||
|
"position": 1,
|
||||||
|
"identifier": "WY3TP9SL",
|
||||||
|
"ask_during_checkin": true,
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"identifier": "LVETRWVU",
|
||||||
|
"position": 0,
|
||||||
|
"answer": {"en": "S"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"identifier": "DFEMJWMJ",
|
||||||
|
"position": 1,
|
||||||
|
"answer": {"en": "M"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"identifier": "W9AH7RDE",
|
||||||
|
"position": 2,
|
||||||
|
"answer": {"en": "L"}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example error response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: text/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"reason": "unpaid",
|
||||||
|
"position": {
|
||||||
|
…
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Possible error reasons:
|
||||||
|
|
||||||
|
* ``unpaid`` - Ticket is not paid for or has been refunded
|
||||||
|
* ``already_redeemed`` - Ticket already has been redeemed
|
||||||
|
* ``product`` - Tickets with this product may not be scanned at this device
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:param list: The ID of the check-in list to look for
|
||||||
|
:param id: The ``id`` field of the order position to fetch
|
||||||
|
:statuscode 201: no error
|
||||||
|
:statuscode 400: Invalid or incomplete request, see above
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||||
|
:statuscode 404: The requested order position or check-in list does not exist.
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ name multi-lingual string The event's ful
|
|||||||
slug string A short form of the name, used e.g. in URLs.
|
slug string A short form of the name, used e.g. in URLs.
|
||||||
live boolean If ``true``, the event ticket shop is publicly
|
live boolean If ``true``, the event ticket shop is publicly
|
||||||
available.
|
available.
|
||||||
|
testmode boolean If ``true``, the ticket shop is in test mode.
|
||||||
currency string The currency this event is handled in.
|
currency string The currency this event is handled in.
|
||||||
date_from datetime The event's start date
|
date_from datetime The event's start date
|
||||||
date_to datetime The event's end date (or ``null``)
|
date_to datetime The event's end date (or ``null``)
|
||||||
@@ -25,14 +26,30 @@ presale_start datetime The date at whi
|
|||||||
presale_end datetime The date at which the ticket shop closes (or ``null``)
|
presale_end datetime The date at which the ticket shop closes (or ``null``)
|
||||||
location multi-lingual string The event location (or ``null``)
|
location multi-lingual string The event location (or ``null``)
|
||||||
has_subevents boolean ``True`` if the event series feature is active for this
|
has_subevents boolean ``True`` if the event series feature is active for this
|
||||||
event
|
event. Cannot change after event is created.
|
||||||
meta_data dict Values set for organizer-specific meta data parameters.
|
meta_data dict Values set for organizer-specific meta data parameters.
|
||||||
|
plugins list A list of package names of the enabled plugins for this
|
||||||
|
event.
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
.. versionchanged:: 1.7
|
.. versionchanged:: 1.7
|
||||||
|
|
||||||
The ``meta_data`` field has been added.
|
The ``meta_data`` field has been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.15
|
||||||
|
|
||||||
|
The ``plugins`` field has been added.
|
||||||
|
The operations POST, PATCH, PUT and DELETE have been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.1
|
||||||
|
|
||||||
|
Filters have been added to the list of events.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.5
|
||||||
|
|
||||||
|
The ``testmode`` attribute has been added.
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
|
|
||||||
@@ -40,6 +57,8 @@ Endpoints
|
|||||||
|
|
||||||
Returns a list of all events within a given organizer the authenticated user/token has access to.
|
Returns a list of all events within a given organizer the authenticated user/token has access to.
|
||||||
|
|
||||||
|
Permission required: "Can change event settings"
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@@ -65,6 +84,7 @@ Endpoints
|
|||||||
"name": {"en": "Sample Conference"},
|
"name": {"en": "Sample Conference"},
|
||||||
"slug": "sampleconf",
|
"slug": "sampleconf",
|
||||||
"live": false,
|
"live": false,
|
||||||
|
"testmode": false,
|
||||||
"currency": "EUR",
|
"currency": "EUR",
|
||||||
"date_from": "2017-12-27T10:00:00Z",
|
"date_from": "2017-12-27T10:00:00Z",
|
||||||
"date_to": null,
|
"date_to": null,
|
||||||
@@ -74,12 +94,24 @@ Endpoints
|
|||||||
"presale_end": null,
|
"presale_end": null,
|
||||||
"location": null,
|
"location": null,
|
||||||
"has_subevents": false,
|
"has_subevents": false,
|
||||||
"meta_data": {}
|
"meta_data": {},
|
||||||
|
"plugins": [
|
||||||
|
"pretix.plugins.banktransfer"
|
||||||
|
"pretix.plugins.stripe"
|
||||||
|
"pretix.plugins.paypal"
|
||||||
|
"pretix.plugins.ticketoutputpdf"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
:query page: The page number in case of a multi-page result set, default is 1
|
:query page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:query is_public: If set to ``true``/``false``, only events with a matching value of ``is_public`` are returned.
|
||||||
|
:query live: If set to ``true``/``false``, only events with a matching value of ``live`` are returned.
|
||||||
|
:query has_subevents: If set to ``true``/``false``, only events with a matching value of ``has_subevents`` are returned.
|
||||||
|
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned. Event series are never (always) returned.
|
||||||
|
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned. Event series are never (always) returned.
|
||||||
|
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned. Event series are never returned.
|
||||||
:param organizer: The ``slug`` field of a valid organizer
|
:param organizer: The ``slug`` field of a valid organizer
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
@@ -89,6 +121,8 @@ Endpoints
|
|||||||
|
|
||||||
Returns information on one event, identified by its slug.
|
Returns information on one event, identified by its slug.
|
||||||
|
|
||||||
|
Permission required: "Can change event settings"
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@@ -109,6 +143,7 @@ Endpoints
|
|||||||
"name": {"en": "Sample Conference"},
|
"name": {"en": "Sample Conference"},
|
||||||
"slug": "sampleconf",
|
"slug": "sampleconf",
|
||||||
"live": false,
|
"live": false,
|
||||||
|
"testmode": false,
|
||||||
"currency": "EUR",
|
"currency": "EUR",
|
||||||
"date_from": "2017-12-27T10:00:00Z",
|
"date_from": "2017-12-27T10:00:00Z",
|
||||||
"date_to": null,
|
"date_to": null,
|
||||||
@@ -118,7 +153,13 @@ Endpoints
|
|||||||
"presale_end": null,
|
"presale_end": null,
|
||||||
"location": null,
|
"location": null,
|
||||||
"has_subevents": false,
|
"has_subevents": false,
|
||||||
"meta_data": {}
|
"meta_data": {},
|
||||||
|
"plugins": [
|
||||||
|
"pretix.plugins.banktransfer"
|
||||||
|
"pretix.plugins.stripe"
|
||||||
|
"pretix.plugins.paypal"
|
||||||
|
"pretix.plugins.ticketoutputpdf"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -126,3 +167,247 @@ Endpoints
|
|||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/
|
||||||
|
|
||||||
|
Creates a new event
|
||||||
|
|
||||||
|
Please note that events cannot be created as 'live' using this endpoint. Quotas and payment must be added to the
|
||||||
|
event before sales can go live.
|
||||||
|
|
||||||
|
Permission required: "Can create events"
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": {"en": "Sample Conference"},
|
||||||
|
"slug": "sampleconf",
|
||||||
|
"live": false,
|
||||||
|
"testmode": false,
|
||||||
|
"currency": "EUR",
|
||||||
|
"date_from": "2017-12-27T10:00:00Z",
|
||||||
|
"date_to": null,
|
||||||
|
"date_admission": null,
|
||||||
|
"is_public": false,
|
||||||
|
"presale_start": null,
|
||||||
|
"presale_end": null,
|
||||||
|
"location": null,
|
||||||
|
"has_subevents": false,
|
||||||
|
"meta_data": {},
|
||||||
|
"plugins": [
|
||||||
|
"pretix.plugins.stripe",
|
||||||
|
"pretix.plugins.paypal"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": {"en": "Sample Conference"},
|
||||||
|
"slug": "sampleconf",
|
||||||
|
"live": false,
|
||||||
|
"testmode": false,
|
||||||
|
"currency": "EUR",
|
||||||
|
"date_from": "2017-12-27T10:00:00Z",
|
||||||
|
"date_to": null,
|
||||||
|
"date_admission": null,
|
||||||
|
"is_public": false,
|
||||||
|
"presale_start": null,
|
||||||
|
"presale_end": null,
|
||||||
|
"location": null,
|
||||||
|
"has_subevents": false,
|
||||||
|
"meta_data": {},
|
||||||
|
"plugins": [
|
||||||
|
"pretix.plugins.stripe",
|
||||||
|
"pretix.plugins.paypal"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer of the event to create.
|
||||||
|
:statuscode 201: no error
|
||||||
|
:statuscode 400: The event 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: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',
|
||||||
|
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.
|
||||||
|
|
||||||
|
Please note that you can only copy from events under the same organizer.
|
||||||
|
|
||||||
|
Permission required: "Can create events"
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/clone/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": {"en": "Sample Conference"},
|
||||||
|
"slug": "sampleconf",
|
||||||
|
"live": false,
|
||||||
|
"testmode": false,
|
||||||
|
"currency": "EUR",
|
||||||
|
"date_from": "2017-12-27T10:00:00Z",
|
||||||
|
"date_to": null,
|
||||||
|
"date_admission": null,
|
||||||
|
"is_public": false,
|
||||||
|
"presale_start": null,
|
||||||
|
"presale_end": null,
|
||||||
|
"location": null,
|
||||||
|
"has_subevents": false,
|
||||||
|
"meta_data": {},
|
||||||
|
"plugins": [
|
||||||
|
"pretix.plugins.stripe",
|
||||||
|
"pretix.plugins.paypal"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": {"en": "Sample Conference"},
|
||||||
|
"slug": "sampleconf",
|
||||||
|
"live": false,
|
||||||
|
"testmode": false,
|
||||||
|
"currency": "EUR",
|
||||||
|
"date_from": "2017-12-27T10:00:00Z",
|
||||||
|
"date_to": null,
|
||||||
|
"date_admission": null,
|
||||||
|
"is_public": false,
|
||||||
|
"presale_start": null,
|
||||||
|
"presale_end": null,
|
||||||
|
"location": null,
|
||||||
|
"has_subevents": false,
|
||||||
|
"meta_data": {},
|
||||||
|
"plugins": [
|
||||||
|
"pretix.plugins.stripe",
|
||||||
|
"pretix.plugins.paypal"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer of the event to create.
|
||||||
|
:param event: The ``slug`` field of the event to copy settings and items from.
|
||||||
|
:statuscode 201: no error
|
||||||
|
:statuscode 400: The event 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)/events/(event)/
|
||||||
|
|
||||||
|
Updates an event
|
||||||
|
|
||||||
|
Permission required: "Can change event settings"
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PATCH /api/v1/organizers/bigevents/events/sampleconf/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"plugins": [
|
||||||
|
"pretix.plugins.banktransfer",
|
||||||
|
"pretix.plugins.stripe",
|
||||||
|
"pretix.plugins.paypal",
|
||||||
|
"pretix.plugins.pretixdroid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": {"en": "Sample Conference"},
|
||||||
|
"slug": "sampleconf",
|
||||||
|
"live": false,
|
||||||
|
"testmode": false,
|
||||||
|
"currency": "EUR",
|
||||||
|
"date_from": "2017-12-27T10:00:00Z",
|
||||||
|
"date_to": null,
|
||||||
|
"date_admission": null,
|
||||||
|
"is_public": false,
|
||||||
|
"presale_start": null,
|
||||||
|
"presale_end": null,
|
||||||
|
"location": null,
|
||||||
|
"has_subevents": false,
|
||||||
|
"meta_data": {},
|
||||||
|
"plugins": [
|
||||||
|
"pretix.plugins.banktransfer",
|
||||||
|
"pretix.plugins.stripe",
|
||||||
|
"pretix.plugins.paypal",
|
||||||
|
"pretix.plugins.pretixdroid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer of the event to update
|
||||||
|
:param event: The ``slug`` field of the event to update
|
||||||
|
:statuscode 201: no error
|
||||||
|
:statuscode 400: The event could not be created due to invalid submitted data.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
|
||||||
|
|
||||||
|
|
||||||
|
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/items/(id)/
|
||||||
|
|
||||||
|
Delete an event. Note that events with orders cannot be deleted to ensure data integrity.
|
||||||
|
|
||||||
|
Permission required: "Can change event settings"
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
DELETE /api/v1/organizers/bigevents/events/sampleconf/ 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 delete
|
||||||
|
:statuscode 204: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
|
||||||
|
|||||||
@@ -20,3 +20,5 @@ Resources and endpoints
|
|||||||
vouchers
|
vouchers
|
||||||
checkinlists
|
checkinlists
|
||||||
waitinglist
|
waitinglist
|
||||||
|
carts
|
||||||
|
webhooks
|
||||||
|
|||||||
@@ -223,3 +223,59 @@ Endpoints
|
|||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||||
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
|
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
|
||||||
seconds.
|
seconds.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(invoice_no)/reissue/
|
||||||
|
|
||||||
|
Cancels the invoice and creates a new one.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/invoices/00001/reissue/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/pdf
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:param invoice_no: The ``invoice_no`` field of the invoice to reissue
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The invoice has already been canceled
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(invoice_no)/regenerate/
|
||||||
|
|
||||||
|
Re-generates the invoice from order data.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/invoices/00001/regenerate/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/pdf
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:param invoice_no: The ``invoice_no`` field of the invoice to regenerate
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The invoice has already been canceled
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
.. _rest-items:
|
||||||
|
|
||||||
Items
|
Items
|
||||||
=====
|
=====
|
||||||
|
|
||||||
@@ -14,6 +16,7 @@ Field Type Description
|
|||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
id integer Internal ID of the item
|
id integer Internal ID of the item
|
||||||
name multi-lingual string The item's visible name
|
name multi-lingual string The item's visible name
|
||||||
|
internal_name string An optional name that is only used in the backend
|
||||||
default_price money (string) The item price that is applied if the price is not
|
default_price money (string) The item price that is applied if the price is not
|
||||||
overwritten by variations or other options.
|
overwritten by variations or other options.
|
||||||
category integer The ID of the category this item belongs to
|
category integer The ID of the category this item belongs to
|
||||||
@@ -34,6 +37,8 @@ admission boolean ``True`` for it
|
|||||||
position integer An integer, used for sorting
|
position integer An integer, used for sorting
|
||||||
picture string A product picture to be displayed in the shop
|
picture string A product picture to be displayed in the shop
|
||||||
(read-only).
|
(read-only).
|
||||||
|
sales_channels list of strings Sales channels this product is available on, such as
|
||||||
|
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
|
||||||
available_from datetime The first date time at which this item can be bought
|
available_from datetime The first date time at which this item can be bought
|
||||||
(or ``null``).
|
(or ``null``).
|
||||||
available_until datetime The last date time at which this item can be bought
|
available_until datetime The last date time at which this item can be bought
|
||||||
@@ -54,11 +59,23 @@ max_per_order integer This product ca
|
|||||||
checkin_attention boolean If ``True``, the check-in app should show a warning
|
checkin_attention boolean If ``True``, the check-in app should show a warning
|
||||||
that this ticket requires special attention if such
|
that this ticket requires special attention if such
|
||||||
a product is being scanned.
|
a product is being scanned.
|
||||||
|
original_price money (string) An original price, shown for comparison, not used
|
||||||
|
for price calculations.
|
||||||
|
require_approval boolean If ``True``, orders with this product will need to be
|
||||||
|
approved by the event organizer before they can be
|
||||||
|
paid.
|
||||||
|
generate_tickets boolean If ``False``, tickets are never generated for this
|
||||||
|
product, regardless of other settings. If ``True``,
|
||||||
|
tickets are generated even if this is a
|
||||||
|
non-admission or add-on product, regardless of event
|
||||||
|
settings. If this is ``null``, regular ticketing
|
||||||
|
rules apply.
|
||||||
has_variations boolean Shows whether or not this item has variations.
|
has_variations boolean Shows whether or not this item has variations.
|
||||||
variations list of objects A list with one object for each variation of this item.
|
variations list of objects A list with one object for each variation of this item.
|
||||||
Can be empty. Only writable during creation,
|
Can be empty. Only writable during creation,
|
||||||
use separate endpoint to modify this later.
|
use separate endpoint to modify this later.
|
||||||
├ id integer Internal ID of the variation
|
├ id integer Internal ID of the variation
|
||||||
|
├ value multi-lingual string The "name" of the variation
|
||||||
├ default_price money (string) The price set directly for this variation or ``null``
|
├ default_price money (string) The price set directly for this variation or ``null``
|
||||||
├ price money (string) The price used for this variation. This is either the
|
├ price money (string) The price used for this variation. This is either the
|
||||||
same as ``default_price`` if that value is set or equal
|
same as ``default_price`` if that value is set or equal
|
||||||
@@ -88,6 +105,22 @@ addons list of objects Definition of a
|
|||||||
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
|
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
|
||||||
The attribute ``price_included`` has been added to ``addons``.
|
The attribute ``price_included`` has been added to ``addons``.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.16
|
||||||
|
|
||||||
|
The ``internal_name`` and ``original_price`` fields have been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0
|
||||||
|
|
||||||
|
The field ``require_approval`` has been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.3
|
||||||
|
|
||||||
|
The ``sales_channels`` attribute has been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.4
|
||||||
|
|
||||||
|
The ``generate_tickets`` attribute has been added.
|
||||||
|
|
||||||
Notes
|
Notes
|
||||||
-----
|
-----
|
||||||
Please note that an item either always has variations or never has. Once created with variations the item can never
|
Please note that an item either always has variations or never has. Once created with variations the item can never
|
||||||
@@ -129,7 +162,10 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Standard ticket"},
|
"name": {"en": "Standard ticket"},
|
||||||
|
"internal_name": "",
|
||||||
|
"sales_channels": ["web"],
|
||||||
"default_price": "23.00",
|
"default_price": "23.00",
|
||||||
|
"original_price": null,
|
||||||
"category": null,
|
"category": null,
|
||||||
"active": true,
|
"active": true,
|
||||||
"description": null,
|
"description": null,
|
||||||
@@ -148,6 +184,8 @@ Endpoints
|
|||||||
"max_per_order": null,
|
"max_per_order": null,
|
||||||
"checkin_attention": false,
|
"checkin_attention": false,
|
||||||
"has_variations": false,
|
"has_variations": false,
|
||||||
|
"generate_tickets": null,
|
||||||
|
"require_approval": false,
|
||||||
"variations": [
|
"variations": [
|
||||||
{
|
{
|
||||||
"value": {"en": "Student"},
|
"value": {"en": "Student"},
|
||||||
@@ -211,7 +249,10 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Standard ticket"},
|
"name": {"en": "Standard ticket"},
|
||||||
|
"internal_name": "",
|
||||||
|
"sales_channels": ["web"],
|
||||||
"default_price": "23.00",
|
"default_price": "23.00",
|
||||||
|
"original_price": null,
|
||||||
"category": null,
|
"category": null,
|
||||||
"active": true,
|
"active": true,
|
||||||
"description": null,
|
"description": null,
|
||||||
@@ -226,10 +267,12 @@ Endpoints
|
|||||||
"require_voucher": false,
|
"require_voucher": false,
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"allow_cancel": true,
|
"allow_cancel": true,
|
||||||
|
"generate_tickets": null,
|
||||||
"min_per_order": null,
|
"min_per_order": null,
|
||||||
"max_per_order": null,
|
"max_per_order": null,
|
||||||
"checkin_attention": false,
|
"checkin_attention": false,
|
||||||
"has_variations": false,
|
"has_variations": false,
|
||||||
|
"require_approval": false,
|
||||||
"variations": [
|
"variations": [
|
||||||
{
|
{
|
||||||
"value": {"en": "Student"},
|
"value": {"en": "Student"},
|
||||||
@@ -274,7 +317,10 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Standard ticket"},
|
"name": {"en": "Standard ticket"},
|
||||||
|
"internal_name": "",
|
||||||
|
"sales_channels": ["web"],
|
||||||
"default_price": "23.00",
|
"default_price": "23.00",
|
||||||
|
"original_price": null,
|
||||||
"category": null,
|
"category": null,
|
||||||
"active": true,
|
"active": true,
|
||||||
"description": null,
|
"description": null,
|
||||||
@@ -289,9 +335,11 @@ Endpoints
|
|||||||
"require_voucher": false,
|
"require_voucher": false,
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"allow_cancel": true,
|
"allow_cancel": true,
|
||||||
|
"generate_tickets": null,
|
||||||
"min_per_order": null,
|
"min_per_order": null,
|
||||||
"max_per_order": null,
|
"max_per_order": null,
|
||||||
"checkin_attention": false,
|
"checkin_attention": false,
|
||||||
|
"require_approval": false,
|
||||||
"variations": [
|
"variations": [
|
||||||
{
|
{
|
||||||
"value": {"en": "Student"},
|
"value": {"en": "Student"},
|
||||||
@@ -324,7 +372,10 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Standard ticket"},
|
"name": {"en": "Standard ticket"},
|
||||||
|
"internal_name": "",
|
||||||
|
"sales_channels": ["web"],
|
||||||
"default_price": "23.00",
|
"default_price": "23.00",
|
||||||
|
"original_price": null,
|
||||||
"category": null,
|
"category": null,
|
||||||
"active": true,
|
"active": true,
|
||||||
"description": null,
|
"description": null,
|
||||||
@@ -341,8 +392,10 @@ Endpoints
|
|||||||
"allow_cancel": true,
|
"allow_cancel": true,
|
||||||
"min_per_order": null,
|
"min_per_order": null,
|
||||||
"max_per_order": null,
|
"max_per_order": null,
|
||||||
|
"generate_tickets": null,
|
||||||
"checkin_attention": false,
|
"checkin_attention": false,
|
||||||
"has_variations": true,
|
"has_variations": true,
|
||||||
|
"require_approval": false,
|
||||||
"variations": [
|
"variations": [
|
||||||
{
|
{
|
||||||
"value": {"en": "Student"},
|
"value": {"en": "Student"},
|
||||||
@@ -406,7 +459,10 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Ticket"},
|
"name": {"en": "Ticket"},
|
||||||
|
"internal_name": "",
|
||||||
|
"sales_channels": ["web"],
|
||||||
"default_price": "25.00",
|
"default_price": "25.00",
|
||||||
|
"original_price": null,
|
||||||
"category": null,
|
"category": null,
|
||||||
"active": true,
|
"active": true,
|
||||||
"description": null,
|
"description": null,
|
||||||
@@ -420,11 +476,13 @@ Endpoints
|
|||||||
"available_until": null,
|
"available_until": null,
|
||||||
"require_voucher": false,
|
"require_voucher": false,
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
|
"generate_tickets": null,
|
||||||
"allow_cancel": true,
|
"allow_cancel": true,
|
||||||
"min_per_order": null,
|
"min_per_order": null,
|
||||||
"max_per_order": null,
|
"max_per_order": null,
|
||||||
"checkin_attention": false,
|
"checkin_attention": false,
|
||||||
"has_variations": true,
|
"has_variations": true,
|
||||||
|
"require_approval": false,
|
||||||
"variations": [
|
"variations": [
|
||||||
{
|
{
|
||||||
"value": {"en": "Student"},
|
"value": {"en": "Student"},
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -128,7 +128,7 @@ Endpoints
|
|||||||
POST /api/v1/organizers/bigevents/events/sampleconf/questions/1/options/ HTTP/1.1
|
POST /api/v1/organizers/bigevents/events/sampleconf/questions/1/options/ HTTP/1.1
|
||||||
Host: pretix.eu
|
Host: pretix.eu
|
||||||
Accept: application/json, text/javascript
|
Accept: application/json, text/javascript
|
||||||
Content: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"identifier": "LVETRWVU",
|
"identifier": "LVETRWVU",
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
.. spelling:: checkin
|
.. spelling:: checkin
|
||||||
|
|
||||||
|
.. _rest-questions:
|
||||||
|
|
||||||
Questions
|
Questions
|
||||||
=========
|
=========
|
||||||
|
|
||||||
@@ -59,6 +61,11 @@ options list of objects In case of ques
|
|||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
|
|
||||||
|
.. versionchanged:: 1.15
|
||||||
|
|
||||||
|
The questions endpoint has been extended by the filter queries ``ask_during_checkin``, ``requred``, and
|
||||||
|
``identifier``.
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/questions/
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/questions/
|
||||||
|
|
||||||
Returns a list of all questions within a given event.
|
Returns a list of all questions within a given event.
|
||||||
@@ -120,6 +127,9 @@ Endpoints
|
|||||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id`` and ``position``.
|
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id`` and ``position``.
|
||||||
Default: ``position``
|
Default: ``position``
|
||||||
|
:query string identifier: Only return questions with the given identifier string
|
||||||
|
:query boolean ask_during_checkin: Only return questions that are or are not to be asked during check-in
|
||||||
|
:query boolean required: Only return questions that are or are not required to fill in
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:param event: The ``slug`` field of the event to fetch
|
:param event: The ``slug`` field of the event to fetch
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
.. _rest-quotas:
|
||||||
|
|
||||||
Quotas
|
Quotas
|
||||||
======
|
======
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
.. _rest-subevents:
|
||||||
|
|
||||||
Event series dates / Sub-events
|
Event series dates / Sub-events
|
||||||
===============================
|
===============================
|
||||||
|
|
||||||
@@ -15,6 +17,7 @@ Field Type Description
|
|||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
id integer Internal ID of the sub-event
|
id integer Internal ID of the sub-event
|
||||||
name multi-lingual string The sub-event's full name
|
name multi-lingual string The sub-event's full name
|
||||||
|
event string The slug of the parent event
|
||||||
active boolean If ``true``, the sub-event ticket shop is publicly
|
active boolean If ``true``, the sub-event ticket shop is publicly
|
||||||
available.
|
available.
|
||||||
date_from datetime The sub-event's start date
|
date_from datetime The sub-event's start date
|
||||||
@@ -38,6 +41,10 @@ meta_data dict Values set for
|
|||||||
|
|
||||||
The ``meta_data`` field has been added.
|
The ``meta_data`` field has been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.1
|
||||||
|
|
||||||
|
The ``event`` field has been added, together with filters on the list of dates and an organizer-level list.
|
||||||
|
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
@@ -70,6 +77,7 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "First Sample Conference"},
|
"name": {"en": "First Sample Conference"},
|
||||||
|
"event": "sampleconf",
|
||||||
"active": false,
|
"active": false,
|
||||||
"date_from": "2017-12-27T10:00:00Z",
|
"date_from": "2017-12-27T10:00:00Z",
|
||||||
"date_to": null,
|
"date_to": null,
|
||||||
@@ -90,6 +98,10 @@ Endpoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
:query page: The page number in case of a multi-page result set, default is 1
|
:query page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:query active: If set to ``true``/``false``, only events with a matching value of ``active`` are returned.
|
||||||
|
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
|
||||||
|
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned.
|
||||||
|
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned.
|
||||||
:param organizer: The ``slug`` field of a valid organizer
|
:param organizer: The ``slug`` field of a valid organizer
|
||||||
:param event: The ``slug`` field of the event to fetch
|
:param event: The ``slug`` field of the event to fetch
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
@@ -119,6 +131,7 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "First Sample Conference"},
|
"name": {"en": "First Sample Conference"},
|
||||||
|
"event": "sampleconf",
|
||||||
"active": false,
|
"active": false,
|
||||||
"date_from": "2017-12-27T10:00:00Z",
|
"date_from": "2017-12-27T10:00:00Z",
|
||||||
"date_to": null,
|
"date_to": null,
|
||||||
@@ -142,3 +155,63 @@ Endpoints
|
|||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/subevents/
|
||||||
|
|
||||||
|
Returns a list of all sub-events of any event series you have access to within an organizer account.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/subevents/ 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": {"en": "First Sample Conference"},
|
||||||
|
"event": "sampleconf",
|
||||||
|
"active": false,
|
||||||
|
"date_from": "2017-12-27T10:00:00Z",
|
||||||
|
"date_to": null,
|
||||||
|
"date_admission": null,
|
||||||
|
"presale_start": null,
|
||||||
|
"presale_end": null,
|
||||||
|
"location": null,
|
||||||
|
"item_price_overrides": [
|
||||||
|
{
|
||||||
|
"item": 2,
|
||||||
|
"price": "12.00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variation_price_overrides": [],
|
||||||
|
"meta_data": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:query page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:query active: If set to ``true``/``false``, only events with a matching value of ``active`` are returned.
|
||||||
|
:query event__live: If set to ``true``/``false``, only events with a matching value of ``live`` on the parent event are returned.
|
||||||
|
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
|
||||||
|
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned.
|
||||||
|
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned.
|
||||||
|
:param organizer: The ``slug`` field of a valid organizer
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
.. _rest-taxrules:
|
||||||
|
|
||||||
Tax rules
|
Tax rules
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
|||||||
@@ -231,6 +231,76 @@ Endpoints
|
|||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
|
||||||
:statuscode 409: The server was unable to acquire a lock and could not process your request. You can try again after a short waiting period.
|
:statuscode 409: The server was unable to acquire a lock and could not process your request. You can try again after a short waiting period.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/vouchers/batch_create/
|
||||||
|
|
||||||
|
Creates multiple new vouchers atomically.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/vouchers/batch_create/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 408
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"code": "43K6LKM37FBVR2YG",
|
||||||
|
"max_usages": 1,
|
||||||
|
"valid_until": null,
|
||||||
|
"block_quota": false,
|
||||||
|
"allow_ignore_quota": false,
|
||||||
|
"price_mode": "set",
|
||||||
|
"value": "12.00",
|
||||||
|
"item": 1,
|
||||||
|
"variation": null,
|
||||||
|
"quota": null,
|
||||||
|
"tag": "testvoucher",
|
||||||
|
"comment": "",
|
||||||
|
"subevent": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ASDKLJCYXCASDASD",
|
||||||
|
"max_usages": 1,
|
||||||
|
"valid_until": null,
|
||||||
|
"block_quota": false,
|
||||||
|
"allow_ignore_quota": false,
|
||||||
|
"price_mode": "set",
|
||||||
|
"value": "12.00",
|
||||||
|
"item": 1,
|
||||||
|
"variation": null,
|
||||||
|
"quota": null,
|
||||||
|
"tag": "testvoucher",
|
||||||
|
"comment": "",
|
||||||
|
"subevent": null
|
||||||
|
},
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"code": "43K6LKM37FBVR2YG",
|
||||||
|
…
|
||||||
|
}, …
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to create a vouchers for
|
||||||
|
:param event: The ``slug`` field of the event to create a vouchers for
|
||||||
|
:statuscode 201: no error
|
||||||
|
:statuscode 400: The vouchers could not be created due to invalid submitted data.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
|
||||||
|
:statuscode 409: The server was unable to acquire a lock and could not process your request. You can try again after a short waiting period.
|
||||||
|
|
||||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/vouchers/(id)/
|
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/vouchers/(id)/
|
||||||
|
|
||||||
Update a voucher. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
Update a voucher. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||||
@@ -251,7 +321,7 @@ Endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"price_mode": "set",
|
"price_mode": "set",
|
||||||
"value": "24.00",
|
"value": "24.00"
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ subevent integer ID of the date
|
|||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
|
.. versionchanged:: 1.15
|
||||||
|
|
||||||
|
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added as well as a method to send out
|
||||||
|
vouchers.
|
||||||
|
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
|
|
||||||
@@ -121,3 +127,161 @@ Endpoints
|
|||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/waitinglistentries/
|
||||||
|
|
||||||
|
Create a new entry.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/waitinglistentries/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 408
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "waiting@example.org",
|
||||||
|
"item": 3,
|
||||||
|
"variation": null,
|
||||||
|
"locale": "de",
|
||||||
|
"subevent": null
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"created": "2017-12-01T10:00:00Z",
|
||||||
|
"email": "waiting@example.org",
|
||||||
|
"voucher": null,
|
||||||
|
"item": 3,
|
||||||
|
"variation": null,
|
||||||
|
"locale": "de",
|
||||||
|
"subevent": null
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to create an entry for
|
||||||
|
:param event: The ``slug`` field of the event to create an entry for
|
||||||
|
:statuscode 201: no error
|
||||||
|
:statuscode 400: The voucher could not be created due to invalid submitted data.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this
|
||||||
|
resource **or** entries cannot be created for this item at this time.
|
||||||
|
|
||||||
|
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/waitinglistentries/(id)/
|
||||||
|
|
||||||
|
Update an entry. 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``, ``voucher`` and ``created`` fields. You can only change
|
||||||
|
an entry as long as no ``voucher`` is set.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PATCH /api/v1/organizers/bigevents/events/sampleconf/waitinglistentries/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 408
|
||||||
|
|
||||||
|
{
|
||||||
|
"item": 4
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"created": "2017-12-01T10:00:00Z",
|
||||||
|
"email": "waiting@example.org",
|
||||||
|
"voucher": null,
|
||||||
|
"item": 4,
|
||||||
|
"variation": null,
|
||||||
|
"locale": "de",
|
||||||
|
"subevent": null
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param event: The ``slug`` field of the event to modify
|
||||||
|
:param id: The ``id`` field of the entry to modify
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The entry could not be modified due to invalid submitted data
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this
|
||||||
|
resource **or** entries cannot be created for this item at this time **or** this entry already
|
||||||
|
has a voucher assigned
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/waitinglistentries/(id)/send_voucher/
|
||||||
|
|
||||||
|
Manually sends a voucher to someone on the waiting list
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/waitinglistentries/1/send_voucher/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 0
|
||||||
|
|
||||||
|
**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 entry to modify
|
||||||
|
:statuscode 204: no error
|
||||||
|
:statuscode 400: The voucher could not be sent out, see body for details (e.g. voucher has already been sent or
|
||||||
|
item is not available).
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to do this
|
||||||
|
|
||||||
|
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/waitinglistentries/(id)/
|
||||||
|
|
||||||
|
Delete an entry. Note that you cannot delete an entry once it is assigned a voucher.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
DELETE /api/v1/organizers/bigevents/events/sampleconf/waitinglistentries/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 entry to delete
|
||||||
|
:statuscode 204: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this
|
||||||
|
resource **or** this entry already has a voucher assigned.
|
||||||
|
|||||||
242
doc/api/resources/webhooks.rst
Normal file
242
doc/api/resources/webhooks.rst
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
.. _`rest-webhooks`:
|
||||||
|
|
||||||
|
Webhooks
|
||||||
|
========
|
||||||
|
|
||||||
|
.. note:: This page is about how to modify webhook settings themselves through the REST API. If you just want to know
|
||||||
|
how webhooks work, go here: :ref:`webhooks`
|
||||||
|
|
||||||
|
Resource description
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
The webhook resource contains the following public fields:
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
id integer Internal ID of the webhook
|
||||||
|
enabled boolean If ``False``, this webhook will not receive any notifications
|
||||||
|
target_url string The URL to call
|
||||||
|
all_events boolean If ``True``, this webhook will receive notifications
|
||||||
|
on all events of this organizer
|
||||||
|
limit_events list of strings If ``all_events`` is ``False``, this is a list of
|
||||||
|
event slugs this webhook is active for
|
||||||
|
action_types list of strings A list of action type filters that limit the
|
||||||
|
notifications sent to this webhook. See below for
|
||||||
|
valid values
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
The following values for ``action_types`` are valid with pretix core:
|
||||||
|
|
||||||
|
* ``pretix.event.order.placed``
|
||||||
|
* ``pretix.event.order.paid``
|
||||||
|
* ``pretix.event.order.canceled``
|
||||||
|
* ``pretix.event.order.expired``
|
||||||
|
* ``pretix.event.order.modified``
|
||||||
|
* ``pretix.event.order.contact.changed``
|
||||||
|
* ``pretix.event.order.changed.*``
|
||||||
|
* ``pretix.event.order.refund.created.externally``
|
||||||
|
* ``pretix.event.order.approved``
|
||||||
|
* ``pretix.event.order.denied``
|
||||||
|
* ``pretix.event.checkin``
|
||||||
|
* ``pretix.event.checkin.reverted``
|
||||||
|
|
||||||
|
Installed plugins might register more valid values.
|
||||||
|
|
||||||
|
|
||||||
|
Endpoints
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/webhooks/
|
||||||
|
|
||||||
|
Returns a list of all webhooks within a given organizer.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/webhooks/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"enabled": true,
|
||||||
|
"target_url": "https://httpstat.us/200",
|
||||||
|
"all_events": false,
|
||||||
|
"limit_events": ["democon"],
|
||||||
|
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
: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)/webhooks/(id)/
|
||||||
|
|
||||||
|
Returns information on one webhook, identified by its ID.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/webhooks/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"enabled": true,
|
||||||
|
"target_url": "https://httpstat.us/200",
|
||||||
|
"all_events": false,
|
||||||
|
"limit_events": ["democon"],
|
||||||
|
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"]
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param id: The ``id`` field of the webhook 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)/webhooks/
|
||||||
|
|
||||||
|
Creates a new webhook
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/webhooks/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"target_url": "https://httpstat.us/200",
|
||||||
|
"all_events": false,
|
||||||
|
"limit_events": ["democon"],
|
||||||
|
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"]
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"enabled": true,
|
||||||
|
"target_url": "https://httpstat.us/200",
|
||||||
|
"all_events": false,
|
||||||
|
"limit_events": ["democon"],
|
||||||
|
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"]
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to create a webhook for
|
||||||
|
:statuscode 201: no error
|
||||||
|
:statuscode 400: The webhook 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)/webhooks/(id)/
|
||||||
|
|
||||||
|
Update a webhook. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||||
|
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||||
|
want to change.
|
||||||
|
|
||||||
|
You can change all fields of the resource except the ``id`` field.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PATCH /api/v1/organizers/bigevents/webhooks/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 94
|
||||||
|
|
||||||
|
{
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"enabled": false,
|
||||||
|
"target_url": "https://httpstat.us/200",
|
||||||
|
"all_events": false,
|
||||||
|
"limit_events": ["democon"],
|
||||||
|
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"]
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param id: The ``id`` field of the webhook to modify
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The webhook 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)/webhook/(id)/
|
||||||
|
|
||||||
|
Delete a webhook. Currently, this will not delete but just disable the webhook.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
DELETE /api/v1/organizers/bigevents/webhooks/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
Vary: Accept
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param id: The ``id`` field of the webhook to delete
|
||||||
|
:statuscode 204: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource.
|
||||||
36
doc/api/tokenauth.rst
Normal file
36
doc/api/tokenauth.rst
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
.. _`rest-tokenauth`:
|
||||||
|
|
||||||
|
Token-based authentication
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Obtaining an API token
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
To authenticate your API requests with Tokens, you need to obtain a team-level API token.
|
||||||
|
You can create a token in the pretix web interface on the level of organizer teams. Create
|
||||||
|
a new team or choose an existing team that has the level of permissions the token should
|
||||||
|
have and create a new token using the form below the list of team members:
|
||||||
|
|
||||||
|
.. image:: img/token_form.png
|
||||||
|
:class: screenshot
|
||||||
|
|
||||||
|
You can enter a description for the token to distinguish from other tokens later on.
|
||||||
|
Once you click "Add", you will be provided with an API token in the success message.
|
||||||
|
Copy this token, as you won't be able to retrieve it again.
|
||||||
|
|
||||||
|
.. image:: img/token_success.png
|
||||||
|
:class: screenshot
|
||||||
|
|
||||||
|
Using an API token
|
||||||
|
------------------
|
||||||
|
|
||||||
|
You need to include the API token with every request to pretix' API in the ``Authorization`` header
|
||||||
|
like the following:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
:emphasize-lines: 3
|
||||||
|
|
||||||
|
GET /api/v1/organizers/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Authorization: Token e1l6gq2ye72thbwkacj7jbri7a7tvxe614ojv8ybureain92ocub46t5gab5966k
|
||||||
|
|
||||||
108
doc/api/webhooks.rst
Normal file
108
doc/api/webhooks.rst
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
.. _`webhooks`:
|
||||||
|
|
||||||
|
Webhooks
|
||||||
|
========
|
||||||
|
|
||||||
|
pretix can send webhook calls to notify your application of any changes that happen inside pretix. This is especially
|
||||||
|
useful for everything triggered by an actual user, such as a new ticket sale or the arrival of a payment.
|
||||||
|
|
||||||
|
You can register any number of webhook URLs that pretix will notify any time one of the supported events occurs inside
|
||||||
|
your organizer account. A great example use case of webhooks would be to add the buyer to your mailing list every time
|
||||||
|
a new order comes in.
|
||||||
|
|
||||||
|
Configuring webhooks
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
You can find the list of your active webhooks in the "Webhook" section of your organizer account:
|
||||||
|
|
||||||
|
.. thumbnail:: ../screens/organizer/webhook_list.png
|
||||||
|
:align: center
|
||||||
|
:class: screenshot
|
||||||
|
|
||||||
|
Click "Create webhook" if you want to add a new URL. You will then be able to enter the URL pretix shall call for
|
||||||
|
notifications. You need to select any number of notification types that you want to receive and you can optionally
|
||||||
|
filter the events you want to receive notifications for.
|
||||||
|
|
||||||
|
.. thumbnail:: ../screens/organizer/webhook_edit.png
|
||||||
|
:align: center
|
||||||
|
:class: screenshot
|
||||||
|
|
||||||
|
You can also configure webhooks :ref:`through the API itself <rest-webhooks>`.
|
||||||
|
|
||||||
|
Receiving webhooks
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Creating a webhook endpoint on your server is no different from creating any other page on your website. If your
|
||||||
|
website is written in PHP, you might just create a new ``.php`` file on your server; if you use a web framework like
|
||||||
|
Symfony or Django, you would just create a new route with the desired URL.
|
||||||
|
|
||||||
|
We will call your URL with a HTTP ``POST`` request with a ``JSON`` body. In PHP, you can parse this like this::
|
||||||
|
|
||||||
|
$input = @file_get_contents('php://input');
|
||||||
|
$event_json = json_decode($input);
|
||||||
|
// Do something with $event_json
|
||||||
|
|
||||||
|
In Django, you would create a view like this::
|
||||||
|
|
||||||
|
def my_webhook_view(request):
|
||||||
|
event_json = json.loads(request.body)
|
||||||
|
# Do something with event_json
|
||||||
|
return HttpResponse(status=200)
|
||||||
|
|
||||||
|
More samples for the language of your choice are easy to find online.
|
||||||
|
|
||||||
|
The exact body of the request varies by notification type, but for the main types included with pretix core, such as
|
||||||
|
those related to changes of an order, it will look like this::
|
||||||
|
|
||||||
|
{
|
||||||
|
"notification_id": 123455,
|
||||||
|
"organizer": "acmecorp",
|
||||||
|
"event": "democon",
|
||||||
|
"code": "ABC23",
|
||||||
|
"action": "pretix.event.order.placed"
|
||||||
|
}
|
||||||
|
|
||||||
|
Notifications regarding a check-in will contain more details like ``orderposition_id``
|
||||||
|
and ``checkin_list``.
|
||||||
|
|
||||||
|
.. warning:: You should not trust data supplied to your webhook, but only use it as a trigger to fetch updated data.
|
||||||
|
Anyone could send data there if they guess the correct URL and you won't be able to tell. Therefore, we
|
||||||
|
only include the minimum amount of data necessary for you to fetch the changed objects from our
|
||||||
|
:ref:`rest-api` in an authenticated way.
|
||||||
|
|
||||||
|
If you want to further prevent others from accessing your webhook URL, you can also use `Basic authentication`_ and
|
||||||
|
supply the URL to us in the format of ``https://username:password@domain.com/path/``.
|
||||||
|
We recommend that you use HTTPS for your webhook URL and might require it in the future. If HTTPS is used, we require
|
||||||
|
that a valid certificate is in use.
|
||||||
|
|
||||||
|
.. note:: If you use a web framework that makes use of automatic CSRF protection, this protection might prevent us
|
||||||
|
from calling your webhook URL. In this case, we recommend that you turn of CSRF protection selectively
|
||||||
|
for that route. In Django, you can do this by putting the ``@csrf_exempt`` decorator on your view. In
|
||||||
|
Rails, you can pass an ``except`` parameter to ``protect_from_forgery``.
|
||||||
|
|
||||||
|
|
||||||
|
Responding to a webhook
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
If you successfully received a webhook call, your endpoint should return a HTTP status code between ``200`` and ``299``.
|
||||||
|
If any other status code is returned, we will assume you did not receive the call. This does mean that any redirection
|
||||||
|
or ``304 Not Modified`` response will be treated as a failure. pretix will not follow any ``301`` or ``302`` redirect
|
||||||
|
headers and pretix will ignore all other information in your response headers or body.
|
||||||
|
|
||||||
|
If we do not receive a status code in the range of ``200`` and ``299``, pretix will retry to deliver for up to three
|
||||||
|
days with an exponential back off. Therefore, we recommend that you implement your endpoint in a way where calling it
|
||||||
|
multiple times for the same event due to a perceived error does not do any harm.
|
||||||
|
|
||||||
|
There is only one exception: If status code ``410 Gone`` is returned, we will assume the
|
||||||
|
endpoint does not exist any more and automatically disable the webhook.
|
||||||
|
|
||||||
|
.. note:: If you use a self-hosted version of pretix (i.e. not our SaaS offering at pretix.eu) and you did not
|
||||||
|
configure a background task queue, failed webhooks will not be retried.
|
||||||
|
|
||||||
|
Debugging webhooks
|
||||||
|
------------------
|
||||||
|
|
||||||
|
If you want to debug your webhooks, you can view a log of all sent notifications and the responses of your server for
|
||||||
|
30 days right next to your configuration.
|
||||||
|
|
||||||
|
.. _Basic authentication: https://en.wikipedia.org/wiki/Basic_access_authentication
|
||||||
@@ -64,7 +64,7 @@ Similarly, there is ``organizer_permission_required`` and ``OrganizerPermissionR
|
|||||||
event-related views, there is also a signal that allows you to add the view to the event navigation like this::
|
event-related views, there is also a signal that allows you to add the view to the event navigation like this::
|
||||||
|
|
||||||
|
|
||||||
from django.core.urlresolvers import resolve, reverse
|
from django.urls import resolve, reverse
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from pretix.control.signals import nav_event
|
from pretix.control.signals import nav_event
|
||||||
|
|||||||
109
doc/development/api/email.rst
Normal file
109
doc/development/api/email.rst
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
.. highlight:: python
|
||||||
|
:linenothreshold: 5
|
||||||
|
|
||||||
|
Writing an HTML e-mail renderer plugin
|
||||||
|
======================================
|
||||||
|
|
||||||
|
An email renderer class controls how the HTML part of e-mails sent by pretix is built.
|
||||||
|
The creation of such a plugin is very similar to creating an export output.
|
||||||
|
|
||||||
|
Please read :ref:`Creating a plugin <pluginsetup>` first, if you haven't already.
|
||||||
|
|
||||||
|
Output registration
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
The email HTML renderer API does not make a lot of usage from signals, however, it
|
||||||
|
does use a signal to get a list of all available email renderers. Your plugin
|
||||||
|
should listen for this signal and return the subclass of ``pretix.base.email.BaseHTMLMailRenderer``
|
||||||
|
that we'll provide in this plugin::
|
||||||
|
|
||||||
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
from pretix.base.signals import register_html_mail_renderers
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(register_html_mail_renderers, dispatch_uid="renderer_custom")
|
||||||
|
def register_mail_renderers(sender, **kwargs):
|
||||||
|
from .email import MyMailRenderer
|
||||||
|
return MyMailRenderer
|
||||||
|
|
||||||
|
|
||||||
|
The renderer class
|
||||||
|
------------------
|
||||||
|
|
||||||
|
.. class:: pretix.base.email.BaseHTMLMailRenderer
|
||||||
|
|
||||||
|
The central object of each email renderer is the subclass of ``BaseHTMLMailRenderer``.
|
||||||
|
|
||||||
|
.. py:attribute:: BaseHTMLMailRenderer.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:: thumbnail_filename
|
||||||
|
|
||||||
|
This is an abstract attribute, you **must** override this!
|
||||||
|
|
||||||
|
.. autoattribute:: is_available
|
||||||
|
|
||||||
|
.. automethod:: render
|
||||||
|
|
||||||
|
This is an abstract method, you **must** implement this!
|
||||||
|
|
||||||
|
Helper class for template-base renderers
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
The email renderer that ships with pretix is based on Django templates to generate HTML.
|
||||||
|
In case you also want to render emails based on a template, we provided a ready-made base
|
||||||
|
class ``TemplateBasedMailRenderer`` that you can re-use to perform the following steps:
|
||||||
|
|
||||||
|
* Convert the body text and the signature to HTML using our markdown renderer
|
||||||
|
|
||||||
|
* Render the template
|
||||||
|
|
||||||
|
* Call `inlinestyler`_ to convert all ``<style>`` style sheets to inline ``style=""``
|
||||||
|
attributes for better compatibility
|
||||||
|
|
||||||
|
To use it, you just need to implement some variables::
|
||||||
|
|
||||||
|
class ClassicMailRenderer(TemplateBasedMailRenderer):
|
||||||
|
verbose_name = _('pretix default')
|
||||||
|
identifier = 'classic'
|
||||||
|
thumbnail_filename = 'pretixbase/email/thumb.png'
|
||||||
|
template_name = 'pretixbase/email/plainwrapper.html'
|
||||||
|
|
||||||
|
The template is passed the following context variables:
|
||||||
|
|
||||||
|
``site``
|
||||||
|
Name of the pretix installation (``settings.PRETIX_INSTANCE_NAME``)
|
||||||
|
|
||||||
|
``site_url``
|
||||||
|
Root URL of the pretix installation (``settings.SITE_URL``)
|
||||||
|
|
||||||
|
``body``
|
||||||
|
The body as markdown (render with ``{{ body|safe }}``)
|
||||||
|
|
||||||
|
``subject``
|
||||||
|
The email subject
|
||||||
|
|
||||||
|
``color``
|
||||||
|
The primary color of the event
|
||||||
|
|
||||||
|
``event``
|
||||||
|
The ``Event`` object
|
||||||
|
|
||||||
|
``signature`` (optional, only if configured)
|
||||||
|
The body as markdown (render with ``{{ signature|safe }}``)
|
||||||
|
|
||||||
|
``order`` (optional, only if applicable)
|
||||||
|
The ``Order`` object
|
||||||
|
|
||||||
|
.. _inlinestyler: https://pypi.org/project/inlinestyler/
|
||||||
@@ -11,7 +11,8 @@ Core
|
|||||||
----
|
----
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types
|
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types,
|
||||||
|
item_copy_data, register_sales_channels
|
||||||
|
|
||||||
Order events
|
Order events
|
||||||
""""""""""""
|
""""""""""""
|
||||||
@@ -25,7 +26,7 @@ Frontend
|
|||||||
--------
|
--------
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
:members: html_head, html_footer, footer_links, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content
|
: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
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
@@ -47,7 +48,8 @@ Backend
|
|||||||
-------
|
-------
|
||||||
|
|
||||||
.. automodule:: pretix.control.signals
|
.. automodule:: pretix.control.signals
|
||||||
:members: nav_event, html_head, quota_detail_html, nav_topbar, nav_global, nav_organizer, nav_event_settings, order_info, event_settings_widget
|
: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
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
@@ -56,6 +58,12 @@ Backend
|
|||||||
Vouchers
|
Vouchers
|
||||||
""""""""
|
""""""""
|
||||||
|
|
||||||
|
.. automodule:: pretix.control.signals
|
||||||
|
:members: item_forms
|
||||||
|
|
||||||
|
Vouchers
|
||||||
|
""""""""
|
||||||
|
|
||||||
.. automodule:: pretix.control.signals
|
.. automodule:: pretix.control.signals
|
||||||
:members: voucher_form_class, voucher_form_html, voucher_form_validation
|
:members: voucher_form_class, voucher_form_html, voucher_form_validation
|
||||||
|
|
||||||
@@ -68,5 +76,5 @@ Dashboards
|
|||||||
Ticket designs
|
Ticket designs
|
||||||
""""""""""""""
|
""""""""""""""
|
||||||
|
|
||||||
.. automodule:: pretix.plugins.ticketoutputpdf.signals
|
.. automodule:: pretix.base.signals
|
||||||
:members: layout_text_variables
|
:members: layout_text_variables
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ Contents:
|
|||||||
exporter
|
exporter
|
||||||
ticketoutput
|
ticketoutput
|
||||||
payment
|
payment
|
||||||
|
payment_2.0
|
||||||
|
email
|
||||||
invoice
|
invoice
|
||||||
|
shredder
|
||||||
customview
|
customview
|
||||||
general
|
general
|
||||||
|
quality
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ that we'll provide in this plugin::
|
|||||||
|
|
||||||
|
|
||||||
@receiver(register_invoice_renderers, dispatch_uid="output_custom")
|
@receiver(register_invoice_renderers, dispatch_uid="output_custom")
|
||||||
def register_infoice_renderers(sender, **kwargs):
|
def register_invoice_renderers(sender, **kwargs):
|
||||||
from .invoice import MyInvoiceRenderer
|
from .invoice import MyInvoiceRenderer
|
||||||
return MyInvoiceRenderer
|
return MyInvoiceRenderer
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ is very similar to creating an export output.
|
|||||||
|
|
||||||
Please read :ref:`Creating a plugin <pluginsetup>` first, if you haven't already.
|
Please read :ref:`Creating a plugin <pluginsetup>` first, if you haven't already.
|
||||||
|
|
||||||
|
.. warning:: We changed our payment provider API a lot in pretix 2.x. Our documentation page on :ref:`payment2.0`
|
||||||
|
might be insightful even if you do not have a payment provider to port, as it outlines the rationale
|
||||||
|
behind the current design.
|
||||||
|
|
||||||
Provider registration
|
Provider registration
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
@@ -31,7 +35,7 @@ that the plugin will provide::
|
|||||||
The provider class
|
The provider class
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
.. class:: pretix.base.payment.BasePaymentProvider
|
.. py:class:: pretix.base.payment.BasePaymentProvider
|
||||||
|
|
||||||
The central object of each payment provider is the subclass of ``BasePaymentProvider``.
|
The central object of each payment provider is the subclass of ``BasePaymentProvider``.
|
||||||
|
|
||||||
@@ -54,55 +58,63 @@ The provider class
|
|||||||
|
|
||||||
This is an abstract attribute, you **must** override this!
|
This is an abstract attribute, you **must** override this!
|
||||||
|
|
||||||
.. autoattribute:: is_enabled
|
.. autoattribute:: public_name
|
||||||
|
|
||||||
.. automethod:: calculate_fee
|
.. autoattribute:: is_enabled
|
||||||
|
|
||||||
.. autoattribute:: settings_form_fields
|
.. autoattribute:: settings_form_fields
|
||||||
|
|
||||||
|
.. automethod:: settings_form_clean
|
||||||
|
|
||||||
.. automethod:: settings_content_render
|
.. automethod:: settings_content_render
|
||||||
|
|
||||||
.. automethod:: render_invoice_text
|
.. automethod:: is_allowed
|
||||||
|
|
||||||
.. automethod:: payment_form_render
|
.. automethod:: payment_form_render
|
||||||
|
|
||||||
.. automethod:: payment_form
|
.. automethod:: payment_form
|
||||||
|
|
||||||
.. automethod:: is_allowed
|
|
||||||
|
|
||||||
.. autoattribute:: payment_form_fields
|
.. autoattribute:: payment_form_fields
|
||||||
|
|
||||||
.. automethod:: checkout_prepare
|
|
||||||
|
|
||||||
.. automethod:: payment_is_valid_session
|
.. automethod:: payment_is_valid_session
|
||||||
|
|
||||||
|
.. automethod:: checkout_prepare
|
||||||
|
|
||||||
.. automethod:: checkout_confirm_render
|
.. automethod:: checkout_confirm_render
|
||||||
|
|
||||||
This is an abstract method, you **must** override this!
|
This is an abstract method, you **must** override this!
|
||||||
|
|
||||||
.. automethod:: payment_perform
|
.. automethod:: execute_payment
|
||||||
|
|
||||||
|
.. automethod:: calculate_fee
|
||||||
|
|
||||||
.. automethod:: order_pending_mail_render
|
.. automethod:: order_pending_mail_render
|
||||||
|
|
||||||
.. automethod:: order_pending_render
|
.. automethod:: payment_pending_render
|
||||||
|
|
||||||
This is an abstract method, you **must** override this!
|
.. autoattribute:: abort_pending_allowed
|
||||||
|
|
||||||
|
.. automethod:: render_invoice_text
|
||||||
|
|
||||||
.. automethod:: order_change_allowed
|
.. automethod:: order_change_allowed
|
||||||
|
|
||||||
.. automethod:: order_can_retry
|
.. automethod:: payment_prepare
|
||||||
|
|
||||||
.. automethod:: order_prepare
|
.. automethod:: payment_control_render
|
||||||
|
|
||||||
.. automethod:: order_paid_render
|
.. automethod:: payment_refund_supported
|
||||||
|
|
||||||
.. automethod:: order_control_render
|
.. automethod:: payment_partial_refund_supported
|
||||||
|
|
||||||
.. automethod:: order_control_refund_render
|
.. automethod:: execute_refund
|
||||||
|
|
||||||
.. automethod:: order_control_refund_perform
|
.. automethod:: shred_payment_info
|
||||||
|
|
||||||
.. automethod:: is_implicit
|
.. autoattribute:: is_implicit
|
||||||
|
|
||||||
|
.. autoattribute:: is_meta
|
||||||
|
|
||||||
|
.. autoattribute:: test_mode_message
|
||||||
|
|
||||||
|
|
||||||
Additional views
|
Additional views
|
||||||
|
|||||||
129
doc/development/api/payment_2.0.rst
Normal file
129
doc/development/api/payment_2.0.rst
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
.. highlight:: python
|
||||||
|
:linenothreshold: 5
|
||||||
|
|
||||||
|
.. _`payment2.0`:
|
||||||
|
|
||||||
|
Porting a payment provider from pretix 1.x to pretix 2.x
|
||||||
|
========================================================
|
||||||
|
|
||||||
|
In pretix 2.x, we changed large parts of the payment provider API. This documentation details the changes we made
|
||||||
|
and shows you how you can make an existing pretix 1.x payment provider compatible with pretix 2.x
|
||||||
|
|
||||||
|
Conceptual overview
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
In pretix 1.x, an order was always directly connected to a payment provider for the full life of an order. As long as
|
||||||
|
an order was unpaid, this could still be changed in some cases, but once an order was paid, no changes to the payment
|
||||||
|
provider were possible any more. Additionally, the internal state of orders allowed orders only to be fully paid or
|
||||||
|
not paid at all. This leads to a couple of consequences:
|
||||||
|
|
||||||
|
* Payment-related functions (like "execute payment" or "do a refund") always operated on full orders.
|
||||||
|
|
||||||
|
* Changing the total of an order was basically impossible once an order was paid, since there was no concept of
|
||||||
|
partial payments or partial refunds.
|
||||||
|
|
||||||
|
* Payment provider plugins needed to take complicated steps to detect cases that require human intervention, like e.g.
|
||||||
|
|
||||||
|
* An order has expired, no quota is left to revive it, but a payment has been received
|
||||||
|
|
||||||
|
* A payment has been received for a canceled order
|
||||||
|
|
||||||
|
* A payment has been received for an order that has already been paid with a different payment method
|
||||||
|
|
||||||
|
* An external payment service notified us of a refund/dispute
|
||||||
|
|
||||||
|
We noticed that we copied and repeated large portions of code in all our official payment provider plugins, just
|
||||||
|
to deal with some of these cases.
|
||||||
|
|
||||||
|
* Sometimes, there is the need to mark an order as refunded within pretix, without automatically triggering a refund
|
||||||
|
with an external API. Every payment method needed to implement a user interface for this independently.
|
||||||
|
|
||||||
|
* If a refund was not possible automatically, there was no way user to track which payments actually have been refunded
|
||||||
|
manually and which are still left to do.
|
||||||
|
|
||||||
|
* When the payment with one payment provider failed and the user changed to a different payment provider, all
|
||||||
|
information about the first payment was lost from the order object and could only be retrieved from order log data,
|
||||||
|
which also made it hard to design a data shredder API to get rid of this data.
|
||||||
|
|
||||||
|
In pretix 2.x, we introduced two new models, :py:class:`OrderPayment <pretix.base.models.OrderPayment>` and
|
||||||
|
:py:class:`OrderRefund <pretix.base.models.OrderRefund>`. Each instance of these is connected to an order and
|
||||||
|
represents one single attempt to pay or refund a specific amount of money. Each one of these has an individual state,
|
||||||
|
can individually fail or succeed, and carries an amount variable that can differ from the order total.
|
||||||
|
|
||||||
|
This has the following advantages:
|
||||||
|
|
||||||
|
* The system can now detect orders that are over- or underpaid, independent of the payment providers in use.
|
||||||
|
|
||||||
|
* Therefore, we can now allow partial payments, partial refunds, and changing paid orders, and automatically detect
|
||||||
|
the cases listed above and notify the user.
|
||||||
|
|
||||||
|
Payment providers now interact with those payment and refund objects more than with orders.
|
||||||
|
|
||||||
|
Your to-do list
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Payment processing
|
||||||
|
""""""""""""""""""
|
||||||
|
|
||||||
|
* The method ``BasePaymentProvider.order_pending_render`` has been removed and replaced by a new
|
||||||
|
``BasePaymentProvider.payment_pending_render(request, payment)`` method that is passed an ``OrderPayment``
|
||||||
|
object instead of an ``Order``.
|
||||||
|
|
||||||
|
* The method ``BasePaymentProvider.payment_form_render`` now receives a new ``total`` parameter.
|
||||||
|
|
||||||
|
* The method ``BasePaymentProvider.payment_perform`` has been removed and replaced by a new method
|
||||||
|
``BasePaymentProvider.execute_payment(request, payment)`` that is passed an ``OrderPayment``
|
||||||
|
object instead of an ``Order``.
|
||||||
|
|
||||||
|
* The function ``pretix.base.services.mark_order_paid`` has been removed, instead call ``payment.confirm()``
|
||||||
|
on a pending ``OrderPayment`` object. If no further payments are required for this order, this will also
|
||||||
|
mark the order as paid automatically. Note that ``payment.confirm()`` can still throw a ``QuotaExceededException``,
|
||||||
|
however it will still mark the payment as complete (not the order!), so you should catch this exception and
|
||||||
|
inform the user, but not abort the transaction.
|
||||||
|
|
||||||
|
* A new property ``BasePaymentProvider.abort_pending_allowed`` has been introduced. Only if set, the user will
|
||||||
|
be able to retry a payment or switch the payment method when the order currently has a payment object in
|
||||||
|
state ``"pending"``. This replaces ``BasePaymentProvider.order_can_retry``, which no longer exists.
|
||||||
|
|
||||||
|
* The methods ``BasePaymentProvider.retry_prepare`` and ``BasePaymentProvider.order_prepare`` have both been
|
||||||
|
replaced by a new method ``BasePaymentProvider.payment_prepare(request, payment)`` that is passed an ``OrderPayment``
|
||||||
|
object instead of an ``Order``. **Keep in mind that this payment object might have an amount property that
|
||||||
|
differs from the order total, if the order is already partially paid.**
|
||||||
|
|
||||||
|
* The method ``BasePaymentProvider.order_paid_render`` has been removed.
|
||||||
|
|
||||||
|
* The method ``BasePaymentProvider.order_control_render`` has been removed and replaced by a new method
|
||||||
|
``BasePaymentProvider.payment_control_render(request, payment)`` that is passed an ``OrderPayment``
|
||||||
|
object instead of an ``Order``.
|
||||||
|
|
||||||
|
* There's no need to manually deal with excess payments or duplicate payments anymore, just setting the ``OrderPayment``
|
||||||
|
methods to the correct state will do the job.
|
||||||
|
|
||||||
|
Creating refunds
|
||||||
|
""""""""""""""""
|
||||||
|
|
||||||
|
* The methods ``BasePaymentProvider.order_control_refund_render`` and ``BasePaymentProvider.order_control_refund_perform``
|
||||||
|
have been removed.
|
||||||
|
|
||||||
|
* Two new boolean methods ``BasePaymentProvider.payment_refund_supported(payment)`` and ``BasePaymentProvider.payment_partial_refund_supported(payment)``
|
||||||
|
have been introduced. They should be set to return ``True`` if and only if the payment API allows to *automatically*
|
||||||
|
transfer the money back to the customer.
|
||||||
|
|
||||||
|
* A new method ``BasePaymentProvider.execute_refund(refund)`` has been introduced. This method is called using a
|
||||||
|
``OrderRefund`` object in ``"created"`` state and is expected to transfer the money back and confirm success with
|
||||||
|
calling ``refund.done()``. This will only ever be called if either ``BasePaymentProvider.payment_refund_supported(payment)``
|
||||||
|
or ``BasePaymentProvider.payment_partial_refund_supported(payment)`` return ``True``.
|
||||||
|
|
||||||
|
Processing external refunds
|
||||||
|
"""""""""""""""""""""""""""
|
||||||
|
|
||||||
|
* If e.g. a webhook API notifies you that a payment has been disputed or refunded with the external API, you are
|
||||||
|
expected to call ``OrderPayment.create_external_refund(self, amount, execution_date, info='{}')`` on this payment.
|
||||||
|
This will create and return an appropriate ``OrderRefund`` object and send out a notification. However, it will not
|
||||||
|
mark the order as refunded, but will ask the event organizer for a decision.
|
||||||
|
|
||||||
|
Data shredders
|
||||||
|
""""""""""""""
|
||||||
|
|
||||||
|
* The method ``BasePaymentProvider.shred_payment_info`` is no longer passed an order, but instead **either**
|
||||||
|
an ``OrderPayment`` **or** an ``OrderRefund``.
|
||||||
@@ -79,6 +79,9 @@ human-readable error messages. We recommend using the ``django.utils.functional.
|
|||||||
decorator, as it might get called a lot. You can also implement ``compatibility_warnings``,
|
decorator, as it might get called a lot. You can also implement ``compatibility_warnings``,
|
||||||
those will be displayed but not block the plugin execution.
|
those will be displayed but not block the plugin execution.
|
||||||
|
|
||||||
|
The ``AppConfig`` class may implement a method ``is_available(event)`` that checks if a plugin
|
||||||
|
is available for a specific event. If not, it will not be shown in the plugin list of that event.
|
||||||
|
|
||||||
Plugin registration
|
Plugin registration
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
|||||||
125
doc/development/api/quality.rst
Normal file
125
doc/development/api/quality.rst
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
.. highlight:: python
|
||||||
|
:linenothreshold: 5
|
||||||
|
|
||||||
|
.. _`pluginquality`:
|
||||||
|
|
||||||
|
Plugin quality checklist
|
||||||
|
========================
|
||||||
|
|
||||||
|
If you want to write a high-quality pretix plugin, this is a list of things you should check before
|
||||||
|
you publish it. This is also a list of things that we check, if we consider installing an externally
|
||||||
|
developed plugin on our hosted infrastructure.
|
||||||
|
|
||||||
|
A. Meta
|
||||||
|
-------
|
||||||
|
|
||||||
|
#. The plugin is clearly licensed under an appropriate license.
|
||||||
|
|
||||||
|
#. The plugin has an unambiguous name, description, and author metadata.
|
||||||
|
|
||||||
|
#. The plugin has a clear versioning scheme and the latest version of the plugin is kept compatible to the latest
|
||||||
|
stable version of pretix.
|
||||||
|
|
||||||
|
#. The plugin is properly packaged using standard Python packaging tools.
|
||||||
|
|
||||||
|
#. The plugin correctly declares its external dependencies.
|
||||||
|
|
||||||
|
#. A contact address is provided in case of security issues.
|
||||||
|
|
||||||
|
B. Isolation
|
||||||
|
------------
|
||||||
|
|
||||||
|
#. If any signal receivers use the `dispatch_uid`_ feature, the UIDs are prefixed by the plugin's name and do not
|
||||||
|
clash with other plugins.
|
||||||
|
|
||||||
|
#. If any templates or static files are shipped, they are located in subdirectories with the name of the plugin and do
|
||||||
|
not clash with other plugins or core files.
|
||||||
|
|
||||||
|
#. Any keys stored to the settings store are prefixed with the plugin's name and do not clash with other plugins or
|
||||||
|
core.
|
||||||
|
|
||||||
|
#. Any keys stored to the user session are prefixed with the plugin's name and do not clash with other plugins or
|
||||||
|
core.
|
||||||
|
|
||||||
|
#. Any registered URLs are unlikely to clash with other plugins or future core URLs.
|
||||||
|
|
||||||
|
C. Security
|
||||||
|
-----------
|
||||||
|
|
||||||
|
#. All important actions are logged to the :ref:`shared log storage <logging>` and a signal receiver is registered to
|
||||||
|
provide a human-readable representation of the log entry.
|
||||||
|
|
||||||
|
#. All views require appropriate permissions and use the ``event_urls`` mechanism if appropriate.
|
||||||
|
:ref:`Read more <customview>`
|
||||||
|
|
||||||
|
#. Any session data for customers is stored in the cart session system if appropriate.
|
||||||
|
|
||||||
|
#. If the plugin is a payment provider:
|
||||||
|
|
||||||
|
#. No credit card numbers may be stored within pretix.
|
||||||
|
|
||||||
|
#. A notification/webhook system is implemented to notify pretix of any refunds.
|
||||||
|
|
||||||
|
#. If such a webhook system is implemented, contents of incoming webhooks are either verified using a cryptographic
|
||||||
|
signature or are not being trusted and all data is fetched from an API instead.
|
||||||
|
|
||||||
|
D. Privacy
|
||||||
|
----------
|
||||||
|
|
||||||
|
#. No personal data is stored that is not required for the plugin's functionality.
|
||||||
|
|
||||||
|
#. For any personal data that is saved to the database, an appropriate :ref:`data shredder <shredder>` is provided
|
||||||
|
that offers the data for download and then removes it from the database (including log entries).
|
||||||
|
|
||||||
|
E. Internationalization
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
#. All user-facing strings in templates, Python code, and templates are wrapped in `gettext calls`_.
|
||||||
|
|
||||||
|
#. No languages, time zones, date formats, or time formats are hardcoded.
|
||||||
|
|
||||||
|
#. Installing the plugin automatically compiles ``.po`` files to ``.mo`` files. This is fulfilled automatically if
|
||||||
|
you use the ``setup.py`` file form our plugin cookiecutter.
|
||||||
|
|
||||||
|
F. Functionality
|
||||||
|
----------------
|
||||||
|
|
||||||
|
#. If the plugin adds any database models or relationships from the settings storage to database models, it registers
|
||||||
|
a receiver to the :py:attr:`pretix.base.signals.event_copy_data` or :py:attr:`pretix.base.signals.item_copy_data`
|
||||||
|
signals.
|
||||||
|
|
||||||
|
#. If the plugin is a payment provider:
|
||||||
|
|
||||||
|
#. A webhook-like system is implemented if payment confirmations are not sent instantly.
|
||||||
|
|
||||||
|
#. Refunds are implemented, if possible.
|
||||||
|
|
||||||
|
#. In case of overpayment or external refunds, a "required action" is created to notify the event organizer.
|
||||||
|
|
||||||
|
#. If the plugin adds steps to the checkout process, it has been tested in combination with the pretix widget.
|
||||||
|
|
||||||
|
G. Code quality
|
||||||
|
---------------
|
||||||
|
|
||||||
|
#. `isort`_ and `flake8`_ are used to ensure consistent code styling.
|
||||||
|
|
||||||
|
#. Unit tests are provided for important pieces of business logic.
|
||||||
|
|
||||||
|
#. Functional tests are provided for important interface parts.
|
||||||
|
|
||||||
|
#. Tests are provided to check that permission checks are working.
|
||||||
|
|
||||||
|
#. Continuous Integration is set up to check that tests are passing and styling is consistent.
|
||||||
|
|
||||||
|
H. Specific to pretix.eu
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
#. pretix.eu integrates the data stored by this plugin with its data report features.
|
||||||
|
|
||||||
|
#. pretix.eu integrates this plugin in its generated privacy statements, if necessary.
|
||||||
|
|
||||||
|
|
||||||
|
.. _isort: https://www.google.de/search?q=isort&oq=isort&aqs=chrome..69i57j0j69i59j69i60l2j69i59.599j0j4&sourceid=chrome&ie=UTF-8
|
||||||
|
.. _flake8: http://flake8.pycqa.org/en/latest/
|
||||||
|
.. _gettext calls: https://docs.djangoproject.com/en/2.0/topics/i18n/translation/
|
||||||
|
.. _dispatch_uid: https://docs.djangoproject.com/en/2.0/topics/signals/#django.dispatch.Signal.connect
|
||||||
94
doc/development/api/shredder.rst
Normal file
94
doc/development/api/shredder.rst
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
.. highlight:: python
|
||||||
|
:linenothreshold: 5
|
||||||
|
|
||||||
|
.. _`shredder`:
|
||||||
|
|
||||||
|
Writing a data shredder
|
||||||
|
=======================
|
||||||
|
|
||||||
|
If your plugin adds the ability to store personal data within pretix, you should also implement a "data shredder"
|
||||||
|
to anonymize or pseudonymize the data later.
|
||||||
|
|
||||||
|
Shredder registration
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
The data shredder API does not make a lot of usage from signals, however, it
|
||||||
|
does use a signal to get a list of all available data shredders. Your plugin
|
||||||
|
should listen for this signal and return the subclass of ``pretix.base.shredder.BaseDataShredder``
|
||||||
|
that we'll provide in this plugin:
|
||||||
|
|
||||||
|
.. sourcecode:: python
|
||||||
|
|
||||||
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
from pretix.base.signals import register_data_shredders
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(register_data_shredders, dispatch_uid="custom_data_shredders")
|
||||||
|
def register_shredder(sender, **kwargs):
|
||||||
|
return [
|
||||||
|
PluginDataShredder,
|
||||||
|
]
|
||||||
|
|
||||||
|
The shredder class
|
||||||
|
------------------
|
||||||
|
|
||||||
|
.. class:: pretix.base.shredder.BaseDataShredder
|
||||||
|
|
||||||
|
The central object of each invoice renderer is the subclass of ``BaseInvoiceRenderer``.
|
||||||
|
|
||||||
|
.. py:attribute:: BaseInvoiceRenderer.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:: description
|
||||||
|
|
||||||
|
This is an abstract attribute, you **must** override this!
|
||||||
|
|
||||||
|
.. automethod:: generate_files
|
||||||
|
|
||||||
|
.. automethod:: shred_data
|
||||||
|
|
||||||
|
Example
|
||||||
|
-------
|
||||||
|
|
||||||
|
For example, the core data shredder responsible for removing invoice address information including their history
|
||||||
|
looks like this:
|
||||||
|
|
||||||
|
.. sourcecode:: python
|
||||||
|
|
||||||
|
class InvoiceAddressShredder(BaseDataShredder):
|
||||||
|
verbose_name = _('Invoice addresses')
|
||||||
|
identifier = 'invoice_addresses'
|
||||||
|
description = _('This will remove all invoice addresses from orders, '
|
||||||
|
'as well as logged changes to them.')
|
||||||
|
|
||||||
|
def generate_files(self) -> List[Tuple[str, str, str]]:
|
||||||
|
yield 'invoice-addresses.json', 'application/json', json.dumps({
|
||||||
|
ia.order.code: InvoiceAdddressSerializer(ia).data
|
||||||
|
for ia in InvoiceAddress.objects.filter(order__event=self.event)
|
||||||
|
}, indent=4)
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def shred_data(self):
|
||||||
|
InvoiceAddress.objects.filter(order__event=self.event).delete()
|
||||||
|
|
||||||
|
for le in self.event.logentry_set.filter(action_type="pretix.event.order.modified"):
|
||||||
|
d = le.parsed_data
|
||||||
|
if 'invoice_data' in d and not isinstance(d['invoice_data'], bool):
|
||||||
|
for field in d['invoice_data']:
|
||||||
|
if d['invoice_data'][field]:
|
||||||
|
d['invoice_data'][field] = '█'
|
||||||
|
le.data = json.dumps(d)
|
||||||
|
le.shredded = True
|
||||||
|
le.save(update_fields=['data', 'shredded'])
|
||||||
|
|
||||||
@@ -82,6 +82,12 @@ Orders
|
|||||||
^^^^^^
|
^^^^^^
|
||||||
|
|
||||||
If a customer completes the checkout process, an **Order** will be created containing all the entered information.
|
If a customer completes the checkout process, an **Order** will be created containing all the entered information.
|
||||||
An order can be in one of currently five states that are listed in the diagram below:
|
An order can be in one of currently four states that are listed in the diagram below:
|
||||||
|
|
||||||
.. image:: /images/order_states.png
|
.. image:: /images/order_states.png
|
||||||
|
|
||||||
|
There are additional "fake" states that are displayed like states but not represented as states in the system:
|
||||||
|
|
||||||
|
* An order is considered **canceled (with paid fee)** if it is in **paid** status but does not include any non-cancelled positions.
|
||||||
|
|
||||||
|
* An order is considered **requiring approval** if it is in **pending** status with the ``require_approval`` attribute set to ``True``.
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ Logging and notifications
|
|||||||
As pretix is handling monetary transactions, we are very careful to make it possible to review all changes
|
As pretix is handling monetary transactions, we are very careful to make it possible to review all changes
|
||||||
in the system that lead to the current state.
|
in the system that lead to the current state.
|
||||||
|
|
||||||
|
.. _`logging`:
|
||||||
|
|
||||||
Logging changes
|
Logging changes
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,15 @@ Carts and Orders
|
|||||||
.. autoclass:: pretix.base.models.OrderPosition
|
.. autoclass:: pretix.base.models.OrderPosition
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: pretix.base.models.OrderFee
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: pretix.base.models.OrderPayment
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: pretix.base.models.OrderRefund
|
||||||
|
:members:
|
||||||
|
|
||||||
.. autoclass:: pretix.base.models.CartPosition
|
.. autoclass:: pretix.base.models.CartPosition
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ External Dependencies
|
|||||||
---------------------
|
---------------------
|
||||||
Your should install the following on your system:
|
Your should install the following on your system:
|
||||||
|
|
||||||
* Python 3.4 or newer
|
* Python 3.5 or newer
|
||||||
* ``pip`` for Python 3 (Debian package: ``python3-pip``)
|
* ``pip`` for Python 3 (Debian package: ``python3-pip``)
|
||||||
* ``python-dev`` for Python 3 (Debian package: ``python3-dev``)
|
* ``python-dev`` for Python 3 (Debian package: ``python3-dev``)
|
||||||
* ``libffi`` (Debian package: ``libffi-dev``)
|
* ``libffi`` (Debian package: ``libffi-dev``)
|
||||||
@@ -54,10 +54,6 @@ The first thing you need are all the main application's dependencies::
|
|||||||
cd src/
|
cd src/
|
||||||
pip3 install -r requirements.txt -r requirements/dev.txt
|
pip3 install -r requirements.txt -r requirements/dev.txt
|
||||||
|
|
||||||
If you are working with Python 3.4, you will also need (you can skip this for Python 3.5+)::
|
|
||||||
|
|
||||||
pip3 install -r requirements/py34.txt
|
|
||||||
|
|
||||||
Next, you need to copy the SCSS files from the source folder to the STATIC_ROOT directory::
|
Next, you need to copy the SCSS files from the source folder to the STATIC_ROOT directory::
|
||||||
|
|
||||||
python manage.py collectstatic --noinput
|
python manage.py collectstatic --noinput
|
||||||
@@ -115,12 +111,21 @@ Execute the following command to run pretix' test suite (might take a couple of
|
|||||||
``NUM`` being the number of threads you want to use.
|
``NUM`` being the number of threads you want to use.
|
||||||
|
|
||||||
It is a good idea to put this command into your git hook ``.git/hooks/pre-commit``,
|
It is a good idea to put this command into your git hook ``.git/hooks/pre-commit``,
|
||||||
for example::
|
for example, to check for any errors in any staged files when committing::
|
||||||
|
|
||||||
#!/bin/sh
|
#!/bin/bash
|
||||||
cd $GIT_DIR/../src
|
cd $GIT_DIR/../src
|
||||||
flake8 . || exit 1
|
export GIT_WORK_TREE=../
|
||||||
isort -q -rc -c . || exit 1
|
export GIT_DIR=../.git
|
||||||
|
source ../env/bin/activate # Adjust to however you activate your virtual environment
|
||||||
|
for file in $(git diff --cached --name-only | grep -E '\.py$' | grep -Ev "migrations|mt940\.py|pretix/settings\.py|make_testdata\.py|testutils/settings\.py|tests/settings\.py|pretix/base/models/__init__\.py")
|
||||||
|
do
|
||||||
|
echo $file
|
||||||
|
git show ":$file" | flake8 - --stdin-display-name="$file" || exit 1 # we only want to lint the staged changes, not any un-staged changes
|
||||||
|
git show ":$file" | isort -df --check-only - | grep ERROR && exit 1 || true
|
||||||
|
done
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
This keeps you from accidentally creating commits violating the style guide.
|
This keeps you from accidentally creating commits violating the style guide.
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 53 KiB |
@@ -4,7 +4,6 @@ Pending: Order is expecting payment\nOrder reduces quotas
|
|||||||
Expired: Payment period is over\nOrder does not affect quotas
|
Expired: Payment period is over\nOrder does not affect quotas
|
||||||
Paid: Order was successful\nOrder reduces quotas
|
Paid: Order was successful\nOrder reduces quotas
|
||||||
Canceled: Order has been canceled\nOrder does not affect quotas
|
Canceled: Order has been canceled\nOrder does not affect quotas
|
||||||
Refunded: Order has been refunded\nOrder does not affect quotas
|
|
||||||
|
|
||||||
[*] --> Pending: customer\nplaces order
|
[*] --> Pending: customer\nplaces order
|
||||||
Pending --> Paid: successful payment
|
Pending --> Paid: successful payment
|
||||||
@@ -12,8 +11,9 @@ Pending --> Expired: automatically\nor manually\non admin action
|
|||||||
Expired --> Paid: if payment is received\nonly if quota left
|
Expired --> Paid: if payment is received\nonly if quota left
|
||||||
Expired --> Canceled
|
Expired --> Canceled
|
||||||
Expired --> Pending: manually\non admin action
|
Expired --> Pending: manually\non admin action
|
||||||
Paid --> Refunded: manually on\nadmin action\nor if an external\npayment provider\nnotifies about a\npayment refund
|
Paid --> Canceled: manually on\nadmin action\nor if an external\npayment provider\nnotifies about a\npayment refund
|
||||||
Pending --> Canceled: on admin or\ncustomer action
|
Pending --> Canceled: on admin or\ncustomer action
|
||||||
Paid -> Pending: manually on admin action
|
Paid -> Pending: manually on admin action
|
||||||
|
[*] --> Paid: customer\nplaces free order
|
||||||
|
|
||||||
@enduml
|
@enduml
|
||||||
|
|||||||
110
doc/plugins/badges.rst
Normal file
110
doc/plugins/badges.rst
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
Badges
|
||||||
|
======
|
||||||
|
|
||||||
|
The badges plugin provides a HTTP API that exposes the various layouts used to generate PDF badges.
|
||||||
|
|
||||||
|
Resource description
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
The badge layout resource contains the following public fields:
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
id integer Internal layout ID
|
||||||
|
name string Internal layout description
|
||||||
|
default boolean ``true`` if this is the default layout
|
||||||
|
layout object Layout specification for libpretixprint
|
||||||
|
background URL Background PDF file
|
||||||
|
item_assignments list of objects Products this layout is assigned to
|
||||||
|
└ item integer Item ID
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
.. versionchanged:: 1.16
|
||||||
|
|
||||||
|
This resource has been added.
|
||||||
|
|
||||||
|
|
||||||
|
Endpoints
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/badgelayouts/
|
||||||
|
|
||||||
|
Returns a list of all badge layouts
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/democon/badgelayouts/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Default layout",
|
||||||
|
"default": true,
|
||||||
|
"layout": {…},
|
||||||
|
"background": {},
|
||||||
|
"item_assignments": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
: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 a valid event
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/badgelayouts/(id)/
|
||||||
|
|
||||||
|
Returns information on layout.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/democon/layoutsbadge/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: text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Default layout",
|
||||||
|
"default": true,
|
||||||
|
"layout": {…},
|
||||||
|
"background": {},
|
||||||
|
"item_assignments": []
|
||||||
|
}
|
||||||
|
|
||||||
|
: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 layout to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.
|
||||||
@@ -12,3 +12,5 @@ If you want to **create** a plugin, please go to the
|
|||||||
list
|
list
|
||||||
pretixdroid
|
pretixdroid
|
||||||
banktransfer
|
banktransfer
|
||||||
|
ticketoutputpdf
|
||||||
|
badges
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ pretixdroid HTTP API
|
|||||||
The pretixdroid plugin provides a HTTP API that the `pretixdroid Android app`_
|
The pretixdroid plugin provides a HTTP API that the `pretixdroid Android app`_
|
||||||
uses to communicate with the pretix server.
|
uses to communicate with the pretix server.
|
||||||
|
|
||||||
.. warning:: This API is intended **only** to serve the pretixdroid Android app. There are no backwards compatibility
|
.. warning:: This API is **DEPRECATED** and will probably go away soon. It is used **only** to serve the pretixdroid
|
||||||
guarantees on this API. We will not add features that are not required for the Android App. There is a
|
Android app. There are no backwards compatibility guarantees on this API. We will not add features that
|
||||||
general-purpose :ref:`rest-api` that not yet provides all features that this API provides, but will do
|
are not required for the Android App. There is a general-purpose :ref:`rest-api` that provides all
|
||||||
so in the future.
|
features that you need to check in.
|
||||||
|
|
||||||
.. versionchanged:: 1.12
|
.. versionchanged:: 1.12
|
||||||
|
|
||||||
@@ -81,6 +81,7 @@ uses to communicate with the pretix server.
|
|||||||
"attention": false,
|
"attention": false,
|
||||||
"redeemed": true,
|
"redeemed": true,
|
||||||
"checkin_allowed": true,
|
"checkin_allowed": true,
|
||||||
|
"addons_text": "Parking spot",
|
||||||
"paid": true
|
"paid": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,6 +107,7 @@ uses to communicate with the pretix server.
|
|||||||
"attention": false,
|
"attention": false,
|
||||||
"redeemed": true,
|
"redeemed": true,
|
||||||
"checkin_allowed": true,
|
"checkin_allowed": true,
|
||||||
|
"addons_text": "Parking spot",
|
||||||
"paid": true
|
"paid": true
|
||||||
},
|
},
|
||||||
"questions": [
|
"questions": [
|
||||||
@@ -152,6 +154,7 @@ uses to communicate with the pretix server.
|
|||||||
"attention": false,
|
"attention": false,
|
||||||
"redeemed": true,
|
"redeemed": true,
|
||||||
"checkin_allowed": true,
|
"checkin_allowed": true,
|
||||||
|
"addons_text": "Parking spot",
|
||||||
"paid": true
|
"paid": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,6 +215,7 @@ uses to communicate with the pretix server.
|
|||||||
"redeemed": false,
|
"redeemed": false,
|
||||||
"attention": false,
|
"attention": false,
|
||||||
"checkin_allowed": true,
|
"checkin_allowed": true,
|
||||||
|
"addons_text": "Parking spot",
|
||||||
"paid": true
|
"paid": true
|
||||||
},
|
},
|
||||||
...
|
...
|
||||||
|
|||||||
116
doc/plugins/ticketoutputpdf.rst
Normal file
116
doc/plugins/ticketoutputpdf.rst
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
PDF ticket output
|
||||||
|
=================
|
||||||
|
|
||||||
|
The PDF ticket output plugin provides a HTTP API that exposes the various layouts used
|
||||||
|
to generate PDF tickets.
|
||||||
|
|
||||||
|
Resource description
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
The ticket layout resource contains the following public fields:
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
id integer Internal layout ID
|
||||||
|
name string Internal layout description
|
||||||
|
default boolean ``true`` if this is the default layout
|
||||||
|
layout object Layout specification for libpretixprint
|
||||||
|
background URL Background PDF file
|
||||||
|
item_assignments list of objects Products this layout is assigned to
|
||||||
|
├ sales_channel string Sales channel (defaults to ``web``).
|
||||||
|
└ item integer Item ID
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
.. versionchanged:: 1.16
|
||||||
|
|
||||||
|
This resource has been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.3
|
||||||
|
|
||||||
|
The ``item_assignments.sales_channel`` field has been added.
|
||||||
|
|
||||||
|
|
||||||
|
Endpoints
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/
|
||||||
|
|
||||||
|
Returns a list of all ticket layouts
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/democon/ticketlayouts/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Default layout",
|
||||||
|
"default": true,
|
||||||
|
"layout": {…},
|
||||||
|
"background": {},
|
||||||
|
"item_assignments": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
: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 a valid event
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/(id)/
|
||||||
|
|
||||||
|
Returns information on layout.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/democon/ticketlayouts/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: text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Default layout",
|
||||||
|
"default": true,
|
||||||
|
"layout": {…},
|
||||||
|
"background": {},
|
||||||
|
"item_assignments": []
|
||||||
|
}
|
||||||
|
|
||||||
|
: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 layout to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.
|
||||||
BIN
doc/screens/organizer/webhook_edit.png
Normal file
BIN
doc/screens/organizer/webhook_edit.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
BIN
doc/screens/organizer/webhook_list.png
Normal file
BIN
doc/screens/organizer/webhook_list.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
@@ -1,9 +1,12 @@
|
|||||||
addon
|
addon
|
||||||
addons
|
addons
|
||||||
|
Analytics
|
||||||
|
anonymize
|
||||||
api
|
api
|
||||||
auditability
|
auditability
|
||||||
auth
|
auth
|
||||||
autobuild
|
autobuild
|
||||||
|
availabilities
|
||||||
backend
|
backend
|
||||||
backends
|
backends
|
||||||
banktransfer
|
banktransfer
|
||||||
@@ -16,10 +19,13 @@ checksum
|
|||||||
config
|
config
|
||||||
contenttypes
|
contenttypes
|
||||||
contextmanager
|
contextmanager
|
||||||
|
cookiecutter
|
||||||
cron
|
cron
|
||||||
cronjob
|
cronjob
|
||||||
|
cryptographic
|
||||||
debian
|
debian
|
||||||
deduplication
|
deduplication
|
||||||
|
deprovision
|
||||||
discoverable
|
discoverable
|
||||||
django
|
django
|
||||||
dockerfile
|
dockerfile
|
||||||
@@ -34,10 +40,13 @@ gettext
|
|||||||
gunicorn
|
gunicorn
|
||||||
hardcoded
|
hardcoded
|
||||||
hostname
|
hostname
|
||||||
|
idempotency
|
||||||
|
incrementing
|
||||||
inofficial
|
inofficial
|
||||||
invalidations
|
invalidations
|
||||||
iterable
|
iterable
|
||||||
Jimdo
|
Jimdo
|
||||||
|
libpretixprint
|
||||||
libsass
|
libsass
|
||||||
linters
|
linters
|
||||||
memcached
|
memcached
|
||||||
@@ -56,7 +65,9 @@ nginx
|
|||||||
NotificationType
|
NotificationType
|
||||||
ons
|
ons
|
||||||
optimizations
|
optimizations
|
||||||
|
overpayment
|
||||||
param
|
param
|
||||||
|
passphrase
|
||||||
percental
|
percental
|
||||||
positionid
|
positionid
|
||||||
pre
|
pre
|
||||||
@@ -71,6 +82,8 @@ pretixpresale
|
|||||||
prometheus
|
prometheus
|
||||||
proxied
|
proxied
|
||||||
proxying
|
proxying
|
||||||
|
pseudonymize
|
||||||
|
pseudonymization
|
||||||
queryset
|
queryset
|
||||||
redemptions
|
redemptions
|
||||||
redis
|
redis
|
||||||
@@ -79,6 +92,7 @@ regex
|
|||||||
renderer
|
renderer
|
||||||
renderers
|
renderers
|
||||||
reportlab
|
reportlab
|
||||||
|
SaaS
|
||||||
screenshot
|
screenshot
|
||||||
selectable
|
selectable
|
||||||
serializers
|
serializers
|
||||||
@@ -95,9 +109,12 @@ subevent
|
|||||||
subevents
|
subevents
|
||||||
submodule
|
submodule
|
||||||
subpath
|
subpath
|
||||||
|
Symfony
|
||||||
systemd
|
systemd
|
||||||
|
testmode
|
||||||
testutils
|
testutils
|
||||||
timestamp
|
timestamp
|
||||||
|
tuples
|
||||||
un
|
un
|
||||||
unconfigured
|
unconfigured
|
||||||
unix
|
unix
|
||||||
@@ -106,6 +123,7 @@ untrusted
|
|||||||
username
|
username
|
||||||
url
|
url
|
||||||
versa
|
versa
|
||||||
|
versioning
|
||||||
viewset
|
viewset
|
||||||
viewsets
|
viewsets
|
||||||
webhook
|
webhook
|
||||||
|
|||||||
@@ -107,6 +107,13 @@ voucher's settings.
|
|||||||
</div>
|
</div>
|
||||||
</noscript>
|
</noscript>
|
||||||
|
|
||||||
|
Disabling the voucher input
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
If you want to disable voucher input in the widget, you can pass the ``disable-vouchers`` attribute::
|
||||||
|
|
||||||
|
<pretix-widget event="https://pretix.eu/demo/democon/" disable-vouchers></pretix-widget>
|
||||||
|
|
||||||
pretix Button
|
pretix Button
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
@@ -136,14 +143,107 @@ resources. Then, instead of the ``pretix-widget`` tag, use the ``pretix-button``
|
|||||||
As you can see, the ``pretix-button`` element takes an additional ``items`` attribute that specifies the items that
|
As you can see, the ``pretix-button`` element takes an additional ``items`` attribute that specifies the items that
|
||||||
should be added to the cart. The syntax of this attribute is ``item_ITEMID=1,item_ITEMID=2,variation_ITEMID_VARID=4``
|
should be added to the cart. The syntax of this attribute is ``item_ITEMID=1,item_ITEMID=2,variation_ITEMID_VARID=4``
|
||||||
where ``ITEMID`` are the internal IDs of items to be added and ``VARID`` are the internal IDs of variations of those
|
where ``ITEMID`` are the internal IDs of items to be added and ``VARID`` are the internal IDs of variations of those
|
||||||
items, if the items have variations.
|
items, if the items have variations. If you omit the ``items`` attribute, the general start page will be presented.
|
||||||
|
|
||||||
Just as the widget, the button supports the optional attributes ``voucher`` and ``skip-ssl-check``.
|
Just as the widget, the button supports the optional attributes ``voucher`` and ``skip-ssl-check``.
|
||||||
|
|
||||||
You can style the button using the ``pretix-button`` CSS class.
|
You can style the button using the ``pretix-button`` CSS class.
|
||||||
|
|
||||||
.. versionchanged:: 1.13
|
Dynamically loading the widget
|
||||||
|
------------------------------
|
||||||
|
|
||||||
The pretix Button has been added in version 1.13.
|
If you need to control the way or timing the widget loads, for example because you want to modify user data (see
|
||||||
|
below) dynamically via JavaScript, you can register a listener that we will call before creating the widget::
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.pretixWidgetCallback = function () {
|
||||||
|
// Will be run before we create the widget.
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
If you want, you can suppress us loading the widget and/or modify the user data passed to the widget::
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.pretixWidgetCallback = function () {
|
||||||
|
window.PretixWidget.build_widgets = false;
|
||||||
|
window.PretixWidget.widget_data["email"] = "test@example.org";
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
If you then later want to trigger loading the widgets, just call ``window.PretixWidget.buildWidgets()``.
|
||||||
|
|
||||||
|
|
||||||
|
Passing user data to the widget
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
If you display the widget in a restricted area of your website and you want to pre-fill fields in the checkout process
|
||||||
|
with known user data to save your users some typing and increase conversions, you can pass additional data attributes
|
||||||
|
with that information::
|
||||||
|
|
||||||
|
<pretix-widget event="https://pretix.eu/demo/democon/"
|
||||||
|
data-attendee-name-given-name="John"
|
||||||
|
data-attendee-name-family-name="Doe"
|
||||||
|
data-invoice-address-name-given-name="John"
|
||||||
|
data-invoice-address-name-family-name="Doe"
|
||||||
|
data-email="test@example.org"
|
||||||
|
data-question-L9G8NG9M="Foobar">
|
||||||
|
</pretix-widget>
|
||||||
|
|
||||||
|
This works for the pretix Button as well. 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).
|
||||||
|
|
||||||
|
* ``data-question-IDENTIFIER`` will pre-fill the answer for the question with the given identifier. You can view and set
|
||||||
|
identifiers in the *Questions* section of the backend.
|
||||||
|
|
||||||
|
* Depending on the person name scheme configured in your event settings, you can pass one or more of
|
||||||
|
``data-attendee-name-full-name``, ``data-attendee-name-given-name``, ``data-attendee-name-family-name``,
|
||||||
|
``data-attendee-name-middle-name``, ``data-attendee-name-title``, ``data-attendee-name-calling-name``,
|
||||||
|
``data-attendee-name-latin-transcription``. If you don't know or don't care, you can also just pass a string as
|
||||||
|
``data-attendee-name``, which will pre-fill the last part of the name, whatever that is.
|
||||||
|
|
||||||
|
* ``data-invoice-address-FIELD`` will pre-fill the corresponding field of the invoice address. Possible values for
|
||||||
|
``FIELD`` are ``company``, ``street``, ``zipcode``, ``city`` and ``country``, as well as fields specified by the
|
||||||
|
naming scheme such as ``name-title`` or ``name-given-name`` (see above). ``country`` expects a two-character
|
||||||
|
country code.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
* If you use the campaigns plugin, you can pass a campaign ID as a value to ``data-campaign``. This way, all orders
|
||||||
|
made through this widget will be counted towards this campaign.
|
||||||
|
|
||||||
|
* If you use the tracking plugin, you can pass a Google Analytics User ID to enable cross-domain tracking. This will
|
||||||
|
require you to dynamically load the widget, like this::
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||||
|
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||||
|
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||||
|
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||||
|
|
||||||
|
ga('create', 'UA-XXXXXX-1', 'auto');
|
||||||
|
ga('send', 'pageview');
|
||||||
|
|
||||||
|
window.pretixWidgetCallback = function () {
|
||||||
|
window.PretixWidget.build_widgets = false;
|
||||||
|
window.addEventListener('load', function() { // Wait for GA to be loaded
|
||||||
|
if(window.ga && ga.create) {
|
||||||
|
ga(function(tracker) {
|
||||||
|
window.PretixWidget.widget_data["tracking-ga-id"] = tracker.get('clientId');
|
||||||
|
window.PretixWidget.buildWidgets()
|
||||||
|
});
|
||||||
|
} else { // Tracking is probably blocked
|
||||||
|
window.PretixWidget.buildWidgets()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
.. versionchanged:: 2.3
|
||||||
|
|
||||||
|
Data passing options have been added in pretix 2.3. If you use a self-hosted version of pretix, they only work
|
||||||
|
fully if you configured a redis server.
|
||||||
|
|
||||||
.. _Let's Encrypt: https://letsencrypt.org/
|
.. _Let's Encrypt: https://letsencrypt.org/
|
||||||
|
|||||||
@@ -4,22 +4,10 @@ FAQ and Troubleshooting
|
|||||||
How can I test my shop before taking it live?
|
How can I test my shop before taking it live?
|
||||||
---------------------------------------------
|
---------------------------------------------
|
||||||
|
|
||||||
There are multiple ways to do this.
|
On your event dashboard, click on the first tile that shows your shop status. On the lower part of this page, you can
|
||||||
|
place your event into "test mode". In "test mode", everything behaves the same, but orders created during test mode can
|
||||||
First, you could just create some orders in your real shop and cancel/refund them later. If you don't want to process
|
later be fully deleted. Be sure to actually delete them when or after you turn off test mode, since test mode orders
|
||||||
real payments for the tests, you can either use a "manual" payment method like bank transfer and just mark the orders
|
still count toward your quotas and are included in your reports.
|
||||||
as paid with the button in the backend, or if you want to use e.g. Stripe, you can configure pretix to use your keys
|
|
||||||
for the Stripe test system and use their test credit cars. Read our :ref:`Stripe documentation <stripe>` for more
|
|
||||||
information.
|
|
||||||
|
|
||||||
Second, you could create a separate event, just for testing. In the last step of the :ref:`event creation process <event_create>`,
|
|
||||||
you can specify that you want to copy all settings from your real event, so you don't have to do all of it twice.
|
|
||||||
|
|
||||||
We are planning to add a dedicated test mode in a later version of pretix.
|
|
||||||
|
|
||||||
If you are using the hosted service at pretix.eu and want to get rid of the test orders completely, contact us at
|
|
||||||
support@pretix.eu and we can remove them for you. Please note that we only are able to do that *before* you have
|
|
||||||
received any real orders (i.e. taken the shop public). We won't charge any fees for test orders or test events.
|
|
||||||
|
|
||||||
How do I delete an event?
|
How do I delete an event?
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|||||||
6
readthedocs.yml
Normal file
6
readthedocs.yml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
build:
|
||||||
|
image: latest
|
||||||
|
|
||||||
|
python:
|
||||||
|
version: 3.6
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
[run]
|
|
||||||
source = pretix
|
|
||||||
omit = */migrations/*,*/urls.py,*/tests/*,*/testdummy/*,*/admin.py,pretix/wsgi.py,pretix/settings.py
|
|
||||||
|
|
||||||
[report]
|
|
||||||
exclude_lines =
|
|
||||||
pragma: no cover
|
|
||||||
def __str__
|
|
||||||
der __repr__
|
|
||||||
if settings.DEBUG
|
|
||||||
NOQA
|
|
||||||
NotImplementedError
|
|
||||||
@@ -34,4 +34,5 @@ git push
|
|||||||
# Unlock Weblate
|
# Unlock Weblate
|
||||||
for c in $COMPONENTS; do
|
for c in $COMPONENTS; do
|
||||||
wlc unlock $c;
|
wlc unlock $c;
|
||||||
|
wlc pull $c;
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ recursive-include pretix/control/templates *
|
|||||||
recursive-include pretix/presale/templates *
|
recursive-include pretix/presale/templates *
|
||||||
recursive-include pretix/plugins/banktransfer/templates *
|
recursive-include pretix/plugins/banktransfer/templates *
|
||||||
recursive-include pretix/plugins/banktransfer/static *
|
recursive-include pretix/plugins/banktransfer/static *
|
||||||
|
recursive-include pretix/plugins/manualpayment/templates *
|
||||||
|
recursive-include pretix/plugins/manualpayment/static *
|
||||||
recursive-include pretix/plugins/paypal/templates *
|
recursive-include pretix/plugins/paypal/templates *
|
||||||
recursive-include pretix/plugins/pretixdroid/templates *
|
recursive-include pretix/plugins/pretixdroid/templates *
|
||||||
recursive-include pretix/plugins/pretixdroid/static *
|
recursive-include pretix/plugins/pretixdroid/static *
|
||||||
@@ -18,3 +20,5 @@ recursive-include pretix/plugins/stripe/templates *
|
|||||||
recursive-include pretix/plugins/stripe/static *
|
recursive-include pretix/plugins/stripe/static *
|
||||||
recursive-include pretix/plugins/ticketoutputpdf/templates *
|
recursive-include pretix/plugins/ticketoutputpdf/templates *
|
||||||
recursive-include pretix/plugins/ticketoutputpdf/static *
|
recursive-include pretix/plugins/ticketoutputpdf/static *
|
||||||
|
recursive-include pretix/plugins/badges/templates *
|
||||||
|
recursive-include pretix/plugins/badges/static *
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "1.14.0"
|
__version__ = "2.5.0.dev0"
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PretixApiConfig(AppConfig):
|
||||||
|
name = 'pretix.api'
|
||||||
|
label = 'pretixapi'
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from . import signals, webhooks # noqa
|
||||||
|
|
||||||
|
|
||||||
|
default_app_config = 'pretix.api.PretixApiConfig'
|
||||||
|
|||||||
25
src/pretix/api/auth/device.py
Normal file
25
src/pretix/api/auth/device.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
from rest_framework import exceptions
|
||||||
|
from rest_framework.authentication import TokenAuthentication
|
||||||
|
|
||||||
|
from pretix.base.models import Device
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceTokenAuthentication(TokenAuthentication):
|
||||||
|
model = Device
|
||||||
|
keyword = 'Device'
|
||||||
|
|
||||||
|
def authenticate_credentials(self, key):
|
||||||
|
model = self.get_model()
|
||||||
|
try:
|
||||||
|
device = model.objects.select_related('organizer').get(api_token=key)
|
||||||
|
except model.DoesNotExist:
|
||||||
|
raise exceptions.AuthenticationFailed('Invalid token.')
|
||||||
|
|
||||||
|
if not device.initialized:
|
||||||
|
raise exceptions.AuthenticationFailed('Device has not been initialized.')
|
||||||
|
|
||||||
|
if not device.api_token:
|
||||||
|
raise exceptions.AuthenticationFailed('Device access has been revoked.')
|
||||||
|
|
||||||
|
return AnonymousUser(), device
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from rest_framework.permissions import SAFE_METHODS, BasePermission
|
from rest_framework.permissions import SAFE_METHODS, BasePermission
|
||||||
|
|
||||||
from pretix.base.models import Event
|
from pretix.api.models import OAuthAccessToken
|
||||||
|
from pretix.base.models import Device, Event
|
||||||
from pretix.base.models.organizer import Organizer, TeamAPIToken
|
from pretix.base.models.organizer import Organizer, TeamAPIToken
|
||||||
from pretix.helpers.security import (
|
from pretix.helpers.security import (
|
||||||
SessionInvalid, SessionReauthRequired, assert_session_valid,
|
SessionInvalid, SessionReauthRequired, assert_session_valid,
|
||||||
@@ -8,10 +9,9 @@ from pretix.helpers.security import (
|
|||||||
|
|
||||||
|
|
||||||
class EventPermission(BasePermission):
|
class EventPermission(BasePermission):
|
||||||
model = TeamAPIToken
|
|
||||||
|
|
||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
if not request.user.is_authenticated and not isinstance(request.auth, TeamAPIToken):
|
if not request.user.is_authenticated and not isinstance(request.auth, (Device, TeamAPIToken)):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if request.method not in SAFE_METHODS and hasattr(view, 'write_permission'):
|
if request.method not in SAFE_METHODS and hasattr(view, 'write_permission'):
|
||||||
@@ -30,7 +30,7 @@ class EventPermission(BasePermission):
|
|||||||
except SessionReauthRequired:
|
except SessionReauthRequired:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken)
|
perm_holder = (request.auth if isinstance(request.auth, (Device, TeamAPIToken))
|
||||||
else request.user)
|
else request.user)
|
||||||
if 'event' in request.resolver_match.kwargs and 'organizer' in request.resolver_match.kwargs:
|
if 'event' in request.resolver_match.kwargs and 'organizer' in request.resolver_match.kwargs:
|
||||||
request.event = Event.objects.filter(
|
request.event = Event.objects.filter(
|
||||||
@@ -55,4 +55,28 @@ class EventPermission(BasePermission):
|
|||||||
|
|
||||||
if required_permission and required_permission not in request.orgapermset:
|
if required_permission and required_permission not in request.orgapermset:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if isinstance(request.auth, OAuthAccessToken):
|
||||||
|
if not request.auth.allow_scopes(['write']) and request.method not in SAFE_METHODS:
|
||||||
|
return False
|
||||||
|
if not request.auth.allow_scopes(['read']) and request.method in SAFE_METHODS:
|
||||||
|
return False
|
||||||
|
if isinstance(request.auth, OAuthAccessToken) and hasattr(request, 'organizer'):
|
||||||
|
if not request.auth.organizers.filter(pk=request.organizer.pk).exists():
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class EventCRUDPermission(EventPermission):
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
if not super(EventCRUDPermission, self).has_permission(request, view):
|
||||||
|
return False
|
||||||
|
elif view.action == 'create' and 'can_create_events' not in request.orgapermset:
|
||||||
|
return False
|
||||||
|
elif view.action == 'destroy' and 'can_change_event_settings' not in request.eventpermset:
|
||||||
|
return False
|
||||||
|
elif view.action in ['update', 'partial_update'] \
|
||||||
|
and 'can_change_event_settings' not in request.eventpermset:
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ def custom_exception_handler(exc, context):
|
|||||||
if isinstance(exc, LockTimeoutException):
|
if isinstance(exc, LockTimeoutException):
|
||||||
response = Response(
|
response = Response(
|
||||||
{'detail': 'The server was too busy to process your request. Please try again.'},
|
{'detail': 'The server was too busy to process your request. Please try again.'},
|
||||||
status=status.HTTP_409_CONFLICT
|
status=status.HTTP_409_CONFLICT,
|
||||||
|
headers={
|
||||||
|
'Retry-After': 5
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|||||||
128
src/pretix/api/migrations/0001_initial.py
Normal file
128
src/pretix/api/migrations/0001_initial.py
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.13 on 2018-06-04 11:19
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import oauth2_provider.generators
|
||||||
|
import oauth2_provider.validators
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OAuthAccessToken',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
|
('token', models.CharField(max_length=255, unique=True)),
|
||||||
|
('expires', models.DateTimeField()),
|
||||||
|
('scope', models.TextField(blank=True)),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated', models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OAuthApplication',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
|
('client_type',
|
||||||
|
models.CharField(choices=[('confidential', 'Confidential'), ('public', 'Public')], max_length=32)),
|
||||||
|
('authorization_grant_type', models.CharField(
|
||||||
|
choices=[('authorization-code', 'Authorization code'), ('implicit', 'Implicit'),
|
||||||
|
('password', 'Resource owner password-based'),
|
||||||
|
('client-credentials', 'Client credentials')], max_length=32)),
|
||||||
|
('skip_authorization', models.BooleanField(default=False)),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated', models.DateTimeField(auto_now=True)),
|
||||||
|
('name', models.CharField(max_length=255, verbose_name='Application name')),
|
||||||
|
('redirect_uris', models.TextField(help_text='Allowed URIs list, space separated',
|
||||||
|
validators=[oauth2_provider.validators.URIValidator],
|
||||||
|
verbose_name='Redirection URIs')),
|
||||||
|
('client_id',
|
||||||
|
models.CharField(db_index=True, default=oauth2_provider.generators.generate_client_id, max_length=100,
|
||||||
|
unique=True, verbose_name='Client ID')),
|
||||||
|
('client_secret',
|
||||||
|
models.CharField(db_index=True, default=oauth2_provider.generators.generate_client_secret,
|
||||||
|
max_length=255, verbose_name='Client secret')),
|
||||||
|
('active', models.BooleanField(default=True)),
|
||||||
|
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='pretixapi_oauthapplication', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OAuthGrant',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
|
('code', models.CharField(max_length=255, unique=True)),
|
||||||
|
('expires', models.DateTimeField()),
|
||||||
|
('redirect_uri', models.CharField(max_length=255)),
|
||||||
|
('scope', models.TextField(blank=True)),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated', models.DateTimeField(auto_now=True)),
|
||||||
|
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
|
||||||
|
('user',
|
||||||
|
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pretixapi_oauthgrant',
|
||||||
|
to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OAuthRefreshToken',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
|
('token', models.CharField(max_length=255)),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated', models.DateTimeField(auto_now=True)),
|
||||||
|
('revoked', models.DateTimeField(null=True)),
|
||||||
|
('access_token',
|
||||||
|
models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name='refresh_token', to=settings.OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL)),
|
||||||
|
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='pretixapi_oauthrefreshtoken', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='oauthaccesstoken',
|
||||||
|
name='application',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='oauthaccesstoken',
|
||||||
|
name='source_refresh_token',
|
||||||
|
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name='refreshed_access_token',
|
||||||
|
to=settings.OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='oauthaccesstoken',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='pretixapi_oauthaccesstoken', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='oauthrefreshtoken',
|
||||||
|
unique_together=set([('token', 'revoked')]),
|
||||||
|
),
|
||||||
|
]
|
||||||
26
src/pretix/api/migrations/0002_auto_20180604_1120.py
Normal file
26
src/pretix/api/migrations/0002_auto_20180604_1120.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.13 on 2018-06-04 11:20
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0001_initial'),
|
||||||
|
('pretixapi', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='oauthaccesstoken',
|
||||||
|
name='organizers',
|
||||||
|
field=models.ManyToManyField(to='pretixbase.Organizer'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='oauthgrant',
|
||||||
|
name='organizers',
|
||||||
|
field=models.ManyToManyField(to='pretixbase.Organizer'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# Generated by Django 2.1.1 on 2018-11-07 10:46
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0102_auto_20181017_0024'),
|
||||||
|
('pretixapi', '0002_auto_20180604_1120'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='WebHook',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('enabled', models.BooleanField(default=True, verbose_name='Enable webhook')),
|
||||||
|
('target_url', models.URLField(verbose_name='Target URL')),
|
||||||
|
('all_events', models.BooleanField(default=False, verbose_name='All events (including newly created ones)')),
|
||||||
|
('limit_events', models.ManyToManyField(blank=True, to='pretixbase.Event', verbose_name='Limit to events')),
|
||||||
|
('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Organizer')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='WebHookCall',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('datetime', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('target_url', models.URLField()),
|
||||||
|
('is_retry', models.BooleanField(default=False)),
|
||||||
|
('execution_time', models.FloatField(null=True)),
|
||||||
|
('return_code', models.PositiveIntegerField(default=0)),
|
||||||
|
('payload', models.TextField()),
|
||||||
|
('response_body', models.TextField()),
|
||||||
|
('webhook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixapi.WebHook')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='WebHookEventListener',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('action_type', models.CharField(max_length=255)),
|
||||||
|
('webhook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixapi.WebHook')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='webhookcall',
|
||||||
|
name='success',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='webhook',
|
||||||
|
name='all_events',
|
||||||
|
field=models.BooleanField(default=True, verbose_name='All events (including newly created ones)'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='webhook',
|
||||||
|
name='organizer',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='webhooks', to='pretixbase.Organizer'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='webhookcall',
|
||||||
|
name='webhook',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='calls', to='pretixapi.WebHook'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='webhookeventlistener',
|
||||||
|
name='webhook',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='listeners', to='pretixapi.WebHook'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='webhookcall',
|
||||||
|
name='action_type',
|
||||||
|
field=models.CharField(default='', max_length=255),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
0
src/pretix/api/migrations/__init__.py
Normal file
0
src/pretix/api/migrations/__init__.py
Normal file
108
src/pretix/api/models.py
Normal file
108
src/pretix/api/models.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
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 oauth2_provider.generators import (
|
||||||
|
generate_client_id, generate_client_secret,
|
||||||
|
)
|
||||||
|
from oauth2_provider.models import (
|
||||||
|
AbstractAccessToken, AbstractApplication, AbstractGrant,
|
||||||
|
AbstractRefreshToken,
|
||||||
|
)
|
||||||
|
from oauth2_provider.validators import URIValidator
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthApplication(AbstractApplication):
|
||||||
|
name = models.CharField(verbose_name=_("Application name"), max_length=255, blank=False)
|
||||||
|
redirect_uris = models.TextField(
|
||||||
|
blank=False, validators=[URIValidator],
|
||||||
|
verbose_name=_("Redirection URIs"),
|
||||||
|
help_text=_("Allowed URIs list, space separated")
|
||||||
|
)
|
||||||
|
client_id = models.CharField(
|
||||||
|
verbose_name=_("Client ID"),
|
||||||
|
max_length=100, unique=True, default=generate_client_id, db_index=True
|
||||||
|
)
|
||||||
|
client_secret = models.CharField(
|
||||||
|
verbose_name=_("Client secret"),
|
||||||
|
max_length=255, blank=False, default=generate_client_secret, db_index=True
|
||||||
|
)
|
||||||
|
active = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse("control:user.settings.oauth.app", kwargs={'pk': self.id})
|
||||||
|
|
||||||
|
def is_usable(self, request):
|
||||||
|
return self.active and super().is_usable(request)
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthGrant(AbstractGrant):
|
||||||
|
application = models.ForeignKey(
|
||||||
|
OAuthApplication, on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
organizers = models.ManyToManyField('pretixbase.Organizer')
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthAccessToken(AbstractAccessToken):
|
||||||
|
source_refresh_token = models.OneToOneField(
|
||||||
|
# unique=True implied by the OneToOneField
|
||||||
|
'OAuthRefreshToken', on_delete=models.SET_NULL, blank=True, null=True,
|
||||||
|
related_name="refreshed_access_token"
|
||||||
|
)
|
||||||
|
application = models.ForeignKey(
|
||||||
|
OAuthApplication, on_delete=models.CASCADE, blank=True, null=True,
|
||||||
|
)
|
||||||
|
organizers = models.ManyToManyField('pretixbase.Organizer')
|
||||||
|
|
||||||
|
def revoke(self):
|
||||||
|
self.expires = now() - timedelta(hours=1)
|
||||||
|
self.save(update_fields=['expires'])
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthRefreshToken(AbstractRefreshToken):
|
||||||
|
application = models.ForeignKey(
|
||||||
|
OAuthApplication, on_delete=models.CASCADE)
|
||||||
|
access_token = models.OneToOneField(
|
||||||
|
OAuthAccessToken, on_delete=models.SET_NULL, blank=True, null=True,
|
||||||
|
related_name="refresh_token"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WebHook(models.Model):
|
||||||
|
organizer = models.ForeignKey('pretixbase.Organizer', on_delete=models.CASCADE, related_name='webhooks')
|
||||||
|
enabled = models.BooleanField(default=True, verbose_name=_("Enable webhook"))
|
||||||
|
target_url = models.URLField(verbose_name=_("Target URL"))
|
||||||
|
all_events = models.BooleanField(default=True, verbose_name=_("All events (including newly created ones)"))
|
||||||
|
limit_events = models.ManyToManyField('pretixbase.Event', verbose_name=_("Limit to events"), blank=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def action_types(self):
|
||||||
|
return [
|
||||||
|
l.action_type for l in self.listeners.all()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class WebHookEventListener(models.Model):
|
||||||
|
webhook = models.ForeignKey('WebHook', on_delete=models.CASCADE, related_name='listeners')
|
||||||
|
action_type = models.CharField(max_length=255)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ("action_type",)
|
||||||
|
|
||||||
|
|
||||||
|
class WebHookCall(models.Model):
|
||||||
|
webhook = models.ForeignKey('WebHook', on_delete=models.CASCADE, related_name='calls')
|
||||||
|
datetime = models.DateTimeField(auto_now_add=True)
|
||||||
|
target_url = models.URLField()
|
||||||
|
action_type = models.CharField(max_length=255)
|
||||||
|
is_retry = models.BooleanField(default=False)
|
||||||
|
execution_time = models.FloatField(null=True)
|
||||||
|
return_code = models.PositiveIntegerField(default=0)
|
||||||
|
success = models.BooleanField(default=False)
|
||||||
|
payload = models.TextField()
|
||||||
|
response_body = models.TextField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ("-datetime",)
|
||||||
45
src/pretix/api/oauth.py
Normal file
45
src/pretix/api/oauth.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
from oauth2_provider.exceptions import FatalClientError
|
||||||
|
from oauth2_provider.oauth2_validators import Grant, OAuth2Validator
|
||||||
|
from oauth2_provider.settings import oauth2_settings
|
||||||
|
|
||||||
|
|
||||||
|
class Validator(OAuth2Validator):
|
||||||
|
|
||||||
|
def save_authorization_code(self, client_id, code, request, *args, **kwargs):
|
||||||
|
if not getattr(request, 'organizers', None):
|
||||||
|
raise FatalClientError('No organizers selected.')
|
||||||
|
|
||||||
|
expires = timezone.now() + timedelta(
|
||||||
|
seconds=oauth2_settings.AUTHORIZATION_CODE_EXPIRE_SECONDS)
|
||||||
|
g = Grant(application=request.client, user=request.user, code=code["code"],
|
||||||
|
expires=expires, redirect_uri=request.redirect_uri,
|
||||||
|
scope=" ".join(request.scopes))
|
||||||
|
g.save()
|
||||||
|
g.organizers.add(*request.organizers.all())
|
||||||
|
|
||||||
|
def validate_code(self, client_id, code, client, request, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
grant = Grant.objects.get(code=code, application=client)
|
||||||
|
if not grant.is_expired():
|
||||||
|
request.scopes = grant.scope.split(" ")
|
||||||
|
request.user = grant.user
|
||||||
|
request.organizers = grant.organizers.all()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Grant.DoesNotExist:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _create_access_token(self, expires, request, token, source_refresh_token=None):
|
||||||
|
if not getattr(request, 'organizers', None) and not getattr(source_refresh_token, 'access_token'):
|
||||||
|
raise FatalClientError('No organizers selected.')
|
||||||
|
if hasattr(request, 'organizers'):
|
||||||
|
orgs = list(request.organizers.all())
|
||||||
|
else:
|
||||||
|
orgs = list(source_refresh_token.access_token.organizers.all())
|
||||||
|
access_token = super()._create_access_token(expires, request, token, source_refresh_token=None)
|
||||||
|
access_token.organizers.add(*orgs)
|
||||||
|
return access_token
|
||||||
131
src/pretix/api/serializers/cart.py
Normal file
131
src/pretix/api/serializers/cart.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
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 rest_framework import serializers
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
|
from pretix.api.serializers.order import (
|
||||||
|
AnswerCreateSerializer, AnswerSerializer,
|
||||||
|
)
|
||||||
|
from pretix.base.models import Quota
|
||||||
|
from pretix.base.models.orders import CartPosition
|
||||||
|
|
||||||
|
|
||||||
|
class CartPositionSerializer(I18nAwareModelSerializer):
|
||||||
|
answers = AnswerSerializer(many=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CartPosition
|
||||||
|
fields = ('id', 'cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||||
|
'attendee_email', 'voucher', 'addon_to', 'subevent', 'datetime', 'expires', 'includes_tax',
|
||||||
|
'answers',)
|
||||||
|
|
||||||
|
|
||||||
|
class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||||
|
answers = AnswerCreateSerializer(many=True, required=False)
|
||||||
|
expires = serializers.DateTimeField(required=False)
|
||||||
|
attendee_name = serializers.CharField(required=False, allow_null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CartPosition
|
||||||
|
fields = ('cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||||
|
'subevent', 'expires', 'includes_tax', 'answers',)
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
answers_data = validated_data.pop('answers')
|
||||||
|
if not validated_data.get('cart_id'):
|
||||||
|
cid = "{}@api".format(get_random_string(48))
|
||||||
|
while CartPosition.objects.filter(cart_id=cid).exists():
|
||||||
|
cid = "{}@api".format(get_random_string(48))
|
||||||
|
validated_data['cart_id'] = cid
|
||||||
|
|
||||||
|
if not validated_data.get('expires'):
|
||||||
|
validated_data['expires'] = now() + timedelta(
|
||||||
|
minutes=self.context['event'].settings.get('reservation_time', as_type=int)
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.context['event'].lock():
|
||||||
|
new_quotas = (validated_data.get('variation').quotas.filter(subevent=validated_data.get('subevent'))
|
||||||
|
if validated_data.get('variation')
|
||||||
|
else validated_data.get('item').quotas.filter(subevent=validated_data.get('subevent')))
|
||||||
|
if len(new_quotas) == 0:
|
||||||
|
raise ValidationError(
|
||||||
|
ugettext_lazy('The product "{}" is not assigned to a quota.').format(
|
||||||
|
str(validated_data.get('item'))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for quota in new_quotas:
|
||||||
|
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(
|
||||||
|
quota.name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
attendee_name = validated_data.pop('attendee_name', '')
|
||||||
|
if attendee_name and not validated_data.get('attendee_name_parts'):
|
||||||
|
validated_data['attendee_name_parts'] = {
|
||||||
|
'_legacy': attendee_name
|
||||||
|
}
|
||||||
|
cp = CartPosition.objects.create(event=self.context['event'], **validated_data)
|
||||||
|
|
||||||
|
for answ_data in answers_data:
|
||||||
|
options = answ_data.pop('options')
|
||||||
|
answ = cp.answers.create(**answ_data)
|
||||||
|
answ.options.add(*options)
|
||||||
|
return cp
|
||||||
|
|
||||||
|
def validate_cart_id(self, cid):
|
||||||
|
if cid and not cid.endswith('@api'):
|
||||||
|
raise ValidationError('Cart ID should end in @api or be empty.')
|
||||||
|
|
||||||
|
def validate_item(self, item):
|
||||||
|
if item.event != self.context['event']:
|
||||||
|
raise ValidationError(
|
||||||
|
'The specified item does not belong to this event.'
|
||||||
|
)
|
||||||
|
if not item.active:
|
||||||
|
raise ValidationError(
|
||||||
|
'The specified item is not active.'
|
||||||
|
)
|
||||||
|
return item
|
||||||
|
|
||||||
|
def validate_subevent(self, subevent):
|
||||||
|
if self.context['event'].has_subevents:
|
||||||
|
if not subevent:
|
||||||
|
raise ValidationError(
|
||||||
|
'You need to set a subevent.'
|
||||||
|
)
|
||||||
|
if subevent.event != self.context['event']:
|
||||||
|
raise ValidationError(
|
||||||
|
'The specified subevent does not belong to this event.'
|
||||||
|
)
|
||||||
|
elif subevent:
|
||||||
|
raise ValidationError(
|
||||||
|
'You cannot set a subevent for this event.'
|
||||||
|
)
|
||||||
|
return subevent
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
if data.get('item'):
|
||||||
|
if data.get('item').has_variations:
|
||||||
|
if not data.get('variation'):
|
||||||
|
raise ValidationError('You should specify a variation for this item.')
|
||||||
|
else:
|
||||||
|
if data.get('variation').item != data.get('item'):
|
||||||
|
raise ValidationError(
|
||||||
|
'The specified variation does not belong to the specified item.'
|
||||||
|
)
|
||||||
|
elif data.get('variation'):
|
||||||
|
raise ValidationError(
|
||||||
|
'You cannot specify a variation for this item.'
|
||||||
|
)
|
||||||
|
if data.get('attendee_name') and data.get('attendee_name_parts'):
|
||||||
|
raise ValidationError(
|
||||||
|
{'attendee_name': ['Do not specify attendee_name if you specified attendee_name_parts.']}
|
||||||
|
)
|
||||||
|
return data
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
|
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_countries.serializers import CountryFieldMixin
|
from django_countries.serializers import CountryFieldMixin
|
||||||
from rest_framework.fields import Field
|
from rest_framework.fields import Field
|
||||||
|
from rest_framework.relations import SlugRelatedField
|
||||||
|
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.base.models import Event, TaxRule
|
from pretix.base.models import Event, TaxRule
|
||||||
@@ -14,15 +20,162 @@ class MetaDataField(Field):
|
|||||||
v.property.name: v.value for v in value.meta_values.all()
|
v.property.name: v.value for v in value.meta_values.all()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
return {
|
||||||
|
'meta_data': data
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PluginsField(Field):
|
||||||
|
|
||||||
|
def to_representation(self, obj):
|
||||||
|
from pretix.base.plugins import get_all_plugins
|
||||||
|
|
||||||
|
return {
|
||||||
|
p.module for p in get_all_plugins()
|
||||||
|
if not p.name.startswith('.') and getattr(p, 'visible', True) and p.module in obj.get_plugins()
|
||||||
|
}
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
return {
|
||||||
|
'plugins': data
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class EventSerializer(I18nAwareModelSerializer):
|
class EventSerializer(I18nAwareModelSerializer):
|
||||||
meta_data = MetaDataField(source='*')
|
meta_data = MetaDataField(required=False, source='*')
|
||||||
|
plugins = PluginsField(required=False, source='*')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Event
|
model = Event
|
||||||
fields = ('name', 'slug', 'live', 'currency', 'date_from',
|
fields = ('name', 'slug', 'live', 'testmode', 'currency', 'date_from',
|
||||||
'date_to', 'date_admission', 'is_public', 'presale_start',
|
'date_to', 'date_admission', 'is_public', 'presale_start',
|
||||||
'presale_end', 'location', 'has_subevents', 'meta_data')
|
'presale_end', 'location', 'has_subevents', 'meta_data', 'plugins')
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
data = super().validate(data)
|
||||||
|
|
||||||
|
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||||
|
full_data.update(data)
|
||||||
|
|
||||||
|
Event.clean_dates(data.get('date_from'), data.get('date_to'))
|
||||||
|
Event.clean_presale(data.get('presale_start'), data.get('presale_end'))
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def validate_has_subevents(self, value):
|
||||||
|
Event.clean_has_subevents(self.instance, value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def validate_slug(self, value):
|
||||||
|
Event.clean_slug(self.context['request'].organizer, self.instance, value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def validate_live(self, value):
|
||||||
|
if value:
|
||||||
|
if self.instance is None:
|
||||||
|
raise ValidationError(_('Events cannot be created as \'live\'. Quotas and payment must be added to the '
|
||||||
|
'event before sales can go live.'))
|
||||||
|
else:
|
||||||
|
self.instance.clean_live()
|
||||||
|
return value
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def meta_properties(self):
|
||||||
|
return {
|
||||||
|
p.name: p for p in self.context['request'].organizer.meta_properties.all()
|
||||||
|
}
|
||||||
|
|
||||||
|
def validate_meta_data(self, value):
|
||||||
|
for key in value['meta_data'].keys():
|
||||||
|
if key not in self.meta_properties:
|
||||||
|
raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key))
|
||||||
|
return value
|
||||||
|
|
||||||
|
def validate_plugins(self, value):
|
||||||
|
from pretix.base.plugins import get_all_plugins
|
||||||
|
|
||||||
|
plugins_available = {
|
||||||
|
p.module for p in get_all_plugins(self.instance)
|
||||||
|
if not p.name.startswith('.') and getattr(p, 'visible', True)
|
||||||
|
}
|
||||||
|
|
||||||
|
for plugin in value.get('plugins'):
|
||||||
|
if plugin not in plugins_available:
|
||||||
|
raise ValidationError(_('Unknown plugin: \'{name}\'.').format(name=plugin))
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def create(self, validated_data):
|
||||||
|
meta_data = validated_data.pop('meta_data', None)
|
||||||
|
plugins = validated_data.pop('plugins', settings.PRETIX_PLUGINS_DEFAULT.split(','))
|
||||||
|
event = super().create(validated_data)
|
||||||
|
|
||||||
|
# Meta data
|
||||||
|
if meta_data is not None:
|
||||||
|
for key, value in meta_data.items():
|
||||||
|
event.meta_values.create(
|
||||||
|
property=self.meta_properties.get(key),
|
||||||
|
value=value
|
||||||
|
)
|
||||||
|
|
||||||
|
# Plugins
|
||||||
|
if plugins is not None:
|
||||||
|
event.set_active_plugins(plugins)
|
||||||
|
event.save(update_fields=['plugins'])
|
||||||
|
|
||||||
|
return event
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
meta_data = validated_data.pop('meta_data', None)
|
||||||
|
plugins = validated_data.pop('plugins', None)
|
||||||
|
event = super().update(instance, validated_data)
|
||||||
|
|
||||||
|
# Meta data
|
||||||
|
if meta_data is not None:
|
||||||
|
current = {mv.property: mv for mv in event.meta_values.select_related('property')}
|
||||||
|
for key, value in meta_data.items():
|
||||||
|
prop = self.meta_properties.get(key)
|
||||||
|
if prop in current:
|
||||||
|
current[prop].value = value
|
||||||
|
current[prop].save()
|
||||||
|
else:
|
||||||
|
event.meta_values.create(
|
||||||
|
property=self.meta_properties.get(key),
|
||||||
|
value=value
|
||||||
|
)
|
||||||
|
|
||||||
|
for prop, current_object in current.items():
|
||||||
|
if prop.name not in meta_data:
|
||||||
|
current_object.delete()
|
||||||
|
|
||||||
|
# Plugins
|
||||||
|
if plugins is not None:
|
||||||
|
event.set_active_plugins(plugins)
|
||||||
|
event.save()
|
||||||
|
|
||||||
|
return event
|
||||||
|
|
||||||
|
|
||||||
|
class CloneEventSerializer(EventSerializer):
|
||||||
|
@transaction.atomic
|
||||||
|
def create(self, validated_data):
|
||||||
|
plugins = validated_data.pop('plugins', None)
|
||||||
|
is_public = validated_data.pop('is_public', None)
|
||||||
|
new_event = super().create(validated_data)
|
||||||
|
|
||||||
|
event = Event.objects.filter(slug=self.context['event'], organizer=self.context['organizer'].pk).first()
|
||||||
|
new_event.copy_data_from(event)
|
||||||
|
|
||||||
|
if plugins is not None:
|
||||||
|
new_event.set_active_plugins(plugins)
|
||||||
|
if is_public is not None:
|
||||||
|
new_event.is_public = is_public
|
||||||
|
new_event.save()
|
||||||
|
|
||||||
|
return new_event
|
||||||
|
|
||||||
|
|
||||||
class SubEventItemSerializer(I18nAwareModelSerializer):
|
class SubEventItemSerializer(I18nAwareModelSerializer):
|
||||||
@@ -40,12 +193,13 @@ class SubEventItemVariationSerializer(I18nAwareModelSerializer):
|
|||||||
class SubEventSerializer(I18nAwareModelSerializer):
|
class SubEventSerializer(I18nAwareModelSerializer):
|
||||||
item_price_overrides = SubEventItemSerializer(source='subeventitem_set', many=True)
|
item_price_overrides = SubEventItemSerializer(source='subeventitem_set', many=True)
|
||||||
variation_price_overrides = SubEventItemVariationSerializer(source='subeventitemvariation_set', many=True)
|
variation_price_overrides = SubEventItemVariationSerializer(source='subeventitemvariation_set', many=True)
|
||||||
|
event = SlugRelatedField(slug_field='slug', read_only=True)
|
||||||
meta_data = MetaDataField(source='*')
|
meta_data = MetaDataField(source='*')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SubEvent
|
model = SubEvent
|
||||||
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
|
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
|
||||||
'presale_start', 'presale_end', 'location',
|
'presale_start', 'presale_end', 'location', 'event',
|
||||||
'item_price_overrides', 'variation_price_overrides', 'meta_data')
|
'item_price_overrides', 'variation_price_overrides', 'meta_data')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,13 +15,20 @@ class I18nField(Field):
|
|||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
def to_representation(self, value):
|
def to_representation(self, value):
|
||||||
if value is None or value.data is None:
|
if hasattr(value, 'data'):
|
||||||
|
if isinstance(value.data, dict):
|
||||||
|
return value.data
|
||||||
|
elif value.data is None:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
settings.LANGUAGE_CODE: str(value.data)
|
||||||
|
}
|
||||||
|
elif value is None:
|
||||||
return None
|
return None
|
||||||
if isinstance(value.data, dict):
|
|
||||||
return value.data
|
|
||||||
else:
|
else:
|
||||||
return {
|
return {
|
||||||
settings.LANGUAGE_CODE: str(value.data)
|
settings.LANGUAGE_CODE: str(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
|
|||||||
@@ -74,12 +74,12 @@ class ItemSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Item
|
model = Item
|
||||||
fields = ('id', 'category', 'name', 'active', 'description',
|
fields = ('id', 'category', 'name', 'internal_name', 'active', 'sales_channels', 'description',
|
||||||
'default_price', 'free_price', 'tax_rate', 'tax_rule', 'admission',
|
'default_price', 'free_price', 'tax_rate', 'tax_rule', 'admission',
|
||||||
'position', 'picture', 'available_from', 'available_until',
|
'position', 'picture', 'available_from', 'available_until',
|
||||||
'require_voucher', 'hide_without_voucher', 'allow_cancel',
|
'require_voucher', 'hide_without_voucher', 'allow_cancel',
|
||||||
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations',
|
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations',
|
||||||
'variations', 'addons')
|
'variations', 'addons', 'original_price', 'require_approval', 'generate_tickets')
|
||||||
read_only_fields = ('has_variations', 'picture')
|
read_only_fields = ('has_variations', 'picture')
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
@@ -129,7 +129,7 @@ class ItemCategorySerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ItemCategory
|
model = ItemCategory
|
||||||
fields = ('id', 'name', 'description', 'position', 'is_addon')
|
fields = ('id', 'name', 'internal_name', 'description', 'position', 'is_addon')
|
||||||
|
|
||||||
|
|
||||||
class QuestionOptionSerializer(I18nAwareModelSerializer):
|
class QuestionOptionSerializer(I18nAwareModelSerializer):
|
||||||
|
|||||||
@@ -1,18 +1,33 @@
|
|||||||
|
import json
|
||||||
|
from collections import Counter
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from django.utils.translation import ugettext_lazy
|
||||||
|
from django_countries.fields import Country
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
|
from rest_framework.relations import SlugRelatedField
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
|
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
|
from pretix.base.channels import get_all_sales_channels
|
||||||
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Checkin, Invoice, InvoiceAddress, InvoiceLine, Order, OrderPosition,
|
Checkin, Invoice, InvoiceAddress, InvoiceLine, Order, OrderPosition,
|
||||||
QuestionAnswer,
|
Question, QuestionAnswer,
|
||||||
)
|
)
|
||||||
from pretix.base.models.orders import OrderFee
|
from pretix.base.models.orders import (
|
||||||
|
CartPosition, OrderFee, OrderPayment, OrderRefund,
|
||||||
|
)
|
||||||
|
from pretix.base.pdf import get_variables
|
||||||
from pretix.base.signals import register_ticket_outputs
|
from pretix.base.signals import register_ticket_outputs
|
||||||
|
|
||||||
|
|
||||||
class CompatibleCountryField(serializers.Field):
|
class CompatibleCountryField(serializers.Field):
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
return {self.field_name: Country(data)}
|
||||||
|
|
||||||
def to_representation(self, instance: InvoiceAddress):
|
def to_representation(self, instance: InvoiceAddress):
|
||||||
if instance.country:
|
if instance.country:
|
||||||
return str(instance.country)
|
return str(instance.country)
|
||||||
@@ -20,13 +35,30 @@ class CompatibleCountryField(serializers.Field):
|
|||||||
return instance.country_old
|
return instance.country_old
|
||||||
|
|
||||||
|
|
||||||
class InvoiceAdddressSerializer(I18nAwareModelSerializer):
|
class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
||||||
country = CompatibleCountryField(source='*')
|
country = CompatibleCountryField(source='*')
|
||||||
|
name = serializers.CharField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InvoiceAddress
|
model = InvoiceAddress
|
||||||
fields = ('last_modified', 'is_business', 'company', 'name', 'street', 'zipcode', 'city', 'country', 'vat_id',
|
fields = ('last_modified', 'is_business', 'company', 'name', 'name_parts', 'street', 'zipcode', 'city', 'country',
|
||||||
'vat_id_validated', 'internal_reference')
|
'vat_id', 'vat_id_validated', 'internal_reference')
|
||||||
|
read_only_fields = ('last_modified', 'vat_id_validated')
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
for v in self.fields.values():
|
||||||
|
v.required = False
|
||||||
|
v.allow_blank = True
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
if data.get('name') and data.get('name_parts'):
|
||||||
|
raise ValidationError(
|
||||||
|
{'name': ['Do not specify name if you specified name_parts.']}
|
||||||
|
)
|
||||||
|
if data.get('name_parts') and '_scheme' not in data.get('name_parts'):
|
||||||
|
data['name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class AnswerQuestionIdentifierField(serializers.Field):
|
class AnswerQuestionIdentifierField(serializers.Field):
|
||||||
@@ -57,7 +89,8 @@ class CheckinSerializer(I18nAwareModelSerializer):
|
|||||||
class OrderDownloadsField(serializers.Field):
|
class OrderDownloadsField(serializers.Field):
|
||||||
def to_representation(self, instance: Order):
|
def to_representation(self, instance: Order):
|
||||||
if instance.status != Order.STATUS_PAID:
|
if instance.status != Order.STATUS_PAID:
|
||||||
return []
|
if instance.status != Order.STATUS_PENDING or instance.require_approval or not instance.event.settings.ticket_download_pending:
|
||||||
|
return []
|
||||||
|
|
||||||
request = self.context['request']
|
request = self.context['request']
|
||||||
res = []
|
res = []
|
||||||
@@ -80,10 +113,9 @@ class OrderDownloadsField(serializers.Field):
|
|||||||
class PositionDownloadsField(serializers.Field):
|
class PositionDownloadsField(serializers.Field):
|
||||||
def to_representation(self, instance: OrderPosition):
|
def to_representation(self, instance: OrderPosition):
|
||||||
if instance.order.status != Order.STATUS_PAID:
|
if instance.order.status != Order.STATUS_PAID:
|
||||||
return []
|
if instance.order.status != Order.STATUS_PENDING or instance.order.require_approval or not instance.order.event.settings.ticket_download_pending:
|
||||||
if instance.addon_to_id and not instance.order.event.settings.ticket_download_addons:
|
return []
|
||||||
return []
|
if not instance.generate_ticket:
|
||||||
if not instance.item.admission and not instance.order.event.settings.ticket_download_nonadm:
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
request = self.context['request']
|
request = self.context['request']
|
||||||
@@ -104,17 +136,67 @@ class PositionDownloadsField(serializers.Field):
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
class PdfDataSerializer(serializers.Field):
|
||||||
|
def to_representation(self, instance: OrderPosition):
|
||||||
|
res = {}
|
||||||
|
|
||||||
|
ev = instance.subevent or instance.order.event
|
||||||
|
with language(instance.order.locale):
|
||||||
|
# This needs to have some extra performance improvements to avoid creating hundreds of queries when
|
||||||
|
# we serialize a list.
|
||||||
|
|
||||||
|
if 'vars' not in self.context:
|
||||||
|
self.context['vars'] = get_variables(self.context['request'].event)
|
||||||
|
|
||||||
|
for k, f in self.context['vars'].items():
|
||||||
|
res[k] = f['evaluate'](instance, instance.order, ev)
|
||||||
|
|
||||||
|
if not hasattr(ev, '_cached_meta_data'):
|
||||||
|
ev._cached_meta_data = ev.meta_data
|
||||||
|
|
||||||
|
for k, v in ev._cached_meta_data.items():
|
||||||
|
res['meta:' + k] = v
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
class OrderPositionSerializer(I18nAwareModelSerializer):
|
class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||||
checkins = CheckinSerializer(many=True)
|
checkins = CheckinSerializer(many=True)
|
||||||
answers = AnswerSerializer(many=True)
|
answers = AnswerSerializer(many=True)
|
||||||
downloads = PositionDownloadsField(source='*')
|
downloads = PositionDownloadsField(source='*')
|
||||||
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
||||||
|
pdf_data = PdfDataSerializer(source='*')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = OrderPosition
|
model = OrderPosition
|
||||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
|
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||||
'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins', 'downloads',
|
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||||
'answers', 'tax_rule')
|
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data')
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if 'request' in self.context and not self.context['request'].query_params.get('pdf_data', 'false') == 'true':
|
||||||
|
self.fields.pop('pdf_data')
|
||||||
|
|
||||||
|
|
||||||
|
class OrderPaymentTypeField(serializers.Field):
|
||||||
|
# TODO: Remove after pretix 2.2
|
||||||
|
def to_representation(self, instance: Order):
|
||||||
|
t = None
|
||||||
|
for p in instance.payments.all():
|
||||||
|
t = p.provider
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
class OrderPaymentDateField(serializers.DateField):
|
||||||
|
# TODO: Remove after pretix 2.2
|
||||||
|
def to_representation(self, instance: Order):
|
||||||
|
t = None
|
||||||
|
for p in instance.payments.all():
|
||||||
|
t = p.payment_date or t
|
||||||
|
if t:
|
||||||
|
|
||||||
|
return super().to_representation(t.date())
|
||||||
|
|
||||||
|
|
||||||
class OrderFeeSerializer(I18nAwareModelSerializer):
|
class OrderFeeSerializer(I18nAwareModelSerializer):
|
||||||
@@ -123,32 +205,412 @@ class OrderFeeSerializer(I18nAwareModelSerializer):
|
|||||||
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')
|
||||||
|
|
||||||
|
|
||||||
class PaymentFeeLegacyField(serializers.Field):
|
class OrderPaymentSerializer(I18nAwareModelSerializer):
|
||||||
def __init__(self, *args, **kwargs):
|
class Meta:
|
||||||
self.attr = kwargs.pop('attribute')
|
model = OrderPayment
|
||||||
super().__init__(*args, **kwargs)
|
fields = ('local_id', 'state', 'amount', 'created', 'payment_date', 'provider')
|
||||||
|
|
||||||
def to_representation(self, instance: Order):
|
|
||||||
return str(
|
class OrderRefundSerializer(I18nAwareModelSerializer):
|
||||||
sum([getattr(f, self.attr) for f in instance.fees.all() if f.fee_type == OrderFee.FEE_TYPE_PAYMENT],
|
payment = SlugRelatedField(slug_field='local_id', read_only=True)
|
||||||
Decimal('0.00'))
|
|
||||||
)
|
class Meta:
|
||||||
|
model = OrderRefund
|
||||||
|
fields = ('local_id', 'state', 'source', 'amount', 'payment', 'created', 'execution_date', 'provider')
|
||||||
|
|
||||||
|
|
||||||
class OrderSerializer(I18nAwareModelSerializer):
|
class OrderSerializer(I18nAwareModelSerializer):
|
||||||
invoice_address = InvoiceAdddressSerializer()
|
invoice_address = InvoiceAddressSerializer()
|
||||||
positions = OrderPositionSerializer(many=True)
|
positions = OrderPositionSerializer(many=True)
|
||||||
fees = OrderFeeSerializer(many=True)
|
fees = OrderFeeSerializer(many=True)
|
||||||
downloads = OrderDownloadsField(source='*')
|
downloads = OrderDownloadsField(source='*')
|
||||||
payment_fee = PaymentFeeLegacyField(source='*', attribute='value') # TODO: Remove in 1.9
|
payments = OrderPaymentSerializer(many=True)
|
||||||
payment_fee_tax_rate = PaymentFeeLegacyField(source='*', attribute='tax_rate') # TODO: Remove in 1.9
|
refunds = OrderRefundSerializer(many=True)
|
||||||
payment_fee_tax_value = PaymentFeeLegacyField(source='*', attribute='tax_value') # TODO: Remove in 1.9
|
payment_date = OrderPaymentDateField(source='*')
|
||||||
|
payment_provider = OrderPaymentTypeField(source='*')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Order
|
model = Order
|
||||||
fields = ('code', 'status', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
|
fields = ('code', 'status', 'testmode', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
|
||||||
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
|
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
|
||||||
'payment_fee', 'payment_fee_tax_rate', 'payment_fee_tax_value', 'checkin_attention')
|
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel')
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if not self.context['request'].query_params.get('pdf_data', 'false') == 'true':
|
||||||
|
self.fields['positions'].child.fields.pop('pdf_data')
|
||||||
|
|
||||||
|
|
||||||
|
class AnswerCreateSerializer(I18nAwareModelSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = QuestionAnswer
|
||||||
|
fields = ('question', 'answer', 'options')
|
||||||
|
|
||||||
|
def validate_question(self, q):
|
||||||
|
if q.event != self.context['event']:
|
||||||
|
raise ValidationError(
|
||||||
|
'The specified question does not belong to this event.'
|
||||||
|
)
|
||||||
|
return q
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
if data.get('question').type == Question.TYPE_FILE:
|
||||||
|
raise ValidationError(
|
||||||
|
'File uploads are currently not supported via the API.'
|
||||||
|
)
|
||||||
|
elif data.get('question').type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
|
||||||
|
if not data.get('options'):
|
||||||
|
raise ValidationError(
|
||||||
|
'You need to specify options if the question is of a choice type.'
|
||||||
|
)
|
||||||
|
if data.get('question').type == Question.TYPE_CHOICE and len(data.get('options')) > 1:
|
||||||
|
raise ValidationError(
|
||||||
|
'You can specify at most one option for this question.'
|
||||||
|
)
|
||||||
|
data['answer'] = ", ".join([str(o) for o in data.get('options')])
|
||||||
|
|
||||||
|
else:
|
||||||
|
if data.get('options'):
|
||||||
|
raise ValidationError(
|
||||||
|
'You should not specify options if the question is not of a choice type.'
|
||||||
|
)
|
||||||
|
|
||||||
|
if data.get('question').type == Question.TYPE_BOOLEAN:
|
||||||
|
if data.get('answer') in ['true', 'True', '1', 'TRUE']:
|
||||||
|
data['answer'] = 'True'
|
||||||
|
elif data.get('answer') in ['false', 'False', '0', 'FALSE']:
|
||||||
|
data['answer'] = 'False'
|
||||||
|
else:
|
||||||
|
raise ValidationError(
|
||||||
|
'Please specify "true" or "false" for boolean questions.'
|
||||||
|
)
|
||||||
|
elif data.get('question').type == Question.TYPE_NUMBER:
|
||||||
|
serializers.DecimalField(
|
||||||
|
max_digits=50,
|
||||||
|
decimal_places=25
|
||||||
|
).to_internal_value(data.get('answer'))
|
||||||
|
elif data.get('question').type == Question.TYPE_DATE:
|
||||||
|
data['answer'] = serializers.DateField().to_internal_value(data.get('answer'))
|
||||||
|
elif data.get('question').type == Question.TYPE_TIME:
|
||||||
|
data['answer'] = serializers.TimeField().to_internal_value(data.get('answer'))
|
||||||
|
elif data.get('question').type == Question.TYPE_DATETIME:
|
||||||
|
data['answer'] = serializers.DateTimeField().to_internal_value(data.get('answer'))
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class OrderFeeCreateSerializer(I18nAwareModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = OrderFee
|
||||||
|
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rule')
|
||||||
|
|
||||||
|
def validate_tax_rule(self, tr):
|
||||||
|
if tr and tr.event != self.context['event']:
|
||||||
|
raise ValidationError(
|
||||||
|
'The specified tax rate does not belong to this event.'
|
||||||
|
)
|
||||||
|
return tr
|
||||||
|
|
||||||
|
|
||||||
|
class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
||||||
|
answers = AnswerCreateSerializer(many=True, required=False)
|
||||||
|
addon_to = serializers.IntegerField(required=False, allow_null=True)
|
||||||
|
secret = serializers.CharField(required=False)
|
||||||
|
attendee_name = serializers.CharField(required=False, allow_null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = OrderPosition
|
||||||
|
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||||
|
'secret', 'addon_to', 'subevent', 'answers')
|
||||||
|
|
||||||
|
def validate_secret(self, secret):
|
||||||
|
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
|
||||||
|
raise ValidationError(
|
||||||
|
'You cannot assign a position secret that already exists.'
|
||||||
|
)
|
||||||
|
return secret
|
||||||
|
|
||||||
|
def validate_item(self, item):
|
||||||
|
if item.event != self.context['event']:
|
||||||
|
raise ValidationError(
|
||||||
|
'The specified item does not belong to this event.'
|
||||||
|
)
|
||||||
|
if not item.active:
|
||||||
|
raise ValidationError(
|
||||||
|
'The specified item is not active.'
|
||||||
|
)
|
||||||
|
return item
|
||||||
|
|
||||||
|
def validate_subevent(self, subevent):
|
||||||
|
if self.context['event'].has_subevents:
|
||||||
|
if not subevent:
|
||||||
|
raise ValidationError(
|
||||||
|
'You need to set a subevent.'
|
||||||
|
)
|
||||||
|
if subevent.event != self.context['event']:
|
||||||
|
raise ValidationError(
|
||||||
|
'The specified subevent does not belong to this event.'
|
||||||
|
)
|
||||||
|
elif subevent:
|
||||||
|
raise ValidationError(
|
||||||
|
'You cannot set a subevent for this event.'
|
||||||
|
)
|
||||||
|
return subevent
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
if data.get('item'):
|
||||||
|
if data.get('item').has_variations:
|
||||||
|
if not data.get('variation'):
|
||||||
|
raise ValidationError({'variation': ['You should specify a variation for this item.']})
|
||||||
|
else:
|
||||||
|
if data.get('variation').item != data.get('item'):
|
||||||
|
raise ValidationError(
|
||||||
|
{'variation': ['The specified variation does not belong to the specified item.']}
|
||||||
|
)
|
||||||
|
elif data.get('variation'):
|
||||||
|
raise ValidationError(
|
||||||
|
{'variation': ['You cannot specify a variation for this item.']}
|
||||||
|
)
|
||||||
|
if data.get('attendee_name') and data.get('attendee_name_parts'):
|
||||||
|
raise ValidationError(
|
||||||
|
{'attendee_name': ['Do not specify attendee_name if you specified attendee_name_parts.']}
|
||||||
|
)
|
||||||
|
if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'):
|
||||||
|
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class CompatibleJSONField(serializers.JSONField):
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
try:
|
||||||
|
return json.dumps(data)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
self.fail('invalid')
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
if value:
|
||||||
|
return json.loads(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||||
|
invoice_address = InvoiceAddressSerializer(required=False)
|
||||||
|
positions = OrderPositionCreateSerializer(many=True, required=False)
|
||||||
|
fees = OrderFeeCreateSerializer(many=True, required=False)
|
||||||
|
status = serializers.ChoiceField(choices=(
|
||||||
|
('n', Order.STATUS_PENDING),
|
||||||
|
('p', Order.STATUS_PAID),
|
||||||
|
), default='n', required=False)
|
||||||
|
code = serializers.CharField(
|
||||||
|
required=False,
|
||||||
|
max_length=16,
|
||||||
|
min_length=5
|
||||||
|
)
|
||||||
|
comment = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
payment_provider = serializers.CharField(required=True)
|
||||||
|
payment_info = CompatibleJSONField(required=False)
|
||||||
|
consume_carts = serializers.ListField(child=serializers.CharField(), required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Order
|
||||||
|
fields = ('code', 'status', 'testmode', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
||||||
|
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'consume_carts')
|
||||||
|
|
||||||
|
def validate_payment_provider(self, pp):
|
||||||
|
if pp not in self.context['event'].get_payment_providers():
|
||||||
|
raise ValidationError('The given payment provider is not known.')
|
||||||
|
return pp
|
||||||
|
|
||||||
|
def validate_sales_channel(self, channel):
|
||||||
|
if channel not in get_all_sales_channels():
|
||||||
|
raise ValidationError('Unknown sales channel.')
|
||||||
|
return channel
|
||||||
|
|
||||||
|
def validate_code(self, code):
|
||||||
|
if code and Order.objects.filter(event__organizer=self.context['event'].organizer, code=code).exists():
|
||||||
|
raise ValidationError(
|
||||||
|
'This order code is already in use.'
|
||||||
|
)
|
||||||
|
if any(c not in 'ABCDEFGHJKLMNPQRSTUVWXYZ1234567890' for c in code):
|
||||||
|
raise ValidationError(
|
||||||
|
'This order code contains invalid characters.'
|
||||||
|
)
|
||||||
|
return code
|
||||||
|
|
||||||
|
def validate_positions(self, data):
|
||||||
|
if not data:
|
||||||
|
raise ValidationError(
|
||||||
|
'An order cannot be empty.'
|
||||||
|
)
|
||||||
|
errs = [{} for p in data]
|
||||||
|
if any([p.get('positionid') for p in data]):
|
||||||
|
if not all([p.get('positionid') for p in data]):
|
||||||
|
for i, p in enumerate(data):
|
||||||
|
if not p.get('positionid'):
|
||||||
|
errs[i]['positionid'] = [
|
||||||
|
'If you set position IDs manually, you need to do so for all positions.'
|
||||||
|
]
|
||||||
|
raise ValidationError(errs)
|
||||||
|
|
||||||
|
last_non_add_on = None
|
||||||
|
last_posid = 0
|
||||||
|
|
||||||
|
for i, p in enumerate(data):
|
||||||
|
if p['positionid'] != last_posid + 1:
|
||||||
|
errs[i]['positionid'] = [
|
||||||
|
'Position IDs need to be consecutive.'
|
||||||
|
]
|
||||||
|
if p.get('addon_to') and p['addon_to'] != last_non_add_on:
|
||||||
|
errs[i]['addon_to'] = [
|
||||||
|
"If you set addon_to, you need to make sure that the referenced "
|
||||||
|
"position ID exists and is transmitted directly before its add-ons."
|
||||||
|
]
|
||||||
|
|
||||||
|
if not p.get('addon_to'):
|
||||||
|
last_non_add_on = p['positionid']
|
||||||
|
last_posid = p['positionid']
|
||||||
|
|
||||||
|
elif any([p.get('addon_to') for p in data]):
|
||||||
|
errs = [
|
||||||
|
{'positionid': ["If you set addon_to on any position, you need to specify position IDs manually."]}
|
||||||
|
for p in data
|
||||||
|
]
|
||||||
|
|
||||||
|
if any(errs):
|
||||||
|
raise ValidationError(errs)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
fees_data = validated_data.pop('fees') if 'fees' in validated_data else []
|
||||||
|
positions_data = validated_data.pop('positions') if 'positions' in validated_data else []
|
||||||
|
payment_provider = validated_data.pop('payment_provider')
|
||||||
|
payment_info = validated_data.pop('payment_info', '{}')
|
||||||
|
|
||||||
|
if 'invoice_address' in validated_data:
|
||||||
|
iadata = validated_data.pop('invoice_address')
|
||||||
|
name = iadata.pop('name', '')
|
||||||
|
if name and not iadata.get('name_parts'):
|
||||||
|
iadata['name_parts'] = {
|
||||||
|
'_legacy': name
|
||||||
|
}
|
||||||
|
ia = InvoiceAddress(**iadata)
|
||||||
|
else:
|
||||||
|
ia = None
|
||||||
|
|
||||||
|
with self.context['event'].lock() as now_dt:
|
||||||
|
quotadiff = Counter()
|
||||||
|
|
||||||
|
consume_carts = validated_data.pop('consume_carts', [])
|
||||||
|
delete_cps = []
|
||||||
|
quota_avail_cache = {}
|
||||||
|
if consume_carts:
|
||||||
|
for cp in CartPosition.objects.filter(event=self.context['event'], cart_id__in=consume_carts):
|
||||||
|
quotas = (cp.variation.quotas.filter(subevent=cp.subevent)
|
||||||
|
if cp.variation else cp.item.quotas.filter(subevent=cp.subevent))
|
||||||
|
for quota in quotas:
|
||||||
|
if quota not in quota_avail_cache:
|
||||||
|
quota_avail_cache[quota] = list(quota.availability())
|
||||||
|
if quota_avail_cache[quota][1] is not None:
|
||||||
|
quota_avail_cache[quota][1] += 1
|
||||||
|
if cp.expires > now_dt:
|
||||||
|
quotadiff.subtract(quotas)
|
||||||
|
delete_cps.append(cp)
|
||||||
|
|
||||||
|
errs = [{} for p in positions_data]
|
||||||
|
|
||||||
|
for i, pos_data in enumerate(positions_data):
|
||||||
|
new_quotas = (pos_data.get('variation').quotas.filter(subevent=pos_data.get('subevent'))
|
||||||
|
if pos_data.get('variation')
|
||||||
|
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
|
||||||
|
if len(new_quotas) == 0:
|
||||||
|
errs[i]['item'] = [ugettext_lazy('The product "{}" is not assigned to a quota.').format(
|
||||||
|
str(pos_data.get('item'))
|
||||||
|
)]
|
||||||
|
else:
|
||||||
|
for quota in new_quotas:
|
||||||
|
if quota not in quota_avail_cache:
|
||||||
|
quota_avail_cache[quota] = list(quota.availability())
|
||||||
|
|
||||||
|
if quota_avail_cache[quota][1] is not None:
|
||||||
|
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(
|
||||||
|
quota.name
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
quotadiff.update(new_quotas)
|
||||||
|
|
||||||
|
if any(errs):
|
||||||
|
raise ValidationError({'positions': errs})
|
||||||
|
|
||||||
|
if validated_data.get('locale', None) is None:
|
||||||
|
validated_data['locale'] = self.context['event'].settings.locale
|
||||||
|
order = Order(event=self.context['event'], **validated_data)
|
||||||
|
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
|
||||||
|
order.total = sum([p['price'] for p in positions_data]) + sum([f['value'] for f in fees_data], Decimal('0.00'))
|
||||||
|
order.meta_info = "{}"
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID:
|
||||||
|
order.status = Order.STATUS_PAID
|
||||||
|
order.save()
|
||||||
|
order.payments.create(
|
||||||
|
amount=order.total, provider='free', state=OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||||||
|
payment_date=now()
|
||||||
|
)
|
||||||
|
elif payment_provider == "free" and order.total != Decimal('0.00'):
|
||||||
|
raise ValidationError('You cannot use the "free" payment provider for non-free orders.')
|
||||||
|
elif validated_data.get('status') == Order.STATUS_PAID:
|
||||||
|
order.payments.create(
|
||||||
|
amount=order.total,
|
||||||
|
provider=payment_provider,
|
||||||
|
info=payment_info,
|
||||||
|
payment_date=now(),
|
||||||
|
state=OrderPayment.PAYMENT_STATE_CONFIRMED
|
||||||
|
)
|
||||||
|
elif payment_provider:
|
||||||
|
order.payments.create(
|
||||||
|
amount=order.total,
|
||||||
|
provider=payment_provider,
|
||||||
|
info=payment_info,
|
||||||
|
state=OrderPayment.PAYMENT_STATE_CREATED
|
||||||
|
)
|
||||||
|
|
||||||
|
if ia:
|
||||||
|
ia.order = order
|
||||||
|
ia.save()
|
||||||
|
pos_map = {}
|
||||||
|
for pos_data in positions_data:
|
||||||
|
answers_data = pos_data.pop('answers', [])
|
||||||
|
addon_to = pos_data.pop('addon_to', None)
|
||||||
|
attendee_name = pos_data.pop('attendee_name', '')
|
||||||
|
if attendee_name and not pos_data.get('attendee_name_parts'):
|
||||||
|
pos_data['attendee_name_parts'] = {
|
||||||
|
'_legacy': attendee_name
|
||||||
|
}
|
||||||
|
pos = OrderPosition(**pos_data)
|
||||||
|
pos.order = order
|
||||||
|
pos._calculate_tax()
|
||||||
|
if addon_to:
|
||||||
|
pos.addon_to = pos_map[addon_to]
|
||||||
|
pos.save()
|
||||||
|
pos_map[pos.positionid] = pos
|
||||||
|
for answ_data in answers_data:
|
||||||
|
options = answ_data.pop('options', [])
|
||||||
|
answ = pos.answers.create(**answ_data)
|
||||||
|
answ.options.add(*options)
|
||||||
|
|
||||||
|
for cp in delete_cps:
|
||||||
|
cp.delete()
|
||||||
|
for fee_data in fees_data:
|
||||||
|
f = OrderFee(**fee_data)
|
||||||
|
f.order = order
|
||||||
|
f._calculate_tax()
|
||||||
|
f.save()
|
||||||
|
|
||||||
|
return order
|
||||||
|
|
||||||
|
|
||||||
class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
||||||
@@ -168,3 +630,27 @@ class InvoiceSerializer(I18nAwareModelSerializer):
|
|||||||
'introductory_text', 'additional_text', 'payment_provider_text', 'footer_text', 'lines',
|
'introductory_text', 'additional_text', 'payment_provider_text', 'footer_text', 'lines',
|
||||||
'foreign_currency_display', 'foreign_currency_rate', 'foreign_currency_rate_date',
|
'foreign_currency_display', 'foreign_currency_rate', 'foreign_currency_rate_date',
|
||||||
'internal_reference')
|
'internal_reference')
|
||||||
|
|
||||||
|
|
||||||
|
class OrderRefundCreateSerializer(I18nAwareModelSerializer):
|
||||||
|
payment = serializers.IntegerField(required=False, allow_null=True)
|
||||||
|
provider = serializers.CharField(required=True, allow_null=False, allow_blank=False)
|
||||||
|
info = CompatibleJSONField(required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = OrderRefund
|
||||||
|
fields = ('state', 'source', 'amount', 'payment', 'execution_date', 'provider', 'info')
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
pid = validated_data.pop('payment', None)
|
||||||
|
if pid:
|
||||||
|
try:
|
||||||
|
p = self.context['order'].payments.get(local_id=pid)
|
||||||
|
except OrderPayment.DoesNotExist:
|
||||||
|
raise ValidationError('Unknown payment ID.')
|
||||||
|
else:
|
||||||
|
p = None
|
||||||
|
|
||||||
|
order = OrderRefund(order=self.context['order'], payment=p, **validated_data)
|
||||||
|
order.save()
|
||||||
|
return order
|
||||||
|
|||||||
@@ -1,7 +1,27 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.base.models import Voucher
|
from pretix.base.models import Voucher
|
||||||
|
|
||||||
|
|
||||||
|
class VoucherListSerializer(serializers.ListSerializer):
|
||||||
|
def create(self, validated_data):
|
||||||
|
codes = set()
|
||||||
|
errs = []
|
||||||
|
err = False
|
||||||
|
for voucher_data in validated_data:
|
||||||
|
if voucher_data['code'] in codes:
|
||||||
|
err = True
|
||||||
|
errs.append({'code': ['Duplicate voucher code in request.']})
|
||||||
|
else:
|
||||||
|
codes.add(voucher_data['code'])
|
||||||
|
errs.append({})
|
||||||
|
if err:
|
||||||
|
raise ValidationError(errs)
|
||||||
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
|
||||||
class VoucherSerializer(I18nAwareModelSerializer):
|
class VoucherSerializer(I18nAwareModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Voucher
|
model = Voucher
|
||||||
@@ -9,6 +29,7 @@ class VoucherSerializer(I18nAwareModelSerializer):
|
|||||||
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
|
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
|
||||||
'tag', 'comment', 'subevent')
|
'tag', 'comment', 'subevent')
|
||||||
read_only_fields = ('id', 'redeemed')
|
read_only_fields = ('id', 'redeemed')
|
||||||
|
list_serializer_class = VoucherListSerializer
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.base.models import WaitingListEntry
|
from pretix.base.models import WaitingListEntry
|
||||||
|
|
||||||
@@ -6,4 +8,28 @@ class WaitingListSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WaitingListEntry
|
model = WaitingListEntry
|
||||||
fields = ('id', 'created', 'email', 'voucher', 'item', 'variation', 'locale', 'subevent')
|
fields = ('id', 'created', 'email', 'voucher', 'item', 'variation', 'locale', 'subevent', 'priority')
|
||||||
|
read_only_fields = ('id', 'created', 'voucher')
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
data = super().validate(data)
|
||||||
|
event = self.context['event']
|
||||||
|
|
||||||
|
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||||
|
full_data.update(data)
|
||||||
|
|
||||||
|
WaitingListEntry.clean_duplicate(full_data.get('email'), full_data.get('item'), full_data.get('variation'),
|
||||||
|
full_data.get('subevent'), self.instance.pk if self.instance else None)
|
||||||
|
WaitingListEntry.clean_itemvar(event, full_data.get('item'), full_data.get('variation'))
|
||||||
|
WaitingListEntry.clean_subevent(event, full_data.get('subevent'))
|
||||||
|
|
||||||
|
if 'item' in data or 'variation' in data:
|
||||||
|
availability = (
|
||||||
|
full_data.get('variation').check_quotas(count_waitinglist=True, subevent=full_data.get('subevent'))
|
||||||
|
if full_data.get('variation')
|
||||||
|
else full_data.get('item').check_quotas(count_waitinglist=True, subevent=full_data.get('subevent'))
|
||||||
|
)
|
||||||
|
if availability[0] == 100:
|
||||||
|
raise ValidationError("This product is currently available.")
|
||||||
|
|
||||||
|
return data
|
||||||
|
|||||||
71
src/pretix/api/serializers/webhooks.py
Normal file
71
src/pretix/api/serializers/webhooks.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from pretix.api.models import WebHook
|
||||||
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
|
from pretix.api.webhooks import get_all_webhook_events
|
||||||
|
from pretix.base.models import Event
|
||||||
|
|
||||||
|
|
||||||
|
class EventRelatedField(serializers.SlugRelatedField):
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.context['organizer'].events.all()
|
||||||
|
|
||||||
|
|
||||||
|
class ActionTypesField(serializers.Field):
|
||||||
|
def to_representation(self, instance: WebHook):
|
||||||
|
return instance.action_types
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
types = get_all_webhook_events()
|
||||||
|
for d in data:
|
||||||
|
if d not in types:
|
||||||
|
raise ValidationError('Invalid action type "%s".' % d)
|
||||||
|
return {'action_types': data}
|
||||||
|
|
||||||
|
|
||||||
|
class WebHookSerializer(I18nAwareModelSerializer):
|
||||||
|
limit_events = EventRelatedField(
|
||||||
|
slug_field='slug',
|
||||||
|
queryset=Event.objects.none(),
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
action_types = ActionTypesField(source='*')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = WebHook
|
||||||
|
fields = ('id', 'enabled', 'target_url', 'all_events', 'limit_events', 'action_types')
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
data = super().validate(data)
|
||||||
|
|
||||||
|
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||||
|
full_data.update(data)
|
||||||
|
|
||||||
|
for event in full_data.get('limit_events'):
|
||||||
|
if self.context['organizer'] != event.organizer:
|
||||||
|
raise ValidationError('One or more events do not belong to this organizer.')
|
||||||
|
|
||||||
|
if full_data.get('limit_events') and full_data.get('all_events'):
|
||||||
|
raise ValidationError('You can set either limit_events or all_events.')
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
action_types = validated_data.pop('action_types')
|
||||||
|
inst = super().create(validated_data)
|
||||||
|
for l in action_types:
|
||||||
|
inst.listeners.create(action_type=l)
|
||||||
|
return inst
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
action_types = validated_data.pop('action_types', None)
|
||||||
|
instance = super().update(instance, validated_data)
|
||||||
|
if action_types is not None:
|
||||||
|
current_listeners = set(instance.listeners.values_list('action_type', flat=True))
|
||||||
|
new_listeners = set(action_types)
|
||||||
|
for l in current_listeners - new_listeners:
|
||||||
|
instance.listeners.filter(action_type=l).delete()
|
||||||
|
for l in new_listeners - current_listeners:
|
||||||
|
instance.listeners.create(action_type=l)
|
||||||
|
return instance
|
||||||
21
src/pretix/api/signals.py
Normal file
21
src/pretix/api/signals.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.dispatch import Signal, receiver
|
||||||
|
from django.utils.timezone import now
|
||||||
|
|
||||||
|
from pretix.api.models import WebHookCall
|
||||||
|
from pretix.base.signals import periodic_task
|
||||||
|
|
||||||
|
register_webhook_events = Signal(
|
||||||
|
providing_args=[]
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
This signal is sent out to get all known webhook events. Receivers should return an
|
||||||
|
instance of a subclass of pretix.api.webhooks.WebhookEvent or a list of such
|
||||||
|
instances.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(periodic_task)
|
||||||
|
def cleanup_webhook_logs(sender, **kwargs):
|
||||||
|
WebHookCall.objects.filter(datetime__lte=now() - timedelta(days=30)).delete()
|
||||||
@@ -4,16 +4,24 @@ from django.apps import apps
|
|||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
|
|
||||||
from .views import checkin, event, item, order, organizer, voucher, waitinglist
|
from pretix.api.views import cart
|
||||||
|
|
||||||
|
from .views import (
|
||||||
|
checkin, device, event, item, oauth, order, organizer, user, voucher,
|
||||||
|
waitinglist, webhooks,
|
||||||
|
)
|
||||||
|
|
||||||
router = routers.DefaultRouter()
|
router = routers.DefaultRouter()
|
||||||
router.register(r'organizers', organizer.OrganizerViewSet)
|
router.register(r'organizers', organizer.OrganizerViewSet)
|
||||||
|
|
||||||
orga_router = routers.DefaultRouter()
|
orga_router = routers.DefaultRouter()
|
||||||
orga_router.register(r'events', event.EventViewSet)
|
orga_router.register(r'events', event.EventViewSet)
|
||||||
|
orga_router.register(r'subevents', event.SubEventViewSet)
|
||||||
|
orga_router.register(r'webhooks', webhooks.WebHookViewSet)
|
||||||
|
|
||||||
event_router = routers.DefaultRouter()
|
event_router = routers.DefaultRouter()
|
||||||
event_router.register(r'subevents', event.SubEventViewSet)
|
event_router.register(r'subevents', event.SubEventViewSet)
|
||||||
|
event_router.register(r'clone', event.CloneEventViewSet)
|
||||||
event_router.register(r'items', item.ItemViewSet)
|
event_router.register(r'items', item.ItemViewSet)
|
||||||
event_router.register(r'categories', item.ItemCategoryViewSet)
|
event_router.register(r'categories', item.ItemCategoryViewSet)
|
||||||
event_router.register(r'questions', item.QuestionViewSet)
|
event_router.register(r'questions', item.QuestionViewSet)
|
||||||
@@ -25,6 +33,7 @@ event_router.register(r'invoices', order.InvoiceViewSet)
|
|||||||
event_router.register(r'taxrules', event.TaxRuleViewSet)
|
event_router.register(r'taxrules', event.TaxRuleViewSet)
|
||||||
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
|
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
|
||||||
event_router.register(r'checkinlists', checkin.CheckinListViewSet)
|
event_router.register(r'checkinlists', checkin.CheckinListViewSet)
|
||||||
|
event_router.register(r'cartpositions', cart.CartPositionViewSet)
|
||||||
|
|
||||||
checkinlist_router = routers.DefaultRouter()
|
checkinlist_router = routers.DefaultRouter()
|
||||||
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet)
|
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet)
|
||||||
@@ -36,6 +45,10 @@ item_router = routers.DefaultRouter()
|
|||||||
item_router.register(r'variations', item.ItemVariationViewSet)
|
item_router.register(r'variations', item.ItemVariationViewSet)
|
||||||
item_router.register(r'addons', item.ItemAddOnViewSet)
|
item_router.register(r'addons', item.ItemAddOnViewSet)
|
||||||
|
|
||||||
|
order_router = routers.DefaultRouter()
|
||||||
|
order_router.register(r'payments', order.PaymentViewSet)
|
||||||
|
order_router.register(r'refunds', order.RefundViewSet)
|
||||||
|
|
||||||
# Force import of all plugins to give them a chance to register URLs with the router
|
# Force import of all plugins to give them a chance to register URLs with the router
|
||||||
for app in apps.get_app_configs():
|
for app in apps.get_app_configs():
|
||||||
if hasattr(app, 'PretixPluginMeta'):
|
if hasattr(app, 'PretixPluginMeta'):
|
||||||
@@ -51,4 +64,13 @@ urlpatterns = [
|
|||||||
include(question_router.urls)),
|
include(question_router.urls)),
|
||||||
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/checkinlists/(?P<list>[^/]+)/',
|
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/checkinlists/(?P<list>[^/]+)/',
|
||||||
include(checkinlist_router.urls)),
|
include(checkinlist_router.urls)),
|
||||||
|
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/orders/(?P<order>[^/]+)/', include(order_router.urls)),
|
||||||
|
url(r"^oauth/authorize$", oauth.AuthorizationView.as_view(), name="authorize"),
|
||||||
|
url(r"^oauth/token$", oauth.TokenView.as_view(), name="token"),
|
||||||
|
url(r"^oauth/revoke_token$", oauth.RevokeTokenView.as_view(), name="revoke-token"),
|
||||||
|
url(r"^device/initialize$", device.InitializeView.as_view(), name="device.initialize"),
|
||||||
|
url(r"^device/update$", device.UpdateView.as_view(), name="device.update"),
|
||||||
|
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"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
from calendar import timegm
|
||||||
|
|
||||||
|
from django.db.models import Max
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.utils.http import http_date, parse_http_date_safe
|
||||||
from rest_framework.filters import OrderingFilter
|
from rest_framework.filters import OrderingFilter
|
||||||
|
|
||||||
|
|
||||||
@@ -21,3 +26,36 @@ class RichOrderingFilter(OrderingFilter):
|
|||||||
return queryset.order_by(*ordering)
|
return queryset.order_by(*ordering)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class ConditionalListView:
|
||||||
|
|
||||||
|
def list(self, request, **kwargs):
|
||||||
|
if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
|
||||||
|
if if_modified_since:
|
||||||
|
if_modified_since = parse_http_date_safe(if_modified_since)
|
||||||
|
if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
|
||||||
|
if if_unmodified_since:
|
||||||
|
if_unmodified_since = parse_http_date_safe(if_unmodified_since)
|
||||||
|
if not hasattr(request, 'event'):
|
||||||
|
return super().list(request, **kwargs)
|
||||||
|
|
||||||
|
lmd = request.event.logentry_set.filter(
|
||||||
|
content_type__model=self.queryset.model._meta.model_name,
|
||||||
|
content_type__app_label=self.queryset.model._meta.app_label,
|
||||||
|
).aggregate(
|
||||||
|
m=Max('datetime')
|
||||||
|
)['m']
|
||||||
|
if lmd:
|
||||||
|
lmd_ts = timegm(lmd.utctimetuple())
|
||||||
|
|
||||||
|
if if_unmodified_since and lmd and lmd_ts > if_unmodified_since:
|
||||||
|
return HttpResponse(status=412)
|
||||||
|
|
||||||
|
if if_modified_since and lmd and lmd_ts <= if_modified_since:
|
||||||
|
return HttpResponse(status=304)
|
||||||
|
|
||||||
|
resp = super().list(request, **kwargs)
|
||||||
|
if lmd:
|
||||||
|
resp['Last-Modified'] = http_date(lmd_ts)
|
||||||
|
return resp
|
||||||
|
|||||||
46
src/pretix/api/views/cart.py
Normal file
46
src/pretix/api/views/cart.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
from django.db import transaction
|
||||||
|
from rest_framework import status, viewsets
|
||||||
|
from rest_framework.filters import OrderingFilter
|
||||||
|
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from pretix.api.serializers.cart import (
|
||||||
|
CartPositionCreateSerializer, CartPositionSerializer,
|
||||||
|
)
|
||||||
|
from pretix.base.models import CartPosition
|
||||||
|
|
||||||
|
|
||||||
|
class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||||
|
serializer_class = CartPositionSerializer
|
||||||
|
queryset = CartPosition.objects.none()
|
||||||
|
filter_backends = (OrderingFilter,)
|
||||||
|
ordering = ('datetime',)
|
||||||
|
ordering_fields = ('datetime', 'cart_id')
|
||||||
|
lookup_field = 'id'
|
||||||
|
permission = 'can_view_orders'
|
||||||
|
write_permission = 'can_change_orders'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return CartPosition.objects.filter(
|
||||||
|
event=self.request.event,
|
||||||
|
cart_id__endswith="@api"
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
ctx = super().get_serializer_context()
|
||||||
|
ctx['event'] = self.request.event
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
serializer = CartPositionCreateSerializer(data=request.data, context=self.get_serializer_context())
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
with transaction.atomic():
|
||||||
|
self.perform_create(serializer)
|
||||||
|
cp = serializer.instance
|
||||||
|
serializer = CartPositionSerializer(cp, context=serializer.context)
|
||||||
|
|
||||||
|
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()
|
||||||
@@ -1,16 +1,27 @@
|
|||||||
import django_filters
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import F, Max, OuterRef, Prefetch, Q, Subquery
|
from django.db.models import Count, F, Max, OuterRef, Prefetch, Q, Subquery
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
|
from django.http import Http404
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
from django.utils.timezone import now
|
||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
|
from rest_framework.decorators import detail_route
|
||||||
|
from rest_framework.fields import DateTimeField
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from pretix.api.serializers.checkin import CheckinListSerializer
|
from pretix.api.serializers.checkin import CheckinListSerializer
|
||||||
|
from pretix.api.serializers.item import QuestionSerializer
|
||||||
from pretix.api.serializers.order import OrderPositionSerializer
|
from pretix.api.serializers.order import OrderPositionSerializer
|
||||||
from pretix.api.views import RichOrderingFilter
|
from pretix.api.views import RichOrderingFilter
|
||||||
from pretix.base.models import Checkin, CheckinList, Order, OrderPosition
|
from pretix.api.views.order import OrderPositionFilter
|
||||||
from pretix.base.models.organizer import TeamAPIToken
|
from pretix.base.models import (
|
||||||
|
Checkin, CheckinList, Event, Order, OrderPosition,
|
||||||
|
)
|
||||||
|
from pretix.base.services.checkin import (
|
||||||
|
CheckInError, RequiredQuestionsError, perform_checkin,
|
||||||
|
)
|
||||||
from pretix.helpers.database import FixedOrderBy
|
from pretix.helpers.database import FixedOrderBy
|
||||||
|
|
||||||
|
|
||||||
@@ -24,7 +35,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
|||||||
serializer_class = CheckinListSerializer
|
serializer_class = CheckinListSerializer
|
||||||
queryset = CheckinList.objects.none()
|
queryset = CheckinList.objects.none()
|
||||||
filter_backends = (DjangoFilterBackend,)
|
filter_backends = (DjangoFilterBackend,)
|
||||||
filter_class = CheckinListFilter
|
filterset_class = CheckinListFilter
|
||||||
permission = 'can_view_orders'
|
permission = 'can_view_orders'
|
||||||
write_permission = 'can_change_event_settings'
|
write_permission = 'can_change_event_settings'
|
||||||
|
|
||||||
@@ -40,7 +51,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.checkinlist.added',
|
'pretix.event.checkinlist.added',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,7 +65,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.checkinlist.changed',
|
'pretix.event.checkinlist.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -62,32 +73,90 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
|||||||
instance.log_action(
|
instance.log_action(
|
||||||
'pretix.event.checkinlist.deleted',
|
'pretix.event.checkinlist.deleted',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
)
|
)
|
||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
|
|
||||||
|
@detail_route(methods=['GET'])
|
||||||
|
def status(self, *args, **kwargs):
|
||||||
|
clist = self.get_object()
|
||||||
|
cqs = Checkin.objects.filter(
|
||||||
|
position__order__event=clist.event,
|
||||||
|
position__order__status__in=[Order.STATUS_PAID] + ([Order.STATUS_PENDING] if clist.include_pending else []),
|
||||||
|
list=clist
|
||||||
|
)
|
||||||
|
pqs = OrderPosition.objects.filter(
|
||||||
|
order__event=clist.event,
|
||||||
|
order__status__in=[Order.STATUS_PAID] + ([Order.STATUS_PENDING] if clist.include_pending else []),
|
||||||
|
subevent=clist.subevent,
|
||||||
|
)
|
||||||
|
if not clist.all_products:
|
||||||
|
pqs = pqs.filter(item__in=clist.limit_products.values_list('id', flat=True))
|
||||||
|
|
||||||
class OrderPositionFilter(FilterSet):
|
ev = clist.subevent or clist.event
|
||||||
order = django_filters.CharFilter(name='order', lookup_expr='code')
|
response = {
|
||||||
has_checkin = django_filters.rest_framework.BooleanFilter(method='has_checkin_qs')
|
'event': {
|
||||||
attendee_name = django_filters.CharFilter(method='attendee_name_qs')
|
'name': str(ev.name),
|
||||||
|
},
|
||||||
|
'checkin_count': cqs.count(),
|
||||||
|
'position_count': pqs.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
op_by_item = {
|
||||||
|
p['item']: p['cnt']
|
||||||
|
for p in pqs.order_by().values('item').annotate(cnt=Count('id'))
|
||||||
|
}
|
||||||
|
op_by_variation = {
|
||||||
|
p['variation']: p['cnt']
|
||||||
|
for p in pqs.order_by().values('variation').annotate(cnt=Count('id'))
|
||||||
|
}
|
||||||
|
c_by_item = {
|
||||||
|
p['position__item']: p['cnt']
|
||||||
|
for p in cqs.order_by().values('position__item').annotate(cnt=Count('id'))
|
||||||
|
}
|
||||||
|
c_by_variation = {
|
||||||
|
p['position__variation']: p['cnt']
|
||||||
|
for p in cqs.order_by().values('position__variation').annotate(cnt=Count('id'))
|
||||||
|
}
|
||||||
|
|
||||||
|
if not clist.all_products:
|
||||||
|
items = clist.limit_products
|
||||||
|
else:
|
||||||
|
items = clist.event.items
|
||||||
|
|
||||||
|
response['items'] = []
|
||||||
|
for item in items.order_by('category__position', 'position', 'pk').prefetch_related('variations'):
|
||||||
|
i = {
|
||||||
|
'id': item.pk,
|
||||||
|
'name': str(item),
|
||||||
|
'admission': item.admission,
|
||||||
|
'checkin_count': c_by_item.get(item.pk, 0),
|
||||||
|
'position_count': op_by_item.get(item.pk, 0),
|
||||||
|
'variations': []
|
||||||
|
}
|
||||||
|
for var in item.variations.all():
|
||||||
|
i['variations'].append({
|
||||||
|
'id': var.pk,
|
||||||
|
'value': str(var),
|
||||||
|
'checkin_count': c_by_variation.get(var.pk, 0),
|
||||||
|
'position_count': op_by_variation.get(var.pk, 0),
|
||||||
|
})
|
||||||
|
response['items'].append(i)
|
||||||
|
|
||||||
|
return Response(response)
|
||||||
|
|
||||||
|
|
||||||
|
class CheckinOrderPositionFilter(OrderPositionFilter):
|
||||||
|
|
||||||
def has_checkin_qs(self, queryset, name, value):
|
def has_checkin_qs(self, queryset, name, value):
|
||||||
return queryset.filter(last_checked_in__isnull=not value)
|
return queryset.filter(last_checked_in__isnull=not value)
|
||||||
|
|
||||||
def attendee_name_qs(self, queryset, name, value):
|
|
||||||
return queryset.filter(Q(attendee_name=value) | Q(addon_to__attendee_name=value))
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = OrderPosition
|
|
||||||
fields = ['item', 'variation', 'attendee_name', 'secret', 'order', 'has_checkin', 'addon_to', 'subevent']
|
|
||||||
|
|
||||||
|
|
||||||
class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
serializer_class = OrderPositionSerializer
|
serializer_class = OrderPositionSerializer
|
||||||
queryset = OrderPosition.objects.none()
|
queryset = OrderPosition.objects.none()
|
||||||
filter_backends = (DjangoFilterBackend, RichOrderingFilter)
|
filter_backends = (DjangoFilterBackend, RichOrderingFilter)
|
||||||
ordering = ('attendee_name', 'positionid')
|
ordering = ('attendee_name_cached', 'positionid')
|
||||||
ordering_fields = (
|
ordering_fields = (
|
||||||
'order__code', 'order__datetime', 'positionid', 'attendee_name',
|
'order__code', 'order__datetime', 'positionid', 'attendee_name',
|
||||||
'last_checked_in', 'order__email',
|
'last_checked_in', 'order__email',
|
||||||
@@ -95,11 +164,11 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
ordering_custom = {
|
ordering_custom = {
|
||||||
'attendee_name': {
|
'attendee_name': {
|
||||||
'_order': F('display_name').asc(nulls_first=True),
|
'_order': F('display_name').asc(nulls_first=True),
|
||||||
'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')
|
'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached')
|
||||||
},
|
},
|
||||||
'-attendee_name': {
|
'-attendee_name': {
|
||||||
'_order': F('display_name').desc(nulls_last=True),
|
'_order': F('display_name').desc(nulls_last=True),
|
||||||
'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')
|
'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached')
|
||||||
},
|
},
|
||||||
'last_checked_in': {
|
'last_checked_in': {
|
||||||
'_order': FixedOrderBy(F('last_checked_in'), nulls_first=True),
|
'_order': FixedOrderBy(F('last_checked_in'), nulls_first=True),
|
||||||
@@ -109,12 +178,16 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
filter_class = OrderPositionFilter
|
filterset_class = CheckinOrderPositionFilter
|
||||||
permission = 'can_view_orders'
|
permission = 'can_view_orders'
|
||||||
|
write_permission = 'can_change_orders'
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def checkinlist(self):
|
def checkinlist(self):
|
||||||
return get_object_or_404(CheckinList, event=self.request.event, pk=self.kwargs.get("list"))
|
try:
|
||||||
|
return get_object_or_404(CheckinList, event=self.request.event, pk=self.kwargs.get("list"))
|
||||||
|
except ValueError:
|
||||||
|
raise Http404()
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
cqs = Checkin.objects.filter(
|
cqs = Checkin.objects.filter(
|
||||||
@@ -130,14 +203,107 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
subevent=self.checkinlist.subevent
|
subevent=self.checkinlist.subevent
|
||||||
).annotate(
|
).annotate(
|
||||||
last_checked_in=Subquery(cqs)
|
last_checked_in=Subquery(cqs)
|
||||||
).prefetch_related(
|
)
|
||||||
Prefetch(
|
if self.request.query_params.get('pdf_data', 'false') == 'true':
|
||||||
lookup='checkins',
|
qs = qs.prefetch_related(
|
||||||
queryset=Checkin.objects.filter(list_id=self.checkinlist.pk)
|
Prefetch(
|
||||||
|
lookup='checkins',
|
||||||
|
queryset=Checkin.objects.filter(list_id=self.checkinlist.pk)
|
||||||
|
),
|
||||||
|
'checkins', 'answers', 'answers__options', 'answers__question',
|
||||||
|
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
|
||||||
|
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
'event',
|
||||||
|
Event.objects.select_related('organizer')
|
||||||
|
),
|
||||||
|
Prefetch(
|
||||||
|
'positions',
|
||||||
|
OrderPosition.objects.prefetch_related(
|
||||||
|
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
).select_related(
|
||||||
|
'item', 'variation', 'item__category', 'addon_to'
|
||||||
)
|
)
|
||||||
).select_related('item', 'variation', 'order', 'addon_to')
|
else:
|
||||||
|
qs = qs.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
lookup='checkins',
|
||||||
|
queryset=Checkin.objects.filter(list_id=self.checkinlist.pk)
|
||||||
|
),
|
||||||
|
'answers', 'answers__options', 'answers__question',
|
||||||
|
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
|
||||||
|
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address')
|
||||||
|
|
||||||
if not self.checkinlist.all_products:
|
if not self.checkinlist.all_products:
|
||||||
qs = qs.filter(item__in=self.checkinlist.limit_products.values_list('id', flat=True))
|
qs = qs.filter(item__in=self.checkinlist.limit_products.values_list('id', flat=True))
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
@detail_route(methods=['POST'])
|
||||||
|
def redeem(self, *args, **kwargs):
|
||||||
|
force = bool(self.request.data.get('force', False))
|
||||||
|
ignore_unpaid = bool(self.request.data.get('ignore_unpaid', False))
|
||||||
|
nonce = self.request.data.get('nonce')
|
||||||
|
op = self.get_object()
|
||||||
|
|
||||||
|
if 'datetime' in self.request.data:
|
||||||
|
dt = DateTimeField().to_internal_value(self.request.data.get('datetime'))
|
||||||
|
else:
|
||||||
|
dt = now()
|
||||||
|
|
||||||
|
given_answers = {}
|
||||||
|
if 'answers' in self.request.data:
|
||||||
|
aws = self.request.data.get('answers')
|
||||||
|
for q in op.item.questions.filter(ask_during_checkin=True):
|
||||||
|
if str(q.pk) in aws:
|
||||||
|
try:
|
||||||
|
given_answers[q] = q.clean_answer(aws[str(q.pk)])
|
||||||
|
except ValidationError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
perform_checkin(
|
||||||
|
op=op,
|
||||||
|
clist=self.checkinlist,
|
||||||
|
given_answers=given_answers,
|
||||||
|
force=force,
|
||||||
|
ignore_unpaid=ignore_unpaid,
|
||||||
|
nonce=nonce,
|
||||||
|
datetime=dt,
|
||||||
|
questions_supported=self.request.data.get('questions_supported', True),
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
)
|
||||||
|
except RequiredQuestionsError as e:
|
||||||
|
return Response({
|
||||||
|
'status': 'incomplete',
|
||||||
|
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
|
||||||
|
'position': OrderPositionSerializer(op, context=self.get_serializer_context()).data,
|
||||||
|
'questions': [
|
||||||
|
QuestionSerializer(q).data for q in e.questions
|
||||||
|
]
|
||||||
|
}, status=400)
|
||||||
|
except CheckInError as e:
|
||||||
|
return Response({
|
||||||
|
'status': 'error',
|
||||||
|
'reason': e.code,
|
||||||
|
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
|
||||||
|
'position': OrderPositionSerializer(op, context=self.get_serializer_context()).data
|
||||||
|
}, status=400)
|
||||||
|
else:
|
||||||
|
return Response({
|
||||||
|
'status': 'ok',
|
||||||
|
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
|
||||||
|
'position': OrderPositionSerializer(op, context=self.get_serializer_context()).data
|
||||||
|
}, status=201)
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
if self.kwargs['pk'].isnumeric():
|
||||||
|
obj = get_object_or_404(queryset, Q(pk=self.kwargs['pk']) | Q(secret=self.kwargs['pk']))
|
||||||
|
else:
|
||||||
|
obj = get_object_or_404(queryset, secret=self.kwargs['pk'])
|
||||||
|
return obj
|
||||||
|
|||||||
113
src/pretix/api/views/device.py
Normal file
113
src/pretix/api/views/device.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from rest_framework import serializers
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
from pretix.api.auth.device import DeviceTokenAuthentication
|
||||||
|
from pretix.base.models import Device
|
||||||
|
from pretix.base.models.devices import generate_api_token
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class InitializationRequestSerializer(serializers.Serializer):
|
||||||
|
token = serializers.CharField(max_length=190)
|
||||||
|
hardware_brand = serializers.CharField(max_length=190)
|
||||||
|
hardware_model = serializers.CharField(max_length=190)
|
||||||
|
software_brand = serializers.CharField(max_length=190)
|
||||||
|
software_version = serializers.CharField(max_length=190)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateRequestSerializer(serializers.Serializer):
|
||||||
|
hardware_brand = serializers.CharField(max_length=190)
|
||||||
|
hardware_model = serializers.CharField(max_length=190)
|
||||||
|
software_brand = serializers.CharField(max_length=190)
|
||||||
|
software_version = serializers.CharField(max_length=190)
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceSerializer(serializers.ModelSerializer):
|
||||||
|
organizer = serializers.SlugRelatedField(slug_field='slug', read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Device
|
||||||
|
fields = [
|
||||||
|
'organizer', 'device_id', 'unique_serial', 'api_token',
|
||||||
|
'name'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class InitializeView(APIView):
|
||||||
|
authentication_classes = tuple()
|
||||||
|
permission_classes = tuple()
|
||||||
|
|
||||||
|
def post(self, request, format=None):
|
||||||
|
serializer = InitializationRequestSerializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
device = Device.objects.get(initialization_token=serializer.validated_data.get('token'))
|
||||||
|
except Device.DoesNotExist:
|
||||||
|
raise ValidationError({'token': ['Unknown initialization token.']})
|
||||||
|
|
||||||
|
if device.initialized:
|
||||||
|
raise ValidationError({'token': ['This initialization token has already been used.']})
|
||||||
|
|
||||||
|
device.initialized = now()
|
||||||
|
device.hardware_brand = serializer.validated_data.get('hardware_brand')
|
||||||
|
device.hardware_model = serializer.validated_data.get('hardware_model')
|
||||||
|
device.software_brand = serializer.validated_data.get('software_brand')
|
||||||
|
device.software_version = serializer.validated_data.get('software_version')
|
||||||
|
device.api_token = generate_api_token()
|
||||||
|
device.save()
|
||||||
|
|
||||||
|
device.log_action('pretix.device.initialized', data=serializer.validated_data, auth=device)
|
||||||
|
|
||||||
|
serializer = DeviceSerializer(device)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateView(APIView):
|
||||||
|
authentication_classes = (DeviceTokenAuthentication,)
|
||||||
|
|
||||||
|
def post(self, request, format=None):
|
||||||
|
serializer = UpdateRequestSerializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
device = request.auth
|
||||||
|
device.hardware_brand = serializer.validated_data.get('hardware_brand')
|
||||||
|
device.hardware_model = serializer.validated_data.get('hardware_model')
|
||||||
|
device.software_brand = serializer.validated_data.get('software_brand')
|
||||||
|
device.software_version = serializer.validated_data.get('software_version')
|
||||||
|
device.save()
|
||||||
|
device.log_action('pretix.device.updated', data=serializer.validated_data, auth=device)
|
||||||
|
|
||||||
|
serializer = DeviceSerializer(device)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
|
class RollKeyView(APIView):
|
||||||
|
authentication_classes = (DeviceTokenAuthentication,)
|
||||||
|
|
||||||
|
def post(self, request, format=None):
|
||||||
|
device = request.auth
|
||||||
|
device.api_token = generate_api_token()
|
||||||
|
device.save()
|
||||||
|
device.log_action('pretix.device.keyroll', auth=device)
|
||||||
|
|
||||||
|
serializer = DeviceSerializer(device)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
|
class RevokeKeyView(APIView):
|
||||||
|
authentication_classes = (DeviceTokenAuthentication,)
|
||||||
|
|
||||||
|
def post(self, request, format=None):
|
||||||
|
device = request.auth
|
||||||
|
device.api_token = None
|
||||||
|
device.save()
|
||||||
|
device.log_action('pretix.device.revoked', auth=device)
|
||||||
|
|
||||||
|
serializer = DeviceSerializer(device)
|
||||||
|
return Response(serializer.data)
|
||||||
@@ -1,44 +1,247 @@
|
|||||||
|
import django_filters
|
||||||
|
from django.db import transaction
|
||||||
|
from django.db.models import ProtectedError, Q
|
||||||
|
from django.utils.timezone import now
|
||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from rest_framework import filters, viewsets
|
from rest_framework import filters, viewsets
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
|
||||||
|
from pretix.api.auth.permission import EventCRUDPermission
|
||||||
from pretix.api.serializers.event import (
|
from pretix.api.serializers.event import (
|
||||||
EventSerializer, SubEventSerializer, TaxRuleSerializer,
|
CloneEventSerializer, EventSerializer, SubEventSerializer,
|
||||||
|
TaxRuleSerializer,
|
||||||
|
)
|
||||||
|
from pretix.api.views import ConditionalListView
|
||||||
|
from pretix.base.models import (
|
||||||
|
Device, Event, ItemCategory, TaxRule, TeamAPIToken,
|
||||||
)
|
)
|
||||||
from pretix.base.models import Event, ItemCategory, TaxRule
|
|
||||||
from pretix.base.models.event import SubEvent
|
from pretix.base.models.event import SubEvent
|
||||||
from pretix.base.models.organizer import TeamAPIToken
|
from pretix.helpers.dicts import merge_dicts
|
||||||
|
|
||||||
|
|
||||||
class EventViewSet(viewsets.ReadOnlyModelViewSet):
|
class EventFilter(FilterSet):
|
||||||
|
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
|
||||||
|
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
|
||||||
|
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Event
|
||||||
|
fields = ['is_public', 'live', 'has_subevents']
|
||||||
|
|
||||||
|
def ends_after_qs(self, queryset, name, value):
|
||||||
|
expr = (
|
||||||
|
Q(has_subevents=False) &
|
||||||
|
Q(
|
||||||
|
Q(Q(date_to__isnull=True) & Q(date_from__gte=value))
|
||||||
|
| Q(Q(date_to__isnull=False) & Q(date_to__gte=value))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return queryset.filter(expr)
|
||||||
|
|
||||||
|
def is_past_qs(self, queryset, name, value):
|
||||||
|
expr = (
|
||||||
|
Q(has_subevents=False) &
|
||||||
|
Q(
|
||||||
|
Q(Q(date_to__isnull=True) & Q(date_from__lt=now()))
|
||||||
|
| Q(Q(date_to__isnull=False) & Q(date_to__lt=now()))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if value:
|
||||||
|
return queryset.filter(expr)
|
||||||
|
else:
|
||||||
|
return queryset.exclude(expr)
|
||||||
|
|
||||||
|
def is_future_qs(self, queryset, name, value):
|
||||||
|
expr = (
|
||||||
|
Q(has_subevents=False) &
|
||||||
|
Q(
|
||||||
|
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
|
||||||
|
| Q(Q(date_to__isnull=False) & Q(date_to__gte=now()))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if value:
|
||||||
|
return queryset.filter(expr)
|
||||||
|
else:
|
||||||
|
return queryset.exclude(expr)
|
||||||
|
|
||||||
|
|
||||||
|
class EventViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = EventSerializer
|
serializer_class = EventSerializer
|
||||||
queryset = Event.objects.none()
|
queryset = Event.objects.none()
|
||||||
lookup_field = 'slug'
|
lookup_field = 'slug'
|
||||||
lookup_url_kwarg = 'event'
|
lookup_url_kwarg = 'event'
|
||||||
|
permission_classes = (EventCRUDPermission,)
|
||||||
|
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
|
||||||
|
filterset_class = EventFilter
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.organizer.events.prefetch_related('meta_values', 'meta_values__property')
|
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||||
|
qs = self.request.auth.get_events_with_any_permission()
|
||||||
|
elif self.request.user.is_authenticated:
|
||||||
|
qs = self.request.user.get_events_with_any_permission(self.request).filter(
|
||||||
|
organizer=self.request.organizer
|
||||||
|
)
|
||||||
|
|
||||||
|
return qs.prefetch_related(
|
||||||
|
'meta_values', 'meta_values__property'
|
||||||
|
)
|
||||||
|
|
||||||
|
def perform_update(self, serializer):
|
||||||
|
current_live_value = serializer.instance.live
|
||||||
|
updated_live_value = serializer.validated_data.get('live', None)
|
||||||
|
current_plugins_value = serializer.instance.get_plugins()
|
||||||
|
updated_plugins_value = serializer.validated_data.get('plugins', None)
|
||||||
|
|
||||||
|
super().perform_update(serializer)
|
||||||
|
|
||||||
|
if updated_live_value is not None and updated_live_value != current_live_value:
|
||||||
|
log_action = 'pretix.event.live.activated' if updated_live_value else 'pretix.event.live.deactivated'
|
||||||
|
serializer.instance.log_action(
|
||||||
|
log_action,
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data=self.request.data
|
||||||
|
)
|
||||||
|
|
||||||
|
if updated_plugins_value is not None and set(updated_plugins_value) != set(current_plugins_value):
|
||||||
|
enabled = {m: 'enabled' for m in updated_plugins_value if m not in current_plugins_value}
|
||||||
|
disabled = {m: 'disabled' for m in current_plugins_value if m not in updated_plugins_value}
|
||||||
|
changed = merge_dicts(enabled, disabled)
|
||||||
|
|
||||||
|
for module, action in changed.items():
|
||||||
|
serializer.instance.log_action(
|
||||||
|
'pretix.event.plugins.' + action,
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data={'plugin': module}
|
||||||
|
)
|
||||||
|
|
||||||
|
other_keys = {k: v for k, v in serializer.validated_data.items() if k not in ['plugins', 'live']}
|
||||||
|
if other_keys:
|
||||||
|
serializer.instance.log_action(
|
||||||
|
'pretix.event.changed',
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data=self.request.data
|
||||||
|
)
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
serializer.save(organizer=self.request.organizer)
|
||||||
|
serializer.instance.log_action(
|
||||||
|
'pretix.event.added',
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data=self.request.data
|
||||||
|
)
|
||||||
|
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
if not instance.allow_delete():
|
||||||
|
raise PermissionDenied('The event can not be deleted as it already contains orders. Please set \'live\''
|
||||||
|
' to false to hide the event and take the shop offline instead.')
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
instance.organizer.log_action(
|
||||||
|
'pretix.event.deleted', user=self.request.user,
|
||||||
|
data={
|
||||||
|
'event_id': instance.pk,
|
||||||
|
'name': str(instance.name),
|
||||||
|
'logentries': list(instance.logentry_set.values_list('pk', flat=True))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
instance.delete_sub_objects()
|
||||||
|
super().perform_destroy(instance)
|
||||||
|
except ProtectedError:
|
||||||
|
raise PermissionDenied('The event could not be deleted as some constraints (e.g. data created by plug-ins) '
|
||||||
|
'do not allow it.')
|
||||||
|
|
||||||
|
|
||||||
|
class CloneEventViewSet(viewsets.ModelViewSet):
|
||||||
|
serializer_class = CloneEventSerializer
|
||||||
|
queryset = Event.objects.none()
|
||||||
|
lookup_field = 'slug'
|
||||||
|
lookup_url_kwarg = 'event'
|
||||||
|
http_method_names = ['post']
|
||||||
|
write_permission = 'can_create_events'
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
ctx = super().get_serializer_context()
|
||||||
|
ctx['event'] = self.kwargs['event']
|
||||||
|
ctx['organizer'] = self.request.organizer
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
serializer.save(organizer=self.request.organizer)
|
||||||
|
|
||||||
|
serializer.instance.log_action(
|
||||||
|
'pretix.event.added',
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data=self.request.data
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SubEventFilter(FilterSet):
|
class SubEventFilter(FilterSet):
|
||||||
|
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
|
||||||
|
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
|
||||||
|
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SubEvent
|
model = SubEvent
|
||||||
fields = ['active']
|
fields = ['active', 'event__live']
|
||||||
|
|
||||||
|
def ends_after_qs(self, queryset, name, value):
|
||||||
|
expr = Q(
|
||||||
|
Q(Q(date_to__isnull=True) & Q(date_from__gte=value))
|
||||||
|
| Q(Q(date_to__isnull=False) & Q(date_to__gte=value))
|
||||||
|
)
|
||||||
|
return queryset.filter(expr)
|
||||||
|
|
||||||
|
def is_past_qs(self, queryset, name, value):
|
||||||
|
expr = Q(
|
||||||
|
Q(Q(date_to__isnull=True) & Q(date_from__lt=now()))
|
||||||
|
| Q(Q(date_to__isnull=False) & Q(date_to__lt=now()))
|
||||||
|
)
|
||||||
|
if value:
|
||||||
|
return queryset.filter(expr)
|
||||||
|
else:
|
||||||
|
return queryset.exclude(expr)
|
||||||
|
|
||||||
|
def is_future_qs(self, queryset, name, value):
|
||||||
|
expr = Q(
|
||||||
|
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
|
||||||
|
| Q(Q(date_to__isnull=False) & Q(date_to__gte=now()))
|
||||||
|
)
|
||||||
|
if value:
|
||||||
|
return queryset.filter(expr)
|
||||||
|
else:
|
||||||
|
return queryset.exclude(expr)
|
||||||
|
|
||||||
|
|
||||||
class SubEventViewSet(viewsets.ReadOnlyModelViewSet):
|
class SubEventViewSet(ConditionalListView, viewsets.ReadOnlyModelViewSet):
|
||||||
serializer_class = SubEventSerializer
|
serializer_class = SubEventSerializer
|
||||||
queryset = ItemCategory.objects.none()
|
queryset = ItemCategory.objects.none()
|
||||||
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
|
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
|
||||||
filter_class = SubEventFilter
|
filterset_class = SubEventFilter
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.subevents.prefetch_related(
|
if getattr(self.request, 'event', None):
|
||||||
|
qs = self.request.event.subevents
|
||||||
|
elif isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||||
|
qs = SubEvent.objects.filter(
|
||||||
|
event__organizer=self.request.organizer,
|
||||||
|
event__in=self.request.auth.get_events_with_any_permission()
|
||||||
|
)
|
||||||
|
elif self.request.user.is_authenticated:
|
||||||
|
qs = SubEvent.objects.filter(
|
||||||
|
event__organizer=self.request.organizer,
|
||||||
|
event__in=self.request.user.get_events_with_any_permission()
|
||||||
|
)
|
||||||
|
return qs.prefetch_related(
|
||||||
'subeventitem_set', 'subeventitemvariation_set'
|
'subeventitem_set', 'subeventitemvariation_set'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TaxRuleViewSet(viewsets.ModelViewSet):
|
class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||||
serializer_class = TaxRuleSerializer
|
serializer_class = TaxRuleSerializer
|
||||||
queryset = TaxRule.objects.none()
|
queryset = TaxRule.objects.none()
|
||||||
write_permission = 'can_change_event_settings'
|
write_permission = 'can_change_event_settings'
|
||||||
@@ -51,7 +254,7 @@ class TaxRuleViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.taxrule.changed',
|
'pretix.event.taxrule.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -60,7 +263,7 @@ class TaxRuleViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.taxrule.added',
|
'pretix.event.taxrule.added',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -71,6 +274,6 @@ class TaxRuleViewSet(viewsets.ModelViewSet):
|
|||||||
instance.log_action(
|
instance.log_action(
|
||||||
'pretix.event.taxrule.deleted',
|
'pretix.event.taxrule.deleted',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
)
|
)
|
||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ from pretix.api.serializers.item import (
|
|||||||
ItemVariationSerializer, QuestionOptionSerializer, QuestionSerializer,
|
ItemVariationSerializer, QuestionOptionSerializer, QuestionSerializer,
|
||||||
QuotaSerializer,
|
QuotaSerializer,
|
||||||
)
|
)
|
||||||
|
from pretix.api.views import ConditionalListView
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Item, ItemAddOn, ItemCategory, ItemVariation, Question, QuestionOption,
|
Item, ItemAddOn, ItemCategory, ItemVariation, Question, QuestionOption,
|
||||||
Quota,
|
Quota,
|
||||||
)
|
)
|
||||||
from pretix.base.models.organizer import TeamAPIToken
|
|
||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
|
|
||||||
|
|
||||||
@@ -35,14 +35,14 @@ class ItemFilter(FilterSet):
|
|||||||
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
|
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
|
||||||
|
|
||||||
|
|
||||||
class ItemViewSet(viewsets.ModelViewSet):
|
class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||||
serializer_class = ItemSerializer
|
serializer_class = ItemSerializer
|
||||||
queryset = Item.objects.none()
|
queryset = Item.objects.none()
|
||||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||||
ordering_fields = ('id', 'position')
|
ordering_fields = ('id', 'position')
|
||||||
ordering = ('position', 'id')
|
ordering = ('position', 'id')
|
||||||
filter_class = ItemFilter
|
filterset_class = ItemFilter
|
||||||
permission = 'can_change_items'
|
permission = None
|
||||||
write_permission = 'can_change_items'
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@@ -53,7 +53,7 @@ class ItemViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.item.added',
|
'pretix.event.item.added',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ class ItemViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.item.changed',
|
'pretix.event.item.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -81,8 +81,9 @@ class ItemViewSet(viewsets.ModelViewSet):
|
|||||||
instance.log_action(
|
instance.log_action(
|
||||||
'pretix.event.item.deleted',
|
'pretix.event.item.deleted',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
)
|
)
|
||||||
|
self.get_object().cartposition_set.all().delete()
|
||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
|
|
||||||
|
|
||||||
@@ -92,7 +93,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
|||||||
filter_backends = (DjangoFilterBackend, OrderingFilter,)
|
filter_backends = (DjangoFilterBackend, OrderingFilter,)
|
||||||
ordering_fields = ('id', 'position')
|
ordering_fields = ('id', 'position')
|
||||||
ordering = ('id',)
|
ordering = ('id',)
|
||||||
permission = 'can_change_items'
|
permission = None
|
||||||
write_permission = 'can_change_items'
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@@ -113,7 +114,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
|||||||
item.log_action(
|
item.log_action(
|
||||||
'pretix.event.item.variation.added',
|
'pretix.event.item.variation.added',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk},
|
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk},
|
||||||
{'value': serializer.instance.value})
|
{'value': serializer.instance.value})
|
||||||
)
|
)
|
||||||
@@ -123,7 +124,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.item.log_action(
|
serializer.instance.item.log_action(
|
||||||
'pretix.event.item.variation.changed',
|
'pretix.event.item.variation.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk},
|
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk},
|
||||||
{'value': serializer.instance.value})
|
{'value': serializer.instance.value})
|
||||||
)
|
)
|
||||||
@@ -140,7 +141,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
|||||||
instance.item.log_action(
|
instance.item.log_action(
|
||||||
'pretix.event.item.variation.deleted',
|
'pretix.event.item.variation.deleted',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data={
|
data={
|
||||||
'value': instance.value,
|
'value': instance.value,
|
||||||
'id': self.kwargs['pk']
|
'id': self.kwargs['pk']
|
||||||
@@ -154,7 +155,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
|
|||||||
filter_backends = (DjangoFilterBackend, OrderingFilter,)
|
filter_backends = (DjangoFilterBackend, OrderingFilter,)
|
||||||
ordering_fields = ('id', 'position')
|
ordering_fields = ('id', 'position')
|
||||||
ordering = ('id',)
|
ordering = ('id',)
|
||||||
permission = 'can_change_items'
|
permission = None
|
||||||
write_permission = 'can_change_items'
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@@ -174,7 +175,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
|
|||||||
item.log_action(
|
item.log_action(
|
||||||
'pretix.event.item.addons.added',
|
'pretix.event.item.addons.added',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -183,7 +184,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.base_item.log_action(
|
serializer.instance.base_item.log_action(
|
||||||
'pretix.event.item.addons.changed',
|
'pretix.event.item.addons.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -192,7 +193,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
|
|||||||
instance.base_item.log_action(
|
instance.base_item.log_action(
|
||||||
'pretix.event.item.addons.removed',
|
'pretix.event.item.addons.removed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data={'category': instance.addon_category.pk}
|
data={'category': instance.addon_category.pk}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -203,14 +204,14 @@ class ItemCategoryFilter(FilterSet):
|
|||||||
fields = ['is_addon']
|
fields = ['is_addon']
|
||||||
|
|
||||||
|
|
||||||
class ItemCategoryViewSet(viewsets.ModelViewSet):
|
class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||||
serializer_class = ItemCategorySerializer
|
serializer_class = ItemCategorySerializer
|
||||||
queryset = ItemCategory.objects.none()
|
queryset = ItemCategory.objects.none()
|
||||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||||
filter_class = ItemCategoryFilter
|
filterset_class = ItemCategoryFilter
|
||||||
ordering_fields = ('id', 'position')
|
ordering_fields = ('id', 'position')
|
||||||
ordering = ('position', 'id')
|
ordering = ('position', 'id')
|
||||||
permission = 'can_change_items'
|
permission = None
|
||||||
write_permission = 'can_change_items'
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@@ -221,7 +222,7 @@ class ItemCategoryViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.category.added',
|
'pretix.event.category.added',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -235,7 +236,7 @@ class ItemCategoryViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.category.changed',
|
'pretix.event.category.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -246,18 +247,26 @@ class ItemCategoryViewSet(viewsets.ModelViewSet):
|
|||||||
instance.log_action(
|
instance.log_action(
|
||||||
'pretix.event.category.deleted',
|
'pretix.event.category.deleted',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
)
|
)
|
||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
|
|
||||||
|
|
||||||
class QuestionViewSet(viewsets.ModelViewSet):
|
class QuestionFilter(FilterSet):
|
||||||
|
class Meta:
|
||||||
|
model = Question
|
||||||
|
fields = ['ask_during_checkin', 'required', 'identifier']
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||||
serializer_class = QuestionSerializer
|
serializer_class = QuestionSerializer
|
||||||
queryset = Question.objects.none()
|
queryset = Question.objects.none()
|
||||||
filter_backends = (OrderingFilter,)
|
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||||
|
filterset_class = QuestionFilter
|
||||||
ordering_fields = ('id', 'position')
|
ordering_fields = ('id', 'position')
|
||||||
ordering = ('position', 'id')
|
ordering = ('position', 'id')
|
||||||
permission = 'can_change_items'
|
permission = None
|
||||||
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.questions.prefetch_related('options').all()
|
return self.request.event.questions.prefetch_related('options').all()
|
||||||
@@ -267,7 +276,7 @@ class QuestionViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.question.added',
|
'pretix.event.question.added',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -281,7 +290,7 @@ class QuestionViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.question.changed',
|
'pretix.event.question.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -289,7 +298,7 @@ class QuestionViewSet(viewsets.ModelViewSet):
|
|||||||
instance.log_action(
|
instance.log_action(
|
||||||
'pretix.event.question.deleted',
|
'pretix.event.question.deleted',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
)
|
)
|
||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
|
|
||||||
@@ -300,7 +309,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
|||||||
filter_backends = (DjangoFilterBackend, OrderingFilter,)
|
filter_backends = (DjangoFilterBackend, OrderingFilter,)
|
||||||
ordering_fields = ('id', 'position')
|
ordering_fields = ('id', 'position')
|
||||||
ordering = ('position',)
|
ordering = ('position',)
|
||||||
permission = 'can_change_items'
|
permission = None
|
||||||
write_permission = 'can_change_items'
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@@ -319,7 +328,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
|||||||
q.log_action(
|
q.log_action(
|
||||||
'pretix.event.question.option.added',
|
'pretix.event.question.option.added',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -328,7 +337,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.question.log_action(
|
serializer.instance.question.log_action(
|
||||||
'pretix.event.question.option.changed',
|
'pretix.event.question.option.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -336,7 +345,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
|||||||
instance.question.log_action(
|
instance.question.log_action(
|
||||||
'pretix.event.question.option.deleted',
|
'pretix.event.question.option.deleted',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data={'id': instance.pk}
|
data={'id': instance.pk}
|
||||||
)
|
)
|
||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
@@ -348,14 +357,14 @@ class QuotaFilter(FilterSet):
|
|||||||
fields = ['subevent']
|
fields = ['subevent']
|
||||||
|
|
||||||
|
|
||||||
class QuotaViewSet(viewsets.ModelViewSet):
|
class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||||
serializer_class = QuotaSerializer
|
serializer_class = QuotaSerializer
|
||||||
queryset = Quota.objects.none()
|
queryset = Quota.objects.none()
|
||||||
filter_backends = (DjangoFilterBackend, OrderingFilter,)
|
filter_backends = (DjangoFilterBackend, OrderingFilter,)
|
||||||
filter_class = QuotaFilter
|
filterset_class = QuotaFilter
|
||||||
ordering_fields = ('id', 'size')
|
ordering_fields = ('id', 'size')
|
||||||
ordering = ('id',)
|
ordering = ('id',)
|
||||||
permission = 'can_change_items'
|
permission = None
|
||||||
write_permission = 'can_change_items'
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@@ -366,14 +375,14 @@ class QuotaViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.quota.added',
|
'pretix.event.quota.added',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
if serializer.instance.subevent:
|
if serializer.instance.subevent:
|
||||||
serializer.instance.subevent.log_action(
|
serializer.instance.subevent.log_action(
|
||||||
'pretix.subevent.quota.added',
|
'pretix.subevent.quota.added',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -389,7 +398,7 @@ class QuotaViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.quota.changed',
|
'pretix.event.quota.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
if current_subevent == request_subevent:
|
if current_subevent == request_subevent:
|
||||||
@@ -397,7 +406,7 @@ class QuotaViewSet(viewsets.ModelViewSet):
|
|||||||
current_subevent.log_action(
|
current_subevent.log_action(
|
||||||
'pretix.subevent.quota.changed',
|
'pretix.subevent.quota.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -405,14 +414,14 @@ class QuotaViewSet(viewsets.ModelViewSet):
|
|||||||
request_subevent.log_action(
|
request_subevent.log_action(
|
||||||
'pretix.subevent.quota.added',
|
'pretix.subevent.quota.added',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
if current_subevent is not None:
|
if current_subevent is not None:
|
||||||
current_subevent.log_action(
|
current_subevent.log_action(
|
||||||
'pretix.subevent.quota.deleted',
|
'pretix.subevent.quota.deleted',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
)
|
)
|
||||||
serializer.instance.rebuild_cache()
|
serializer.instance.rebuild_cache()
|
||||||
|
|
||||||
@@ -420,13 +429,13 @@ class QuotaViewSet(viewsets.ModelViewSet):
|
|||||||
instance.log_action(
|
instance.log_action(
|
||||||
'pretix.event.quota.deleted',
|
'pretix.event.quota.deleted',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
)
|
)
|
||||||
if instance.subevent:
|
if instance.subevent:
|
||||||
instance.subevent.log_action(
|
instance.subevent.log_action(
|
||||||
'pretix.subevent.quota.deleted',
|
'pretix.subevent.quota.deleted',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
)
|
)
|
||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
|
|
||||||
|
|||||||
92
src/pretix/api/views/oauth.py
Normal file
92
src/pretix/api/views/oauth.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
from oauth2_provider.exceptions import OAuthToolkitError
|
||||||
|
from oauth2_provider.forms import AllowForm
|
||||||
|
from oauth2_provider.views import (
|
||||||
|
AuthorizationView as BaseAuthorizationView,
|
||||||
|
RevokeTokenView as BaseRevokeTokenView, TokenView as BaseTokenView,
|
||||||
|
)
|
||||||
|
|
||||||
|
from pretix.api.models import OAuthApplication
|
||||||
|
from pretix.base.models import Organizer
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthAllowForm(AllowForm):
|
||||||
|
organizers = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=Organizer.objects.none(),
|
||||||
|
widget=forms.CheckboxSelectMultiple
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
user = kwargs.pop('user')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['organizers'].queryset = Organizer.objects.filter(
|
||||||
|
pk__in=user.teams.values_list('organizer', flat=True))
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorizationView(BaseAuthorizationView):
|
||||||
|
template_name = "pretixcontrol/auth/oauth_authorization.html"
|
||||||
|
form_class = OAuthAllowForm
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
kwargs['user'] = self.request.user
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
ctx['settings'] = settings
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def create_authorization_response(self, request, scopes, credentials, allow, organizers):
|
||||||
|
credentials["organizers"] = organizers
|
||||||
|
return super().create_authorization_response(request, scopes, credentials, allow)
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
client_id = form.cleaned_data["client_id"]
|
||||||
|
application = OAuthApplication.objects.get(client_id=client_id)
|
||||||
|
credentials = {
|
||||||
|
"client_id": form.cleaned_data.get("client_id"),
|
||||||
|
"redirect_uri": form.cleaned_data.get("redirect_uri"),
|
||||||
|
"response_type": form.cleaned_data.get("response_type", None),
|
||||||
|
"state": form.cleaned_data.get("state", None),
|
||||||
|
}
|
||||||
|
scopes = form.cleaned_data.get("scope")
|
||||||
|
allow = form.cleaned_data.get("allow")
|
||||||
|
|
||||||
|
try:
|
||||||
|
uri, headers, body, status = self.create_authorization_response(
|
||||||
|
request=self.request, scopes=scopes, credentials=credentials, allow=allow,
|
||||||
|
organizers=form.cleaned_data.get("organizers")
|
||||||
|
)
|
||||||
|
except OAuthToolkitError as error:
|
||||||
|
return self.error_response(error, application)
|
||||||
|
|
||||||
|
self.success_url = uri
|
||||||
|
logger.debug("Success url for the request: {0}".format(self.success_url))
|
||||||
|
|
||||||
|
msgs = [
|
||||||
|
_('The application "{application_name}" has been authorized to access your account.').format(
|
||||||
|
application_name=application.name
|
||||||
|
)
|
||||||
|
]
|
||||||
|
self.request.user.send_security_notice(msgs)
|
||||||
|
self.request.user.log_action('pretix.user.oauth.authorized', user=self.request.user, data={
|
||||||
|
'application_id': application.pk,
|
||||||
|
'application_name': application.name,
|
||||||
|
})
|
||||||
|
|
||||||
|
return self.redirect(self.success_url, application)
|
||||||
|
|
||||||
|
|
||||||
|
class TokenView(BaseTokenView):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RevokeTokenView(BaseRevokeTokenView):
|
||||||
|
pass
|
||||||
@@ -1,60 +1,105 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
import pytz
|
import pytz
|
||||||
from django.db.models import Q
|
from django.db import transaction
|
||||||
from django.db.models.functions import Concat
|
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
|
||||||
from django.utils.timezone import make_aware
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.utils.timezone import make_aware, now
|
||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from rest_framework import serializers, status, viewsets
|
from rest_framework import mixins, serializers, status, viewsets
|
||||||
from rest_framework.decorators import detail_route
|
from rest_framework.decorators import detail_route
|
||||||
from rest_framework.exceptions import APIException, NotFound, PermissionDenied
|
from rest_framework.exceptions import (
|
||||||
|
APIException, NotFound, PermissionDenied, ValidationError,
|
||||||
|
)
|
||||||
from rest_framework.filters import OrderingFilter
|
from rest_framework.filters import OrderingFilter
|
||||||
|
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from pretix.api.models import OAuthAccessToken
|
||||||
from pretix.api.serializers.order import (
|
from pretix.api.serializers.order import (
|
||||||
InvoiceSerializer, OrderPositionSerializer, OrderSerializer,
|
InvoiceSerializer, OrderCreateSerializer, OrderPaymentSerializer,
|
||||||
|
OrderPositionSerializer, OrderRefundCreateSerializer,
|
||||||
|
OrderRefundSerializer, OrderSerializer,
|
||||||
|
)
|
||||||
|
from pretix.base.models import (
|
||||||
|
CachedCombinedTicket, CachedTicket, Device, Event, Invoice, Order,
|
||||||
|
OrderPayment, OrderPosition, OrderRefund, Quota, TeamAPIToken,
|
||||||
|
)
|
||||||
|
from pretix.base.payment import PaymentException
|
||||||
|
from pretix.base.services.invoices import (
|
||||||
|
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
|
||||||
|
regenerate_invoice,
|
||||||
)
|
)
|
||||||
from pretix.base.models import Invoice, Order, OrderPosition, Quota
|
|
||||||
from pretix.base.models.organizer import TeamAPIToken
|
|
||||||
from pretix.base.services.invoices import invoice_pdf
|
|
||||||
from pretix.base.services.mail import SendMailException
|
from pretix.base.services.mail import SendMailException
|
||||||
from pretix.base.services.orders import (
|
from pretix.base.services.orders import (
|
||||||
OrderError, cancel_order, extend_order, mark_order_expired,
|
OrderChangeManager, OrderError, approve_order, cancel_order, deny_order,
|
||||||
mark_order_paid,
|
extend_order, mark_order_expired, mark_order_refunded,
|
||||||
)
|
)
|
||||||
from pretix.base.services.tickets import (
|
from pretix.base.services.tickets import generate
|
||||||
get_cachedticket_for_order, get_cachedticket_for_position,
|
from pretix.base.signals import order_placed, register_ticket_outputs
|
||||||
)
|
|
||||||
from pretix.base.signals import register_ticket_outputs
|
|
||||||
|
|
||||||
|
|
||||||
class OrderFilter(FilterSet):
|
class OrderFilter(FilterSet):
|
||||||
|
email = django_filters.CharFilter(field_name='email', lookup_expr='iexact')
|
||||||
|
code = django_filters.CharFilter(field_name='code', lookup_expr='iexact')
|
||||||
|
status = django_filters.CharFilter(field_name='status', lookup_expr='iexact')
|
||||||
|
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Order
|
model = Order
|
||||||
fields = ['code', 'status', 'email', 'locale']
|
fields = ['code', 'status', 'email', 'locale', 'testmode', 'require_approval']
|
||||||
|
|
||||||
|
|
||||||
class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
class OrderViewSet(DestroyModelMixin, CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||||
serializer_class = OrderSerializer
|
serializer_class = OrderSerializer
|
||||||
queryset = Order.objects.none()
|
queryset = Order.objects.none()
|
||||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||||
ordering = ('datetime',)
|
ordering = ('datetime',)
|
||||||
ordering_fields = ('datetime', 'code', 'status')
|
ordering_fields = ('datetime', 'code', 'status', 'last_modified')
|
||||||
filter_class = OrderFilter
|
filterset_class = OrderFilter
|
||||||
lookup_field = 'code'
|
lookup_field = 'code'
|
||||||
permission = 'can_view_orders'
|
permission = 'can_view_orders'
|
||||||
write_permission = 'can_change_orders'
|
write_permission = 'can_change_orders'
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
ctx = super().get_serializer_context()
|
||||||
|
ctx['event'] = self.request.event
|
||||||
|
return ctx
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.orders.prefetch_related(
|
qs = self.request.event.orders.prefetch_related(
|
||||||
'positions', 'positions__checkins', 'positions__item', 'positions__answers', 'positions__answers__options',
|
'fees', 'payments', 'refunds', 'refunds__payment'
|
||||||
'positions__answers__question', 'fees'
|
|
||||||
).select_related(
|
).select_related(
|
||||||
'invoice_address'
|
'invoice_address'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.request.query_params.get('pdf_data', 'false') == 'true':
|
||||||
|
qs = qs.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
'positions',
|
||||||
|
OrderPosition.objects.all().prefetch_related(
|
||||||
|
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||||
|
'item__category', 'addon_to',
|
||||||
|
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
qs = qs.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
'positions',
|
||||||
|
OrderPosition.objects.all().prefetch_related(
|
||||||
|
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return qs
|
||||||
|
|
||||||
def _get_output_provider(self, identifier):
|
def _get_output_provider(self, identifier):
|
||||||
responses = register_ticket_outputs.send(self.request.event)
|
responses = register_ticket_outputs.send(self.request.event)
|
||||||
for receiver, response in responses:
|
for receiver, response in responses:
|
||||||
@@ -63,6 +108,20 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
return prov
|
return prov
|
||||||
raise NotFound('Unknown output provider.')
|
raise NotFound('Unknown output provider.')
|
||||||
|
|
||||||
|
def list(self, request, **kwargs):
|
||||||
|
date = serializers.DateTimeField().to_representation(now())
|
||||||
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
|
||||||
|
page = self.paginate_queryset(queryset)
|
||||||
|
if page is not None:
|
||||||
|
serializer = self.get_serializer(page, many=True)
|
||||||
|
resp = self.get_paginated_response(serializer.data)
|
||||||
|
resp['X-Page-Generated'] = date
|
||||||
|
return resp
|
||||||
|
|
||||||
|
serializer = self.get_serializer(queryset, many=True)
|
||||||
|
return Response(serializer.data, headers={'X-Page-Generated': date})
|
||||||
|
|
||||||
@detail_route(url_name='download', url_path='download/(?P<output>[^/]+)')
|
@detail_route(url_name='download', url_path='download/(?P<output>[^/]+)')
|
||||||
def download(self, request, output, **kwargs):
|
def download(self, request, output, **kwargs):
|
||||||
provider = self._get_output_provider(output)
|
provider = self._get_output_provider(output)
|
||||||
@@ -71,9 +130,11 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
if order.status != Order.STATUS_PAID:
|
if order.status != Order.STATUS_PAID:
|
||||||
raise PermissionDenied("Downloads are not available for unpaid orders.")
|
raise PermissionDenied("Downloads are not available for unpaid orders.")
|
||||||
|
|
||||||
ct = get_cachedticket_for_order(order, provider.identifier)
|
ct = CachedCombinedTicket.objects.filter(
|
||||||
|
order=order, provider=provider.identifier, file__isnull=False
|
||||||
if not ct.file:
|
).last()
|
||||||
|
if not ct or not ct.file:
|
||||||
|
generate.apply_async(args=('order', order.pk, provider.identifier))
|
||||||
raise RetryException()
|
raise RetryException()
|
||||||
else:
|
else:
|
||||||
resp = FileResponse(ct.file.file, content_type=ct.type)
|
resp = FileResponse(ct.file.file, content_type=ct.type)
|
||||||
@@ -88,14 +149,33 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
order = self.get_object()
|
order = self.get_object()
|
||||||
|
|
||||||
if order.status in (Order.STATUS_PENDING, Order.STATUS_EXPIRED):
|
if order.status in (Order.STATUS_PENDING, Order.STATUS_EXPIRED):
|
||||||
|
|
||||||
|
ps = order.pending_sum
|
||||||
try:
|
try:
|
||||||
mark_order_paid(
|
p = order.payments.get(
|
||||||
order, manual=True,
|
state__in=(OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED),
|
||||||
user=request.user if request.user.is_authenticated else None,
|
provider='manual',
|
||||||
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
|
amount=ps
|
||||||
)
|
)
|
||||||
|
except OrderPayment.DoesNotExist:
|
||||||
|
order.payments.filter(state__in=(OrderPayment.PAYMENT_STATE_PENDING,
|
||||||
|
OrderPayment.PAYMENT_STATE_CREATED)) \
|
||||||
|
.update(state=OrderPayment.PAYMENT_STATE_CANCELED)
|
||||||
|
p = order.payments.create(
|
||||||
|
state=OrderPayment.PAYMENT_STATE_CREATED,
|
||||||
|
provider='manual',
|
||||||
|
amount=ps,
|
||||||
|
fee=None
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
p.confirm(auth=self.request.auth,
|
||||||
|
user=self.request.user if request.user.is_authenticated else None,
|
||||||
|
count_waitinglist=False)
|
||||||
except Quota.QuotaExceededException as e:
|
except Quota.QuotaExceededException as e:
|
||||||
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
except PaymentException as e:
|
||||||
|
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
except SendMailException:
|
except SendMailException:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -108,6 +188,12 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
@detail_route(methods=['POST'])
|
@detail_route(methods=['POST'])
|
||||||
def mark_canceled(self, request, **kwargs):
|
def mark_canceled(self, request, **kwargs):
|
||||||
send_mail = request.data.get('send_email', True)
|
send_mail = request.data.get('send_email', True)
|
||||||
|
cancellation_fee = request.data.get('cancellation_fee', None)
|
||||||
|
if cancellation_fee:
|
||||||
|
try:
|
||||||
|
cancellation_fee = float(Decimal(cancellation_fee))
|
||||||
|
except:
|
||||||
|
cancellation_fee = None
|
||||||
|
|
||||||
order = self.get_object()
|
order = self.get_object()
|
||||||
if not order.cancel_allowed():
|
if not order.cancel_allowed():
|
||||||
@@ -116,12 +202,57 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
status=status.HTTP_400_BAD_REQUEST
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
)
|
)
|
||||||
|
|
||||||
cancel_order(
|
try:
|
||||||
order,
|
cancel_order(
|
||||||
user=request.user if request.user.is_authenticated else None,
|
order,
|
||||||
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
|
user=request.user if request.user.is_authenticated else None,
|
||||||
send_mail=send_mail
|
api_token=request.auth if isinstance(request.auth, TeamAPIToken) else None,
|
||||||
)
|
device=request.auth if isinstance(request.auth, Device) else None,
|
||||||
|
oauth_application=request.auth.application if isinstance(request.auth, OAuthAccessToken) else None,
|
||||||
|
send_mail=send_mail,
|
||||||
|
cancellation_fee=cancellation_fee
|
||||||
|
)
|
||||||
|
except OrderError as e:
|
||||||
|
return Response(
|
||||||
|
{'detail': str(e)},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
return self.retrieve(request, [], **kwargs)
|
||||||
|
|
||||||
|
@detail_route(methods=['POST'])
|
||||||
|
def approve(self, request, **kwargs):
|
||||||
|
send_mail = request.data.get('send_email', True)
|
||||||
|
|
||||||
|
order = self.get_object()
|
||||||
|
try:
|
||||||
|
approve_order(
|
||||||
|
order,
|
||||||
|
user=request.user if request.user.is_authenticated else None,
|
||||||
|
auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None,
|
||||||
|
send_mail=send_mail,
|
||||||
|
)
|
||||||
|
except Quota.QuotaExceededException as e:
|
||||||
|
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
except OrderError as e:
|
||||||
|
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
return self.retrieve(request, [], **kwargs)
|
||||||
|
|
||||||
|
@detail_route(methods=['POST'])
|
||||||
|
def deny(self, request, **kwargs):
|
||||||
|
send_mail = request.data.get('send_email', True)
|
||||||
|
comment = request.data.get('comment', '')
|
||||||
|
|
||||||
|
order = self.get_object()
|
||||||
|
try:
|
||||||
|
deny_order(
|
||||||
|
order,
|
||||||
|
user=request.user if request.user.is_authenticated else None,
|
||||||
|
auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None,
|
||||||
|
send_mail=send_mail,
|
||||||
|
comment=comment,
|
||||||
|
)
|
||||||
|
except OrderError as e:
|
||||||
|
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
return self.retrieve(request, [], **kwargs)
|
return self.retrieve(request, [], **kwargs)
|
||||||
|
|
||||||
@detail_route(methods=['POST'])
|
@detail_route(methods=['POST'])
|
||||||
@@ -135,12 +266,11 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
order.status = Order.STATUS_PENDING
|
order.status = Order.STATUS_PENDING
|
||||||
order.payment_manual = True
|
order.save(update_fields=['status'])
|
||||||
order.save()
|
|
||||||
order.log_action(
|
order.log_action(
|
||||||
'pretix.event.order.unpaid',
|
'pretix.event.order.unpaid',
|
||||||
user=request.user if request.user.is_authenticated else None,
|
user=request.user if request.user.is_authenticated else None,
|
||||||
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
|
auth=request.auth,
|
||||||
)
|
)
|
||||||
return self.retrieve(request, [], **kwargs)
|
return self.retrieve(request, [], **kwargs)
|
||||||
|
|
||||||
@@ -157,11 +287,26 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
mark_order_expired(
|
mark_order_expired(
|
||||||
order,
|
order,
|
||||||
user=request.user if request.user.is_authenticated else None,
|
user=request.user if request.user.is_authenticated else None,
|
||||||
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
|
auth=request.auth,
|
||||||
)
|
)
|
||||||
return self.retrieve(request, [], **kwargs)
|
return self.retrieve(request, [], **kwargs)
|
||||||
|
|
||||||
# TODO: Find a way to implement mark_refunded
|
@detail_route(methods=['POST'])
|
||||||
|
def mark_refunded(self, request, **kwargs):
|
||||||
|
order = self.get_object()
|
||||||
|
|
||||||
|
if order.status != Order.STATUS_PAID:
|
||||||
|
return Response(
|
||||||
|
{'detail': 'The order is not paid.'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
mark_order_refunded(
|
||||||
|
order,
|
||||||
|
user=request.user if request.user.is_authenticated else None,
|
||||||
|
auth=(request.auth if isinstance(request.auth, (TeamAPIToken, OAuthAccessToken, Device)) else None),
|
||||||
|
)
|
||||||
|
return self.retrieve(request, [], **kwargs)
|
||||||
|
|
||||||
@detail_route(methods=['POST'])
|
@detail_route(methods=['POST'])
|
||||||
def extend(self, request, **kwargs):
|
def extend(self, request, **kwargs):
|
||||||
@@ -196,7 +341,7 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
new_date=new_date,
|
new_date=new_date,
|
||||||
force=force,
|
force=force,
|
||||||
user=request.user if request.user.is_authenticated else None,
|
user=request.user if request.user.is_authenticated else None,
|
||||||
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
|
auth=request.auth,
|
||||||
)
|
)
|
||||||
return self.retrieve(request, [], **kwargs)
|
return self.retrieve(request, [], **kwargs)
|
||||||
except OrderError as e:
|
except OrderError as e:
|
||||||
@@ -205,39 +350,127 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
status=status.HTTP_400_BAD_REQUEST
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
serializer = OrderCreateSerializer(data=request.data, context=self.get_serializer_context())
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
with transaction.atomic():
|
||||||
|
self.perform_create(serializer)
|
||||||
|
order = serializer.instance
|
||||||
|
serializer = OrderSerializer(order, context=serializer.context)
|
||||||
|
|
||||||
|
order.log_action(
|
||||||
|
'pretix.event.order.placed',
|
||||||
|
user=request.user if request.user.is_authenticated else None,
|
||||||
|
auth=request.auth,
|
||||||
|
)
|
||||||
|
order_placed.send(self.request.event, order=order)
|
||||||
|
|
||||||
|
gen_invoice = invoice_qualified(order) and (
|
||||||
|
(order.event.settings.get('invoice_generate') == 'True') or
|
||||||
|
(order.event.settings.get('invoice_generate') == 'paid' and order.status == Order.STATUS_PAID)
|
||||||
|
) and not order.invoices.last()
|
||||||
|
if gen_invoice:
|
||||||
|
generate_invoice(order, trigger_pdf=True)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
if not instance.testmode:
|
||||||
|
raise PermissionDenied('Only test mode orders can be deleted.')
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
self.get_object().gracefully_delete(user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
|
||||||
|
|
||||||
|
|
||||||
class OrderPositionFilter(FilterSet):
|
class OrderPositionFilter(FilterSet):
|
||||||
order = django_filters.CharFilter(name='order', lookup_expr='code')
|
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
|
||||||
has_checkin = django_filters.rest_framework.BooleanFilter(method='has_checkin_qs')
|
has_checkin = django_filters.rest_framework.BooleanFilter(method='has_checkin_qs')
|
||||||
attendee_name = django_filters.CharFilter(method='attendee_name_qs')
|
attendee_name = django_filters.CharFilter(method='attendee_name_qs')
|
||||||
|
search = django_filters.CharFilter(method='search_qs')
|
||||||
|
|
||||||
|
def search_qs(self, queryset, name, value):
|
||||||
|
return queryset.filter(
|
||||||
|
Q(secret__istartswith=value)
|
||||||
|
| Q(attendee_name_cached__icontains=value)
|
||||||
|
| Q(addon_to__attendee_name_cached__icontains=value)
|
||||||
|
| Q(order__code__istartswith=value)
|
||||||
|
| Q(order__invoice_address__name_cached__icontains=value)
|
||||||
|
| Q(order__email__icontains=value)
|
||||||
|
)
|
||||||
|
|
||||||
def has_checkin_qs(self, queryset, name, value):
|
def has_checkin_qs(self, queryset, name, value):
|
||||||
return queryset.filter(checkins__isnull=not value)
|
return queryset.filter(checkins__isnull=not value)
|
||||||
|
|
||||||
def attendee_name_qs(self, queryset, name, value):
|
def attendee_name_qs(self, queryset, name, value):
|
||||||
return queryset.filter(Q(attendee_name=value) | Q(addon_to__attendee_name=value))
|
return queryset.filter(Q(attendee_name_cached__iexact=value) | Q(addon_to__attendee_name_cached__iexact=value))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = OrderPosition
|
model = OrderPosition
|
||||||
fields = ['item', 'variation', 'attendee_name', 'secret', 'order', 'order__status', 'has_checkin',
|
fields = {
|
||||||
'addon_to', 'subevent']
|
'item': ['exact', 'in'],
|
||||||
|
'variation': ['exact', 'in'],
|
||||||
|
'secret': ['exact'],
|
||||||
|
'order__status': ['exact', 'in'],
|
||||||
|
'addon_to': ['exact', 'in'],
|
||||||
|
'subevent': ['exact', 'in'],
|
||||||
|
'pseudonymization_id': ['exact'],
|
||||||
|
'voucher__code': ['exact'],
|
||||||
|
'voucher': ['exact'],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class OrderPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||||
serializer_class = OrderPositionSerializer
|
serializer_class = OrderPositionSerializer
|
||||||
queryset = OrderPosition.objects.none()
|
queryset = OrderPosition.objects.none()
|
||||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||||
ordering = ('order__datetime', 'positionid')
|
ordering = ('order__datetime', 'positionid')
|
||||||
ordering_fields = ('order__code', 'order__datetime', 'positionid', 'attendee_name', 'order__status',)
|
ordering_fields = ('order__code', 'order__datetime', 'positionid', 'attendee_name', 'order__status',)
|
||||||
filter_class = OrderPositionFilter
|
filterset_class = OrderPositionFilter
|
||||||
permission = 'can_view_orders'
|
permission = 'can_view_orders'
|
||||||
|
write_permission = 'can_change_orders'
|
||||||
|
ordering_custom = {
|
||||||
|
'attendee_name': {
|
||||||
|
'_order': F('display_name').asc(nulls_first=True),
|
||||||
|
'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached')
|
||||||
|
},
|
||||||
|
'-attendee_name': {
|
||||||
|
'_order': F('display_name').asc(nulls_last=True),
|
||||||
|
'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return OrderPosition.objects.filter(order__event=self.request.event).prefetch_related(
|
qs = OrderPosition.objects.filter(order__event=self.request.event)
|
||||||
'checkins', 'answers', 'answers__options', 'answers__question'
|
if self.request.query_params.get('pdf_data', 'false') == 'true':
|
||||||
).select_related(
|
qs = qs.prefetch_related(
|
||||||
'item', 'order', 'order__event', 'order__event__organizer'
|
'checkins', 'answers', 'answers__options', 'answers__question',
|
||||||
)
|
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
|
||||||
|
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
'event',
|
||||||
|
Event.objects.select_related('organizer')
|
||||||
|
),
|
||||||
|
Prefetch(
|
||||||
|
'positions',
|
||||||
|
OrderPosition.objects.prefetch_related(
|
||||||
|
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
).select_related(
|
||||||
|
'item', 'variation', 'item__category', 'addon_to'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
qs = qs.prefetch_related(
|
||||||
|
'checkins', 'answers', 'answers__options', 'answers__question'
|
||||||
|
).select_related(
|
||||||
|
'item', 'order', 'order__event', 'order__event__organizer'
|
||||||
|
)
|
||||||
|
return qs
|
||||||
|
|
||||||
def _get_output_provider(self, identifier):
|
def _get_output_provider(self, identifier):
|
||||||
responses = register_ticket_outputs.send(self.request.event)
|
responses = register_ticket_outputs.send(self.request.event)
|
||||||
@@ -254,14 +487,14 @@ class OrderPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
|
|
||||||
if pos.order.status != Order.STATUS_PAID:
|
if pos.order.status != Order.STATUS_PAID:
|
||||||
raise PermissionDenied("Downloads are not available for unpaid orders.")
|
raise PermissionDenied("Downloads are not available for unpaid orders.")
|
||||||
if pos.addon_to_id and not request.event.settings.ticket_download_addons:
|
if not pos.generate_ticket:
|
||||||
raise PermissionDenied("Downloads are not enabled for add-on products.")
|
raise PermissionDenied("Downloads are not enabled for this product.")
|
||||||
if not pos.item.admission and not request.event.settings.ticket_download_nonadm:
|
|
||||||
raise PermissionDenied("Downloads are not enabled for non-admission products.")
|
|
||||||
|
|
||||||
ct = get_cachedticket_for_position(pos, provider.identifier)
|
ct = CachedTicket.objects.filter(
|
||||||
|
order_position=pos, provider=provider.identifier, file__isnull=False
|
||||||
if not ct.file:
|
).last()
|
||||||
|
if not ct or not ct.file:
|
||||||
|
generate.apply_async(args=('orderposition', pos.pk, provider.identifier))
|
||||||
raise RetryException()
|
raise RetryException()
|
||||||
else:
|
else:
|
||||||
resp = FileResponse(ct.file.file, content_type=ct.type)
|
resp = FileResponse(ct.file.file, content_type=ct.type)
|
||||||
@@ -271,11 +504,242 @@ class OrderPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
)
|
)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
try:
|
||||||
|
ocm = OrderChangeManager(
|
||||||
|
instance.order,
|
||||||
|
user=self.request.user if self.request.user.is_authenticated else None,
|
||||||
|
auth=self.request.auth,
|
||||||
|
notify=False
|
||||||
|
)
|
||||||
|
ocm.cancel(instance)
|
||||||
|
ocm.commit()
|
||||||
|
except OrderError as e:
|
||||||
|
raise ValidationError(str(e))
|
||||||
|
except Quota.QuotaExceededException as e:
|
||||||
|
raise ValidationError(str(e))
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
|
serializer_class = OrderPaymentSerializer
|
||||||
|
queryset = OrderPayment.objects.none()
|
||||||
|
permission = 'can_view_orders'
|
||||||
|
write_permission = 'can_change_orders'
|
||||||
|
lookup_field = 'local_id'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
order = get_object_or_404(Order, code=self.kwargs['order'], event=self.request.event)
|
||||||
|
return order.payments.all()
|
||||||
|
|
||||||
|
@detail_route(methods=['POST'])
|
||||||
|
def confirm(self, request, **kwargs):
|
||||||
|
payment = self.get_object()
|
||||||
|
force = request.data.get('force', False)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
try:
|
||||||
|
payment.confirm(user=self.request.user if self.request.user.is_authenticated else None,
|
||||||
|
auth=self.request.auth,
|
||||||
|
count_waitinglist=False,
|
||||||
|
force=force)
|
||||||
|
except Quota.QuotaExceededException as e:
|
||||||
|
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
except PaymentException as e:
|
||||||
|
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
except SendMailException:
|
||||||
|
pass
|
||||||
|
return self.retrieve(request, [], **kwargs)
|
||||||
|
|
||||||
|
@detail_route(methods=['POST'])
|
||||||
|
def refund(self, request, **kwargs):
|
||||||
|
payment = self.get_object()
|
||||||
|
amount = serializers.DecimalField(max_digits=10, decimal_places=2).to_internal_value(
|
||||||
|
request.data.get('amount', str(payment.amount))
|
||||||
|
)
|
||||||
|
if 'mark_refunded' in request.data:
|
||||||
|
mark_refunded = request.data.get('mark_refunded', False)
|
||||||
|
else:
|
||||||
|
mark_refunded = request.data.get('mark_canceled', False)
|
||||||
|
|
||||||
|
if payment.state != OrderPayment.PAYMENT_STATE_CONFIRMED:
|
||||||
|
return Response({'detail': 'Invalid state of payment.'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
full_refund_possible = payment.payment_provider.payment_refund_supported(payment)
|
||||||
|
partial_refund_possible = payment.payment_provider.payment_partial_refund_supported(payment)
|
||||||
|
available_amount = payment.amount - payment.refunded_amount
|
||||||
|
|
||||||
|
if amount <= 0:
|
||||||
|
return Response({'amount': ['Invalid refund amount.']}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
if amount > available_amount:
|
||||||
|
return Response(
|
||||||
|
{'amount': ['Invalid refund amount, only {} are available to refund.'.format(available_amount)]},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
if amount != payment.amount and not partial_refund_possible:
|
||||||
|
return Response({'amount': ['Partial refund not available for this payment method.']},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
if amount == payment.amount and not full_refund_possible:
|
||||||
|
return Response({'amount': ['Full refund not available for this payment method.']},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
r = payment.order.refunds.create(
|
||||||
|
payment=payment,
|
||||||
|
source=OrderRefund.REFUND_SOURCE_ADMIN,
|
||||||
|
state=OrderRefund.REFUND_STATE_CREATED,
|
||||||
|
amount=amount,
|
||||||
|
provider=payment.provider
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
r.payment_provider.execute_refund(r)
|
||||||
|
except PaymentException as e:
|
||||||
|
r.state = OrderRefund.REFUND_STATE_FAILED
|
||||||
|
r.save()
|
||||||
|
return Response({'detail': 'External error: {}'.format(str(e))},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
else:
|
||||||
|
payment.order.log_action('pretix.event.order.refund.created', {
|
||||||
|
'local_id': r.local_id,
|
||||||
|
'provider': r.provider,
|
||||||
|
}, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
|
||||||
|
if payment.order.pending_sum > 0:
|
||||||
|
if mark_refunded:
|
||||||
|
mark_order_refunded(payment.order,
|
||||||
|
user=self.request.user if self.request.user.is_authenticated else None,
|
||||||
|
auth=self.request.auth)
|
||||||
|
else:
|
||||||
|
payment.order.status = Order.STATUS_PENDING
|
||||||
|
payment.order.set_expires(
|
||||||
|
now(),
|
||||||
|
payment.order.event.subevents.filter(
|
||||||
|
id__in=payment.order.positions.values_list('subevent_id', flat=True))
|
||||||
|
)
|
||||||
|
payment.order.save(update_fields=['status', 'expires'])
|
||||||
|
return Response(OrderRefundSerializer(r).data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
@detail_route(methods=['POST'])
|
||||||
|
def cancel(self, request, **kwargs):
|
||||||
|
payment = self.get_object()
|
||||||
|
|
||||||
|
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)
|
||||||
|
return self.retrieve(request, [], **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||||
|
serializer_class = OrderRefundSerializer
|
||||||
|
queryset = OrderRefund.objects.none()
|
||||||
|
permission = 'can_view_orders'
|
||||||
|
write_permission = 'can_change_orders'
|
||||||
|
lookup_field = 'local_id'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
order = get_object_or_404(Order, code=self.kwargs['order'], event=self.request.event)
|
||||||
|
return order.refunds.all()
|
||||||
|
|
||||||
|
@detail_route(methods=['POST'])
|
||||||
|
def cancel(self, request, **kwargs):
|
||||||
|
refund = self.get_object()
|
||||||
|
|
||||||
|
if refund.state not in (OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_TRANSIT,
|
||||||
|
OrderRefund.REFUND_STATE_EXTERNAL):
|
||||||
|
return Response({'detail': 'Invalid state of refund'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
refund.state = OrderRefund.REFUND_STATE_CANCELED
|
||||||
|
refund.save()
|
||||||
|
refund.order.log_action('pretix.event.order.refund.canceled', {
|
||||||
|
'local_id': refund.local_id,
|
||||||
|
'provider': refund.provider,
|
||||||
|
}, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
|
||||||
|
return self.retrieve(request, [], **kwargs)
|
||||||
|
|
||||||
|
@detail_route(methods=['POST'])
|
||||||
|
def process(self, request, **kwargs):
|
||||||
|
refund = self.get_object()
|
||||||
|
|
||||||
|
if refund.state != OrderRefund.REFUND_STATE_EXTERNAL:
|
||||||
|
return Response({'detail': 'Invalid state of refund'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
refund.done(user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
|
||||||
|
if 'mark_refunded' in request.data:
|
||||||
|
mark_refunded = request.data.get('mark_refunded', False)
|
||||||
|
else:
|
||||||
|
mark_refunded = request.data.get('mark_canceled', False)
|
||||||
|
if mark_refunded:
|
||||||
|
mark_order_refunded(refund.order, user=self.request.user if self.request.user.is_authenticated else None,
|
||||||
|
auth=self.request.auth)
|
||||||
|
elif not (refund.order.status == Order.STATUS_PAID and refund.order.pending_sum <= 0):
|
||||||
|
refund.order.status = Order.STATUS_PENDING
|
||||||
|
refund.order.set_expires(
|
||||||
|
now(),
|
||||||
|
refund.order.event.subevents.filter(
|
||||||
|
id__in=refund.order.positions.values_list('subevent_id', flat=True))
|
||||||
|
)
|
||||||
|
refund.order.save(update_fields=['status', 'expires'])
|
||||||
|
return self.retrieve(request, [], **kwargs)
|
||||||
|
|
||||||
|
@detail_route(methods=['POST'])
|
||||||
|
def done(self, request, **kwargs):
|
||||||
|
refund = self.get_object()
|
||||||
|
|
||||||
|
if refund.state not in (OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_TRANSIT):
|
||||||
|
return Response({'detail': 'Invalid state of refund'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
refund.done(user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
|
||||||
|
return self.retrieve(request, [], **kwargs)
|
||||||
|
|
||||||
|
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 create(self, request, *args, **kwargs):
|
||||||
|
if 'mark_refunded' in request.data:
|
||||||
|
mark_refunded = request.data.pop('mark_refunded', False)
|
||||||
|
else:
|
||||||
|
mark_refunded = request.data.pop('mark_canceled', False)
|
||||||
|
serializer = OrderRefundCreateSerializer(data=request.data, context=self.get_serializer_context())
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
with transaction.atomic():
|
||||||
|
self.perform_create(serializer)
|
||||||
|
r = serializer.instance
|
||||||
|
serializer = OrderRefundSerializer(r, context=serializer.context)
|
||||||
|
|
||||||
|
r.order.log_action(
|
||||||
|
'pretix.event.order.refund.created', {
|
||||||
|
'local_id': r.local_id,
|
||||||
|
'provider': r.provider,
|
||||||
|
},
|
||||||
|
user=request.user if request.user.is_authenticated else None,
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
class InvoiceFilter(FilterSet):
|
class InvoiceFilter(FilterSet):
|
||||||
refers = django_filters.CharFilter(method='refers_qs')
|
refers = django_filters.CharFilter(method='refers_qs')
|
||||||
number = django_filters.CharFilter(method='nr_qs')
|
number = django_filters.CharFilter(method='nr_qs')
|
||||||
order = django_filters.CharFilter(name='order', lookup_expr='code__iexact')
|
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
|
||||||
|
|
||||||
def refers_qs(self, queryset, name, value):
|
def refers_qs(self, queryset, name, value):
|
||||||
return queryset.annotate(
|
return queryset.annotate(
|
||||||
@@ -302,10 +766,11 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||||
ordering = ('nr',)
|
ordering = ('nr',)
|
||||||
ordering_fields = ('nr', 'date')
|
ordering_fields = ('nr', 'date')
|
||||||
filter_class = InvoiceFilter
|
filterset_class = InvoiceFilter
|
||||||
permission = 'can_view_orders'
|
permission = 'can_view_orders'
|
||||||
lookup_url_kwarg = 'number'
|
lookup_url_kwarg = 'number'
|
||||||
lookup_field = 'nr'
|
lookup_field = 'nr'
|
||||||
|
write_permission = 'can_change_orders'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.invoices.prefetch_related('lines').select_related('order', 'refers').annotate(
|
return self.request.event.invoices.prefetch_related('lines').select_related('order', 'refers').annotate(
|
||||||
@@ -320,9 +785,54 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
invoice_pdf(invoice.pk)
|
invoice_pdf(invoice.pk)
|
||||||
invoice.refresh_from_db()
|
invoice.refresh_from_db()
|
||||||
|
|
||||||
|
if invoice.shredded:
|
||||||
|
raise PermissionDenied('The invoice file is no longer stored on the server.')
|
||||||
|
|
||||||
if not invoice.file:
|
if not invoice.file:
|
||||||
raise RetryException()
|
raise RetryException()
|
||||||
|
|
||||||
resp = FileResponse(invoice.file.file, content_type='application/pdf')
|
resp = FileResponse(invoice.file.file, content_type='application/pdf')
|
||||||
resp['Content-Disposition'] = 'attachment; filename="{}.pdf"'.format(invoice.number)
|
resp['Content-Disposition'] = 'attachment; filename="{}.pdf"'.format(invoice.number)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
@detail_route(methods=['POST'])
|
||||||
|
def regenerate(self, request, **kwarts):
|
||||||
|
inv = self.get_object()
|
||||||
|
if inv.canceled:
|
||||||
|
raise ValidationError('The invoice has already been canceled.')
|
||||||
|
elif inv.shredded:
|
||||||
|
raise PermissionDenied('The invoice file is no longer stored on the server.')
|
||||||
|
else:
|
||||||
|
inv = regenerate_invoice(inv)
|
||||||
|
inv.order.log_action(
|
||||||
|
'pretix.event.order.invoice.regenerated',
|
||||||
|
data={
|
||||||
|
'invoice': inv.pk
|
||||||
|
},
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
)
|
||||||
|
return Response(status=204)
|
||||||
|
|
||||||
|
@detail_route(methods=['POST'])
|
||||||
|
def reissue(self, request, **kwarts):
|
||||||
|
inv = self.get_object()
|
||||||
|
if inv.canceled:
|
||||||
|
raise ValidationError('The invoice has already been canceled.')
|
||||||
|
elif inv.shredded:
|
||||||
|
raise PermissionDenied('The invoice file is no longer stored on the server.')
|
||||||
|
else:
|
||||||
|
c = generate_cancellation(inv)
|
||||||
|
if inv.order.status != Order.STATUS_CANCELED:
|
||||||
|
inv = generate_invoice(inv.order)
|
||||||
|
else:
|
||||||
|
inv = c
|
||||||
|
inv.order.log_action(
|
||||||
|
'pretix.event.order.invoice.reissued',
|
||||||
|
data={
|
||||||
|
'invoice': inv.pk
|
||||||
|
},
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
)
|
||||||
|
return Response(status=204)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
|
|
||||||
|
from pretix.api.models import OAuthAccessToken
|
||||||
from pretix.api.serializers.organizer import OrganizerSerializer
|
from pretix.api.serializers.organizer import OrganizerSerializer
|
||||||
from pretix.base.models import Organizer
|
from pretix.base.models import Organizer
|
||||||
|
|
||||||
@@ -11,10 +12,18 @@ class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
lookup_url_kwarg = 'organizer'
|
lookup_url_kwarg = 'organizer'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
if self.request.user.is_authenticated():
|
if self.request.user.is_authenticated:
|
||||||
if self.request.user.has_active_staff_session(self.request.session.session_key):
|
if self.request.user.has_active_staff_session(self.request.session.session_key):
|
||||||
return Organizer.objects.all()
|
return Organizer.objects.all()
|
||||||
|
elif isinstance(self.request.auth, OAuthAccessToken):
|
||||||
|
return Organizer.objects.filter(
|
||||||
|
pk__in=self.request.user.teams.values_list('organizer', flat=True)
|
||||||
|
).filter(
|
||||||
|
pk__in=self.request.auth.organizers.values_list('pk', flat=True)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return Organizer.objects.filter(pk__in=self.request.user.teams.values_list('organizer', flat=True))
|
return Organizer.objects.filter(pk__in=self.request.user.teams.values_list('organizer', flat=True))
|
||||||
|
elif hasattr(self.request.auth, 'organizer_id'):
|
||||||
|
return Organizer.objects.filter(pk=self.request.auth.organizer_id)
|
||||||
else:
|
else:
|
||||||
return Organizer.objects.filter(pk=self.request.auth.team.organizer_id)
|
return Organizer.objects.filter(pk=self.request.auth.team.organizer_id)
|
||||||
|
|||||||
16
src/pretix/api/views/user.py
Normal file
16
src/pretix/api/views/user.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from oauth2_provider.contrib.rest_framework import OAuth2Authentication
|
||||||
|
from rest_framework.authentication import SessionAuthentication
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
|
||||||
|
class MeView(APIView):
|
||||||
|
authentication_classes = (SessionAuthentication, OAuth2Authentication)
|
||||||
|
|
||||||
|
def get(self, request, format=None):
|
||||||
|
return Response({
|
||||||
|
'email': request.user.email,
|
||||||
|
'fullname': request.user.fullname,
|
||||||
|
'locale': request.user.locale,
|
||||||
|
'timezone': request.user.timezone
|
||||||
|
})
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
|
import contextlib
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
from django.db.models import F, Q
|
from django.db.models import F, Q
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django_filters.rest_framework import (
|
from django_filters.rest_framework import (
|
||||||
BooleanFilter, DjangoFilterBackend, FilterSet,
|
BooleanFilter, DjangoFilterBackend, FilterSet,
|
||||||
)
|
)
|
||||||
from rest_framework import viewsets
|
from rest_framework import status, viewsets
|
||||||
|
from rest_framework.decorators import list_route
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
from rest_framework.filters import OrderingFilter
|
from rest_framework.filters import OrderingFilter
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from pretix.api.serializers.voucher import VoucherSerializer
|
from pretix.api.serializers.voucher import VoucherSerializer
|
||||||
from pretix.base.models import Voucher
|
from pretix.base.models import Voucher
|
||||||
from pretix.base.models.organizer import TeamAPIToken
|
|
||||||
|
|
||||||
|
|
||||||
class VoucherFilter(FilterSet):
|
class VoucherFilter(FilterSet):
|
||||||
@@ -35,15 +39,36 @@ class VoucherViewSet(viewsets.ModelViewSet):
|
|||||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||||
ordering = ('id',)
|
ordering = ('id',)
|
||||||
ordering_fields = ('id', 'code', 'max_usages', 'valid_until', 'value')
|
ordering_fields = ('id', 'code', 'max_usages', 'valid_until', 'value')
|
||||||
filter_class = VoucherFilter
|
filterset_class = VoucherFilter
|
||||||
permission = 'can_view_vouchers'
|
permission = 'can_view_vouchers'
|
||||||
write_permission = 'can_change_vouchers'
|
write_permission = 'can_change_vouchers'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.vouchers.all()
|
return self.request.event.vouchers.all()
|
||||||
|
|
||||||
|
def _predict_quota_check(self, data, instance):
|
||||||
|
# This method predicts if Voucher.clean_quota_needs_checking
|
||||||
|
# *migh* later require a quota check. It is only approximate
|
||||||
|
# and returns True a little too often. The point is to avoid
|
||||||
|
# locks when we know we won't need them.
|
||||||
|
if 'allow_ignore_quota' in data and data.get('allow_ignore_quota'):
|
||||||
|
return False
|
||||||
|
if instance and 'allow_ignore_quota' not in data and instance.allow_ignore_quota:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if 'block_quota' in data and not data.get('block_quota'):
|
||||||
|
return False
|
||||||
|
if instance and 'block_quota' not in data and not instance.block_quota:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
with request.event.lock():
|
if self._predict_quota_check(request.data, None):
|
||||||
|
lockfn = request.event.lock
|
||||||
|
else:
|
||||||
|
lockfn = contextlib.suppress # noop context manager
|
||||||
|
with lockfn():
|
||||||
return super().create(request, *args, **kwargs)
|
return super().create(request, *args, **kwargs)
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
@@ -51,7 +76,7 @@ class VoucherViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.voucher.added',
|
'pretix.voucher.added',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -61,7 +86,11 @@ class VoucherViewSet(viewsets.ModelViewSet):
|
|||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def update(self, request, *args, **kwargs):
|
def update(self, request, *args, **kwargs):
|
||||||
with request.event.lock():
|
if self._predict_quota_check(request.data, self.get_object()):
|
||||||
|
lockfn = request.event.lock
|
||||||
|
else:
|
||||||
|
lockfn = contextlib.suppress # noop context manager
|
||||||
|
with lockfn():
|
||||||
return super().update(request, *args, **kwargs)
|
return super().update(request, *args, **kwargs)
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
@@ -69,7 +98,7 @@ class VoucherViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.voucher.changed',
|
'pretix.voucher.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -80,6 +109,27 @@ class VoucherViewSet(viewsets.ModelViewSet):
|
|||||||
instance.log_action(
|
instance.log_action(
|
||||||
'pretix.voucher.deleted',
|
'pretix.voucher.deleted',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
auth=self.request.auth,
|
||||||
)
|
)
|
||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
|
|
||||||
|
@list_route(methods=['POST'])
|
||||||
|
def batch_create(self, request, *args, **kwargs):
|
||||||
|
if any(self._predict_quota_check(d, None) for d in request.data):
|
||||||
|
lockfn = request.event.lock
|
||||||
|
else:
|
||||||
|
lockfn = contextlib.suppress # noop context manager
|
||||||
|
with lockfn():
|
||||||
|
serializer = self.get_serializer(data=request.data, many=True)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
with transaction.atomic():
|
||||||
|
serializer.save(event=self.request.event)
|
||||||
|
for i, v in enumerate(serializer.instance):
|
||||||
|
v.log_action(
|
||||||
|
'pretix.voucher.added',
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data=self.request.data[i]
|
||||||
|
)
|
||||||
|
headers = self.get_success_headers(serializer.data)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
|
from rest_framework.decorators import detail_route
|
||||||
|
from rest_framework.exceptions import PermissionDenied, ValidationError
|
||||||
from rest_framework.filters import OrderingFilter
|
from rest_framework.filters import OrderingFilter
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from pretix.api.serializers.waitinglist import WaitingListSerializer
|
from pretix.api.serializers.waitinglist import WaitingListSerializer
|
||||||
from pretix.base.models import WaitingListEntry
|
from pretix.base.models import WaitingListEntry
|
||||||
|
from pretix.base.models.waitinglist import WaitingListException
|
||||||
|
|
||||||
|
|
||||||
class WaitingListFilter(FilterSet):
|
class WaitingListFilter(FilterSet):
|
||||||
@@ -18,14 +22,61 @@ class WaitingListFilter(FilterSet):
|
|||||||
fields = ['item', 'variation', 'email', 'locale', 'has_voucher', 'subevent']
|
fields = ['item', 'variation', 'email', 'locale', 'has_voucher', 'subevent']
|
||||||
|
|
||||||
|
|
||||||
class WaitingListViewSet(viewsets.ReadOnlyModelViewSet):
|
class WaitingListViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = WaitingListSerializer
|
serializer_class = WaitingListSerializer
|
||||||
queryset = WaitingListEntry.objects.none()
|
queryset = WaitingListEntry.objects.none()
|
||||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||||
ordering = ('created',)
|
ordering = ('created',)
|
||||||
ordering_fields = ('id', 'created', 'email', 'item')
|
ordering_fields = ('id', 'created', 'email', 'item')
|
||||||
filter_class = WaitingListFilter
|
filterset_class = WaitingListFilter
|
||||||
permission = 'can_view_orders'
|
permission = 'can_view_orders'
|
||||||
|
write_permission = 'can_change_orders'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.waitinglistentries.all()
|
return self.request.event.waitinglistentries.all()
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
ctx = super().get_serializer_context()
|
||||||
|
ctx['event'] = self.request.event
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
serializer.save(event=self.request.event)
|
||||||
|
serializer.instance.log_action(
|
||||||
|
'pretix.event.orders.waitinglist.added',
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
)
|
||||||
|
|
||||||
|
def perform_update(self, serializer):
|
||||||
|
if serializer.instance.voucher:
|
||||||
|
raise PermissionDenied('This entry can not be changed as it has already been assigned a voucher.')
|
||||||
|
serializer.save(event=self.request.event)
|
||||||
|
serializer.instance.log_action(
|
||||||
|
'pretix.event.orders.waitinglist.changed',
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
)
|
||||||
|
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
if instance.voucher:
|
||||||
|
raise PermissionDenied('This entry can not be deleted as it has already been assigned a voucher.')
|
||||||
|
|
||||||
|
instance.log_action(
|
||||||
|
'pretix.event.orders.waitinglist.deleted',
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
)
|
||||||
|
super().perform_destroy(instance)
|
||||||
|
|
||||||
|
@detail_route(methods=['POST'])
|
||||||
|
def send_voucher(self, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
self.get_object().send_voucher(
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
)
|
||||||
|
except WaitingListException as e:
|
||||||
|
raise ValidationError(str(e))
|
||||||
|
else:
|
||||||
|
return Response(status=204)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user