forked from CGM_Public/pretix_original
Compare commits
568 Commits
release/1.
...
transfer-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83c297c0a8 | ||
|
|
60d1f02d26 | ||
|
|
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/static/lightbox/* linguist-vendored
|
||||
src/static/typeahead/* linguist-vendored
|
||||
src/static/moment/* linguist-vendored
|
||||
src/static/datetimepicker/* linguist-vendored
|
||||
src/static/colorpicker/* linguist-vendored
|
||||
src/static/fileupload/* linguist-vendored
|
||||
src/static/vuejs/* linguist-vendored
|
||||
src/static/select2/* linguist-vendored
|
||||
src/static/charts/* linguist-vendored
|
||||
src/static/rrule/* linguist-vendored
|
||||
src/static/iframeresizer/* linguist-vendored
|
||||
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/fabric.* linguist-vendored
|
||||
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/pdf.* linguist-vendored
|
||||
src/pretix/static/fontawesome/* linguist-vendored
|
||||
src/pretix/static/lightbox/* linguist-vendored
|
||||
src/pretix/static/typeahead/* linguist-vendored
|
||||
src/pretix/static/moment/* linguist-vendored
|
||||
src/pretix/static/datetimepicker/* linguist-vendored
|
||||
src/pretix/static/colorpicker/* linguist-vendored
|
||||
src/pretix/static/fileupload/* linguist-vendored
|
||||
src/pretix/static/vuejs/* linguist-vendored
|
||||
src/pretix/static/select2/* linguist-vendored
|
||||
src/pretix/static/charts/* linguist-vendored
|
||||
src/pretix/static/rrule/* linguist-vendored
|
||||
src/pretix/static/iframeresizer/* linguist-vendored
|
||||
src/pretix/static/pdfjs/* linguist-vendored
|
||||
src/pretix/static/fabric/* linguist-vendored
|
||||
|
||||
# Denote all files that are truly binary and should not be modified.
|
||||
*.eot binary
|
||||
|
||||
@@ -17,7 +17,7 @@ pypi:
|
||||
- virtualenv env
|
||||
- source env/bin/activate
|
||||
- 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
|
||||
- python setup.py sdist
|
||||
- pip install dist/pretix-*.tar.gz
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
-r src/requirements/py34.txt
|
||||
-r doc/requirements.txt
|
||||
|
||||
19
.travis.sh
19
.travis.sh
@@ -15,17 +15,17 @@ if [ "$PRETIX_CONFIG_FILE" == "tests/travis_postgres.cfg" ]; then
|
||||
fi
|
||||
|
||||
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
|
||||
flake8 .
|
||||
isort -c -rc -df .
|
||||
fi
|
||||
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
|
||||
make doctest
|
||||
fi
|
||||
if [ "$1" == "spelling" ]; then
|
||||
if [ "$1" == "doc-spelling" ]; then
|
||||
XDG_CACHE_HOME=/cache pip3 install -Ur doc/requirements.txt
|
||||
cd doc
|
||||
make spelling
|
||||
@@ -33,22 +33,27 @@ if [ "$1" == "spelling" ]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
if [ "$1" == "translation-spelling" ]; then
|
||||
XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements/dev.txt
|
||||
cd src
|
||||
potypo
|
||||
fi
|
||||
if [ "$1" == "tests" ]; then
|
||||
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt -r src/requirements/py34.txt
|
||||
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt pytest-xdist
|
||||
cd src
|
||||
python manage.py check
|
||||
make all compress
|
||||
py.test --reruns 5 tests
|
||||
py.test --reruns 5 -n 2 tests
|
||||
fi
|
||||
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
|
||||
python manage.py check
|
||||
make all compress
|
||||
coverage run -m py.test --reruns 5 tests && codecov
|
||||
fi
|
||||
if [ "$1" == "plugins" ]; then
|
||||
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt -r src/requirements/py34.txt
|
||||
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt
|
||||
cd src
|
||||
python setup.py develop
|
||||
make all compress
|
||||
|
||||
18
.travis.yml
18
.travis.yml
@@ -15,34 +15,28 @@ matrix:
|
||||
- python: 3.6
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
|
||||
- python: 3.6
|
||||
env: JOB=tests-cov
|
||||
env: JOB=tests-cov PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||
- python: 3.6
|
||||
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
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
|
||||
- python: 3.4
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||
- python: 3.5
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||
- python: 3.6
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||
- python: 3.6
|
||||
env: JOB=plugins
|
||||
- python: 3.6
|
||||
env: JOB=spelling
|
||||
env: JOB=doc-spelling
|
||||
- python: 3.6
|
||||
env: JOB=translation-spelling
|
||||
addons:
|
||||
postgresql: "9.4"
|
||||
apt:
|
||||
packages:
|
||||
- enchant
|
||||
- myspell-de-de
|
||||
- aspell-en
|
||||
branches:
|
||||
except:
|
||||
- /^weblate-.*/
|
||||
|
||||
@@ -3,7 +3,7 @@ FROM python:3.6
|
||||
RUN apt-get update && \
|
||||
apt-get install -y git libxml2-dev libxslt1-dev python-dev python-virtualenv locales \
|
||||
libffi-dev build-essential python3-dev zlib1g-dev libssl-dev gettext libpq-dev \
|
||||
libmysqlclient-dev libmemcached-dev libjpeg-dev supervisor nginx sudo \
|
||||
default-libmysqlclient-dev libmemcached-dev libjpeg-dev supervisor nginx sudo \
|
||||
--no-install-recommends && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
|
||||
@@ -3,7 +3,7 @@ cd /pretix/src
|
||||
export DJANGO_SETTINGS_MODULE=production_settings
|
||||
export DATA_DIR=/data/
|
||||
export HOME=/pretix
|
||||
NUM_WORKERS=10
|
||||
export NUM_WORKERS=$((2 * $(nproc --all)))
|
||||
|
||||
if [ ! -d /data/logs ]; then
|
||||
mkdir /data/logs;
|
||||
|
||||
@@ -53,6 +53,10 @@ Example::
|
||||
A comma-separated list of plugins that are enabled by default for all new events.
|
||||
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``
|
||||
The cookie domain to be set. Defaults to ``None``.
|
||||
|
||||
|
||||
@@ -121,8 +121,7 @@ command if you're running PostgreSQL::
|
||||
|
||||
(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.
|
||||
You can find out your Python version using ``python -V``.
|
||||
Note that you need Python 3.5 or newer. You can find out your Python version using ``python -V``.
|
||||
|
||||
We also need to create a data directory::
|
||||
|
||||
|
||||
@@ -6,27 +6,13 @@ with pretix' REST API, such as authentication, pagination and similar definition
|
||||
|
||||
.. _`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
|
||||
--------------
|
||||
|
||||
If you're building an application for end users, we strongly recommend that you use our
|
||||
:ref:`OAuth-based authentication progress <rest-oauth>`. However, for simpler needs, you
|
||||
can also go with static API tokens that you can create on a per-team basis (see below).
|
||||
|
||||
You need to include the API token with every request to pretix' API in the ``Authorization`` header
|
||||
like the following:
|
||||
|
||||
@@ -44,6 +30,43 @@ like the following:
|
||||
adding OAuth2 support in the future for user-level authentication. If you want
|
||||
to use session authentication, be sure to comply with Django's `CSRF policies`_.
|
||||
|
||||
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
|
||||
|
||||
Permissions
|
||||
-----------
|
||||
|
||||
The API follows pretix team based permissions model. Each organizer can have several teams
|
||||
each with it's own set of permissions. Each team can have any number of API keys attached.
|
||||
|
||||
To access a given endpoint the team the API key belongs to needs to have the corresponding
|
||||
permission for the organizer/event being accessed.
|
||||
|
||||
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
|
||||
-------------
|
||||
|
||||
@@ -90,6 +113,41 @@ respective page.
|
||||
The field ``results`` contains a list of objects representing the first results. For most
|
||||
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
|
||||
------
|
||||
|
||||
|
||||
@@ -14,4 +14,5 @@ in functionality over time.
|
||||
:maxdepth: 2
|
||||
|
||||
fundamentals
|
||||
oauth
|
||||
resources/index
|
||||
|
||||
171
doc/api/oauth.rst
Normal file
171
doc/api/oauth.rst
Normal file
@@ -0,0 +1,171 @@
|
||||
.. _`rest-oauth`:
|
||||
|
||||
OAuth support / "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.
|
||||
|
||||
.. _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
|
||||
258
doc/api/resources/carts.rst
Normal file
258
doc/api/resources/carts.rst
Normal file
@@ -0,0 +1,258 @@
|
||||
.. _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_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_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_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`` (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": "Peter",
|
||||
"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
|
||||
===============
|
||||
|
||||
@@ -14,6 +16,7 @@ Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the category
|
||||
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
|
||||
be ``null``)
|
||||
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.
|
||||
|
||||
.. versionchanged:: 1.16
|
||||
|
||||
The field ``internal_name`` has been added.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
@@ -58,6 +65,7 @@ Endpoints
|
||||
{
|
||||
"id": 1,
|
||||
"name": {"en": "Tickets"},
|
||||
"internal_name": "",
|
||||
"description": {"en": "Tickets are what you need to get in."},
|
||||
"position": 1,
|
||||
"is_addon": false
|
||||
@@ -99,6 +107,7 @@ Endpoints
|
||||
{
|
||||
"id": 1,
|
||||
"name": {"en": "Tickets"},
|
||||
"internal_name": "",
|
||||
"description": {"en": "Tickets are what you need to get in."},
|
||||
"position": 1,
|
||||
"is_addon": false
|
||||
@@ -126,6 +135,7 @@ Endpoints
|
||||
|
||||
{
|
||||
"name": {"en": "Tickets"},
|
||||
"internal_name": "",
|
||||
"description": {"en": "Tickets are what you need to get in."},
|
||||
"position": 1,
|
||||
"is_addon": false
|
||||
@@ -142,6 +152,7 @@ Endpoints
|
||||
{
|
||||
"id": 1,
|
||||
"name": {"en": "Tickets"},
|
||||
"internal_name": "",
|
||||
"description": {"en": "Tickets are what you need to get in."},
|
||||
"position": 1,
|
||||
"is_addon": false
|
||||
@@ -187,6 +198,7 @@ Endpoints
|
||||
{
|
||||
"id": 1,
|
||||
"name": {"en": "Tickets"},
|
||||
"internal_name": "",
|
||||
"description": {"en": "Tickets are what you need to get in."},
|
||||
"position": 1,
|
||||
"is_addon": true
|
||||
|
||||
@@ -44,6 +44,10 @@ include_pending boolean If ``true``, th
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. versionchanged:: 1.15
|
||||
|
||||
The ``../status/`` detail endpoint has been added.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/
|
||||
|
||||
Returns a list of all check-in lists within a given event.
|
||||
@@ -128,6 +132,72 @@ Endpoints
|
||||
: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)/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/
|
||||
|
||||
Creates a new check-in list.
|
||||
@@ -254,6 +324,18 @@ 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/
|
||||
|
||||
Returns a list of all order positions within a given event. The result is the same as
|
||||
@@ -297,6 +379,7 @@ Order position endpoints
|
||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||
"addon_to": null,
|
||||
"subevent": null,
|
||||
"pseudonymization_id": "MQLJvANO3B",
|
||||
"checkins": [
|
||||
{
|
||||
"list": 1,
|
||||
@@ -325,15 +408,26 @@ Order position endpoints
|
||||
``order__datetime``, ``positionid``, ``attendee_name``, ``last_checked_in`` and ``order__email``. Default:
|
||||
``attendee_name,positionid``
|
||||
: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__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__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
|
||||
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 bollean has_checkin: If set to ``true`` or ``false``, only return positions that have or have not been
|
||||
checked in already on this list.
|
||||
:query string order__status: Only return positions with the given order status.
|
||||
: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__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__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 event: The ``slug`` field of the event to fetch
|
||||
:param list: The ID of the check-in list to look for
|
||||
@@ -342,7 +436,7 @@ Order position endpoints
|
||||
: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.
|
||||
|
||||
.. 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.
|
||||
The result format is the same as the :ref:`order-position-resource`, with one important difference: the
|
||||
@@ -352,7 +446,7 @@ Order position endpoints
|
||||
|
||||
.. 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
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
@@ -380,6 +474,7 @@ Order position endpoints
|
||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||
"addon_to": null,
|
||||
"subevent": null,
|
||||
"pseudonymization_id": "MQLJvANO3B",
|
||||
"checkins": [
|
||||
{
|
||||
"list": 1,
|
||||
@@ -409,3 +504,127 @@ Order position endpoints
|
||||
: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.
|
||||
|
||||
.. 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.
|
||||
|
||||
:<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"
|
||||
}
|
||||
|
||||
**Example response with required questions**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 400 Bad Request
|
||||
Content-Type: text/json
|
||||
|
||||
{
|
||||
"status": "incomplete"
|
||||
"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",
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
@@ -25,14 +25,22 @@ presale_start datetime The date at whi
|
||||
presale_end datetime The date at which the ticket shop closes (or ``null``)
|
||||
location multi-lingual string The event location (or ``null``)
|
||||
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.
|
||||
plugins list A list of package names of the enabled plugins for this
|
||||
event.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
|
||||
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.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -40,6 +48,8 @@ Endpoints
|
||||
|
||||
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**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@@ -74,7 +84,13 @@ Endpoints
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"has_subevents": false,
|
||||
"meta_data": {}
|
||||
"meta_data": {},
|
||||
"plugins": [
|
||||
"pretix.plugins.banktransfer"
|
||||
"pretix.plugins.stripe"
|
||||
"pretix.plugins.paypal"
|
||||
"pretix.plugins.ticketoutputpdf"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -89,6 +105,8 @@ Endpoints
|
||||
|
||||
Returns information on one event, identified by its slug.
|
||||
|
||||
Permission required: "Can change event settings"
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@@ -118,7 +136,13 @@ Endpoints
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"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
|
||||
@@ -126,3 +150,242 @@ Endpoints
|
||||
: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.
|
||||
|
||||
.. 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,
|
||||
"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,
|
||||
"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,
|
||||
"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,
|
||||
"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,
|
||||
"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,4 @@ Resources and endpoints
|
||||
vouchers
|
||||
checkinlists
|
||||
waitinglist
|
||||
carts
|
||||
|
||||
@@ -223,3 +223,59 @@ Endpoints
|
||||
: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
|
||||
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
|
||||
=====
|
||||
|
||||
@@ -14,6 +16,7 @@ Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the item
|
||||
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
|
||||
overwritten by variations or other options.
|
||||
category integer The ID of the category this item belongs to
|
||||
@@ -54,11 +57,17 @@ max_per_order integer This product ca
|
||||
checkin_attention boolean If ``True``, the check-in app should show a warning
|
||||
that this ticket requires special attention if such
|
||||
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.
|
||||
has_variations boolean Shows whether or not this item has variations.
|
||||
variations list of objects A list with one object for each variation of this item.
|
||||
Can be empty. Only writable during creation,
|
||||
use separate endpoint to modify this later.
|
||||
├ 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``
|
||||
├ price money (string) The price used for this variation. This is either the
|
||||
same as ``default_price`` if that value is set or equal
|
||||
@@ -88,6 +97,14 @@ addons list of objects Definition of a
|
||||
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
|
||||
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.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Please note that an item either always has variations or never has. Once created with variations the item can never
|
||||
@@ -129,7 +146,9 @@ Endpoints
|
||||
{
|
||||
"id": 1,
|
||||
"name": {"en": "Standard ticket"},
|
||||
"internal_name": "",
|
||||
"default_price": "23.00",
|
||||
"original_price": null,
|
||||
"category": null,
|
||||
"active": true,
|
||||
"description": null,
|
||||
@@ -148,6 +167,7 @@ Endpoints
|
||||
"max_per_order": null,
|
||||
"checkin_attention": false,
|
||||
"has_variations": false,
|
||||
"require_approval": false,
|
||||
"variations": [
|
||||
{
|
||||
"value": {"en": "Student"},
|
||||
@@ -211,7 +231,9 @@ Endpoints
|
||||
{
|
||||
"id": 1,
|
||||
"name": {"en": "Standard ticket"},
|
||||
"internal_name": "",
|
||||
"default_price": "23.00",
|
||||
"original_price": null,
|
||||
"category": null,
|
||||
"active": true,
|
||||
"description": null,
|
||||
@@ -230,6 +252,7 @@ Endpoints
|
||||
"max_per_order": null,
|
||||
"checkin_attention": false,
|
||||
"has_variations": false,
|
||||
"require_approval": false,
|
||||
"variations": [
|
||||
{
|
||||
"value": {"en": "Student"},
|
||||
@@ -274,7 +297,9 @@ Endpoints
|
||||
{
|
||||
"id": 1,
|
||||
"name": {"en": "Standard ticket"},
|
||||
"internal_name": "",
|
||||
"default_price": "23.00",
|
||||
"original_price": null,
|
||||
"category": null,
|
||||
"active": true,
|
||||
"description": null,
|
||||
@@ -292,6 +317,7 @@ Endpoints
|
||||
"min_per_order": null,
|
||||
"max_per_order": null,
|
||||
"checkin_attention": false,
|
||||
"require_approval": false,
|
||||
"variations": [
|
||||
{
|
||||
"value": {"en": "Student"},
|
||||
@@ -324,7 +350,9 @@ Endpoints
|
||||
{
|
||||
"id": 1,
|
||||
"name": {"en": "Standard ticket"},
|
||||
"internal_name": "",
|
||||
"default_price": "23.00",
|
||||
"original_price": null,
|
||||
"category": null,
|
||||
"active": true,
|
||||
"description": null,
|
||||
@@ -343,6 +371,7 @@ Endpoints
|
||||
"max_per_order": null,
|
||||
"checkin_attention": false,
|
||||
"has_variations": true,
|
||||
"require_approval": false,
|
||||
"variations": [
|
||||
{
|
||||
"value": {"en": "Student"},
|
||||
@@ -406,7 +435,9 @@ Endpoints
|
||||
{
|
||||
"id": 1,
|
||||
"name": {"en": "Ticket"},
|
||||
"internal_name": "",
|
||||
"default_price": "25.00",
|
||||
"original_price": null,
|
||||
"category": null,
|
||||
"active": true,
|
||||
"description": null,
|
||||
@@ -425,6 +456,7 @@ Endpoints
|
||||
"max_per_order": null,
|
||||
"checkin_attention": false,
|
||||
"has_variations": true,
|
||||
"require_approval": false,
|
||||
"variations": [
|
||||
{
|
||||
"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
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"identifier": "LVETRWVU",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
.. spelling:: checkin
|
||||
|
||||
.. _rest-questions:
|
||||
|
||||
Questions
|
||||
=========
|
||||
|
||||
@@ -59,6 +61,11 @@ options list of objects In case of ques
|
||||
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/
|
||||
|
||||
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 string ordering: Manually set the ordering of results. Valid fields to be used are ``id`` and ``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 event: The ``slug`` field of the event to fetch
|
||||
:statuscode 200: no error
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
.. _rest-quotas:
|
||||
|
||||
Quotas
|
||||
======
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
.. _rest-subevents:
|
||||
|
||||
Event series dates / Sub-events
|
||||
===============================
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
.. _rest-taxrules:
|
||||
|
||||
Tax rules
|
||||
=========
|
||||
|
||||
|
||||
@@ -251,7 +251,7 @@ Endpoints
|
||||
|
||||
{
|
||||
"price_mode": "set",
|
||||
"value": "24.00",
|
||||
"value": "24.00"
|
||||
}
|
||||
|
||||
**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
|
||||
---------
|
||||
|
||||
@@ -121,3 +127,161 @@ Endpoints
|
||||
: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)/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.
|
||||
|
||||
@@ -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::
|
||||
|
||||
|
||||
from django.core.urlresolvers import resolve, reverse
|
||||
from django.urls import resolve, reverse
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
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
|
||||
: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
|
||||
|
||||
Order events
|
||||
""""""""""""
|
||||
@@ -47,7 +48,8 @@ Backend
|
||||
-------
|
||||
|
||||
.. 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
|
||||
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
@@ -56,6 +58,12 @@ Backend
|
||||
Vouchers
|
||||
""""""""
|
||||
|
||||
.. automodule:: pretix.control.signals
|
||||
:members: item_forms
|
||||
|
||||
Vouchers
|
||||
""""""""
|
||||
|
||||
.. automodule:: pretix.control.signals
|
||||
:members: voucher_form_class, voucher_form_html, voucher_form_validation
|
||||
|
||||
@@ -68,5 +76,5 @@ Dashboards
|
||||
Ticket designs
|
||||
""""""""""""""
|
||||
|
||||
.. automodule:: pretix.plugins.ticketoutputpdf.signals
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: layout_text_variables
|
||||
|
||||
@@ -10,6 +10,10 @@ Contents:
|
||||
exporter
|
||||
ticketoutput
|
||||
payment
|
||||
payment_2.0
|
||||
email
|
||||
invoice
|
||||
shredder
|
||||
customview
|
||||
general
|
||||
quality
|
||||
|
||||
@@ -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.
|
||||
|
||||
.. 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
|
||||
---------------------
|
||||
|
||||
@@ -31,7 +35,7 @@ that the plugin will provide::
|
||||
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``.
|
||||
|
||||
@@ -54,55 +58,61 @@ The provider class
|
||||
|
||||
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
|
||||
|
||||
.. automethod:: settings_content_render
|
||||
|
||||
.. automethod:: render_invoice_text
|
||||
.. automethod:: is_allowed
|
||||
|
||||
.. automethod:: payment_form_render
|
||||
|
||||
.. automethod:: payment_form
|
||||
|
||||
.. automethod:: is_allowed
|
||||
|
||||
.. autoattribute:: payment_form_fields
|
||||
|
||||
.. automethod:: checkout_prepare
|
||||
|
||||
.. automethod:: payment_is_valid_session
|
||||
|
||||
.. automethod:: checkout_prepare
|
||||
|
||||
.. automethod:: checkout_confirm_render
|
||||
|
||||
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_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_can_retry
|
||||
|
||||
.. automethod:: order_prepare
|
||||
.. automethod:: payment_prepare
|
||||
|
||||
.. automethod:: order_paid_render
|
||||
.. automethod:: payment_control_render
|
||||
|
||||
.. automethod:: order_control_render
|
||||
.. automethod:: payment_refund_supported
|
||||
|
||||
.. automethod:: order_control_refund_render
|
||||
.. automethod:: payment_partial_refund_supported
|
||||
|
||||
.. automethod:: order_control_refund_perform
|
||||
.. automethod:: execute_refund
|
||||
|
||||
.. automethod:: is_implicit
|
||||
.. automethod:: shred_payment_info
|
||||
|
||||
.. autoattribute:: is_implicit
|
||||
|
||||
.. autoattribute:: is_meta
|
||||
|
||||
|
||||
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``.
|
||||
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'])
|
||||
|
||||
@@ -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
|
||||
in the system that lead to the current state.
|
||||
|
||||
.. _`logging`:
|
||||
|
||||
Logging changes
|
||||
---------------
|
||||
|
||||
|
||||
@@ -86,6 +86,15 @@ Carts and Orders
|
||||
.. autoclass:: pretix.base.models.OrderPosition
|
||||
:members:
|
||||
|
||||
.. autoclass:: pretix.base.models.OrderFee
|
||||
:members:
|
||||
|
||||
.. autoclass:: pretix.base.models.OrderPayment
|
||||
:members:
|
||||
|
||||
.. autoclass:: pretix.base.models.OrderRefund
|
||||
:members:
|
||||
|
||||
.. autoclass:: pretix.base.models.CartPosition
|
||||
:members:
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ External Dependencies
|
||||
---------------------
|
||||
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``)
|
||||
* ``python-dev`` for Python 3 (Debian package: ``python3-dev``)
|
||||
* ``libffi`` (Debian package: ``libffi-dev``)
|
||||
@@ -54,10 +54,6 @@ The first thing you need are all the main application's dependencies::
|
||||
cd src/
|
||||
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::
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
flake8 . || exit 1
|
||||
isort -q -rc -c . || exit 1
|
||||
export GIT_WORK_TREE=../
|
||||
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.
|
||||
|
||||
|
||||
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
|
||||
pretixdroid
|
||||
banktransfer
|
||||
ticketoutputpdf
|
||||
badges
|
||||
|
||||
@@ -4,10 +4,10 @@ pretixdroid HTTP API
|
||||
The pretixdroid plugin provides a HTTP API that the `pretixdroid Android app`_
|
||||
uses to communicate with the pretix server.
|
||||
|
||||
.. warning:: This API is intended **only** to serve the pretixdroid Android app. There are no backwards compatibility
|
||||
guarantees on this API. We will not add features that are not required for the Android App. There is a
|
||||
general-purpose :ref:`rest-api` that not yet provides all features that this API provides, but will do
|
||||
so in the future.
|
||||
.. warning:: This API is **DEPRECATED** and will probably go away soon. It is used **only** to serve the pretixdroid
|
||||
Android app. There are no backwards compatibility guarantees on this API. We will not add features that
|
||||
are not required for the Android App. There is a general-purpose :ref:`rest-api` that provides all
|
||||
features that you need to check in.
|
||||
|
||||
.. versionchanged:: 1.12
|
||||
|
||||
@@ -81,6 +81,7 @@ uses to communicate with the pretix server.
|
||||
"attention": false,
|
||||
"redeemed": true,
|
||||
"checkin_allowed": true,
|
||||
"addons_text": "Parking spot",
|
||||
"paid": true
|
||||
}
|
||||
}
|
||||
@@ -106,6 +107,7 @@ uses to communicate with the pretix server.
|
||||
"attention": false,
|
||||
"redeemed": true,
|
||||
"checkin_allowed": true,
|
||||
"addons_text": "Parking spot",
|
||||
"paid": true
|
||||
},
|
||||
"questions": [
|
||||
@@ -152,6 +154,7 @@ uses to communicate with the pretix server.
|
||||
"attention": false,
|
||||
"redeemed": true,
|
||||
"checkin_allowed": true,
|
||||
"addons_text": "Parking spot",
|
||||
"paid": true
|
||||
}
|
||||
}
|
||||
@@ -212,6 +215,7 @@ uses to communicate with the pretix server.
|
||||
"redeemed": false,
|
||||
"attention": false,
|
||||
"checkin_allowed": true,
|
||||
"addons_text": "Parking spot",
|
||||
"paid": true
|
||||
},
|
||||
...
|
||||
|
||||
111
doc/plugins/ticketoutputpdf.rst
Normal file
111
doc/plugins/ticketoutputpdf.rst
Normal file
@@ -0,0 +1,111 @@
|
||||
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
|
||||
└ item integer Item ID
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.16
|
||||
|
||||
This resource 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.
|
||||
@@ -1,5 +1,6 @@
|
||||
addon
|
||||
addons
|
||||
anonymize
|
||||
api
|
||||
auditability
|
||||
auth
|
||||
@@ -16,8 +17,10 @@ checksum
|
||||
config
|
||||
contenttypes
|
||||
contextmanager
|
||||
cookiecutter
|
||||
cron
|
||||
cronjob
|
||||
cryptographic
|
||||
debian
|
||||
deduplication
|
||||
discoverable
|
||||
@@ -34,10 +37,13 @@ gettext
|
||||
gunicorn
|
||||
hardcoded
|
||||
hostname
|
||||
idempotency
|
||||
incrementing
|
||||
inofficial
|
||||
invalidations
|
||||
iterable
|
||||
Jimdo
|
||||
libpretixprint
|
||||
libsass
|
||||
linters
|
||||
memcached
|
||||
@@ -56,6 +62,7 @@ nginx
|
||||
NotificationType
|
||||
ons
|
||||
optimizations
|
||||
overpayment
|
||||
param
|
||||
percental
|
||||
positionid
|
||||
@@ -71,6 +78,8 @@ pretixpresale
|
||||
prometheus
|
||||
proxied
|
||||
proxying
|
||||
pseudonymize
|
||||
pseudonymization
|
||||
queryset
|
||||
redemptions
|
||||
redis
|
||||
@@ -98,6 +107,7 @@ subpath
|
||||
systemd
|
||||
testutils
|
||||
timestamp
|
||||
tuples
|
||||
un
|
||||
unconfigured
|
||||
unix
|
||||
@@ -106,6 +116,7 @@ untrusted
|
||||
username
|
||||
url
|
||||
versa
|
||||
versioning
|
||||
viewset
|
||||
viewsets
|
||||
webhook
|
||||
|
||||
@@ -107,6 +107,13 @@ voucher's settings.
|
||||
</div>
|
||||
</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
|
||||
-------------
|
||||
|
||||
@@ -136,7 +143,7 @@ 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
|
||||
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
|
||||
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``.
|
||||
|
||||
|
||||
6
readthedocs.yml
Normal file
6
readthedocs.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
build:
|
||||
image: latest
|
||||
|
||||
python:
|
||||
version: 3.6
|
||||
@@ -8,6 +8,8 @@ recursive-include pretix/control/templates *
|
||||
recursive-include pretix/presale/templates *
|
||||
recursive-include pretix/plugins/banktransfer/templates *
|
||||
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/pretixdroid/templates *
|
||||
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/ticketoutputpdf/templates *
|
||||
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.1.0.dev0"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PretixApiConfig(AppConfig):
|
||||
name = 'pretix.api'
|
||||
label = 'pretixapi'
|
||||
|
||||
|
||||
default_app_config = 'pretix.api.PretixApiConfig'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from rest_framework.permissions import SAFE_METHODS, BasePermission
|
||||
|
||||
from pretix.api.models import OAuthAccessToken
|
||||
from pretix.base.models import Event
|
||||
from pretix.base.models.organizer import Organizer, TeamAPIToken
|
||||
from pretix.helpers.security import (
|
||||
@@ -55,4 +56,28 @@ class EventPermission(BasePermission):
|
||||
|
||||
if required_permission and required_permission not in request.orgapermset:
|
||||
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 ['retrieve', 'update', 'partial_update'] \
|
||||
and 'can_change_event_settings' not in request.eventpermset:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
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
src/pretix/api/migrations/__init__.py
Normal file
0
src/pretix/api/migrations/__init__.py
Normal file
70
src/pretix/api/models.py
Normal file
70
src/pretix/api/models.py
Normal file
@@ -0,0 +1,70 @@
|
||||
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"
|
||||
)
|
||||
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
|
||||
121
src/pretix/api/serializers/cart.py
Normal file
121
src/pretix/api/serializers/cart.py
Normal file
@@ -0,0 +1,121 @@
|
||||
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_email',
|
||||
'voucher', 'addon_to', 'subevent', 'datetime', 'expires', 'includes_tax',
|
||||
'answers',)
|
||||
|
||||
|
||||
class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
answers = AnswerCreateSerializer(many=True, required=False)
|
||||
expires = serializers.DateTimeField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = CartPosition
|
||||
fields = ('cart_id', 'item', 'variation', 'price', 'attendee_name', '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
|
||||
)
|
||||
)
|
||||
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.'
|
||||
)
|
||||
return data
|
||||
@@ -1,3 +1,7 @@
|
||||
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 rest_framework.fields import Field
|
||||
|
||||
@@ -14,15 +18,161 @@ class MetaDataField(Field):
|
||||
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):
|
||||
meta_data = MetaDataField(source='*')
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
plugins = PluginsField(required=False, source='*')
|
||||
|
||||
class Meta:
|
||||
model = Event
|
||||
fields = ('name', 'slug', 'live', 'currency', 'date_from',
|
||||
'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()
|
||||
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', None)
|
||||
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)
|
||||
|
||||
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):
|
||||
|
||||
@@ -74,12 +74,12 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
fields = ('id', 'category', 'name', 'active', 'description',
|
||||
fields = ('id', 'category', 'name', 'internal_name', 'active', 'description',
|
||||
'default_price', 'free_price', 'tax_rate', 'tax_rule', 'admission',
|
||||
'position', 'picture', 'available_from', 'available_until',
|
||||
'require_voucher', 'hide_without_voucher', 'allow_cancel',
|
||||
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations',
|
||||
'variations', 'addons')
|
||||
'variations', 'addons', 'original_price', 'require_approval')
|
||||
read_only_fields = ('has_variations', 'picture')
|
||||
|
||||
def get_serializer_context(self):
|
||||
@@ -129,7 +129,7 @@ class ItemCategorySerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ItemCategory
|
||||
fields = ('id', 'name', 'description', 'position', 'is_addon')
|
||||
fields = ('id', 'name', 'internal_name', 'description', 'position', 'is_addon')
|
||||
|
||||
|
||||
class QuestionOptionSerializer(I18nAwareModelSerializer):
|
||||
|
||||
@@ -1,18 +1,31 @@
|
||||
import json
|
||||
from collections import Counter
|
||||
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.exceptions import ValidationError
|
||||
from rest_framework.relations import SlugRelatedField
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.models import (
|
||||
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
|
||||
|
||||
|
||||
class CompatibleCountryField(serializers.Field):
|
||||
def to_internal_value(self, data):
|
||||
return {self.field_name: Country(data)}
|
||||
|
||||
def to_representation(self, instance: InvoiceAddress):
|
||||
if instance.country:
|
||||
return str(instance.country)
|
||||
@@ -20,13 +33,20 @@ class CompatibleCountryField(serializers.Field):
|
||||
return instance.country_old
|
||||
|
||||
|
||||
class InvoiceAdddressSerializer(I18nAwareModelSerializer):
|
||||
class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
||||
country = CompatibleCountryField(source='*')
|
||||
|
||||
class Meta:
|
||||
model = InvoiceAddress
|
||||
fields = ('last_modified', 'is_business', 'company', 'name', 'street', 'zipcode', 'city', 'country', '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
|
||||
|
||||
|
||||
class AnswerQuestionIdentifierField(serializers.Field):
|
||||
@@ -104,17 +124,59 @@ class PositionDownloadsField(serializers.Field):
|
||||
return res
|
||||
|
||||
|
||||
class PdfDataSerializer(serializers.Field):
|
||||
def to_representation(self, instance: OrderPosition):
|
||||
res = {}
|
||||
|
||||
ev = instance.subevent or instance.order.event
|
||||
|
||||
pdfvars = get_variables(instance.order.event)
|
||||
for k, f in pdfvars.items():
|
||||
res[k] = f['evaluate'](instance, instance.order, ev)
|
||||
|
||||
for k, v in ev.meta_data.items():
|
||||
res['meta:' + k] = v
|
||||
|
||||
return res
|
||||
|
||||
|
||||
class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
checkins = CheckinSerializer(many=True)
|
||||
answers = AnswerSerializer(many=True)
|
||||
downloads = PositionDownloadsField(source='*')
|
||||
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
||||
pdf_data = PdfDataSerializer(source='*')
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
|
||||
'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins', 'downloads',
|
||||
'answers', 'tax_rule')
|
||||
'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):
|
||||
@@ -123,32 +185,386 @@ class OrderFeeSerializer(I18nAwareModelSerializer):
|
||||
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule')
|
||||
|
||||
|
||||
class PaymentFeeLegacyField(serializers.Field):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.attr = kwargs.pop('attribute')
|
||||
super().__init__(*args, **kwargs)
|
||||
class OrderPaymentSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = OrderPayment
|
||||
fields = ('local_id', 'state', 'amount', 'created', 'payment_date', 'provider')
|
||||
|
||||
def to_representation(self, instance: Order):
|
||||
return str(
|
||||
sum([getattr(f, self.attr) for f in instance.fees.all() if f.fee_type == OrderFee.FEE_TYPE_PAYMENT],
|
||||
Decimal('0.00'))
|
||||
)
|
||||
|
||||
class OrderRefundSerializer(I18nAwareModelSerializer):
|
||||
payment = SlugRelatedField(slug_field='local_id', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = OrderRefund
|
||||
fields = ('local_id', 'state', 'source', 'amount', 'payment', 'created', 'execution_date', 'provider')
|
||||
|
||||
|
||||
class OrderSerializer(I18nAwareModelSerializer):
|
||||
invoice_address = InvoiceAdddressSerializer()
|
||||
invoice_address = InvoiceAddressSerializer()
|
||||
positions = OrderPositionSerializer(many=True)
|
||||
fees = OrderFeeSerializer(many=True)
|
||||
downloads = OrderDownloadsField(source='*')
|
||||
payment_fee = PaymentFeeLegacyField(source='*', attribute='value') # TODO: Remove in 1.9
|
||||
payment_fee_tax_rate = PaymentFeeLegacyField(source='*', attribute='tax_rate') # TODO: Remove in 1.9
|
||||
payment_fee_tax_value = PaymentFeeLegacyField(source='*', attribute='tax_value') # TODO: Remove in 1.9
|
||||
payments = OrderPaymentSerializer(many=True)
|
||||
refunds = OrderRefundSerializer(many=True)
|
||||
payment_date = OrderPaymentDateField(source='*')
|
||||
payment_provider = OrderPaymentTypeField(source='*')
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ('code', 'status', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
|
||||
'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')
|
||||
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
|
||||
'secret', 'addon_to', 'subevent', 'answers')
|
||||
|
||||
def validate_secret(self, secret):
|
||||
if secret and OrderPosition.objects.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.']}
|
||||
)
|
||||
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', 'email', 'locale', 'payment_provider', 'fees', 'comment',
|
||||
'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_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:
|
||||
ia = InvoiceAddress(**validated_data.pop('invoice_address'))
|
||||
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})
|
||||
|
||||
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
|
||||
)
|
||||
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)
|
||||
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):
|
||||
@@ -168,3 +584,27 @@ class InvoiceSerializer(I18nAwareModelSerializer):
|
||||
'introductory_text', 'additional_text', 'payment_provider_text', 'footer_text', 'lines',
|
||||
'foreign_currency_display', 'foreign_currency_rate', 'foreign_currency_rate_date',
|
||||
'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,3 +1,5 @@
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.models import WaitingListEntry
|
||||
|
||||
@@ -6,4 +8,28 @@ class WaitingListSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
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
|
||||
|
||||
@@ -4,7 +4,11 @@ from django.apps import apps
|
||||
from django.conf.urls import include, url
|
||||
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, event, item, oauth, order, organizer, voucher, waitinglist,
|
||||
)
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.register(r'organizers', organizer.OrganizerViewSet)
|
||||
@@ -14,6 +18,7 @@ orga_router.register(r'events', event.EventViewSet)
|
||||
|
||||
event_router = routers.DefaultRouter()
|
||||
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'categories', item.ItemCategoryViewSet)
|
||||
event_router.register(r'questions', item.QuestionViewSet)
|
||||
@@ -25,6 +30,7 @@ event_router.register(r'invoices', order.InvoiceViewSet)
|
||||
event_router.register(r'taxrules', event.TaxRuleViewSet)
|
||||
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
|
||||
event_router.register(r'checkinlists', checkin.CheckinListViewSet)
|
||||
event_router.register(r'cartpositions', cart.CartPositionViewSet)
|
||||
|
||||
checkinlist_router = routers.DefaultRouter()
|
||||
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet)
|
||||
@@ -36,6 +42,10 @@ item_router = routers.DefaultRouter()
|
||||
item_router.register(r'variations', item.ItemVariationViewSet)
|
||||
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
|
||||
for app in apps.get_app_configs():
|
||||
if hasattr(app, 'PretixPluginMeta'):
|
||||
@@ -51,4 +61,8 @@ urlpatterns = [
|
||||
include(question_router.urls)),
|
||||
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/checkinlists/(?P<list>[^/]+)/',
|
||||
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"),
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -21,3 +26,33 @@ class RichOrderingFilter(OrderingFilter):
|
||||
return queryset.order_by(*ordering)
|
||||
|
||||
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)
|
||||
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,25 @@
|
||||
import django_filters
|
||||
from django.db.models import F, Max, OuterRef, Prefetch, Q, Subquery
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Count, F, Max, OuterRef, Prefetch, Subquery
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from 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.item import QuestionSerializer
|
||||
from pretix.api.serializers.order import OrderPositionSerializer
|
||||
from pretix.api.views import RichOrderingFilter
|
||||
from pretix.api.views.order import OrderPositionFilter
|
||||
from pretix.base.models import Checkin, CheckinList, Order, OrderPosition
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
from pretix.base.services.checkin import (
|
||||
CheckInError, RequiredQuestionsError, perform_checkin,
|
||||
)
|
||||
from pretix.helpers.database import FixedOrderBy
|
||||
|
||||
|
||||
@@ -24,7 +33,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = CheckinListSerializer
|
||||
queryset = CheckinList.objects.none()
|
||||
filter_backends = (DjangoFilterBackend,)
|
||||
filter_class = CheckinListFilter
|
||||
filterset_class = CheckinListFilter
|
||||
permission = 'can_view_orders'
|
||||
write_permission = 'can_change_event_settings'
|
||||
|
||||
@@ -40,7 +49,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.checkinlist.added',
|
||||
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
|
||||
)
|
||||
|
||||
@@ -54,7 +63,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.checkinlist.changed',
|
||||
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
|
||||
)
|
||||
|
||||
@@ -62,26 +71,84 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
instance.log_action(
|
||||
'pretix.event.checkinlist.deleted',
|
||||
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)
|
||||
|
||||
@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):
|
||||
order = django_filters.CharFilter(name='order', lookup_expr='code')
|
||||
has_checkin = django_filters.rest_framework.BooleanFilter(method='has_checkin_qs')
|
||||
attendee_name = django_filters.CharFilter(method='attendee_name_qs')
|
||||
ev = clist.subevent or clist.event
|
||||
response = {
|
||||
'event': {
|
||||
'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):
|
||||
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):
|
||||
serializer_class = OrderPositionSerializer
|
||||
@@ -109,12 +176,16 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
},
|
||||
}
|
||||
|
||||
filter_class = OrderPositionFilter
|
||||
filterset_class = CheckinOrderPositionFilter
|
||||
permission = 'can_view_orders'
|
||||
write_permission = 'can_change_orders'
|
||||
|
||||
@cached_property
|
||||
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):
|
||||
cqs = Checkin.objects.filter(
|
||||
@@ -141,3 +212,53 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
qs = qs.filter(item__in=self.checkinlist.limit_products.values_list('id', flat=True))
|
||||
|
||||
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)
|
||||
)
|
||||
except RequiredQuestionsError as e:
|
||||
return Response({
|
||||
'status': 'incomplete',
|
||||
'questions': [
|
||||
QuestionSerializer(q).data for q in e.questions
|
||||
]
|
||||
}, status=400)
|
||||
except CheckInError as e:
|
||||
return Response({
|
||||
'status': 'error',
|
||||
'reason': e.code
|
||||
}, status=400)
|
||||
else:
|
||||
return Response({
|
||||
'status': 'ok',
|
||||
}, status=201)
|
||||
|
||||
@@ -1,24 +1,123 @@
|
||||
from django.db import transaction
|
||||
from django.db.models import ProtectedError
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from rest_framework import filters, viewsets
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
from pretix.api.auth.permission import EventCRUDPermission
|
||||
from pretix.api.serializers.event import (
|
||||
EventSerializer, SubEventSerializer, TaxRuleSerializer,
|
||||
CloneEventSerializer, EventSerializer, SubEventSerializer,
|
||||
TaxRuleSerializer,
|
||||
)
|
||||
from pretix.api.views import ConditionalListView
|
||||
from pretix.base.models import Event, ItemCategory, TaxRule
|
||||
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 EventViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = EventSerializer
|
||||
queryset = Event.objects.none()
|
||||
lookup_field = 'slug'
|
||||
lookup_url_kwarg = 'event'
|
||||
permission_classes = (EventCRUDPermission,)
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.organizer.events.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 Meta:
|
||||
@@ -26,11 +125,11 @@ class SubEventFilter(FilterSet):
|
||||
fields = ['active']
|
||||
|
||||
|
||||
class SubEventViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
class SubEventViewSet(ConditionalListView, viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = SubEventSerializer
|
||||
queryset = ItemCategory.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
|
||||
filter_class = SubEventFilter
|
||||
filterset_class = SubEventFilter
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.subevents.prefetch_related(
|
||||
@@ -38,7 +137,7 @@ class SubEventViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
)
|
||||
|
||||
|
||||
class TaxRuleViewSet(viewsets.ModelViewSet):
|
||||
class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
serializer_class = TaxRuleSerializer
|
||||
queryset = TaxRule.objects.none()
|
||||
write_permission = 'can_change_event_settings'
|
||||
@@ -51,7 +150,7 @@ class TaxRuleViewSet(viewsets.ModelViewSet):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.taxrule.changed',
|
||||
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
|
||||
)
|
||||
|
||||
@@ -60,7 +159,7 @@ class TaxRuleViewSet(viewsets.ModelViewSet):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.taxrule.added',
|
||||
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
|
||||
)
|
||||
|
||||
@@ -71,6 +170,6 @@ class TaxRuleViewSet(viewsets.ModelViewSet):
|
||||
instance.log_action(
|
||||
'pretix.event.taxrule.deleted',
|
||||
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)
|
||||
|
||||
@@ -13,11 +13,11 @@ from pretix.api.serializers.item import (
|
||||
ItemVariationSerializer, QuestionOptionSerializer, QuestionSerializer,
|
||||
QuotaSerializer,
|
||||
)
|
||||
from pretix.api.views import ConditionalListView
|
||||
from pretix.base.models import (
|
||||
Item, ItemAddOn, ItemCategory, ItemVariation, Question, QuestionOption,
|
||||
Quota,
|
||||
)
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
|
||||
|
||||
@@ -35,13 +35,13 @@ class ItemFilter(FilterSet):
|
||||
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
|
||||
|
||||
|
||||
class ItemViewSet(viewsets.ModelViewSet):
|
||||
class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
serializer_class = ItemSerializer
|
||||
queryset = Item.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('position', 'id')
|
||||
filter_class = ItemFilter
|
||||
filterset_class = ItemFilter
|
||||
permission = 'can_change_items'
|
||||
write_permission = 'can_change_items'
|
||||
|
||||
@@ -53,7 +53,7 @@ class ItemViewSet(viewsets.ModelViewSet):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.item.added',
|
||||
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
|
||||
)
|
||||
|
||||
@@ -68,7 +68,7 @@ class ItemViewSet(viewsets.ModelViewSet):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.item.changed',
|
||||
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
|
||||
)
|
||||
|
||||
@@ -81,7 +81,7 @@ class ItemViewSet(viewsets.ModelViewSet):
|
||||
instance.log_action(
|
||||
'pretix.event.item.deleted',
|
||||
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)
|
||||
|
||||
@@ -113,7 +113,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
||||
item.log_action(
|
||||
'pretix.event.item.variation.added',
|
||||
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},
|
||||
{'value': serializer.instance.value})
|
||||
)
|
||||
@@ -123,7 +123,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
||||
serializer.instance.item.log_action(
|
||||
'pretix.event.item.variation.changed',
|
||||
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},
|
||||
{'value': serializer.instance.value})
|
||||
)
|
||||
@@ -140,7 +140,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
||||
instance.item.log_action(
|
||||
'pretix.event.item.variation.deleted',
|
||||
user=self.request.user,
|
||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
||||
auth=self.request.auth,
|
||||
data={
|
||||
'value': instance.value,
|
||||
'id': self.kwargs['pk']
|
||||
@@ -174,7 +174,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
|
||||
item.log_action(
|
||||
'pretix.event.item.addons.added',
|
||||
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})
|
||||
)
|
||||
|
||||
@@ -183,7 +183,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
|
||||
serializer.instance.base_item.log_action(
|
||||
'pretix.event.item.addons.changed',
|
||||
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})
|
||||
)
|
||||
|
||||
@@ -192,7 +192,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
|
||||
instance.base_item.log_action(
|
||||
'pretix.event.item.addons.removed',
|
||||
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}
|
||||
)
|
||||
|
||||
@@ -203,11 +203,11 @@ class ItemCategoryFilter(FilterSet):
|
||||
fields = ['is_addon']
|
||||
|
||||
|
||||
class ItemCategoryViewSet(viewsets.ModelViewSet):
|
||||
class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
serializer_class = ItemCategorySerializer
|
||||
queryset = ItemCategory.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||
filter_class = ItemCategoryFilter
|
||||
filterset_class = ItemCategoryFilter
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('position', 'id')
|
||||
permission = 'can_change_items'
|
||||
@@ -221,7 +221,7 @@ class ItemCategoryViewSet(viewsets.ModelViewSet):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.category.added',
|
||||
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
|
||||
)
|
||||
|
||||
@@ -235,7 +235,7 @@ class ItemCategoryViewSet(viewsets.ModelViewSet):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.category.changed',
|
||||
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
|
||||
)
|
||||
|
||||
@@ -246,15 +246,22 @@ class ItemCategoryViewSet(viewsets.ModelViewSet):
|
||||
instance.log_action(
|
||||
'pretix.event.category.deleted',
|
||||
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)
|
||||
|
||||
|
||||
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
|
||||
queryset = Question.objects.none()
|
||||
filter_backends = (OrderingFilter,)
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||
filterset_class = QuestionFilter
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('position', 'id')
|
||||
permission = 'can_change_items'
|
||||
@@ -267,7 +274,7 @@ class QuestionViewSet(viewsets.ModelViewSet):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.question.added',
|
||||
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
|
||||
)
|
||||
|
||||
@@ -281,7 +288,7 @@ class QuestionViewSet(viewsets.ModelViewSet):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.question.changed',
|
||||
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
|
||||
)
|
||||
|
||||
@@ -289,7 +296,7 @@ class QuestionViewSet(viewsets.ModelViewSet):
|
||||
instance.log_action(
|
||||
'pretix.event.question.deleted',
|
||||
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)
|
||||
|
||||
@@ -319,7 +326,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
||||
q.log_action(
|
||||
'pretix.event.question.option.added',
|
||||
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})
|
||||
)
|
||||
|
||||
@@ -328,7 +335,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
||||
serializer.instance.question.log_action(
|
||||
'pretix.event.question.option.changed',
|
||||
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})
|
||||
)
|
||||
|
||||
@@ -336,7 +343,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
||||
instance.question.log_action(
|
||||
'pretix.event.question.option.deleted',
|
||||
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}
|
||||
)
|
||||
super().perform_destroy(instance)
|
||||
@@ -348,11 +355,11 @@ class QuotaFilter(FilterSet):
|
||||
fields = ['subevent']
|
||||
|
||||
|
||||
class QuotaViewSet(viewsets.ModelViewSet):
|
||||
class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
serializer_class = QuotaSerializer
|
||||
queryset = Quota.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter,)
|
||||
filter_class = QuotaFilter
|
||||
filterset_class = QuotaFilter
|
||||
ordering_fields = ('id', 'size')
|
||||
ordering = ('id',)
|
||||
permission = 'can_change_items'
|
||||
@@ -366,14 +373,14 @@ class QuotaViewSet(viewsets.ModelViewSet):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.quota.added',
|
||||
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
|
||||
)
|
||||
if serializer.instance.subevent:
|
||||
serializer.instance.subevent.log_action(
|
||||
'pretix.subevent.quota.added',
|
||||
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
|
||||
)
|
||||
|
||||
@@ -389,7 +396,7 @@ class QuotaViewSet(viewsets.ModelViewSet):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.quota.changed',
|
||||
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
|
||||
)
|
||||
if current_subevent == request_subevent:
|
||||
@@ -397,7 +404,7 @@ class QuotaViewSet(viewsets.ModelViewSet):
|
||||
current_subevent.log_action(
|
||||
'pretix.subevent.quota.changed',
|
||||
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
|
||||
)
|
||||
else:
|
||||
@@ -405,14 +412,14 @@ class QuotaViewSet(viewsets.ModelViewSet):
|
||||
request_subevent.log_action(
|
||||
'pretix.subevent.quota.added',
|
||||
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
|
||||
)
|
||||
if current_subevent is not None:
|
||||
current_subevent.log_action(
|
||||
'pretix.subevent.quota.deleted',
|
||||
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()
|
||||
|
||||
@@ -420,13 +427,13 @@ class QuotaViewSet(viewsets.ModelViewSet):
|
||||
instance.log_action(
|
||||
'pretix.event.quota.deleted',
|
||||
user=self.request.user,
|
||||
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
|
||||
auth=self.request.auth,
|
||||
)
|
||||
if instance.subevent:
|
||||
instance.subevent.log_action(
|
||||
'pretix.subevent.quota.deleted',
|
||||
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)
|
||||
|
||||
|
||||
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
|
||||
@@ -2,55 +2,79 @@ import datetime
|
||||
|
||||
import django_filters
|
||||
import pytz
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.db.models.functions import Concat
|
||||
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 rest_framework import serializers, status, viewsets
|
||||
from rest_framework import mixins, serializers, status, viewsets
|
||||
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.mixins import CreateModelMixin
|
||||
from rest_framework.response import Response
|
||||
|
||||
from pretix.api.models import OAuthAccessToken
|
||||
from pretix.api.serializers.order import (
|
||||
InvoiceSerializer, OrderPositionSerializer, OrderSerializer,
|
||||
InvoiceSerializer, OrderCreateSerializer, OrderPaymentSerializer,
|
||||
OrderPositionSerializer, OrderRefundCreateSerializer,
|
||||
OrderRefundSerializer, OrderSerializer,
|
||||
)
|
||||
from pretix.base.models import (
|
||||
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.orders import (
|
||||
OrderError, cancel_order, extend_order, mark_order_expired,
|
||||
mark_order_paid,
|
||||
OrderChangeManager, OrderError, approve_order, cancel_order, deny_order,
|
||||
extend_order, mark_order_expired, mark_order_refunded,
|
||||
)
|
||||
from pretix.base.services.tickets import (
|
||||
get_cachedticket_for_order, get_cachedticket_for_position,
|
||||
)
|
||||
from pretix.base.signals import register_ticket_outputs
|
||||
from pretix.base.signals import order_placed, register_ticket_outputs
|
||||
|
||||
|
||||
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:
|
||||
model = Order
|
||||
fields = ['code', 'status', 'email', 'locale']
|
||||
fields = ['code', 'status', 'email', 'locale', 'require_approval']
|
||||
|
||||
|
||||
class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = OrderSerializer
|
||||
queryset = Order.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||
ordering = ('datetime',)
|
||||
ordering_fields = ('datetime', 'code', 'status')
|
||||
filter_class = OrderFilter
|
||||
filterset_class = OrderFilter
|
||||
lookup_field = 'code'
|
||||
permission = 'can_view_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):
|
||||
return self.request.event.orders.prefetch_related(
|
||||
'positions', 'positions__checkins', 'positions__item', 'positions__answers', 'positions__answers__options',
|
||||
'positions__answers__question', 'fees'
|
||||
'positions__answers__question', 'fees', 'payments', 'refunds', 'refunds__payment'
|
||||
).select_related(
|
||||
'invoice_address'
|
||||
)
|
||||
@@ -63,6 +87,20 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
return prov
|
||||
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>[^/]+)')
|
||||
def download(self, request, output, **kwargs):
|
||||
provider = self._get_output_provider(output)
|
||||
@@ -88,14 +126,33 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
order = self.get_object()
|
||||
|
||||
if order.status in (Order.STATUS_PENDING, Order.STATUS_EXPIRED):
|
||||
|
||||
ps = order.pending_sum
|
||||
try:
|
||||
mark_order_paid(
|
||||
order, manual=True,
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
|
||||
p = order.payments.get(
|
||||
state__in=(OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED),
|
||||
provider='manual',
|
||||
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:
|
||||
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
|
||||
|
||||
@@ -119,11 +176,48 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
cancel_order(
|
||||
order,
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
|
||||
api_token=request.auth if isinstance(request.auth, TeamAPIToken) else None,
|
||||
oauth_application=request.auth.application if isinstance(request.auth, OAuthAccessToken) else None,
|
||||
send_mail=send_mail
|
||||
)
|
||||
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, (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, (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)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
def mark_pending(self, request, **kwargs):
|
||||
order = self.get_object()
|
||||
@@ -135,12 +229,11 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
)
|
||||
|
||||
order.status = Order.STATUS_PENDING
|
||||
order.payment_manual = True
|
||||
order.save()
|
||||
order.log_action(
|
||||
'pretix.event.order.unpaid',
|
||||
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)
|
||||
|
||||
@@ -157,11 +250,26 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
mark_order_expired(
|
||||
order,
|
||||
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)
|
||||
|
||||
# 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,
|
||||
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
|
||||
)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
def extend(self, request, **kwargs):
|
||||
@@ -196,7 +304,7 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
new_date=new_date,
|
||||
force=force,
|
||||
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)
|
||||
except OrderError as e:
|
||||
@@ -205,32 +313,80 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
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()
|
||||
|
||||
|
||||
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')
|
||||
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__icontains=value)
|
||||
| Q(addon_to__attendee_name__icontains=value)
|
||||
| Q(order__code__istartswith=value)
|
||||
| Q(order__invoice_address__name__icontains=value)
|
||||
)
|
||||
|
||||
def has_checkin_qs(self, queryset, name, value):
|
||||
return queryset.filter(checkins__isnull=not value)
|
||||
|
||||
def attendee_name_qs(self, queryset, name, value):
|
||||
return queryset.filter(Q(attendee_name=value) | Q(addon_to__attendee_name=value))
|
||||
return queryset.filter(Q(attendee_name__iexact=value) | Q(addon_to__attendee_name__iexact=value))
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = ['item', 'variation', 'attendee_name', 'secret', 'order', 'order__status', 'has_checkin',
|
||||
'addon_to', 'subevent']
|
||||
fields = {
|
||||
'item': ['exact', 'in'],
|
||||
'variation': ['exact', 'in'],
|
||||
'secret': ['exact'],
|
||||
'order__status': ['exact', 'in'],
|
||||
'addon_to': ['exact', 'in'],
|
||||
'subevent': ['exact', 'in'],
|
||||
'pseudonymization_id': ['exact'],
|
||||
'voucher__code': ['exact'],
|
||||
'voucher': ['exact'],
|
||||
}
|
||||
|
||||
|
||||
class OrderPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = OrderPositionSerializer
|
||||
queryset = OrderPosition.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||
ordering = ('order__datetime', 'positionid')
|
||||
ordering_fields = ('order__code', 'order__datetime', 'positionid', 'attendee_name', 'order__status',)
|
||||
filter_class = OrderPositionFilter
|
||||
filterset_class = OrderPositionFilter
|
||||
permission = 'can_view_orders'
|
||||
write_permission = 'can_change_orders'
|
||||
|
||||
def get_queryset(self):
|
||||
return OrderPosition.objects.filter(order__event=self.request.event).prefetch_related(
|
||||
@@ -271,11 +427,232 @@ class OrderPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
)
|
||||
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))
|
||||
)
|
||||
mark_refunded = request.data.get('mark_refunded', 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()
|
||||
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 request.data.get('mark_refunded', False):
|
||||
mark_order_refunded(refund.order, user=self.request.user if self.request.user.is_authenticated else None,
|
||||
auth=self.request.auth)
|
||||
else:
|
||||
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()
|
||||
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):
|
||||
mark_refunded = request.data.pop('mark_refunded', 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):
|
||||
refers = django_filters.CharFilter(method='refers_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):
|
||||
return queryset.annotate(
|
||||
@@ -302,10 +679,11 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||
ordering = ('nr',)
|
||||
ordering_fields = ('nr', 'date')
|
||||
filter_class = InvoiceFilter
|
||||
filterset_class = InvoiceFilter
|
||||
permission = 'can_view_orders'
|
||||
lookup_url_kwarg = 'number'
|
||||
lookup_field = 'nr'
|
||||
write_permission = 'can_change_orders'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.invoices.prefetch_related('lines').select_related('order', 'refers').annotate(
|
||||
@@ -320,9 +698,54 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
invoice_pdf(invoice.pk)
|
||||
invoice.refresh_from_db()
|
||||
|
||||
if invoice.shredded:
|
||||
raise PermissionDenied('The invoice file is no longer stored on the server.')
|
||||
|
||||
if not invoice.file:
|
||||
raise RetryException()
|
||||
|
||||
resp = FileResponse(invoice.file.file, content_type='application/pdf')
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}.pdf"'.format(invoice.number)
|
||||
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 not in (Order.STATUS_CANCELED, Order.STATUS_REFUNDED):
|
||||
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 pretix.api.models import OAuthAccessToken
|
||||
from pretix.api.serializers.organizer import OrganizerSerializer
|
||||
from pretix.base.models import Organizer
|
||||
|
||||
@@ -11,9 +12,15 @@ class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
lookup_url_kwarg = 'organizer'
|
||||
|
||||
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):
|
||||
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:
|
||||
return Organizer.objects.filter(pk__in=self.request.user.teams.values_list('organizer', flat=True))
|
||||
else:
|
||||
|
||||
@@ -9,7 +9,6 @@ from rest_framework.filters import OrderingFilter
|
||||
|
||||
from pretix.api.serializers.voucher import VoucherSerializer
|
||||
from pretix.base.models import Voucher
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
|
||||
|
||||
class VoucherFilter(FilterSet):
|
||||
@@ -35,7 +34,7 @@ class VoucherViewSet(viewsets.ModelViewSet):
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||
ordering = ('id',)
|
||||
ordering_fields = ('id', 'code', 'max_usages', 'valid_until', 'value')
|
||||
filter_class = VoucherFilter
|
||||
filterset_class = VoucherFilter
|
||||
permission = 'can_view_vouchers'
|
||||
write_permission = 'can_change_vouchers'
|
||||
|
||||
@@ -51,7 +50,7 @@ class VoucherViewSet(viewsets.ModelViewSet):
|
||||
serializer.instance.log_action(
|
||||
'pretix.voucher.added',
|
||||
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
|
||||
)
|
||||
|
||||
@@ -69,7 +68,7 @@ class VoucherViewSet(viewsets.ModelViewSet):
|
||||
serializer.instance.log_action(
|
||||
'pretix.voucher.changed',
|
||||
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
|
||||
)
|
||||
|
||||
@@ -80,6 +79,6 @@ class VoucherViewSet(viewsets.ModelViewSet):
|
||||
instance.log_action(
|
||||
'pretix.voucher.deleted',
|
||||
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)
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import django_filters
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
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.response import Response
|
||||
|
||||
from pretix.api.serializers.waitinglist import WaitingListSerializer
|
||||
from pretix.base.models import WaitingListEntry
|
||||
from pretix.base.models.waitinglist import WaitingListException
|
||||
|
||||
|
||||
class WaitingListFilter(FilterSet):
|
||||
@@ -18,14 +22,61 @@ class WaitingListFilter(FilterSet):
|
||||
fields = ['item', 'variation', 'email', 'locale', 'has_voucher', 'subevent']
|
||||
|
||||
|
||||
class WaitingListViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
class WaitingListViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = WaitingListSerializer
|
||||
queryset = WaitingListEntry.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||
ordering = ('created',)
|
||||
ordering_fields = ('id', 'created', 'email', 'item')
|
||||
filter_class = WaitingListFilter
|
||||
filterset_class = WaitingListFilter
|
||||
permission = 'can_view_orders'
|
||||
write_permission = 'can_change_orders'
|
||||
|
||||
def get_queryset(self):
|
||||
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)
|
||||
|
||||
@@ -12,6 +12,7 @@ class PretixBaseConfig(AppConfig):
|
||||
from . import exporters # NOQA
|
||||
from . import invoice # NOQA
|
||||
from . import notifications # NOQA
|
||||
from . import email # NOQA
|
||||
from .services import auth, export, mail, tickets, cart, orders, invoices, cleanup, update_check, quotas, notifications # NOQA
|
||||
|
||||
try:
|
||||
|
||||
@@ -3,9 +3,9 @@ from decimal import ROUND_HALF_UP, Decimal
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def round_decimal(dec, currency=None):
|
||||
def round_decimal(dec, currency=None, places_dict=settings.CURRENCY_PLACES):
|
||||
if currency:
|
||||
places = settings.CURRENCY_PLACES.get(currency, 2)
|
||||
places = places_dict.get(currency, 2)
|
||||
return Decimal(dec).quantize(
|
||||
Decimal('1') / 10 ** places, ROUND_HALF_UP
|
||||
)
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
import logging
|
||||
from smtplib import SMTPRecipientsRefused, SMTPSenderRefused
|
||||
|
||||
import bleach
|
||||
import markdown
|
||||
from django.conf import settings
|
||||
from django.core.mail.backends.smtp import EmailBackend
|
||||
from django.dispatch import receiver
|
||||
from django.template.loader import get_template
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from inlinestyler.utils import inline_css
|
||||
|
||||
from pretix.base.models import Event, Order
|
||||
from pretix.base.signals import register_html_mail_renderers
|
||||
from pretix.base.templatetags.rich_text import markdown_compile
|
||||
|
||||
logger = logging.getLogger('pretix.base.email')
|
||||
|
||||
@@ -24,3 +35,103 @@ class CustomSMTPBackend(EmailBackend):
|
||||
raise SMTPRecipientsRefused(senderrs)
|
||||
finally:
|
||||
self.close()
|
||||
|
||||
|
||||
class BaseHTMLMailRenderer:
|
||||
"""
|
||||
This is the base class for all HTML e-mail renderers.
|
||||
"""
|
||||
|
||||
def __init__(self, event: Event):
|
||||
self.event = event
|
||||
|
||||
def __str__(self):
|
||||
return self.identifier
|
||||
|
||||
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order=None) -> str:
|
||||
"""
|
||||
This method should generate the HTML part of the email.
|
||||
|
||||
:param plain_body: The body of the email in plain text.
|
||||
:param plain_signature: The signature with event organizer contact details in plain text.
|
||||
:param subject: The email subject.
|
||||
:param order: The order if this email is connected to one, otherwise ``None``.
|
||||
:return: An HTML string
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def verbose_name(self) -> str:
|
||||
"""
|
||||
A human-readable name for this renderer. This should be short but self-explanatory.
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
"""
|
||||
A short and unique identifier for this renderer.
|
||||
This should only contain lowercase letters and in most cases will be the same as your package name or prefixed
|
||||
with your package name.
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
@property
|
||||
def thumbnail_filename(self) -> str:
|
||||
"""
|
||||
A file name discoverable in the static file storage that contains a preview of your renderer. This should
|
||||
be with aspect resolution 4:3.
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
@property
|
||||
def is_available(self) -> bool:
|
||||
"""
|
||||
This renderer will only be available if this returns ``True``. You can use this to limit this renderer
|
||||
to certain events. Defaults to ``True``.
|
||||
"""
|
||||
return True
|
||||
|
||||
|
||||
class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
||||
|
||||
@property
|
||||
def template_name(self):
|
||||
raise NotImplemented
|
||||
|
||||
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order) -> str:
|
||||
body_md = bleach.linkify(markdown_compile(plain_body))
|
||||
htmlctx = {
|
||||
'site': settings.PRETIX_INSTANCE_NAME,
|
||||
'site_url': settings.SITE_URL,
|
||||
'body': body_md,
|
||||
'subject': str(subject),
|
||||
'color': '#8E44B3'
|
||||
}
|
||||
if self.event:
|
||||
htmlctx['event'] = self.event
|
||||
htmlctx['color'] = self.event.settings.primary_color
|
||||
|
||||
if plain_signature:
|
||||
signature_md = plain_signature.replace('\n', '<br>\n')
|
||||
signature_md = bleach.linkify(bleach.clean(markdown.markdown(signature_md), tags=bleach.ALLOWED_TAGS + ['p', 'br']))
|
||||
htmlctx['signature'] = signature_md
|
||||
|
||||
if order:
|
||||
htmlctx['order'] = order
|
||||
|
||||
tpl = get_template(self.template_name)
|
||||
body_html = inline_css(tpl.render(htmlctx))
|
||||
return body_html
|
||||
|
||||
|
||||
class ClassicMailRenderer(TemplateBasedMailRenderer):
|
||||
verbose_name = _('pretix default')
|
||||
identifier = 'classic'
|
||||
thumbnail_filename = 'pretixbase/email/thumb.png'
|
||||
template_name = 'pretixbase/email/plainwrapper.html'
|
||||
|
||||
|
||||
@receiver(register_html_mail_renderers, dispatch_uid="pretixbase_email_renderers")
|
||||
def base_renderers(sender, **kwargs):
|
||||
return [ClassicMailRenderer]
|
||||
|
||||
@@ -5,9 +5,12 @@ from zipfile import ZipFile
|
||||
|
||||
import dateutil.parser
|
||||
from django import forms
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import OrderPayment
|
||||
|
||||
from ..exporter import BaseExporter
|
||||
from ..services.invoices import invoice_pdf_task
|
||||
from ..signals import register_data_exporters
|
||||
@@ -18,10 +21,17 @@ class InvoiceExporter(BaseExporter):
|
||||
verbose_name = _('All invoices')
|
||||
|
||||
def render(self, form_data: dict):
|
||||
qs = self.event.invoices.all()
|
||||
qs = self.event.invoices.filter(shredded=False)
|
||||
|
||||
if form_data.get('payment_provider'):
|
||||
qs = qs.filter(order__payment_provider=form_data.get('payment_provider'))
|
||||
qs = qs.annotate(
|
||||
has_payment_with_provider=Exists(
|
||||
OrderPayment.objects.filter(
|
||||
Q(order=OuterRef('pk')) & Q(provider=form_data.get('payment_provider'))
|
||||
)
|
||||
)
|
||||
)
|
||||
qs = qs.filter(has_payment_with_provider=1)
|
||||
|
||||
if form_data.get('date_from'):
|
||||
date_value = form_data.get('date_from')
|
||||
@@ -38,12 +48,19 @@ class InvoiceExporter(BaseExporter):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
|
||||
for i in qs:
|
||||
if not i.file:
|
||||
try:
|
||||
if not i.file:
|
||||
invoice_pdf_task.apply(args=(i.pk,))
|
||||
i.refresh_from_db()
|
||||
i.file.open('rb')
|
||||
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
|
||||
i.file.close()
|
||||
except FileNotFoundError:
|
||||
invoice_pdf_task.apply(args=(i.pk,))
|
||||
i.refresh_from_db()
|
||||
i.file.open('rb')
|
||||
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
|
||||
i.file.close()
|
||||
i.file.open('rb')
|
||||
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
|
||||
i.file.close()
|
||||
|
||||
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
|
||||
return '{}_invoices.zip'.format(self.event.slug), 'application/zip', zipf.read()
|
||||
@@ -77,10 +94,10 @@ class InvoiceExporter(BaseExporter):
|
||||
(k, v.verbose_name) for k, v in self.event.get_payment_providers().items()
|
||||
],
|
||||
required=False,
|
||||
help_text=_('Only include invoices for orders that are currently set to this payment provider. '
|
||||
'Note that this might include some invoices of other payment providers or misses '
|
||||
'some invoices if the payment provider of an order has been changed and a new invoice '
|
||||
'has been generated.')
|
||||
help_text=_('Only include invoices for orders that have at least one payment attempt '
|
||||
'with this payment provider. '
|
||||
'Note that this might include some invoices of orders which in the end have been '
|
||||
'fully or partially paid with a different provider.')
|
||||
)),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -24,13 +24,15 @@ class JSONExporter(BaseExporter):
|
||||
'categories': [
|
||||
{
|
||||
'id': category.id,
|
||||
'name': str(category.name)
|
||||
'name': str(category.name),
|
||||
'internal_name': category.internal_name
|
||||
} for category in self.event.categories.all()
|
||||
],
|
||||
'items': [
|
||||
{
|
||||
'id': item.id,
|
||||
'name': str(item.name),
|
||||
'internal_name': str(item.internal_name),
|
||||
'category': item.category_id,
|
||||
'price': item.default_price,
|
||||
'tax_rate': item.tax_rule.rate if item.tax_rule else Decimal('0.00'),
|
||||
|
||||
@@ -5,13 +5,13 @@ from decimal import Decimal
|
||||
import pytz
|
||||
from defusedcsv import csv
|
||||
from django import forms
|
||||
from django.db.models import Sum
|
||||
from django.db.models import DateTimeField, Max, OuterRef, Subquery, Sum
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import localize
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
from pretix.base.models import InvoiceAddress, Order, OrderPosition
|
||||
from pretix.base.models.orders import OrderFee
|
||||
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
|
||||
|
||||
from ..exporter import BaseExporter
|
||||
from ..signals import register_data_exporters
|
||||
@@ -55,7 +55,19 @@ class OrderListExporter(BaseExporter):
|
||||
tz = pytz.timezone(self.event.settings.timezone)
|
||||
writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC, delimiter=",")
|
||||
|
||||
qs = self.event.orders.all().select_related('invoice_address').prefetch_related('invoices')
|
||||
p_date = OrderPayment.objects.filter(
|
||||
order=OuterRef('pk'),
|
||||
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
|
||||
payment_date__isnull=False
|
||||
).values('order').annotate(
|
||||
m=Max('payment_date')
|
||||
).values(
|
||||
'm'
|
||||
).order_by()
|
||||
|
||||
qs = self.event.orders.annotate(
|
||||
payment_date=Subquery(p_date, output_field=DateTimeField())
|
||||
).select_related('invoice_address').prefetch_related('invoices')
|
||||
if form_data['paid_only']:
|
||||
qs = qs.filter(status=Order.STATUS_PAID)
|
||||
tax_rates = self._get_all_tax_rates(qs)
|
||||
@@ -63,7 +75,7 @@ class OrderListExporter(BaseExporter):
|
||||
headers = [
|
||||
_('Order code'), _('Order total'), _('Status'), _('Email'), _('Order date'),
|
||||
_('Company'), _('Name'), _('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
|
||||
_('Payment date'), _('Payment type'), _('Fees'),
|
||||
_('Date of last payment'), _('Fees'), _('Order locale')
|
||||
]
|
||||
|
||||
for tr in tax_rates:
|
||||
@@ -77,11 +89,6 @@ class OrderListExporter(BaseExporter):
|
||||
|
||||
writer.writerow(headers)
|
||||
|
||||
provider_names = {
|
||||
k: v.verbose_name
|
||||
for k, v in self.event.get_payment_providers().items()
|
||||
}
|
||||
|
||||
full_fee_sum_cache = {
|
||||
o['order__id']: o['grosssum'] for o in
|
||||
OrderFee.objects.values('tax_rate', 'order__id').order_by().annotate(grosssum=Sum('value'))
|
||||
@@ -114,7 +121,8 @@ class OrderListExporter(BaseExporter):
|
||||
order.invoice_address.street,
|
||||
order.invoice_address.zipcode,
|
||||
order.invoice_address.city,
|
||||
order.invoice_address.country if order.invoice_address.country else order.invoice_address.country_old,
|
||||
order.invoice_address.country if order.invoice_address.country else
|
||||
order.invoice_address.country_old,
|
||||
order.invoice_address.vat_id,
|
||||
]
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
@@ -122,13 +130,14 @@ class OrderListExporter(BaseExporter):
|
||||
|
||||
row += [
|
||||
order.payment_date.astimezone(tz).strftime('%Y-%m-%d') if order.payment_date else '',
|
||||
provider_names.get(order.payment_provider, order.payment_provider),
|
||||
localize(full_fee_sum_cache.get(order.id) or Decimal('0.00'))
|
||||
localize(full_fee_sum_cache.get(order.id) or Decimal('0.00')),
|
||||
order.locale,
|
||||
]
|
||||
|
||||
for tr in tax_rates:
|
||||
taxrate_values = sum_cache.get((order.id, tr), {'grosssum': Decimal('0.00'), 'taxsum': Decimal('0.00')})
|
||||
fee_taxrate_values = fee_sum_cache.get((order.id, tr), {'grosssum': Decimal('0.00'), 'taxsum': Decimal('0.00')})
|
||||
fee_taxrate_values = fee_sum_cache.get((order.id, tr),
|
||||
{'grosssum': Decimal('0.00'), 'taxsum': Decimal('0.00')})
|
||||
|
||||
row += [
|
||||
localize(taxrate_values['grosssum'] + fee_taxrate_values['grosssum']),
|
||||
@@ -143,6 +152,77 @@ class OrderListExporter(BaseExporter):
|
||||
return '{}_orders.csv'.format(self.event.slug), 'text/csv', output.getvalue().encode("utf-8")
|
||||
|
||||
|
||||
class PaymentListExporter(BaseExporter):
|
||||
identifier = 'paymentlistcsv'
|
||||
verbose_name = ugettext_lazy('List of payments and refunds (CSV)')
|
||||
|
||||
@property
|
||||
def export_form_fields(self):
|
||||
return OrderedDict(
|
||||
[
|
||||
('successful_only',
|
||||
forms.BooleanField(
|
||||
label=_('Only successful payments'),
|
||||
initial=True,
|
||||
required=False
|
||||
)),
|
||||
]
|
||||
)
|
||||
|
||||
def render(self, form_data: dict):
|
||||
output = io.StringIO()
|
||||
tz = pytz.timezone(self.event.settings.timezone)
|
||||
writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC, delimiter=",")
|
||||
|
||||
provider_names = {
|
||||
k: v.verbose_name
|
||||
for k, v in self.event.get_payment_providers().items()
|
||||
}
|
||||
|
||||
payments = OrderPayment.objects.filter(
|
||||
order__event=self.event,
|
||||
).order_by('created')
|
||||
refunds = OrderRefund.objects.filter(
|
||||
order__event=self.event
|
||||
).order_by('created')
|
||||
|
||||
if form_data['successful_only']:
|
||||
payments = payments.filter(
|
||||
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
|
||||
)
|
||||
refunds = refunds.filter(
|
||||
state=OrderRefund.REFUND_STATE_DONE,
|
||||
)
|
||||
|
||||
objs = sorted(list(payments) + list(refunds), key=lambda o: o.created)
|
||||
|
||||
headers = [
|
||||
_('Order'), _('Payment ID'), _('Creation date'), _('Completion date'), _('Status'),
|
||||
_('Amount'), _('Payment method')
|
||||
]
|
||||
writer.writerow(headers)
|
||||
|
||||
for obj in objs:
|
||||
if isinstance(obj, OrderPayment) and obj.payment_date:
|
||||
d2 = obj.payment_date.astimezone(tz).date().strftime('%Y-%m-%d')
|
||||
elif isinstance(obj, OrderRefund) and obj.execution_date:
|
||||
d2 = obj.execution_date.astimezone(tz).date().strftime('%Y-%m-%d')
|
||||
else:
|
||||
d2 = ''
|
||||
row = [
|
||||
obj.order.code,
|
||||
obj.full_id,
|
||||
obj.created.astimezone(tz).date().strftime('%Y-%m-%d'),
|
||||
d2,
|
||||
obj.get_state_display(),
|
||||
localize(obj.amount * (-1 if isinstance(obj, OrderRefund) else 1)),
|
||||
provider_names.get(obj.provider, obj.provider)
|
||||
]
|
||||
writer.writerow(row)
|
||||
|
||||
return '{}_payments.csv'.format(self.event.slug), 'text/csv', output.getvalue().encode("utf-8")
|
||||
|
||||
|
||||
class QuotaListExporter(BaseExporter):
|
||||
identifier = 'quotalistcsv'
|
||||
verbose_name = ugettext_lazy('Quota availabilities (CSV)')
|
||||
@@ -179,6 +259,11 @@ def register_orderlist_exporter(sender, **kwargs):
|
||||
return OrderListExporter
|
||||
|
||||
|
||||
@receiver(register_data_exporters, dispatch_uid="exporter_paymentlist")
|
||||
def register_paymentlist_exporter(sender, **kwargs):
|
||||
return PaymentListExporter
|
||||
|
||||
|
||||
@receiver(register_data_exporters, dispatch_uid="exporter_quotalist")
|
||||
def register_quotalist_exporter(sender, **kwargs):
|
||||
return QuotaListExporter
|
||||
|
||||
@@ -39,7 +39,7 @@ class LoginForm(forms.Form):
|
||||
password = self.cleaned_data.get('password')
|
||||
|
||||
if email and password:
|
||||
self.user_cache = authenticate(email=email.lower(), password=password)
|
||||
self.user_cache = authenticate(request=self.request, email=email.lower(), password=password)
|
||||
if self.user_cache is None:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['invalid_login'],
|
||||
@@ -180,12 +180,4 @@ class PasswordForgotForm(forms.Form):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean_email(self):
|
||||
email = self.cleaned_data['email']
|
||||
try:
|
||||
self.cleaned_data['user'] = User.objects.get(email=email)
|
||||
return email
|
||||
except User.DoesNotExist:
|
||||
raise forms.ValidationError(
|
||||
_("We are unable to find a user matching the data you provided."),
|
||||
code='unknown_user'
|
||||
)
|
||||
return self.cleaned_data['email']
|
||||
|
||||
@@ -199,6 +199,16 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
|
||||
if event.settings.invoice_name_required:
|
||||
self.fields['name'].required = True
|
||||
elif event.settings.invoice_address_company_required:
|
||||
self.initial['is_business'] = True
|
||||
|
||||
self.fields['is_business'].widget = BusinessBooleanRadio(require_business=True)
|
||||
self.fields['company'].required = True
|
||||
self.fields['company'].widget.is_required = True
|
||||
self.fields['company'].widget.attrs['required'] = 'required'
|
||||
del self.fields['company'].widget.attrs['data-display-dependency']
|
||||
if 'vat_id' in self.fields:
|
||||
del self.fields['vat_id'].widget.attrs['data-display-dependency']
|
||||
else:
|
||||
self.fields['company'].widget.attrs['data-required-if'] = '#id_is_business_1'
|
||||
self.fields['name'].widget.attrs['data-required-if'] = '#id_is_business_0'
|
||||
@@ -242,3 +252,12 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
'resolve this manually.'))
|
||||
else:
|
||||
self.instance.vat_id_validated = False
|
||||
|
||||
|
||||
class BaseInvoiceNameForm(BaseInvoiceAddressForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for f in list(self.fields.keys()):
|
||||
if f != 'name':
|
||||
del self.fields[f]
|
||||
|
||||
@@ -29,7 +29,7 @@ class PlaceholderValidator(BaseValidator):
|
||||
code='invalid',
|
||||
)
|
||||
|
||||
data_placeholders = list(re.findall(r'({[\w\s]*})', value, re.X))
|
||||
data_placeholders = list(re.findall(r'({[^}]*})', value, re.X))
|
||||
invalid_placeholders = []
|
||||
for placeholder in data_placeholders:
|
||||
if placeholder not in self.limit_value:
|
||||
|
||||
@@ -110,14 +110,22 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
|
||||
|
||||
|
||||
class BusinessBooleanRadio(forms.RadioSelect):
|
||||
def __init__(self, attrs=None):
|
||||
choices = (
|
||||
('individual', _('Individual customer')),
|
||||
('business', _('Business customer')),
|
||||
)
|
||||
def __init__(self, require_business=False, attrs=None):
|
||||
self.require_business = require_business
|
||||
if self.require_business:
|
||||
choices = (
|
||||
('business', _('Business customer')),
|
||||
)
|
||||
else:
|
||||
choices = (
|
||||
('individual', _('Individual customer')),
|
||||
('business', _('Business customer')),
|
||||
)
|
||||
super().__init__(attrs, choices)
|
||||
|
||||
def format_value(self, value):
|
||||
if self.require_business:
|
||||
return 'business'
|
||||
try:
|
||||
return {True: 'business', False: 'individual'}[value]
|
||||
except KeyError:
|
||||
@@ -125,6 +133,8 @@ class BusinessBooleanRadio(forms.RadioSelect):
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
value = data.get(name)
|
||||
if self.require_business:
|
||||
return True
|
||||
return {
|
||||
'business': True,
|
||||
True: True,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from decimal import Decimal
|
||||
from io import BytesIO
|
||||
@@ -8,6 +9,7 @@ from django.contrib.staticfiles import finders
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import date_format, localize
|
||||
from django.utils.translation import pgettext
|
||||
from PIL.Image import BICUBIC
|
||||
from reportlab.lib import pagesizes
|
||||
from reportlab.lib.enums import TA_LEFT
|
||||
from reportlab.lib.styles import ParagraphStyle, StyleSheet1
|
||||
@@ -26,6 +28,8 @@ from pretix.base.models import Event, Invoice
|
||||
from pretix.base.signals import register_invoice_renderers
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseInvoiceRenderer:
|
||||
"""
|
||||
@@ -178,6 +182,19 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
||||
return 'invoice.pdf', 'application/pdf', buffer.read()
|
||||
|
||||
|
||||
class ThumbnailingImageReader(ImageReader):
|
||||
def resize(self, width, height, dpi):
|
||||
if width is None:
|
||||
width = height * self._image.size[0] / self._image.size[1]
|
||||
if height is None:
|
||||
height = width * self._image.size[1] / self._image.size[0]
|
||||
self._image.thumbnail(
|
||||
size=(int(width * dpi / 72), int(height * dpi / 72)),
|
||||
resample=BICUBIC
|
||||
)
|
||||
return width, height
|
||||
|
||||
|
||||
class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
identifier = 'classic'
|
||||
verbose_name = pgettext('invoice', 'Classic renderer (pretix 1.0)')
|
||||
@@ -192,6 +209,18 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
|
||||
canvas.restoreState()
|
||||
|
||||
def _draw_invoice_to(self, canvas):
|
||||
p = Paragraph(self.invoice.invoice_to.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
||||
p.wrapOn(canvas, 85 * mm, 50 * mm)
|
||||
p_size = p.wrap(85 * mm, 50 * mm)
|
||||
p.drawOn(canvas, 25 * mm, (297 - 52) * mm - p_size[1])
|
||||
|
||||
def _draw_invoice_from(self, canvas):
|
||||
p = Paragraph(self.invoice.invoice_from.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
||||
p.wrapOn(canvas, 70 * mm, 50 * mm)
|
||||
p_size = p.wrap(70 * mm, 50 * mm)
|
||||
p.drawOn(canvas, 25 * mm, (297 - 17) * mm - p_size[1])
|
||||
|
||||
def _on_first_page(self, canvas: Canvas, doc):
|
||||
canvas.setCreator('pretix.eu')
|
||||
canvas.setTitle(pgettext('invoice', 'Invoice {num}').format(num=self.invoice.number))
|
||||
@@ -208,20 +237,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
textobject.textLine(pgettext('invoice', 'Invoice from').upper())
|
||||
canvas.drawText(textobject)
|
||||
|
||||
p = Paragraph(self.invoice.invoice_from.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
||||
p.wrapOn(canvas, 70 * mm, 50 * mm)
|
||||
p_size = p.wrap(70 * mm, 50 * mm)
|
||||
p.drawOn(canvas, 25 * mm, (297 - 17) * mm - p_size[1])
|
||||
self._draw_invoice_from(canvas)
|
||||
|
||||
textobject = canvas.beginText(25 * mm, (297 - 50) * mm)
|
||||
textobject.setFont('OpenSansBd', 8)
|
||||
textobject.textLine(pgettext('invoice', 'Invoice to').upper())
|
||||
canvas.drawText(textobject)
|
||||
|
||||
p = Paragraph(self.invoice.invoice_to.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
||||
p.wrapOn(canvas, 85 * mm, 50 * mm)
|
||||
p_size = p.wrap(85 * mm, 50 * mm)
|
||||
p.drawOn(canvas, 25 * mm, (297 - 52) * mm - p_size[1])
|
||||
self._draw_invoice_to(canvas)
|
||||
|
||||
textobject = canvas.beginText(125 * mm, (297 - 38) * mm)
|
||||
textobject.setFont('OpenSansBd', 8)
|
||||
@@ -276,22 +299,42 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
|
||||
if self.invoice.event.settings.invoice_logo_image:
|
||||
logo_file = self.invoice.event.settings.get('invoice_logo_image', binary_file=True)
|
||||
canvas.drawImage(ImageReader(logo_file),
|
||||
ir = ThumbnailingImageReader(logo_file)
|
||||
try:
|
||||
ir.resize(25 * mm, 25 * mm, 300)
|
||||
except:
|
||||
logger.exception("Can not resize image")
|
||||
pass
|
||||
canvas.drawImage(ir,
|
||||
95 * mm, (297 - 38) * mm,
|
||||
width=25 * mm, height=25 * mm,
|
||||
preserveAspectRatio=True, anchor='n',
|
||||
mask='auto')
|
||||
|
||||
if self.invoice.event.settings.show_date_to:
|
||||
p_str = (
|
||||
str(self.invoice.event.name) + '\n' + pgettext('invoice', '{from_date}\nuntil {to_date}').format(
|
||||
from_date=self.invoice.event.get_date_from_display(),
|
||||
to_date=self.invoice.event.get_date_to_display())
|
||||
)
|
||||
def shorten(txt):
|
||||
txt = str(txt)
|
||||
p = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
||||
p_size = p.wrap(65 * mm, 50 * mm)
|
||||
|
||||
while p_size[1] > 2 * self.stylesheet['Normal'].leading:
|
||||
txt = ' '.join(txt.replace('…', '').split()[:-1]) + '…'
|
||||
p = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
||||
p_size = p.wrap(65 * mm, 50 * mm)
|
||||
return txt
|
||||
|
||||
if not self.invoice.event.has_subevents:
|
||||
if self.invoice.event.settings.show_date_to:
|
||||
p_str = (
|
||||
shorten(self.invoice.event.name) + '\n' + pgettext('invoice', '{from_date}\nuntil {to_date}').format(
|
||||
from_date=self.invoice.event.get_date_from_display(),
|
||||
to_date=self.invoice.event.get_date_to_display())
|
||||
)
|
||||
else:
|
||||
p_str = (
|
||||
shorten(self.invoice.event.name) + '\n' + self.invoice.event.get_date_from_display()
|
||||
)
|
||||
else:
|
||||
p_str = (
|
||||
str(self.invoice.event.name) + '\n' + self.invoice.event.get_date_from_display()
|
||||
)
|
||||
p_str = shorten(self.invoice.event.name)
|
||||
|
||||
p = Paragraph(p_str.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
||||
p.wrapOn(canvas, 65 * mm, 50 * mm)
|
||||
|
||||
@@ -8,10 +8,10 @@ class Command(BaseCommand):
|
||||
help = "Rebuild static files and language files"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
call_command('compilemessages', verbosity=1, interactive=False)
|
||||
call_command('compilejsi18n', verbosity=1, interactive=False)
|
||||
call_command('compilemessages', verbosity=1)
|
||||
call_command('compilejsi18n', verbosity=1)
|
||||
call_command('collectstatic', verbosity=1, interactive=False)
|
||||
call_command('compress', verbosity=1, interactive=False)
|
||||
call_command('compress', verbosity=1)
|
||||
try:
|
||||
gs = GlobalSettingsObject()
|
||||
del gs.settings.update_check_last
|
||||
|
||||
@@ -3,8 +3,8 @@ from urllib.parse import urlsplit
|
||||
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import get_script_prefix
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.urls import get_script_prefix
|
||||
from django.utils import timezone, translation
|
||||
from django.utils.cache import patch_vary_headers
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
@@ -172,6 +172,12 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
return resp
|
||||
|
||||
resp['X-XSS-Protection'] = '1'
|
||||
|
||||
# We just need to have a P3P, not matter whats in there
|
||||
# https://blogs.msdn.microsoft.com/ieinternals/2013/09/17/a-quick-look-at-p3p/
|
||||
# https://github.com/pretix/pretix/issues/765
|
||||
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
|
||||
|
||||
h = {
|
||||
'default-src': ["{static}"],
|
||||
'script-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
|
||||
|
||||
@@ -0,0 +1,424 @@
|
||||
# Generated by Django 2.0.8 on 2018-09-11 14:50
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
from django.db.models import F
|
||||
from django.db.models.functions import Concat
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
import pretix.base.models.auth
|
||||
import pretix.base.validators
|
||||
from pretix.base.i18n import language
|
||||
|
||||
|
||||
def create_checkin_lists(apps, schema_editor):
|
||||
Event = apps.get_model('pretixbase', 'Event')
|
||||
Checkin = apps.get_model('pretixbase', 'Checkin')
|
||||
EventSettingsStore = apps.get_model('pretixbase', 'Event_SettingsStore')
|
||||
for e in Event.objects.all():
|
||||
locale = EventSettingsStore.objects.filter(object=e, key='locale').first()
|
||||
if locale:
|
||||
locale = locale.value
|
||||
else:
|
||||
locale = settings.LANGUAGE_CODE
|
||||
|
||||
if e.has_subevents:
|
||||
for se in e.subevents.all():
|
||||
with language(locale):
|
||||
cl = e.checkin_lists.create(name=se.name, subevent=se, all_products=True)
|
||||
Checkin.objects.filter(position__subevent=se, position__order__event=e).update(list=cl)
|
||||
else:
|
||||
with language(locale):
|
||||
cl = e.checkin_lists.create(name=_('Default list'), all_products=True)
|
||||
Checkin.objects.filter(position__order__event=e).update(list=cl)
|
||||
|
||||
|
||||
def set_full_invoice_no(app, schema_editor):
|
||||
Invoice = app.get_model('pretixbase', 'Invoice')
|
||||
Invoice.objects.all().update(
|
||||
full_invoice_no=Concat(F('prefix'), F('invoice_no'))
|
||||
)
|
||||
|
||||
|
||||
def set_position(apps, schema_editor):
|
||||
Question = apps.get_model('pretixbase', 'Question')
|
||||
for q in Question.objects.all():
|
||||
for i, option in enumerate(q.options.all()):
|
||||
option.position = i
|
||||
option.save()
|
||||
|
||||
|
||||
def set_is_staff(apps, schema_editor):
|
||||
User = apps.get_model('pretixbase', 'User')
|
||||
User.objects.filter(is_superuser=True).update(is_staff=True)
|
||||
|
||||
|
||||
def set_identifiers(apps, schema_editor):
|
||||
Question = apps.get_model('pretixbase', 'Question')
|
||||
QuestionOption = apps.get_model('pretixbase', 'QuestionOption')
|
||||
|
||||
for q in Question.objects.select_related('event'):
|
||||
if not q.identifier:
|
||||
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
|
||||
while True:
|
||||
code = get_random_string(length=8, allowed_chars=charset)
|
||||
if not Question.objects.filter(event=q.event, identifier=code).exists():
|
||||
q.identifier = code
|
||||
q.save()
|
||||
break
|
||||
|
||||
for q in QuestionOption.objects.select_related('question', 'question__event'):
|
||||
if not q.identifier:
|
||||
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
|
||||
while True:
|
||||
code = get_random_string(length=8, allowed_chars=charset)
|
||||
if not QuestionOption.objects.filter(question__event=q.question.event, identifier=code).exists():
|
||||
q.identifier = code
|
||||
q.save()
|
||||
break
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
replaces = [('pretixbase', '0077_auto_20171124_1629'), ('pretixbase', '0078_auto_20171206_1603'),
|
||||
('pretixbase', '0079_auto_20180115_0855'), ('pretixbase', '0080_question_ask_during_checkin'),
|
||||
('pretixbase', '0081_auto_20180220_1031'), ('pretixbase', '0082_auto_20180222_0938'),
|
||||
('pretixbase', '0083_auto_20180228_2102'), ('pretixbase', '0084_questionoption_position'),
|
||||
('pretixbase', '0085_auto_20180312_1119'), ('pretixbase', '0086_auto_20180320_1219'),
|
||||
('pretixbase', '0087_auto_20180317_1952'), ('pretixbase', '0088_auto_20180328_1217')]
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0076_orderfee_squashed_0082_invoiceaddress_internal_reference'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='slug',
|
||||
field=models.SlugField(
|
||||
help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes, and must be '
|
||||
'unique among your events. We recommend some kind of abbreviation or a date with less than '
|
||||
'10 characters that can be easily remembered, but you can also choose to use a random '
|
||||
'value. This will be used in URLs, order codes, invoice numbers, and bank transfer '
|
||||
'references.',
|
||||
validators=[django.core.validators.RegexValidator(
|
||||
message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'),
|
||||
pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Short form'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventmetaproperty',
|
||||
name='name',
|
||||
field=models.CharField(db_index=True,
|
||||
help_text='Can not contain spaces or special characters except underscores',
|
||||
max_length=50, validators=[django.core.validators.RegexValidator(
|
||||
message='The property name may only contain letters, numbers and underscores.',
|
||||
regex='^[a-zA-Z0-9_]+$')], verbose_name='Name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='organizer',
|
||||
name='slug',
|
||||
field=models.SlugField(
|
||||
help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can '
|
||||
'only be used once. This is being used in URLs to refer to your organizer accounts and your'
|
||||
' events.',
|
||||
validators=[django.core.validators.RegexValidator(
|
||||
message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'),
|
||||
pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Short form'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CheckinList',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=190)),
|
||||
('all_products',
|
||||
models.BooleanField(default=True, verbose_name='All products (including newly created ones)')),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checkin_lists',
|
||||
to='pretixbase.Event')),
|
||||
('subevent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
|
||||
to='pretixbase.SubEvent', verbose_name='Date')),
|
||||
('limit_products',
|
||||
models.ManyToManyField(blank=True, to='pretixbase.Item', verbose_name='Limit to products')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='checkin',
|
||||
name='list',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='checkins', to='pretixbase.CheckinList'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=create_checkin_lists,
|
||||
reverse_code=django.db.migrations.operations.special.RunPython.noop,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='checkin',
|
||||
name='list',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='checkins',
|
||||
to='pretixbase.CheckinList'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='NotificationSetting',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('action_type', models.CharField(max_length=255)),
|
||||
('method', models.CharField(choices=[('mail', 'E-mail')], max_length=255)),
|
||||
('event', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
|
||||
to='pretixbase.Event')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
('enabled', models.BooleanField(default=True)),
|
||||
],
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='notificationsetting',
|
||||
unique_together={('user', 'action_type', 'event', 'method')},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='logentry',
|
||||
name='visible',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notificationsetting',
|
||||
name='event',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='notification_settings', to='pretixbase.Event'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notificationsetting',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notification_settings',
|
||||
to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='notifications_send',
|
||||
field=models.BooleanField(default=True, help_text='If turned off, you will not get any notifications.',
|
||||
verbose_name='Receive notifications according to my settings below'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='notifications_token',
|
||||
field=models.CharField(default=pretix.base.models.auth.generate_notifications_token, max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='full_invoice_no',
|
||||
field=models.CharField(db_index=True, default='', max_length=190),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='question',
|
||||
name='type',
|
||||
field=models.CharField(
|
||||
choices=[('N', 'Number'), ('S', 'Text (one line)'), ('T', 'Multiline text'), ('B', 'Yes/No'),
|
||||
('C', 'Choose one from a list'), ('M', 'Choose multiple from a list'), ('F', 'File upload'),
|
||||
('D', 'Date'), ('H', 'Time'), ('W', 'Date and time')], max_length=5,
|
||||
verbose_name='Question type'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=set_full_invoice_no,
|
||||
reverse_code=django.db.migrations.operations.special.RunPython.noop,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='question',
|
||||
name='ask_during_checkin',
|
||||
field=models.BooleanField(default=False,
|
||||
help_text='This will only work if you handle your check-in with pretixdroid 1.8 '
|
||||
'or '
|
||||
'newer or pretixdesk 0.2 or newer.',
|
||||
verbose_name='Ask during check-in instead of in the ticket buying process'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='checkinlist',
|
||||
name='include_pending',
|
||||
field=models.BooleanField(default=False,
|
||||
help_text='With this option, people will be able to check in even if the order '
|
||||
'have '
|
||||
'not been paid. This only works with pretixdesk 0.3.0 or newer or '
|
||||
'pretixdroid 1.9 or newer.',
|
||||
verbose_name='Include pending orders'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='presale_end',
|
||||
field=models.DateTimeField(blank=True,
|
||||
help_text='Optional. No products will be sold after this date. If you do not '
|
||||
'set '
|
||||
'this value, the presale will end after the end date of your event.',
|
||||
null=True, verbose_name='End of presale'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='logentry',
|
||||
name='event',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL,
|
||||
to='pretixbase.Event'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='subevent',
|
||||
name='presale_end',
|
||||
field=models.DateTimeField(blank=True,
|
||||
help_text='Optional. No products will be sold after this date. If you do not '
|
||||
'set '
|
||||
'this value, the presale will end after the end date of your event.',
|
||||
null=True, verbose_name='End of presale'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='require_2fa',
|
||||
field=models.BooleanField(default=False, verbose_name='Two-factor authentification is required to log in'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='checkin_attention',
|
||||
field=models.BooleanField(default=False,
|
||||
help_text='If you set this, the check-in app will show a visible warning that '
|
||||
'tickets of this order require special attention. This will not show '
|
||||
'any '
|
||||
'details or custom message, so you need to brief your check-in staff '
|
||||
'how '
|
||||
'to handle these cases.',
|
||||
verbose_name='Requires special attention'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='taxrule',
|
||||
name='custom_rules',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='orderfee',
|
||||
name='fee_type',
|
||||
field=models.CharField(
|
||||
choices=[('payment', 'Payment fee'), ('shipping', 'Shipping fee'), ('service', 'Service fee'),
|
||||
('other', 'Other fees')], max_length=100),
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='questionoption',
|
||||
options={'ordering': ('position', 'id'), 'verbose_name': 'Question option',
|
||||
'verbose_name_plural': 'Question options'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='questionoption',
|
||||
name='position',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='question',
|
||||
name='position',
|
||||
field=models.PositiveIntegerField(default=0, verbose_name='Position'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=set_position,
|
||||
reverse_code=django.db.migrations.operations.special.RunPython.noop,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='question',
|
||||
name='identifier',
|
||||
field=models.CharField(default='', max_length=190),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='questionoption',
|
||||
name='identifier',
|
||||
field=models.CharField(default='', max_length=190),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='locale',
|
||||
field=models.CharField(
|
||||
choices=[('en', 'English'), ('de', 'German'), ('de-informal', 'German (informal)'), ('nl', 'Dutch'),
|
||||
('da', 'Danish'), ('pt-br', 'Portuguese (Brazil)')], default='en', max_length=50,
|
||||
verbose_name='Language'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=set_identifiers,
|
||||
reverse_code=django.db.migrations.operations.special.RunPython.noop,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cachedcombinedticket',
|
||||
name='file',
|
||||
field=models.FileField(blank=True, max_length=255, null=True,
|
||||
upload_to=pretix.base.models.orders.cachedcombinedticket_name),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cachedticket',
|
||||
name='file',
|
||||
field=models.FileField(blank=True, max_length=255, null=True,
|
||||
upload_to=pretix.base.models.orders.cachedticket_name),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoice',
|
||||
name='file',
|
||||
field=models.FileField(blank=True, max_length=255, null=True,
|
||||
upload_to=pretix.base.models.invoices.invoice_filename),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='question',
|
||||
name='identifier',
|
||||
field=models.CharField(
|
||||
help_text='You can enter any value here to make it easier to match the data with other sources. If '
|
||||
'you do '
|
||||
'not input one, we will generate one automatically.',
|
||||
max_length=190, verbose_name='Internal identifier'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='questionanswer',
|
||||
name='file',
|
||||
field=models.FileField(blank=True, max_length=255, null=True,
|
||||
upload_to=pretix.base.models.orders.answerfile_name),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=set_is_staff,
|
||||
reverse_code=django.db.migrations.operations.special.RunPython.noop,
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='is_superuser',
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='StaffSession',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date_start', models.DateTimeField(auto_now_add=True)),
|
||||
('date_end', models.DateTimeField(blank=True, null=True)),
|
||||
('session_key', models.CharField(max_length=255)),
|
||||
('comment', models.TextField()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='StaffSessionAuditLog',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('datetime', models.DateTimeField(auto_now_add=True)),
|
||||
('url', models.CharField(max_length=255)),
|
||||
('session', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='logs',
|
||||
to='pretixbase.StaffSession')),
|
||||
('impersonating', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL)),
|
||||
('method', models.CharField(default='GET', max_length=255)),
|
||||
],
|
||||
options={
|
||||
'ordering': ('datetime',),
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='staffsession',
|
||||
name='user',
|
||||
field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='staffsession',
|
||||
options={'ordering': ('date_start',)},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='item',
|
||||
name='picture',
|
||||
field=models.ImageField(blank=True, max_length=255, null=True,
|
||||
upload_to=pretix.base.models.items.itempicture_upload_to,
|
||||
verbose_name='Product picture'),
|
||||
),
|
||||
]
|
||||
26
src/pretix/base/migrations/0089_auto_20180315_1322.py
Normal file
26
src/pretix/base/migrations/0089_auto_20180315_1322.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.10 on 2018-03-15 13:22
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0088_auto_20180328_1217'),
|
||||
('pretixapi', '0001_initial')
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='logentry',
|
||||
name='shredded',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='shredded',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
29
src/pretix/base/migrations/0090_auto_20180509_0917.py
Normal file
29
src/pretix/base/migrations/0090_auto_20180509_0917.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2018-05-09 09:17
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('pretixbase', '0089_auto_20180315_1322'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='internal_name',
|
||||
field=models.CharField(blank=True,
|
||||
help_text='If you set this, this will be used instead of the public name in the '
|
||||
'backend.',
|
||||
max_length=255, null=True, verbose_name='Internal name'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='itemcategory',
|
||||
name='internal_name',
|
||||
field=models.CharField(blank=True,
|
||||
help_text='If you set this, this will be used instead of the public name in the backend.',
|
||||
max_length=255, null=True, verbose_name='Internal name'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,85 @@
|
||||
# Generated by Django 2.0.8 on 2018-09-11 14:54
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
from django.utils.crypto import get_random_string
|
||||
|
||||
|
||||
def set_pids(apps, schema_editor):
|
||||
OrderPosition = apps.get_model('pretixbase', 'OrderPosition') # noqa
|
||||
taken = set()
|
||||
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
|
||||
for op in OrderPosition.objects.iterator():
|
||||
while True:
|
||||
code = get_random_string(length=10, allowed_chars=charset)
|
||||
if code not in taken:
|
||||
op.pseudonymization_id = code
|
||||
taken.add(code)
|
||||
break
|
||||
op.save(update_fields=['pseudonymization_id'])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
replaces = [('pretixbase', '0090_auto_20180509_0917'), ('pretixbase', '0091_auto_20180513_1641'),
|
||||
('pretixbase', '0092_auto_20180511_1224'), ('pretixbase', '0093_auto_20180528_1432'),
|
||||
('pretixbase', '0094_auto_20180604_1119'), ('pretixbase', '0095_auto_20180604_1129')]
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0089_auto_20180315_1322'),
|
||||
('pretixapi', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='internal_name',
|
||||
field=models.CharField(blank=True,
|
||||
help_text='If you set this, this will be used instead of the public name in the '
|
||||
'backend.',
|
||||
max_length=255, null=True, verbose_name='Internal name'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='itemcategory',
|
||||
name='internal_name',
|
||||
field=models.CharField(blank=True,
|
||||
help_text='If you set this, this will be used instead of the public name in the '
|
||||
'backend.',
|
||||
max_length=255, null=True, verbose_name='Internal name'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='last_modified',
|
||||
field=models.DateTimeField(auto_now=True, db_index=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='original_price',
|
||||
field=models.DecimalField(blank=True, decimal_places=2,
|
||||
help_text='If set, this will be displayed next to the current price to show '
|
||||
'that the current price is a discounted one. This is just a cosmetic '
|
||||
'setting and will not actually impact pricing.',
|
||||
max_digits=7, null=True, verbose_name='Original price'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='pseudonymization_id',
|
||||
field=models.CharField(db_index=True, max_length=16, null=True, unique=True),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=set_pids,
|
||||
reverse_code=django.db.migrations.operations.special.RunPython.noop,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='orderposition',
|
||||
name='pseudonymization_id',
|
||||
field=models.CharField(db_index=True, default='', max_length=16, unique=True),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='logentry',
|
||||
name='oauth_application',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT,
|
||||
to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
|
||||
),
|
||||
]
|
||||
20
src/pretix/base/migrations/0091_auto_20180513_1641.py
Normal file
20
src/pretix/base/migrations/0091_auto_20180513_1641.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2018-05-13 16:41
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0090_auto_20180509_0917'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='last_modified',
|
||||
field=models.DateTimeField(auto_now=True, db_index=True),
|
||||
),
|
||||
]
|
||||
23
src/pretix/base/migrations/0092_auto_20180511_1224.py
Normal file
23
src/pretix/base/migrations/0092_auto_20180511_1224.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2018-05-11 12:24
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('pretixbase', '0091_auto_20180513_1641'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='original_price',
|
||||
field=models.DecimalField(blank=True, decimal_places=2,
|
||||
help_text='If set, this will be displayed next to the current price to show '
|
||||
'that the current price is a discounted one. This is just a cosmetic '
|
||||
'setting and will not actually impact pricing.',
|
||||
max_digits=7, null=True, verbose_name='Original price'),
|
||||
),
|
||||
]
|
||||
44
src/pretix/base/migrations/0093_auto_20180528_1432.py
Normal file
44
src/pretix/base/migrations/0093_auto_20180528_1432.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.13 on 2018-05-28 14:32
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.utils.crypto import get_random_string
|
||||
|
||||
|
||||
def set_pids(apps, schema_editor):
|
||||
OrderPosition = apps.get_model('pretixbase', 'OrderPosition') # noqa
|
||||
taken = set()
|
||||
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
|
||||
for op in OrderPosition.objects.iterator():
|
||||
while True:
|
||||
code = get_random_string(length=10, allowed_chars=charset)
|
||||
if code not in taken:
|
||||
op.pseudonymization_id = code
|
||||
taken.add(code)
|
||||
break
|
||||
op.save(update_fields=['pseudonymization_id'])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('pretixbase', '0092_auto_20180511_1224'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='pseudonymization_id',
|
||||
field=models.CharField(db_index=True, max_length=16, null=True, unique=True),
|
||||
),
|
||||
migrations.RunPython(
|
||||
set_pids,
|
||||
migrations.RunPython.noop,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='orderposition',
|
||||
name='pseudonymization_id',
|
||||
field=models.CharField(db_index=True, default='', max_length=16, unique=True),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
15
src/pretix/base/migrations/0094_auto_20180604_1119.py
Normal file
15
src/pretix/base/migrations/0094_auto_20180604_1119.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.13 on 2018-06-04 11:19
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('pretixbase', '0093_auto_20180528_1432'),
|
||||
('pretixapi', '0001_initial')
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
21
src/pretix/base/migrations/0095_auto_20180604_1129.py
Normal file
21
src/pretix/base/migrations/0095_auto_20180604_1129.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.13 on 2018-06-04 11:29
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('pretixbase', '0094_auto_20180604_1119'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='logentry',
|
||||
name='oauth_application',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT,
|
||||
to='pretixapi.OAuthApplication'),
|
||||
),
|
||||
]
|
||||
81
src/pretix/base/migrations/0096_auto_20180722_0801.py
Normal file
81
src/pretix/base/migrations/0096_auto_20180722_0801.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.13 on 2018-07-22 08:01
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0095_auto_20180604_1129'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='OrderPayment',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('local_id', models.PositiveIntegerField()),
|
||||
('state', models.CharField(choices=[('created', 'created'), ('pending', 'pending'), ('confirmed', 'confirmed'), ('canceled', 'canceled'), ('failed', 'failed'), ('refunded', 'refunded')], max_length=190)),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Amount')),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('payment_date', models.DateTimeField(blank=True, null=True)),
|
||||
('provider', models.CharField(blank=True, max_length=255, null=True, verbose_name='Payment provider')),
|
||||
('info', models.TextField(blank=True, null=True, verbose_name='Payment information')),
|
||||
('migrated', models.BooleanField(default=False)),
|
||||
],
|
||||
options={
|
||||
'ordering': ('local_id',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='OrderRefund',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('local_id', models.PositiveIntegerField()),
|
||||
('state', models.CharField(choices=[('external', 'started externally'), ('created', 'created'), ('transit', 'in transit'), ('done', 'done'), ('failed', 'failed'), ('canceled', 'canceled')], max_length=190)),
|
||||
('source', models.CharField(choices=[('admin', 'Organizer'), ('buyer', 'Customer'), ('external', 'External')], max_length=190)),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Amount')),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('execution_date', models.DateTimeField(blank=True, null=True)),
|
||||
('provider', models.CharField(blank=True, max_length=255, null=True, verbose_name='Payment provider')),
|
||||
('info', models.TextField(blank=True, null=True, verbose_name='Payment information')),
|
||||
('order', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='refunds', to='pretixbase.Order', verbose_name='Order')),
|
||||
('payment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='refunds', to='pretixbase.OrderPayment')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('local_id',),
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='quota',
|
||||
options={'ordering': ('name',), 'verbose_name': 'Quota', 'verbose_name_plural': 'Quotas'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='orderfee',
|
||||
name='fee_type',
|
||||
field=models.CharField(choices=[('payment', 'Payment fee'), ('shipping', 'Shipping fee'), ('service', 'Service fee'), ('other', 'Other fees'), ('giftcard', 'Gift card')], max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='team',
|
||||
name='can_change_organizer_settings',
|
||||
field=models.BooleanField(default=False, help_text='Someone with this setting can get access to most data of all of your events, i.e. via privacy reports, so be careful who you add to this team!', verbose_name='Can change organizer settings'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='require_2fa',
|
||||
field=models.BooleanField(default=False, verbose_name='Two-factor authentication is required to log in'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderpayment',
|
||||
name='fee',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='pretixbase.OrderFee'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderpayment',
|
||||
name='order',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='payments', to='pretixbase.Order', verbose_name='Order'),
|
||||
),
|
||||
]
|
||||
118
src/pretix/base/migrations/0097_auto_20180722_0804.py
Normal file
118
src/pretix/base/migrations/0097_auto_20180722_0804.py
Normal file
@@ -0,0 +1,118 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.13 on 2018-07-22 08:04
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def create_payments(apps, schema_editor):
|
||||
Order = apps.get_model('pretixbase', 'Order') # noqa
|
||||
OrderPayment = apps.get_model('pretixbase', 'OrderPayment') # noqa
|
||||
OrderRefund = apps.get_model('pretixbase', 'OrderRefund') # noqa
|
||||
payments = []
|
||||
refunds = []
|
||||
for o in Order.objects.filter(payments__isnull=True).iterator():
|
||||
if o.status == 'n' or o.status == 'e':
|
||||
payments.append(OrderPayment(
|
||||
local_id=1,
|
||||
state='created',
|
||||
amount=o.total,
|
||||
order=o,
|
||||
provider=o.payment_provider,
|
||||
info=o.payment_info,
|
||||
migrated=True,
|
||||
fee=o.fees.filter(fee_type="payment", internal_type=o.payment_provider).first(),
|
||||
))
|
||||
pass
|
||||
elif o.status == 'p':
|
||||
payments.append(OrderPayment(
|
||||
local_id=1,
|
||||
state='confirmed',
|
||||
amount=o.total,
|
||||
order=o,
|
||||
provider=o.payment_provider,
|
||||
payment_date=o.payment_date,
|
||||
info=o.payment_info,
|
||||
migrated=True,
|
||||
fee=o.fees.filter(fee_type="payment", internal_type=o.payment_provider).first(),
|
||||
))
|
||||
elif o.status == 'r':
|
||||
p = OrderPayment.objects.create(
|
||||
local_id=1,
|
||||
state='refunded',
|
||||
amount=o.total,
|
||||
order=o,
|
||||
provider=o.payment_provider,
|
||||
payment_date=o.payment_date,
|
||||
info=o.payment_info,
|
||||
migrated=True,
|
||||
fee=o.fees.filter(fee_type="payment", internal_type=o.payment_provider).first(),
|
||||
)
|
||||
refunds.append(OrderRefund(
|
||||
local_id=1,
|
||||
state='done',
|
||||
amount=o.total,
|
||||
order=o,
|
||||
provider=o.payment_provider,
|
||||
info=o.payment_info,
|
||||
source='admin',
|
||||
payment=p
|
||||
))
|
||||
elif o.status == 'c':
|
||||
payments.append(OrderPayment(
|
||||
local_id=1,
|
||||
state='canceled',
|
||||
amount=o.total,
|
||||
order=o,
|
||||
provider=o.payment_provider,
|
||||
payment_date=o.payment_date,
|
||||
info=o.payment_info,
|
||||
migrated=True,
|
||||
fee=o.fees.filter(fee_type="payment", internal_type=o.payment_provider).first(),
|
||||
))
|
||||
|
||||
if len(payments) > 500:
|
||||
OrderPayment.objects.bulk_create(payments)
|
||||
payments.clear()
|
||||
if len(refunds) > 500:
|
||||
OrderRefund.objects.bulk_create(refunds)
|
||||
refunds.clear()
|
||||
if len(payments) > 0:
|
||||
OrderPayment.objects.bulk_create(payments)
|
||||
if len(refunds) > 0:
|
||||
OrderRefund.objects.bulk_create(refunds)
|
||||
|
||||
|
||||
def notifications(apps, schema_editor):
|
||||
NotificationSetting = apps.get_model('pretixbase', 'NotificationSetting')
|
||||
for n in NotificationSetting.objects.filter(action_type='pretix.event.action_required'):
|
||||
n.pk = None
|
||||
n.action_type = 'pretix.event.order.refund.created.externally'
|
||||
n.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('pretixbase', '0096_auto_20180722_0801'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(create_payments, migrations.RunPython.noop),
|
||||
migrations.RunPython(notifications, migrations.RunPython.noop),
|
||||
migrations.RemoveField(
|
||||
model_name='order',
|
||||
name='payment_date',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='order',
|
||||
name='payment_info',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='order',
|
||||
name='payment_manual',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='order',
|
||||
name='payment_provider',
|
||||
),
|
||||
]
|
||||
56
src/pretix/base/migrations/0098_auto_20180731_1243.py
Normal file
56
src/pretix/base/migrations/0098_auto_20180731_1243.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# Generated by Django 2.0.7 on 2018-07-31 12:43
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.validators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0097_auto_20180722_0804'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='logentry',
|
||||
options={'ordering': ('-datetime', '-id')},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='orderpayment',
|
||||
name='fee',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='payments', to='pretixbase.OrderFee'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='organizer',
|
||||
name='slug',
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', unique=True, validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Short form'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='staffsession',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='staffsessionauditlog',
|
||||
name='impersonating',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='staffsessionauditlog',
|
||||
name='session',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='logs', to='pretixbase.StaffSession'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='locale',
|
||||
field=models.CharField(choices=[('en', 'English'), ('de', 'German'), ('de-informal', 'German (informal)'), ('nl', 'Dutch'), ('da', 'Danish'), ('tr', 'Turkish'), ('pt-br', 'Portuguese (Brazil)')], default='en', max_length=50, verbose_name='Language'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='event',
|
||||
unique_together={('organizer', 'slug')},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,82 @@
|
||||
# Generated by Django 2.0.8 on 2018-09-11 14:54
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.validators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('pretixbase', '0098_auto_20180731_1243'), ('pretixbase', '0099_auto_20180807_0841'), ('pretixbase', '0100_item_require_approval')]
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0097_auto_20180722_0804'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='logentry',
|
||||
options={'ordering': ('-datetime', '-id')},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='orderpayment',
|
||||
name='fee',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='payments', to='pretixbase.OrderFee'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='organizer',
|
||||
name='slug',
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', unique=True, validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Short form'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='staffsession',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='staffsessionauditlog',
|
||||
name='impersonating',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='staffsessionauditlog',
|
||||
name='session',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='logs', to='pretixbase.StaffSession'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='locale',
|
||||
field=models.CharField(choices=[('en', 'English'), ('de', 'German'), ('de-informal', 'German (informal)'), ('nl', 'Dutch'), ('da', 'Danish'), ('tr', 'Turkish'), ('pt-br', 'Portuguese (Brazil)')], default='en', max_length=50, verbose_name='Language'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='event',
|
||||
unique_together={('organizer', 'slug')},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='waitinglistentry',
|
||||
options={'ordering': ('-priority', 'created'), 'verbose_name': 'Waiting list entry', 'verbose_name_plural': 'Waiting list entries'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='waitinglistentry',
|
||||
name='priority',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='waitinglistentry',
|
||||
name='voucher',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Voucher', verbose_name='Assigned voucher'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='require_approval',
|
||||
field=models.BooleanField(default=False, help_text='If this product is part of an order, the order will be put into an "approval" state and will need to be confirmed by you before it can be paid and completed. You can use this e.g. for discounted tickets that are only available to specific groups.', verbose_name='Buying this product requires approval.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='require_approval',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
28
src/pretix/base/migrations/0099_auto_20180807_0841.py
Normal file
28
src/pretix/base/migrations/0099_auto_20180807_0841.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 2.1 on 2018-08-07 08:41
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0098_auto_20180731_1243'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='waitinglistentry',
|
||||
options={'ordering': ('-priority', 'created'), 'verbose_name': 'Waiting list entry', 'verbose_name_plural': 'Waiting list entries'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='waitinglistentry',
|
||||
name='priority',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='waitinglistentry',
|
||||
name='voucher',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Voucher', verbose_name='Assigned voucher'),
|
||||
),
|
||||
]
|
||||
23
src/pretix/base/migrations/0100_item_require_approval.py
Normal file
23
src/pretix/base/migrations/0100_item_require_approval.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 2.1 on 2018-08-09 15:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0099_auto_20180807_0841'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='require_approval',
|
||||
field=models.BooleanField(default=False, help_text='If this product is part of an order, the order will be put into an "approval" state and will need to be confirmed by you before it can be paid and completed. You can use this e.g. for discounted tickets that are only available to specific groups.', verbose_name='Buying this product requires approval.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='require_approval',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -15,9 +15,9 @@ from .log import LogEntry
|
||||
from .notifications import NotificationSetting
|
||||
from .orders import (
|
||||
AbstractPosition, CachedCombinedTicket, CachedTicket, CartPosition,
|
||||
InvoiceAddress, Order, OrderPosition, QuestionAnswer,
|
||||
cachedcombinedticket_name, cachedticket_name, generate_position_secret,
|
||||
generate_secret,
|
||||
InvoiceAddress, Order, OrderFee, OrderPayment, OrderPosition, OrderRefund,
|
||||
QuestionAnswer, cachedcombinedticket_name, cachedticket_name,
|
||||
generate_position_secret, generate_secret,
|
||||
)
|
||||
from .organizer import (
|
||||
Organizer, Organizer_SettingsStore, Team, TeamAPIToken, TeamInvite,
|
||||
|
||||
@@ -25,13 +25,13 @@ class UserManager(BaseUserManager):
|
||||
model documentation to see what's so special about our user model.
|
||||
"""
|
||||
|
||||
def create_user(self, email: str, password: str=None, **kwargs):
|
||||
def create_user(self, email: str, password: str = None, **kwargs):
|
||||
user = self.model(email=email, **kwargs)
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
return user
|
||||
|
||||
def create_superuser(self, email: str, password: str=None): # NOQA
|
||||
def create_superuser(self, email: str, password: str = None): # NOQA
|
||||
# Not used in the software but required by Django
|
||||
if password is None:
|
||||
raise Exception("You must provide a password")
|
||||
@@ -93,7 +93,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
verbose_name=_('Timezone'))
|
||||
require_2fa = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('Two-factor authentification is required to log in')
|
||||
verbose_name=_('Two-factor authentication is required to log in')
|
||||
)
|
||||
notifications_send = models.BooleanField(
|
||||
default=True,
|
||||
@@ -248,6 +248,8 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
teams = self._get_teams_for_event(organizer, event)
|
||||
if teams:
|
||||
self._teamcache['e{}'.format(event.pk)] = teams
|
||||
if isinstance(perm_name, (tuple, list)):
|
||||
return any([any(team.has_permission(p) for team in teams) for p in perm_name])
|
||||
if not perm_name or any([team.has_permission(perm_name) for team in teams]):
|
||||
return True
|
||||
return False
|
||||
@@ -266,6 +268,8 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
return True
|
||||
teams = self._get_teams_for_organizer(organizer)
|
||||
if teams:
|
||||
if isinstance(perm_name, (tuple, list)):
|
||||
return any([any(team.has_permission(p) for team in teams) for p in perm_name])
|
||||
if not perm_name or any([team.has_permission(perm_name) for team in teams]):
|
||||
return True
|
||||
return False
|
||||
@@ -336,7 +340,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
|
||||
|
||||
class StaffSession(models.Model):
|
||||
user = models.ForeignKey('User')
|
||||
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
||||
date_start = models.DateTimeField(auto_now_add=True)
|
||||
date_end = models.DateTimeField(null=True, blank=True)
|
||||
session_key = models.CharField(max_length=255)
|
||||
@@ -347,11 +351,11 @@ class StaffSession(models.Model):
|
||||
|
||||
|
||||
class StaffSessionAuditLog(models.Model):
|
||||
session = models.ForeignKey('StaffSession', related_name='logs')
|
||||
session = models.ForeignKey('StaffSession', related_name='logs', on_delete=models.PROTECT)
|
||||
datetime = models.DateTimeField(auto_now_add=True)
|
||||
url = models.CharField(max_length=255)
|
||||
method = models.CharField(max_length=255)
|
||||
impersonating = models.ForeignKey('User', null=True, blank=True)
|
||||
impersonating = models.ForeignKey('User', null=True, blank=True, on_delete=models.PROTECT)
|
||||
|
||||
class Meta:
|
||||
ordering = ('datetime',)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user