forked from CGM_Public/pretix_original
Compare commits
626 Commits
v3.0.0
...
release/3.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d29b8eba01 | ||
|
|
f566b353f2 | ||
|
|
c2221fad32 | ||
|
|
06a8a804f4 | ||
|
|
5832429540 | ||
|
|
7913de971c | ||
|
|
9b91e3e4f6 | ||
|
|
402730df43 | ||
|
|
c1d6d9bf1a | ||
|
|
ddbe27f351 | ||
|
|
35f2b10069 | ||
|
|
0d2a534982 | ||
|
|
a1ad00a30c | ||
|
|
07d2463960 | ||
|
|
08de722525 | ||
|
|
891e740ede | ||
|
|
87645a0b1f | ||
|
|
d7a575683e | ||
|
|
ca83a44489 | ||
|
|
40da03f979 | ||
|
|
fdd45a85f0 | ||
|
|
47579d0517 | ||
|
|
8704a7f3dd | ||
|
|
244e0695b1 | ||
|
|
8e2821b398 | ||
|
|
b738e3bd9d | ||
|
|
fa224fd17e | ||
|
|
76359c859f | ||
|
|
ff98ae3200 | ||
|
|
2ffb4edee9 | ||
|
|
902f589ee6 | ||
|
|
2a6dc22d7b | ||
|
|
4d9ec7c8e4 | ||
|
|
b84a0e93ae | ||
|
|
534f09bdc6 | ||
|
|
370aa63ead | ||
|
|
fb7e859e72 | ||
|
|
80a7c45e05 | ||
|
|
38a19bb58b | ||
|
|
19873e2a09 | ||
|
|
eb7e938af6 | ||
|
|
614c40596f | ||
|
|
4db56e218e | ||
|
|
ed9b96a41c | ||
|
|
e839dbc7d4 | ||
|
|
982fb0149d | ||
|
|
4597cb9849 | ||
|
|
9f629fc1c9 | ||
|
|
387e1b4998 | ||
|
|
84415864e5 | ||
|
|
82feca6e38 | ||
|
|
28242e52aa | ||
|
|
488ee19b11 | ||
|
|
ba4f00cfc0 | ||
|
|
eb392ebf20 | ||
|
|
3cef690779 | ||
|
|
294fc4735a | ||
|
|
6d17cad529 | ||
|
|
668380cc3f | ||
|
|
3bf8de39a0 | ||
|
|
1986cdf4b9 | ||
|
|
577729a271 | ||
|
|
9033e5b6f7 | ||
|
|
c1fa0d1559 | ||
|
|
e1a4dd6e43 | ||
|
|
089a468a5d | ||
|
|
018d345008 | ||
|
|
529e2a0286 | ||
|
|
1d36ef3c24 | ||
|
|
282ad2c869 | ||
|
|
e67ff83378 | ||
|
|
21be22e489 | ||
|
|
3da79ad36b | ||
|
|
8a17fedaa6 | ||
|
|
7d6b3e7140 | ||
|
|
f80ba365a5 | ||
|
|
8156cdd539 | ||
|
|
cd55146867 | ||
|
|
3cb7482bae | ||
|
|
99f3db04a9 | ||
|
|
352942b7d6 | ||
|
|
6d3ccc0182 | ||
|
|
49b73fc096 | ||
|
|
24b931e1c3 | ||
|
|
1c99e01af9 | ||
|
|
66183e805e | ||
|
|
d33c9332c6 | ||
|
|
2284def607 | ||
|
|
15c25a5a0d | ||
|
|
cf5ac6af4b | ||
|
|
2a929200b5 | ||
|
|
3f77d34026 | ||
|
|
a395b24b80 | ||
|
|
984ef60099 | ||
|
|
5b6f0df963 | ||
|
|
509c7d98cc | ||
|
|
3bd4959efe | ||
|
|
4faaa8e521 | ||
|
|
0e8832fd54 | ||
|
|
4faa76d9c7 | ||
|
|
8d1f9bf0f3 | ||
|
|
4afef62cbd | ||
|
|
3d5cfdd9c7 | ||
|
|
b3b1d09690 | ||
|
|
381ecd6d75 | ||
|
|
a12fea71e5 | ||
|
|
a6dd6ac537 | ||
|
|
c3041aa8c4 | ||
|
|
e275677a0a | ||
|
|
fff14c31ba | ||
|
|
a74bde60eb | ||
|
|
12b9d23efb | ||
|
|
afec39ce57 | ||
|
|
4ae22c4a1e | ||
|
|
a928d8c781 | ||
|
|
27b7a756fb | ||
|
|
7252167cc7 | ||
|
|
d6ac337bde | ||
|
|
aa9d3ab614 | ||
|
|
186fb851e6 | ||
|
|
97f7507dad | ||
|
|
e4582dd9ed | ||
|
|
090d5bfe94 | ||
|
|
b4e98e21a1 | ||
|
|
4571abe496 | ||
|
|
f86520af37 | ||
|
|
1a5b8a2e07 | ||
|
|
dddf91d3bf | ||
|
|
f88a09a78c | ||
|
|
adebcc31d4 | ||
|
|
5645a07cdc | ||
|
|
1d732e9675 | ||
|
|
61de010bbb | ||
|
|
22eb9d3493 | ||
|
|
4e8bda0c96 | ||
|
|
0c83e5a50c | ||
|
|
745d7f74a1 | ||
|
|
8c86169d3f | ||
|
|
3c87272fdc | ||
|
|
f1142560f6 | ||
|
|
d46278f04f | ||
|
|
098b7363e6 | ||
|
|
11d0c37415 | ||
|
|
99607aa74a | ||
|
|
d44b75388e | ||
|
|
531c8aedc2 | ||
|
|
0474651070 | ||
|
|
fd7ad3cb16 | ||
|
|
d5b932e0d9 | ||
|
|
5462e256ac | ||
|
|
46d4d97c13 | ||
|
|
8b5241d520 | ||
|
|
ee06844b0d | ||
|
|
d4a491fc1b | ||
|
|
bb3348022f | ||
|
|
a10082a46f | ||
|
|
ef555eaff1 | ||
|
|
31adc37599 | ||
|
|
1d02764be2 | ||
|
|
f80f2e9bf9 | ||
|
|
f5e7e0e309 | ||
|
|
bbc70447a2 | ||
|
|
530632d624 | ||
|
|
370130f047 | ||
|
|
14575693b8 | ||
|
|
436dcc68f2 | ||
|
|
b49e02d988 | ||
|
|
037644421b | ||
|
|
654faa5a10 | ||
|
|
53a22e0e88 | ||
|
|
f8a080d180 | ||
|
|
b013737d70 | ||
|
|
24d6816dac | ||
|
|
2eb92bef9b | ||
|
|
8d5b4d190c | ||
|
|
50dabb5b26 | ||
|
|
84fb25e4d9 | ||
|
|
ee4f75c2fb | ||
|
|
1e63eb743c | ||
|
|
09477e1431 | ||
|
|
2be49d8cab | ||
|
|
808ccfee75 | ||
|
|
485766e247 | ||
|
|
bab27f5ab6 | ||
|
|
26141da1b3 | ||
|
|
4c6cf63d68 | ||
|
|
24fa251b05 | ||
|
|
d65eaf7b95 | ||
|
|
e24a7f3887 | ||
|
|
ec66b20798 | ||
|
|
2c3c9a4ba1 | ||
|
|
4c8e1cef7f | ||
|
|
180963c787 | ||
|
|
ee96fb2ca1 | ||
|
|
5563259b11 | ||
|
|
e7b86e0deb | ||
|
|
067c3b1abc | ||
|
|
3fff04bcda | ||
|
|
150ebc7ddb | ||
|
|
2f906bae5c | ||
|
|
82d1927fe7 | ||
|
|
cde87b437a | ||
|
|
9cbf3490cf | ||
|
|
56cfdfc5b3 | ||
|
|
8564d20183 | ||
|
|
c3155ad16d | ||
|
|
7fce062c2a | ||
|
|
05311fda54 | ||
|
|
0396c851ed | ||
|
|
1d2094f5ea | ||
|
|
44b8531614 | ||
|
|
5e9610eecf | ||
|
|
1c40351b27 | ||
|
|
3a4196c087 | ||
|
|
080b5f02cb | ||
|
|
21c052918c | ||
|
|
9515249098 | ||
|
|
a0dd495cc3 | ||
|
|
46ce9904cd | ||
|
|
600310da0f | ||
|
|
3b306de1bb | ||
|
|
a2c1c69d7e | ||
|
|
f79df47b78 | ||
|
|
1703f3d636 | ||
|
|
580ae7a2e9 | ||
|
|
34703cf6de | ||
|
|
1999495638 | ||
|
|
00798d9207 | ||
|
|
ac2ff6c2aa | ||
|
|
35ac2f0d2b | ||
|
|
3432bb089a | ||
|
|
69b03b24a1 | ||
|
|
520dd3e254 | ||
|
|
d98fce3594 | ||
|
|
aa5ae8b2bd | ||
|
|
b148182240 | ||
|
|
339d7f06ed | ||
|
|
b876293453 | ||
|
|
54091b9721 | ||
|
|
06a744ea2d | ||
|
|
c24ab642ce | ||
|
|
2ab3e1d420 | ||
|
|
0e9d2cfc10 | ||
|
|
63a46d0156 | ||
|
|
6896682dd1 | ||
|
|
384e7f8fc1 | ||
|
|
d403b3d433 | ||
|
|
552e523229 | ||
|
|
95ca6da74a | ||
|
|
bce8879804 | ||
|
|
a05cce8af7 | ||
|
|
dd6f1ee70f | ||
|
|
ad91ed4a30 | ||
|
|
e3c3154469 | ||
|
|
54cd38bbaa | ||
|
|
8dbba1cfd3 | ||
|
|
0bf3ee1a6f | ||
|
|
da7e1dee3e | ||
|
|
df3cc1499f | ||
|
|
2d066e3b5e | ||
|
|
25cfa8973c | ||
|
|
3f7f04ed21 | ||
|
|
88b2f4738a | ||
|
|
ea71b1f04e | ||
|
|
03a06b6997 | ||
|
|
1cd319357d | ||
|
|
4c653aa4f1 | ||
|
|
5b4e27c47b | ||
|
|
919afb94c4 | ||
|
|
8f112f8d9a | ||
|
|
d15e37d93e | ||
|
|
47cf019079 | ||
|
|
f51521143b | ||
|
|
a0ca76f0ec | ||
|
|
d7f71948c1 | ||
|
|
7d72611851 | ||
|
|
2e9e5a8994 | ||
|
|
c8289877bb | ||
|
|
bb0ad6fb11 | ||
|
|
6dcdb87725 | ||
|
|
c2c8f36724 | ||
|
|
11c0a38665 | ||
|
|
c1c5388923 | ||
|
|
c7054fe72d | ||
|
|
13be3b7899 | ||
|
|
d9fe3e51da | ||
|
|
e381e50fbf | ||
|
|
73a323528b | ||
|
|
3a0aaa3f92 | ||
|
|
2a9c105e51 | ||
|
|
038533ad63 | ||
|
|
fcf9f0054e | ||
|
|
621b0c8c95 | ||
|
|
4a40312a32 | ||
|
|
b156efaae8 | ||
|
|
f473439f77 | ||
|
|
9ed49fb379 | ||
|
|
70e95b42fa | ||
|
|
1e0e8184c8 | ||
|
|
161f4a8132 | ||
|
|
8ac978062f | ||
|
|
619435d0f6 | ||
|
|
7e9e03cef4 | ||
|
|
dc3e76d6d3 | ||
|
|
df2c9bdce5 | ||
|
|
20a6decb3a | ||
|
|
3d31b95201 | ||
|
|
cc970caad8 | ||
|
|
845f231446 | ||
|
|
538d1b6b58 | ||
|
|
601fefc57f | ||
|
|
645b9696b7 | ||
|
|
ee5ce900d2 | ||
|
|
ab0be57106 | ||
|
|
ee6c4551b3 | ||
|
|
d63956fe25 | ||
|
|
27fb9cd8b0 | ||
|
|
ada2de8bc9 | ||
|
|
7fe3ac3b5e | ||
|
|
13cf24d7fa | ||
|
|
6d95603aa8 | ||
|
|
97fbec804b | ||
|
|
8f16145dda | ||
|
|
3f5e835367 | ||
|
|
fe92af10fb | ||
|
|
5e88dcf329 | ||
|
|
d7c8460c28 | ||
|
|
837775c8d4 | ||
|
|
2a51969b04 | ||
|
|
357972c8f8 | ||
|
|
08460f9918 | ||
|
|
277b7ad71c | ||
|
|
d0340044d3 | ||
|
|
8abaa16fab | ||
|
|
5e4a16bd44 | ||
|
|
b219faafaa | ||
|
|
3006d35622 | ||
|
|
a879a86dff | ||
|
|
926b94c88f | ||
|
|
638f1324fe | ||
|
|
be7fea4e2e | ||
|
|
adf7b6a4ed | ||
|
|
fc4c6444cb | ||
|
|
03c760c2bb | ||
|
|
27538d220e | ||
|
|
19b10e3ca4 | ||
|
|
2b18621c76 | ||
|
|
e8a2f7e349 | ||
|
|
a326ee8f75 | ||
|
|
147b90b9e8 | ||
|
|
1a461531eb | ||
|
|
f854a0f8d6 | ||
|
|
137ed5da6a | ||
|
|
6695fef3ee | ||
|
|
4a8827cc2b | ||
|
|
328b4ec633 | ||
|
|
974a84fefe | ||
|
|
94b6b86696 | ||
|
|
8fe9b35dea | ||
|
|
2c87d5ece3 | ||
|
|
f8433b5cc9 | ||
|
|
0a200de41c | ||
|
|
25a998a510 | ||
|
|
a1c7a6f2b0 | ||
|
|
1fe93ac6b7 | ||
|
|
4b2f25ce8a | ||
|
|
33cee65d7f | ||
|
|
302966808e | ||
|
|
767e679140 | ||
|
|
1a07686b79 | ||
|
|
7b4c3a00a0 | ||
|
|
7db87af754 | ||
|
|
da9c30089a | ||
|
|
89a85392a9 | ||
|
|
02b6cc5aad | ||
|
|
b3e6f44027 | ||
|
|
ac2df35db6 | ||
|
|
ac212b798d | ||
|
|
195d418e00 | ||
|
|
ba286d96cb | ||
|
|
9842fcf7da | ||
|
|
e97ae04581 | ||
|
|
346f215c50 | ||
|
|
e099fad0ca | ||
|
|
4aeada0bfb | ||
|
|
73dd94fe73 | ||
|
|
8c50b7409f | ||
|
|
b07d9d167d | ||
|
|
f22d5915ea | ||
|
|
e37d85f517 | ||
|
|
c68f715e07 | ||
|
|
db71ec92be | ||
|
|
85e7a16880 | ||
|
|
ed370fa913 | ||
|
|
f7f00fe735 | ||
|
|
37b05fb4f4 | ||
|
|
defd00ffdf | ||
|
|
9f44d7bbf5 | ||
|
|
28e27e91b2 | ||
|
|
e519a50805 | ||
|
|
b11b79b559 | ||
|
|
88378be14e | ||
|
|
8a8f8ae10a | ||
|
|
404d88a220 | ||
|
|
a48d461c22 | ||
|
|
5e59d41f6e | ||
|
|
7fb77eef34 | ||
|
|
f1321b67f9 | ||
|
|
8a6a515b6a | ||
|
|
e34511b984 | ||
|
|
fa3b40f53c | ||
|
|
b870dde301 | ||
|
|
a4d8c810ce | ||
|
|
4152ee4e50 | ||
|
|
7eb5b09b84 | ||
|
|
2f7c16d18b | ||
|
|
49bff3cc33 | ||
|
|
80c9d1ff9e | ||
|
|
dc51d51338 | ||
|
|
8aa8a2265f | ||
|
|
971275e774 | ||
|
|
91b586ce08 | ||
|
|
f10d1bd236 | ||
|
|
eafed2e213 | ||
|
|
02c81e0fa7 | ||
|
|
8e9a5e371c | ||
|
|
25345275c7 | ||
|
|
b9a911dd97 | ||
|
|
361488f3e6 | ||
|
|
b704d21e88 | ||
|
|
4bfe0e3784 | ||
|
|
d4d046ca60 | ||
|
|
fb3fc05522 | ||
|
|
fdcd750487 | ||
|
|
92754136a6 | ||
|
|
3b4d39ec27 | ||
|
|
88bb3b483c | ||
|
|
247370839b | ||
|
|
b0510f47b3 | ||
|
|
4fa086bbc5 | ||
|
|
cb03e7c843 | ||
|
|
d012d0804a | ||
|
|
3731d5f431 | ||
|
|
9f04d53564 | ||
|
|
748a389acb | ||
|
|
05a1df244b | ||
|
|
9f7d5156cc | ||
|
|
143fe6c1a6 | ||
|
|
1e3ccc4449 | ||
|
|
d09000b716 | ||
|
|
fc4572767e | ||
|
|
eeedd9a9c2 | ||
|
|
1d0c148170 | ||
|
|
cb37e7435d | ||
|
|
749ddbf21c | ||
|
|
9f6634025f | ||
|
|
ad039ae08e | ||
|
|
eeaaca574d | ||
|
|
bef15ec442 | ||
|
|
447a8b0a8c | ||
|
|
05b4d954d9 | ||
|
|
515d8c4899 | ||
|
|
82497cfb89 | ||
|
|
4ade9d39cd | ||
|
|
4360d5652b | ||
|
|
9fca3188b2 | ||
|
|
0ed48fac7f | ||
|
|
27a32173e6 | ||
|
|
81b6188777 | ||
|
|
9e85d3c94c | ||
|
|
4e58ba7594 | ||
|
|
248493dbf2 | ||
|
|
f1bd240096 | ||
|
|
0b673dc68c | ||
|
|
b2a7fe13da | ||
|
|
d28f735fca | ||
|
|
7a3b450bc6 | ||
|
|
f1ec129c0a | ||
|
|
ce6e46dfd2 | ||
|
|
f296f262e6 | ||
|
|
7f8d290ae1 | ||
|
|
ca0c0f4ae3 | ||
|
|
ac1e69f2d8 | ||
|
|
6338cc69ae | ||
|
|
39eaf3ad6a | ||
|
|
76e75bef65 | ||
|
|
a39822aedc | ||
|
|
73d5a2cec0 | ||
|
|
9f668e5fd6 | ||
|
|
1b92a891d7 | ||
|
|
827925e3c9 | ||
|
|
c0be574974 | ||
|
|
738413e8fd | ||
|
|
2c6125adeb | ||
|
|
3f7807d242 | ||
|
|
59b7f0a03d | ||
|
|
b7dea16db3 | ||
|
|
3a81706aeb | ||
|
|
ad3369b059 | ||
|
|
ab0709558d | ||
|
|
e2e64ac01d | ||
|
|
cf14dcf889 | ||
|
|
a884a25d2b | ||
|
|
8493778028 | ||
|
|
36a276c360 | ||
|
|
795c423d73 | ||
|
|
3ce9ec79f0 | ||
|
|
debc5e255b | ||
|
|
145aa0d7fd | ||
|
|
3997bdd098 | ||
|
|
a51a905512 | ||
|
|
d3ac9e8880 | ||
|
|
07402c9ea0 | ||
|
|
5f81bf55ca | ||
|
|
0120a5a930 | ||
|
|
ba555f956e | ||
|
|
5e3876ddde | ||
|
|
f5719687aa | ||
|
|
18449efcc7 | ||
|
|
3bb20d943a | ||
|
|
586e544fce | ||
|
|
3a4fc69db1 | ||
|
|
8b5d49d82f | ||
|
|
7665faa39f | ||
|
|
027d28e646 | ||
|
|
babcf66a2e | ||
|
|
20f9e88d61 | ||
|
|
9594607a8c | ||
|
|
ad3bcaf43a | ||
|
|
2c4ee3b3c7 | ||
|
|
21451db412 | ||
|
|
12b1f7d90e | ||
|
|
c3901c567e | ||
|
|
0a3eddcd5c | ||
|
|
dfa99cd325 | ||
|
|
62a040255e | ||
|
|
0e2b02c778 | ||
|
|
0f0ed90be9 | ||
|
|
b1e19d776c | ||
|
|
0bcc784aaf | ||
|
|
262fb82237 | ||
|
|
adbe959314 | ||
|
|
02d0a68d57 | ||
|
|
d6985123b4 | ||
|
|
f7a356c340 | ||
|
|
c6265b4517 | ||
|
|
1ee352e114 | ||
|
|
6c830a7d36 | ||
|
|
129c360fff | ||
|
|
b5c7ad92b6 | ||
|
|
33efd8c157 | ||
|
|
f318c8e017 | ||
|
|
319334706d | ||
|
|
1a25138bef | ||
|
|
daa5383b89 | ||
|
|
31333280d2 | ||
|
|
b78f8d70e8 | ||
|
|
a847538a2e | ||
|
|
e3ef9eba9e | ||
|
|
9e4a4402fb | ||
|
|
96ed9f5cf5 | ||
|
|
7216cebce5 | ||
|
|
a3892fd4de | ||
|
|
7718462528 | ||
|
|
60b20829f3 | ||
|
|
e11d03f418 | ||
|
|
69e4db58fd | ||
|
|
9f27a84f52 | ||
|
|
e5c204dc95 | ||
|
|
7fc7dd0163 | ||
|
|
aa99dbc830 | ||
|
|
e3a4ec93fc | ||
|
|
67da6a18a8 | ||
|
|
adc4128f9f | ||
|
|
eed217262f | ||
|
|
4bae824a03 | ||
|
|
be1a1f7995 | ||
|
|
f4b81aa032 | ||
|
|
2559439c4e | ||
|
|
fce9117dfd | ||
|
|
4c8dc8f31c | ||
|
|
b4f69fb13f | ||
|
|
2aed894bd4 | ||
|
|
2ff5416afb | ||
|
|
59f7098a70 | ||
|
|
83dd865b78 | ||
|
|
ebf411b7a0 | ||
|
|
733a4ce8f4 | ||
|
|
ad94263374 | ||
|
|
102772ec55 | ||
|
|
9a826b694f | ||
|
|
bcf8e9cd04 | ||
|
|
200ce93bb4 | ||
|
|
0582a4d9e5 | ||
|
|
1cbab04108 | ||
|
|
98c18b162f | ||
|
|
d972cd4c49 | ||
|
|
985f354293 | ||
|
|
9c23216bd1 | ||
|
|
f8bf44c262 | ||
|
|
6badfdf576 | ||
|
|
c2eba21359 | ||
|
|
5cda04a994 | ||
|
|
bc9d8f5bd8 | ||
|
|
9d6ff20191 | ||
|
|
82684e6df3 | ||
|
|
d681ae4dce | ||
|
|
213e724e18 | ||
|
|
6e0b80706c | ||
|
|
5363f4206e | ||
|
|
9bdb715874 | ||
|
|
90a9709838 | ||
|
|
669b438c91 | ||
|
|
a1353b3773 | ||
|
|
f5c611982a | ||
|
|
e9ab56486a | ||
|
|
74bc495eb7 | ||
|
|
b0b0f7474d | ||
|
|
663ff60d0a | ||
|
|
7bafb0bc76 | ||
|
|
1ab225f40a | ||
|
|
3b09456755 | ||
|
|
d919605d79 | ||
|
|
547f71aac6 | ||
|
|
191729c07a | ||
|
|
8f0a5d859d |
14
.travis.sh
14
.travis.sh
@@ -14,18 +14,18 @@ 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
|
||||
XDG_CACHE_HOME=/cache pip3 install --no-use-pep517 -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
|
||||
XDG_CACHE_HOME=/cache pip3 install --no-use-pep517 -Ur doc/requirements.txt
|
||||
cd doc
|
||||
make doctest
|
||||
fi
|
||||
if [ "$1" == "doc-spelling" ]; then
|
||||
XDG_CACHE_HOME=/cache pip3 install -Ur doc/requirements.txt
|
||||
XDG_CACHE_HOME=/cache pip3 install --no-use-pep517 -Ur doc/requirements.txt
|
||||
cd doc
|
||||
make spelling
|
||||
if [ -s _build/spelling/output.txt ]; then
|
||||
@@ -33,26 +33,26 @@ if [ "$1" == "doc-spelling" ]; then
|
||||
fi
|
||||
fi
|
||||
if [ "$1" == "translation-spelling" ]; then
|
||||
XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements/dev.txt
|
||||
XDG_CACHE_HOME=/cache pip3 install --no-use-pep517 -Ur src/requirements/dev.txt
|
||||
cd src
|
||||
potypo
|
||||
fi
|
||||
if [ "$1" == "tests" ]; then
|
||||
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt
|
||||
pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt
|
||||
cd src
|
||||
python manage.py check
|
||||
make all compress
|
||||
py.test --reruns 5 -n 3 tests
|
||||
fi
|
||||
if [ "$1" == "tests-cov" ]; then
|
||||
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt
|
||||
pip3 install -r src/requirements.txt --no-use-pep517 -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
|
||||
pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt
|
||||
cd src
|
||||
python setup.py develop
|
||||
make all compress
|
||||
|
||||
@@ -3,7 +3,7 @@ Contributing to pretix
|
||||
|
||||
Hey there and welcome to pretix!
|
||||
|
||||
We've got an contributors guide in [our documentation](https://docs.pretix.eu/en/latest/development/contribution/)
|
||||
We've got a contributors guide in [our documentation](https://docs.pretix.eu/en/latest/development/contribution/)
|
||||
together with notes on the [development setup](https://docs.pretix.eu/en/latest/development/setup.html).
|
||||
|
||||
Please note that we have a [Code of Conduct](https://docs.pretix.eu/en/latest/development/contribution/codeofconduct.html)
|
||||
|
||||
@@ -57,6 +57,8 @@ COPY deployment/docker/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY deployment/docker/production_settings.py /pretix/src/production_settings.py
|
||||
COPY src /pretix/src
|
||||
|
||||
RUN cd /pretix/src && pip3 install .
|
||||
|
||||
RUN chmod +x /usr/local/bin/pretix && \
|
||||
rm /etc/nginx/sites-enabled/default && \
|
||||
cd /pretix/src && \
|
||||
|
||||
@@ -57,6 +57,9 @@ Example::
|
||||
A comma-separated list of plugins that are not available even though they are installed.
|
||||
Defaults to an empty string.
|
||||
|
||||
``auth_backends``
|
||||
A comma-separated list of available auth backends. Defaults to ``pretix.base.auth.NativeAuthBackend``.
|
||||
|
||||
``cookie_domain``
|
||||
The cookie domain to be set. Defaults to ``None``.
|
||||
|
||||
@@ -87,6 +90,11 @@ Example::
|
||||
proxy that actively removes and re-adds the header to make sure the correct client IP is the first value.
|
||||
Defaults to ``off``.
|
||||
|
||||
``trust_x_forwarded_proto``
|
||||
Specifies whether the ``X-Forwarded-Proto`` header can be trusted. Only set to ``on`` if you have a reverse
|
||||
proxy that actively removes and re-adds the header to make sure the correct client IP is the first value.
|
||||
Defaults to ``off``.
|
||||
|
||||
|
||||
Locale settings
|
||||
---------------
|
||||
|
||||
@@ -125,6 +125,8 @@ Fill the configuration file ``/etc/pretix/pretix.cfg`` with the following conten
|
||||
; DO NOT change the following value, it has to be set to the location of the
|
||||
; directory *inside* the docker container
|
||||
datadir=/data
|
||||
trust_x_forwarded_for=on
|
||||
trust_x_forwarded_proto=on
|
||||
|
||||
[database]
|
||||
; Replace postgresql with mysql for MySQL
|
||||
@@ -276,7 +278,7 @@ choice)::
|
||||
|
||||
Then, go to that directory and build the image::
|
||||
|
||||
$ docker build -t mypretix
|
||||
$ docker build . -t mypretix
|
||||
|
||||
You can now use that image ``mypretix`` instead of ``pretix/standalone`` in your service file (see above). Be sure
|
||||
to re-build your custom image after you pulled ``pretix/standalone`` if you want to perform an update.
|
||||
|
||||
@@ -68,7 +68,7 @@ generated key and installs the plugin from the URL we told you::
|
||||
mkdir -p /etc/ssh && \
|
||||
ssh-keyscan -t rsa -p 10022 code.rami.io >> /root/.ssh/known_hosts && \
|
||||
echo StrictHostKeyChecking=no >> /root/.ssh/config && \
|
||||
pip3 install -Ue "git+ssh://git@code.rami.io:10022/pretix/pretix-slack.git@stable#egg=pretix-slack" && \
|
||||
DJANGO_SETTINGS_MODULE=pretix.settings pip3 install -U "git+ssh://git@code.rami.io:10022/pretix/pretix-slack.git@stable#egg=pretix-slack" && \
|
||||
cd /pretix/src && \
|
||||
sudo -u pretixuser make production
|
||||
USER pretixuser
|
||||
|
||||
@@ -85,6 +85,8 @@ Fill the configuration file ``/etc/pretix/pretix.cfg`` with the following conten
|
||||
url=https://pretix.mydomain.com
|
||||
currency=EUR
|
||||
datadir=/var/pretix/data
|
||||
trust_x_forwarded_for=on
|
||||
trust_x_forwarded_proto=on
|
||||
|
||||
[database]
|
||||
; For MySQL, replace with "mysql"
|
||||
|
||||
132
doc/api/guides/custom_checkout.rst
Normal file
132
doc/api/guides/custom_checkout.rst
Normal file
@@ -0,0 +1,132 @@
|
||||
Creating an external checkout process
|
||||
=====================================
|
||||
|
||||
Occasionally, we get asked whether it is possible to just use pretix' powerful backend as a ticketing engine but use
|
||||
a fully-customized checkout process that only communicates via the API. This is possible, but with a few limitations.
|
||||
If you go down this route, you will miss out on many of pretix features and safeguards, as well as the added flexibility
|
||||
by most of pretix' plugins. We strongly recommend to talk this through with us before you decide this is the way to go.
|
||||
|
||||
However, this is really useful if you need to tightly integrate pretix into existing web applications that e.g. control
|
||||
the pricing of your products in a way that cannot be mapped to pretix' product structures.
|
||||
|
||||
Creating orders
|
||||
---------------
|
||||
|
||||
After letting your user select the products to buy in your application, you should create a new order object inside
|
||||
pretix. Below, you can see an example of such an order, but most fields are optional and there are some more features
|
||||
supported. Read :ref:`rest-orders-create` to learn more about this endpoint.
|
||||
|
||||
Please note that this endpoint assumes trustworthy input for the most part. By default, the endpoint checks that
|
||||
you do not exceed any quotas, do not sell any seats twice, or do not use any redeemed vouchers. However, it will not
|
||||
complain about violation of any other availability constraints, such as violation of time frames or minimum/maximum
|
||||
amounts of either your product or event. Bundled products will not be added in automatically and fees will not be
|
||||
calculated automatically.
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/democon/events/3vjrh/orders/ HTTP/1.1
|
||||
Host: test.pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Authorization: …
|
||||
|
||||
{
|
||||
"email": "dummy@example.org",
|
||||
"locale": "en",
|
||||
"sales_channel": "web",
|
||||
"payment_provider": "banktransfer",
|
||||
"invoice_address": {
|
||||
"is_business": false,
|
||||
"company": "Sample company",
|
||||
"name_parts": {"full_name": "John Doe"},
|
||||
"street": "Sesam Street 12",
|
||||
"zipcode": "12345",
|
||||
"city": "Sample City",
|
||||
"country": "US",
|
||||
"state": "NY",
|
||||
"internal_reference": "",
|
||||
"vat_id": ""
|
||||
},
|
||||
"positions": [
|
||||
{
|
||||
"item": 21,
|
||||
"variation": null,
|
||||
"attendee_name_parts": {
|
||||
"full_name": "Peter"
|
||||
},
|
||||
"answers": [
|
||||
{
|
||||
"question": 1,
|
||||
"answer": "23",
|
||||
"options": []
|
||||
}
|
||||
],
|
||||
"subevent": null
|
||||
}
|
||||
],
|
||||
"fees": []
|
||||
}
|
||||
|
||||
You will be returned a full order object that you can inspect, store, or use to build emails or confirmation pages for
|
||||
the user. If you don't want to do that yourself, it will also contain the URL to our confirmation page in the ``url``
|
||||
attribute. If you pass the ``"send_mail": true`` option, pretix will also send order confirmations for you.
|
||||
|
||||
Handling payments yourself
|
||||
--------------------------
|
||||
|
||||
If you want to handle payments in your application, you can either just create the orders with status "paid" or you can
|
||||
create them in "pending" state (the default) and later confirm the payment. We strongly advise to use the payment
|
||||
provider ``"manual"`` in this case to avoid interference with payment code with pretix.
|
||||
|
||||
However, it is often unfeasible to implement the payment process yourself, and it also requires you to give up a
|
||||
lot of pretix functionality, such as automatic refunds. Therefore, it is also possible to utilize pretix' native
|
||||
payment process even in this case:
|
||||
|
||||
Using pretix payment providers
|
||||
------------------------------
|
||||
|
||||
If you passed a ``payment_provider`` during order creation above, pretix will have created a payment object with state
|
||||
``created`` that you can see in the returned order object. This payment object will have an attribute ``payment_url``
|
||||
that you can use to let the user pay. For example, you could link or redirect to this page.
|
||||
|
||||
If you want the user to return to your application after the payment is complete, you can pass a query parameter
|
||||
``return_url``. To prepare your event for this, open your event in the pretix backend and go to "Settings", then
|
||||
"Plugins". Enable the plugin "Redirection from order page". Then, go to the new page "Settings", then "Redirection".
|
||||
Enter the base URL of your web application. This will allow you to redirect to pages under this base URL later on.
|
||||
For example, if you want users to be redirected to ``https://example.org/order/return?tx_id=1234``, you could now
|
||||
either enter ``https://example.org`` or ``https://example.org/order/``.
|
||||
|
||||
The user will be redirected back to your page instead of pretix' order confirmation page after the payment,
|
||||
**regardless of whether it was successful or not**. Make sure you use our API to check if the payment actually
|
||||
worked! Your final URL could look like this::
|
||||
|
||||
https://test.pretix.eu/democon/3vjrh/order/NSLEZ/ujbrnsjzbq4dzhck/pay/123/?return_url=https%3A%2F%2Fexample.org%2Forder%2Freturn%3Ftx_id%3D1234
|
||||
|
||||
You can also embed this page in an ``<iframe>`` instead. Note, however, that this causes problems with some payment
|
||||
methods such as PayPal which do not allow being opened in an iframe. pretix can partly work around these issues by
|
||||
opening a new window, but will only to so if you also append an ``iframe=1`` parameter to the URL::
|
||||
|
||||
https://test.pretix.eu/democon/3vjrh/order/NSLEZ/ujbrnsjzbq4dzhck/pay/123/?return_url=https%3A%2F%2Fexample.org%2Forder%2Freturn%3Ftx_id%3D1234&iframe=1
|
||||
|
||||
If you did **not** pass a payment method since you want us to ask the user which payment method they want to use, you
|
||||
need to construct the URL from the ``url`` attribute of the order and the sub-path ``pay/change```. For example, you
|
||||
would end up with the following URL::
|
||||
|
||||
https://test.pretix.eu/democon/3vjrh/order/NSLEZ/ujbrnsjzbq4dzhck/pay/change
|
||||
|
||||
Of course, you can also use the ``iframe`` and ``return_url`` parameters here.
|
||||
|
||||
Optional: Cart reservations
|
||||
---------------------------
|
||||
|
||||
Creating orders is an atomic operation: The order is either created as a whole or not at all. However, pretix'
|
||||
built-in checkout automatically reserves tickets in a user's cart for a configurable amount of time to ensure users
|
||||
will actually get their tickets once they started entering all their details. If you want a similar behavior in your
|
||||
application, you need to create :ref:`rest-carts` through the API.
|
||||
|
||||
When creating your order, you can pass a ``consume_carts`` parameter with the cart ID(s) of your user. This way, the
|
||||
quota reserved by the cart will be credited towards the order and the carts will be destroyed if (and only if) the
|
||||
order creation succeeds.
|
||||
|
||||
Cart creation is currently even more limited than the order creation endpoints, as cart creation currently does not
|
||||
support vouchers or automatic price calculation. If you require these features, please get in touch with us.
|
||||
11
doc/api/guides/index.rst
Normal file
11
doc/api/guides/index.rst
Normal file
@@ -0,0 +1,11 @@
|
||||
.. _`rest-api-guides`:
|
||||
|
||||
API Usage Guides
|
||||
================
|
||||
|
||||
This part of the documentation contains how-to guides on some special use cases of our API.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
custom_checkout
|
||||
@@ -18,3 +18,4 @@ in functionality over time.
|
||||
resources/index
|
||||
ratelimit
|
||||
webhooks
|
||||
guides/index
|
||||
|
||||
@@ -194,6 +194,7 @@ Cart position endpoints
|
||||
* ``subevent`` (optional)
|
||||
* ``expires`` (optional)
|
||||
* ``includes_tax`` (optional)
|
||||
* ``sales_channel`` (optional)
|
||||
* ``answers``
|
||||
|
||||
* ``question``
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
.. spelling:: checkin
|
||||
|
||||
Check-in lists
|
||||
==============
|
||||
|
||||
@@ -27,6 +29,7 @@ subevent integer ID of the date
|
||||
position_count integer Number of tickets that match this list (read-only).
|
||||
checkin_count integer Number of check-ins performed on this list (read-only).
|
||||
include_pending boolean If ``true``, the check-in list also contains tickets from orders in pending state.
|
||||
auto_checkin_sales_channels list of strings All items on the check-in list will be automatically marked as checked-in when purchased through any of the listed sales channels.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.10
|
||||
@@ -41,6 +44,10 @@ include_pending boolean If ``true``, th
|
||||
|
||||
The ``include_pending`` field has been added.
|
||||
|
||||
.. versionchanged:: 3.2
|
||||
|
||||
The ``auto_checkin_sales_channels`` field has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -81,7 +88,10 @@ Endpoints
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"include_pending": false,
|
||||
"subevent": null
|
||||
"subevent": null,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -122,7 +132,10 @@ Endpoints
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"include_pending": false,
|
||||
"subevent": null
|
||||
"subevent": null,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
@@ -215,7 +228,10 @@ Endpoints
|
||||
"name": "VIP entry",
|
||||
"all_products": false,
|
||||
"limit_products": [1, 2],
|
||||
"subevent": null
|
||||
"subevent": null,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -234,7 +250,10 @@ Endpoints
|
||||
"all_products": false,
|
||||
"limit_products": [1, 2],
|
||||
"include_pending": false,
|
||||
"subevent": null
|
||||
"subevent": null,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event/item to create a list for
|
||||
@@ -283,7 +302,10 @@ Endpoints
|
||||
"all_products": false,
|
||||
"limit_products": [1, 2],
|
||||
"include_pending": false,
|
||||
"subevent": null
|
||||
"subevent": null,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
@@ -342,6 +364,11 @@ Order position endpoints
|
||||
``ignore_status`` filter. The ``attendee_name`` field is now "smart" (see below) and the redemption endpoint
|
||||
returns ``400`` instead of ``404`` on tickets which are known but not paid.
|
||||
|
||||
.. versionchanged:: 3.2
|
||||
|
||||
The ``checkins`` dict now also contains a ``auto_checked_in`` value to indicate if the check-in has been performed
|
||||
automatically by the system.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(list)/positions/
|
||||
|
||||
Returns a list of all order positions within a given event. The result is the same as
|
||||
@@ -400,7 +427,8 @@ Order position endpoints
|
||||
"checkins": [
|
||||
{
|
||||
"list": 1,
|
||||
"datetime": "2017-12-25T12:45:23Z"
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": true
|
||||
}
|
||||
],
|
||||
"answers": [
|
||||
@@ -510,7 +538,8 @@ Order position endpoints
|
||||
"checkins": [
|
||||
{
|
||||
"list": 1,
|
||||
"datetime": "2017-12-25T12:45:23Z"
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": true
|
||||
}
|
||||
],
|
||||
"answers": [
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
.. spelling::
|
||||
|
||||
geo
|
||||
lat
|
||||
lon
|
||||
|
||||
Events
|
||||
======
|
||||
|
||||
@@ -25,6 +31,8 @@ is_public boolean If ``true``, th
|
||||
presale_start datetime The date at which the ticket shop opens (or ``null``)
|
||||
presale_end datetime The date at which the ticket shop closes (or ``null``)
|
||||
location multi-lingual string The event location (or ``null``)
|
||||
geo_lat float Latitude of the location (or ``null``)
|
||||
geo_lon float Longitude of the location (or ``null``)
|
||||
has_subevents boolean ``true`` if the event series feature is active for this
|
||||
event. Cannot change after event is created.
|
||||
meta_data object Values set for organizer-specific meta data parameters.
|
||||
@@ -34,6 +42,7 @@ seating_plan integer If reserved sea
|
||||
plan. Otherwise ``null``.
|
||||
seat_category_mapping object An object mapping categories of the seating plan
|
||||
(strings) to items in the event (integers or ``null``).
|
||||
timezone string Event timezone name
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
@@ -62,9 +71,21 @@ seat_category_mapping object An object mappi
|
||||
|
||||
The attributes ``seating_plan`` and ``seat_category_mapping`` have been added.
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
|
||||
The attributes ``geo_lat`` and ``geo_lon`` have been added.
|
||||
|
||||
.. versionchanged:: 3.4
|
||||
|
||||
The attribute ``timezone`` has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
|
||||
The events resource can now be filtered by meta data attributes.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/
|
||||
|
||||
Returns a list of all events within a given organizer the authenticated user/token has access to.
|
||||
@@ -105,10 +126,13 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"has_subevents": false,
|
||||
"meta_data": {},
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"timezone": "Europe/Berlin",
|
||||
"plugins": [
|
||||
"pretix.plugins.banktransfer"
|
||||
"pretix.plugins.stripe"
|
||||
@@ -129,6 +153,10 @@ Endpoints
|
||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``date_from`` and
|
||||
``slug``. Keep in mind that ``date_from`` of event series does not really tell you anything.
|
||||
Default: ``slug``.
|
||||
:query array attr[meta_data_key]: By providing the key and value of a meta data attribute, the list of events will
|
||||
only contain the events matching the set criteria. Providing ``?attr[Format]=Seminar`` would return only those
|
||||
events having set their ``Format`` meta data to ``Seminar``, ``?attr[Format]=`` only those, that have no value
|
||||
set. Please note that this filter will respect default values set on organizer level.
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
@@ -169,10 +197,13 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"has_subevents": false,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"meta_data": {},
|
||||
"timezone": "Europe/Berlin",
|
||||
"plugins": [
|
||||
"pretix.plugins.banktransfer"
|
||||
"pretix.plugins.stripe"
|
||||
@@ -220,8 +251,11 @@ Endpoints
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"has_subevents": false,
|
||||
"meta_data": {},
|
||||
"timezone": "Europe/Berlin",
|
||||
"plugins": [
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal"
|
||||
@@ -249,10 +283,13 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"has_subevents": false,
|
||||
"meta_data": {},
|
||||
"timezone": "Europe/Berlin",
|
||||
"plugins": [
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal"
|
||||
@@ -268,11 +305,11 @@ Endpoints
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/clone/
|
||||
|
||||
Creates a new event with properties as set in the request body. The properties that are copied are: 'is_public',
|
||||
`testmode`, settings, plugin settings, items, variations, add-ons, quotas, categories, tax rules, questions.
|
||||
Creates a new event with properties as set in the request body. The properties that are copied are: ``is_public``,
|
||||
``testmode``, ``has_subevents``, settings, plugin settings, items, variations, add-ons, quotas, categories, tax rules, questions.
|
||||
|
||||
If the 'plugins' and/or 'is_public' fields are present in the post body this will determine their value. Otherwise
|
||||
their value will be copied from the existing event.
|
||||
If the ``plugins``, ``has_subevents`` and/or ``is_public`` fields are present in the post body this will determine their
|
||||
value. Otherwise their value will be copied from the existing event.
|
||||
|
||||
Please note that you can only copy from events under the same organizer.
|
||||
|
||||
@@ -300,10 +337,13 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"has_subevents": false,
|
||||
"meta_data": {},
|
||||
"timezone": "Europe/Berlin",
|
||||
"plugins": [
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal"
|
||||
@@ -331,10 +371,13 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"has_subevents": false,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"meta_data": {},
|
||||
"timezone": "Europe/Berlin",
|
||||
"plugins": [
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal"
|
||||
@@ -394,10 +437,13 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"has_subevents": false,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"meta_data": {},
|
||||
"timezone": "Europe/Berlin",
|
||||
"plugins": [
|
||||
"pretix.plugins.banktransfer",
|
||||
"pretix.plugins.stripe",
|
||||
|
||||
234
doc/api/resources/giftcards.rst
Normal file
234
doc/api/resources/giftcards.rst
Normal file
@@ -0,0 +1,234 @@
|
||||
.. _`rest-giftcards`:
|
||||
|
||||
Gift cards
|
||||
==========
|
||||
|
||||
Resource description
|
||||
--------------------
|
||||
|
||||
The gift card resource contains the following public fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the gift card
|
||||
secret string Gift card code (can not be modified later)
|
||||
value money (string) Current gift card value
|
||||
currency string Currency of the value (can not be modified later)
|
||||
testmode boolean Whether this is a test gift card
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/giftcards/
|
||||
|
||||
Returns a list of all gift cards issued by a given organizer.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/giftcards/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"secret": "HLBYVELFRC77NCQY",
|
||||
"currency": "EUR",
|
||||
"testmode": false,
|
||||
"value": "13.37"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/giftcards/(id)/
|
||||
|
||||
Returns information on one gift card, identified by its ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/giftcards/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"secret": "HLBYVELFRC77NCQY",
|
||||
"currency": "EUR",
|
||||
"testmode": false,
|
||||
"value": "13.37"
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param id: The ``id`` field of the gift card to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/giftcards/
|
||||
|
||||
Creates a new gift card
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/giftcards/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"secret": "HLBYVELFRC77NCQY",
|
||||
"currency": "EUR",
|
||||
"value": "13.37"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"secret": "HLBYVELFRC77NCQY",
|
||||
"testmode": false,
|
||||
"currency": "EUR",
|
||||
"value": "13.37"
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to create a gift card for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The gift card could not be created due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/giftcards/(id)/
|
||||
|
||||
Update a gift card. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||
want to change.
|
||||
|
||||
You can change all fields of the resource except the ``id``, ``secret``, ``testmode``, and ``currency`` fields. Be
|
||||
careful when modifying the ``value`` field to avoid race conditions. We recommend to use the ``transact`` method
|
||||
described below.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/giftcards/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 94
|
||||
|
||||
{
|
||||
"value": "14.00"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"secret": "HLBYVELFRC77NCQY",
|
||||
"testmode": false,
|
||||
"currency": "EUR",
|
||||
"value": "14.00"
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param id: The ``id`` field of the gift card to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The gift card could not be modified due to invalid submitted data
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/giftcards/(id)/transact/
|
||||
|
||||
Atomically change the value of a gift card. A positive amount will increase the value of the gift card,
|
||||
a negative amount will decrease it.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/giftcards/1/transact/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 94
|
||||
|
||||
{
|
||||
"value": "2.00"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"secret": "HLBYVELFRC77NCQY",
|
||||
"currency": "EUR",
|
||||
"testmode": false,
|
||||
"value": "15.37"
|
||||
}
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
This endpoint now returns status code ``409`` if the transaction would lead to a negative gift card value.
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param id: The ``id`` field of the gift card to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The gift card could not be modified due to invalid submitted data
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
|
||||
:statuscode 409: There is not sufficient credit on the gift card.
|
||||
@@ -21,6 +21,7 @@ Resources and endpoints
|
||||
vouchers
|
||||
checkinlists
|
||||
waitinglist
|
||||
giftcards
|
||||
carts
|
||||
webhooks
|
||||
seatingplans
|
||||
|
||||
@@ -28,6 +28,7 @@ payment_provider_text string Text to be prin
|
||||
payment information
|
||||
footer_text string Text to be printed in the page footer area
|
||||
lines list of objects The actual invoice contents
|
||||
├ position integer Number of the line within an invoice.
|
||||
├ description string Text representing the invoice line (e.g. product name)
|
||||
├ gross_value money (string) Price including taxes
|
||||
├ tax_value money (string) Tax amount included
|
||||
@@ -63,6 +64,11 @@ internal_reference string Customer's refe
|
||||
The attribute ``internal_reference`` has been added.
|
||||
|
||||
|
||||
.. versionchanged:: 3.4
|
||||
|
||||
The attribute ``lines.number`` has been added.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -107,6 +113,7 @@ Endpoints
|
||||
"footer_text": "Big Events LLC - Registration No. 123456 - VAT ID: EU0987654321",
|
||||
"lines": [
|
||||
{
|
||||
"position": 1,
|
||||
"description": "Budget Ticket",
|
||||
"gross_value": "23.00",
|
||||
"tax_value": "0.00",
|
||||
@@ -171,6 +178,7 @@ Endpoints
|
||||
"footer_text": "Big Events LLC - Registration No. 123456 - VAT ID: EU0987654321",
|
||||
"lines": [
|
||||
{
|
||||
"position": 1,
|
||||
"description": "Budget Ticket",
|
||||
"gross_value": "23.00",
|
||||
"tax_value": "0.00",
|
||||
|
||||
@@ -77,6 +77,7 @@ generate_tickets boolean If ``false``, t
|
||||
rules apply.
|
||||
allow_waitinglist boolean If ``false``, no waiting list will be shown for this
|
||||
product when it is sold out.
|
||||
issue_giftcard boolean If ``true``, buying this product will yield a gift card.
|
||||
show_quota_left boolean Publicly show how many tickets are still available.
|
||||
If this is ``null``, the event default is used.
|
||||
has_variations boolean Shows whether or not this item has variations.
|
||||
@@ -206,6 +207,7 @@ Endpoints
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
"issue_giftcard": false,
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
"available_from": null,
|
||||
@@ -300,6 +302,7 @@ Endpoints
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
"issue_giftcard": false,
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
"available_from": null,
|
||||
@@ -375,6 +378,7 @@ Endpoints
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
"issue_giftcard": false,
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
"available_from": null,
|
||||
@@ -437,6 +441,7 @@ Endpoints
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
"issue_giftcard": false,
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
"available_from": null,
|
||||
@@ -531,6 +536,7 @@ Endpoints
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
"issue_giftcard": false,
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
"available_from": null,
|
||||
|
||||
@@ -53,15 +53,18 @@ invoice_address object Invoice address
|
||||
├ street string Customer street
|
||||
├ zipcode string Customer ZIP code
|
||||
├ city string Customer city
|
||||
├ country string Customer country
|
||||
├ country string Customer country code
|
||||
├ state string Customer state (ISO 3166-2 code). Only supported in
|
||||
AU, BR, CA, CN, MY, MX, and US.
|
||||
├ internal_reference string Customer's internal reference to be printed on the invoice
|
||||
├ vat_id string Customer VAT ID
|
||||
└ vat_id_validated string ``true``, if the VAT ID has been validated against the
|
||||
EU VAT service and validation was successful. This only
|
||||
happens in rare cases.
|
||||
positions list of objects List of non-canceled order positions (see below)
|
||||
fees list of objects List of non-canceled fees included in the order total
|
||||
(i.e. payment fees)
|
||||
positions list of objects List of order positions (see below). By default, only
|
||||
non-canceled positions are included.
|
||||
fees list of objects List of fees included in the order total. By default, only
|
||||
non-canceled fees are included.
|
||||
├ fee_type string Type of fee (currently ``payment``, ``passbook``,
|
||||
``other``)
|
||||
├ value money (string) Fee amount
|
||||
@@ -70,7 +73,8 @@ fees list of objects List of non-can
|
||||
can be empty
|
||||
├ tax_rate decimal (string) VAT rate applied for this fee
|
||||
├ tax_value money (string) VAT included in this fee
|
||||
└ tax_rule integer The ID of the used tax rule (or ``null``)
|
||||
├ tax_rule integer The ID of the used tax rule (or ``null``)
|
||||
└ canceled boolean Whether or not this fee has been canceled.
|
||||
downloads list of objects List of ticket download options for order-wise ticket
|
||||
downloading. This might be a multi-page PDF or a ZIP
|
||||
file of tickets for outputs that do not support
|
||||
@@ -82,6 +86,7 @@ require_approval boolean If ``true`` and
|
||||
needs approval by an organizer before it can
|
||||
continue. If ``true`` and the order is canceled,
|
||||
this order has been denied by the event organizer.
|
||||
url string The full URL to the order confirmation page
|
||||
payments list of objects List of payment processes (see below)
|
||||
refunds list of objects List of refund processes (see below)
|
||||
last_modified datetime Last modification of this object
|
||||
@@ -128,15 +133,25 @@ last_modified datetime Last modificati
|
||||
|
||||
The ``sales_channel`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 2.4:
|
||||
.. versionchanged:: 2.4
|
||||
|
||||
``order.status`` can no longer be ``r``, ``…/mark_canceled/`` now accepts a ``cancellation_fee`` parameter and
|
||||
``…/mark_refunded/`` has been deprecated.
|
||||
|
||||
.. versionchanged:: 2.5:
|
||||
.. versionchanged:: 2.5
|
||||
|
||||
The ``testmode`` attribute has been added and ``DELETE`` has been implemented for orders.
|
||||
|
||||
.. versionchanged:: 3.1
|
||||
|
||||
The ``invoice_address.state`` and ``url`` attributes have been added. When creating orders through the API,
|
||||
vouchers are now supported and many fields are now optional.
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
The ``order.fees.canceled`` attribute has been added.
|
||||
|
||||
|
||||
.. _order-position-resource:
|
||||
|
||||
Order position resource
|
||||
@@ -150,6 +165,8 @@ Field Type Description
|
||||
id integer Internal ID of the order position
|
||||
order string Order code of the order the position belongs to
|
||||
positionid integer Number of the position within the order
|
||||
canceled boolean Whether or not this position has been canceled. Note that
|
||||
by default, only non-canceled positions are shown.
|
||||
item integer ID of the purchased item
|
||||
variation integer ID of the purchased variation (or ``null``)
|
||||
price money (string) Price of this position
|
||||
@@ -166,7 +183,8 @@ subevent integer ID of the date
|
||||
pseudonymization_id string A random ID, e.g. for use in lead scanning apps
|
||||
checkins list of objects List of check-ins with this ticket
|
||||
├ list integer Internal ID of the check-in list
|
||||
└ datetime datetime Time of check-in
|
||||
├ datetime datetime Time of check-in
|
||||
└ auto_checked_in boolean Indicates if this check-in been performed automatically by the system
|
||||
downloads list of objects List of ticket download options
|
||||
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
|
||||
└ url string Download URL
|
||||
@@ -205,6 +223,19 @@ pdf_data object Data object req
|
||||
|
||||
The attribute ``seat`` has been added.
|
||||
|
||||
.. versionchanged:: 3.2
|
||||
|
||||
The value ``auto_checked_in`` has been added to the ``checkins``-attribute.
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
|
||||
The ``url`` of a ticket ``download`` can now also return a ``text/uri-list`` instead of a file. See
|
||||
:ref:`order-position-ticket-download` for details.
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
The attribute ``canceled`` has been added.
|
||||
|
||||
.. _order-payment-resource:
|
||||
|
||||
Order payment resource
|
||||
@@ -221,13 +252,27 @@ amount money (string) Payment amount
|
||||
created datetime Date and time of creation of this payment
|
||||
payment_date datetime Date and time of completion of this payment (or ``null``)
|
||||
provider string Identification string of the payment provider
|
||||
payment_url string The URL where an user can continue with the payment (or ``null``)
|
||||
details object Payment-specific information. This is a dictionary
|
||||
with various fields that can be different between
|
||||
payment providers, versions, payment states, etc. If
|
||||
you read this field, you always need to be able to
|
||||
deal with situations where values that you expect are
|
||||
missing. Mostly, the field contains various IDs that
|
||||
can be used for matching with other systems. If a
|
||||
payment provider does not implement this feature,
|
||||
the object is empty.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
|
||||
This resource has been added.
|
||||
|
||||
.. _order-payment-resource:
|
||||
.. versionchanged:: 3.1
|
||||
|
||||
The attributes ``payment_url`` and ``details`` have been added.
|
||||
|
||||
.. _order-refund-resource:
|
||||
|
||||
Order refund resource
|
||||
---------------------
|
||||
@@ -257,6 +302,10 @@ List of all orders
|
||||
|
||||
Filtering for emails or order codes is now case-insensitive.
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
The ``include_canceled_positions`` and ``include_canceled_fees`` query parameters have been added.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/
|
||||
|
||||
Returns a list of all orders within a given event.
|
||||
@@ -288,6 +337,7 @@ List of all orders
|
||||
"status": "p",
|
||||
"testmode": false,
|
||||
"secret": "k24fiuwvu8kxz3y1",
|
||||
"url": "https://test.pretix.eu/dummy/dummy/order/ABC12/k24fiuwvu8kxz3y1/",
|
||||
"email": "tester@example.org",
|
||||
"locale": "en",
|
||||
"sales_channel": "web",
|
||||
@@ -310,7 +360,8 @@ List of all orders
|
||||
"street": "Test street 12",
|
||||
"zipcode": "12345",
|
||||
"city": "Testington",
|
||||
"country": "Testikistan",
|
||||
"country": "DE",
|
||||
"state": "",
|
||||
"internal_reference": "",
|
||||
"vat_id": "EU123456789",
|
||||
"vat_id_validated": false
|
||||
@@ -320,6 +371,7 @@ List of all orders
|
||||
"id": 23442,
|
||||
"order": "ABC12",
|
||||
"positionid": 1,
|
||||
"canceled": false,
|
||||
"item": 1345,
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
@@ -340,7 +392,8 @@ List of all orders
|
||||
"checkins": [
|
||||
{
|
||||
"list": 44,
|
||||
"datetime": "2017-12-25T12:45:23Z"
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": false
|
||||
}
|
||||
],
|
||||
"answers": [
|
||||
@@ -373,6 +426,8 @@ List of all orders
|
||||
"amount": "23.00",
|
||||
"created": "2017-12-01T10:00:00Z",
|
||||
"payment_date": "2017-12-04T12:13:12Z",
|
||||
"payment_url": null,
|
||||
"details": {},
|
||||
"provider": "banktransfer"
|
||||
}
|
||||
],
|
||||
@@ -389,6 +444,9 @@ List of all orders
|
||||
:query boolean testmode: Only return orders with ``testmode`` set to ``true`` or ``false``
|
||||
:query boolean require_approval: If set to ``true`` or ``false``, only categories with this value for the field
|
||||
``require_approval`` will be returned.
|
||||
:query include_canceled_positions: If set to ``true``, the output will contain canceled order positions. Note that this
|
||||
only affects position-level cancellations, not fully-canceled orders.
|
||||
:query include_canceled_fees: If set to ``true``, the output will contain canceled order fees.
|
||||
:query string email: Only return orders created with the given email address
|
||||
:query string locale: Only return orders with the given customer locale
|
||||
:query datetime modified_since: Only return orders that have changed since the given date. Be careful: We only
|
||||
@@ -406,6 +464,10 @@ List of all orders
|
||||
Fetching individual orders
|
||||
--------------------------
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
The ``include_canceled_positions`` and ``include_canceled_fees`` query parameters have been added.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/
|
||||
|
||||
Returns information on one order, identified by its order code.
|
||||
@@ -431,6 +493,7 @@ Fetching individual orders
|
||||
"status": "p",
|
||||
"testmode": false,
|
||||
"secret": "k24fiuwvu8kxz3y1",
|
||||
"url": "https://test.pretix.eu/dummy/dummy/order/ABC12/k24fiuwvu8kxz3y1/",
|
||||
"email": "tester@example.org",
|
||||
"locale": "en",
|
||||
"sales_channel": "web",
|
||||
@@ -453,7 +516,8 @@ Fetching individual orders
|
||||
"street": "Test street 12",
|
||||
"zipcode": "12345",
|
||||
"city": "Testington",
|
||||
"country": "Testikistan",
|
||||
"country": "DE",
|
||||
"state": "",
|
||||
"internal_reference": "",
|
||||
"vat_id": "EU123456789",
|
||||
"vat_id_validated": false
|
||||
@@ -463,6 +527,7 @@ Fetching individual orders
|
||||
"id": 23442,
|
||||
"order": "ABC12",
|
||||
"positionid": 1,
|
||||
"canceled": false,
|
||||
"item": 1345,
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
@@ -483,7 +548,8 @@ Fetching individual orders
|
||||
"checkins": [
|
||||
{
|
||||
"list": 44,
|
||||
"datetime": "2017-12-25T12:45:23Z"
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": false
|
||||
}
|
||||
],
|
||||
"answers": [
|
||||
@@ -516,6 +582,8 @@ Fetching individual orders
|
||||
"amount": "23.00",
|
||||
"created": "2017-12-01T10:00:00Z",
|
||||
"payment_date": "2017-12-04T12:13:12Z",
|
||||
"payment_url": null,
|
||||
"details": {},
|
||||
"provider": "banktransfer"
|
||||
}
|
||||
],
|
||||
@@ -525,6 +593,9 @@ Fetching individual orders
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:param code: The ``code`` field of the order to fetch
|
||||
:query include_canceled_positions: If set to ``true``, the output will contain canceled order positions. Note that this
|
||||
only affects position-level cancellations, not fully-canceled orders.
|
||||
:query include_canceled_fees: If set to ``true``, the output will contain canceled order fees.
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
@@ -691,6 +762,8 @@ Deleting orders
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource **or** the order may not be deleted.
|
||||
:statuscode 404: The requested order does not exist.
|
||||
|
||||
.. _rest-orders-create:
|
||||
|
||||
Creating orders
|
||||
---------------
|
||||
|
||||
@@ -716,25 +789,21 @@ Creating orders
|
||||
|
||||
* does not validate the number of items per order or the number of times an item can be included in an order
|
||||
|
||||
* does not validate any requirements related to add-on products
|
||||
* does not validate any requirements related to add-on products and does not add bundled products automatically
|
||||
|
||||
* does not check or calculate prices but believes any prices you send
|
||||
|
||||
* does not support the redemption of vouchers
|
||||
* does not check prices but believes any prices you send
|
||||
|
||||
* does not prevent you from buying items that can only be bought with a voucher
|
||||
|
||||
* does not calculate fees
|
||||
* does not calculate fees automatically
|
||||
|
||||
* does not allow to pass data to plugins and will therefore cause issues with some plugins like the shipping
|
||||
module
|
||||
|
||||
* does not send order confirmations via email
|
||||
|
||||
* does not support reverse charge taxation
|
||||
|
||||
* does not support file upload questions
|
||||
|
||||
* does not support redeeming gift cards
|
||||
|
||||
You can supply the following fields of the resource:
|
||||
|
||||
* ``code`` (optional)
|
||||
@@ -750,9 +819,10 @@ Creating orders
|
||||
* ``email``
|
||||
* ``locale``
|
||||
* ``sales_channel``
|
||||
* ``payment_provider`` – The identifier of the payment provider set for this order. This needs to be an existing
|
||||
payment provider. You should use ``"free"`` for free orders, and we strongly advise to use ``"manual"`` for all
|
||||
orders you create as paid.
|
||||
* ``payment_provider`` (optional) – The identifier of the payment provider set for this order. This needs to be an
|
||||
existing payment provider. You should use ``"free"`` for free orders, and we strongly advise to use ``"manual"``
|
||||
for all orders you create as paid. This field is optional when the order status is ``"n"`` or the order total is
|
||||
zero, otherwise it is required.
|
||||
* ``payment_info`` (optional) – You can pass a nested JSON object that will be set as the internal ``info``
|
||||
value of the payment object that will be created. How this value is handled is up to the payment provider and you
|
||||
should only use this if you know the specific payment provider in detail. Please keep in mind that the payment
|
||||
@@ -770,17 +840,22 @@ Creating orders
|
||||
* ``zipcode``
|
||||
* ``city``
|
||||
* ``country``
|
||||
* ``state``
|
||||
* ``internal_reference``
|
||||
* ``vat_id``
|
||||
* ``vat_id_validated`` (optional) – If you need support for reverse charge (rarely the case), you need to check
|
||||
yourself if the passed VAT ID is a valid EU VAT ID. In that case, set this to ``true``. Only valid VAT IDs will
|
||||
trigger reverse charge taxation. Don't forget to set ``is_business`` as well!
|
||||
|
||||
* ``positions``
|
||||
|
||||
* ``positionid`` (optional, see below)
|
||||
* ``item``
|
||||
* ``variation``
|
||||
* ``price``
|
||||
* ``price`` (optional, if set to ``null`` or missing the price will be computed from the given product)
|
||||
* ``seat`` (The ``seat_guid`` attribute of a seat. Required when the specified ``item`` requires a seat, otherwise must be ``null``.)
|
||||
* ``attendee_name`` **or** ``attendee_name_parts``
|
||||
* ``voucher`` (optional, the ``code`` attribute of a valid voucher)
|
||||
* ``attendee_email``
|
||||
* ``secret`` (optional)
|
||||
* ``addon_to`` (optional, see below)
|
||||
@@ -798,8 +873,18 @@ Creating orders
|
||||
* ``description``
|
||||
* ``internal_type``
|
||||
* ``tax_rule``
|
||||
* ``_treat_value_as_percentage`` (Optional convenience flag. If set to ``true``, your ``value`` parameter will
|
||||
be treated as a percentage and the fee will be calculated using that percentage and the sum of all product
|
||||
prices. Note that this will not include other fees and is calculated once during order generation and will not
|
||||
be respected automatically when the order changes later.)
|
||||
* ``_split_taxes_like_products`` (Optional convenience flag. If set to ``true``, your ``tax_rule`` will be ignored
|
||||
and the fee will be taxed like the products in the order. If the products have multiple tax rates, multiple fees
|
||||
will be generated with weights adjusted to the net price of the products. Note that this will be calculated once
|
||||
during order generation and is not respected automatically when the order changes later.)
|
||||
|
||||
* ``force`` (optional). If set to ``true``, quotas will be ignored.
|
||||
* ``send_mail`` (optional). If set to ``true``, the same emails will be sent as for a regular order. Defaults to
|
||||
``false``.
|
||||
|
||||
If you want to use add-on products, you need to set the ``positionid`` fields of all positions manually
|
||||
to incrementing integers starting with ``1``. Then, you can reference one of these
|
||||
@@ -837,6 +922,7 @@ Creating orders
|
||||
"zipcode": "12345",
|
||||
"city": "Sample City",
|
||||
"country": "UK",
|
||||
"state": "",
|
||||
"internal_reference": "",
|
||||
"vat_id": ""
|
||||
},
|
||||
@@ -860,7 +946,7 @@ Creating orders
|
||||
],
|
||||
"subevent": null
|
||||
}
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -1251,7 +1337,13 @@ List of all order positions
|
||||
The order positions endpoint has been extended by the filter queries ``voucher``, ``voucher__code`` and
|
||||
``pseudonymization_id``.
|
||||
|
||||
.. note:: Individually canceled order positions are currently not visible via the API at all.
|
||||
.. versionchanged:: 3.2
|
||||
|
||||
The value ``auto_checked_in`` has been added to the ``checkins``-attribute.
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
The ``include_canceled_positions`` and ``include_canceled_fees`` query parameters have been added.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/
|
||||
|
||||
@@ -1282,6 +1374,7 @@ List of all order positions
|
||||
"id": 23442,
|
||||
"order": "ABC12",
|
||||
"positionid": 1,
|
||||
"canceled": false,
|
||||
"item": 1345,
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
@@ -1302,7 +1395,8 @@ List of all order positions
|
||||
"checkins": [
|
||||
{
|
||||
"list": 44,
|
||||
"datetime": "2017-12-25T12:45:23Z"
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": false
|
||||
}
|
||||
],
|
||||
"answers": [
|
||||
@@ -1350,6 +1444,8 @@ List of all order positions
|
||||
comma-separated IDs.
|
||||
:query string voucher: Only return positions with a specific voucher.
|
||||
:query string voucher__code: Only return positions with a specific voucher code.
|
||||
:query include_canceled_positions: If set to ``true``, the output will contain canceled order positions. Note that this
|
||||
only affects position-level cancellations, not fully-canceled orders.
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:statuscode 200: no error
|
||||
@@ -1383,6 +1479,7 @@ Fetching individual positions
|
||||
"id": 23442,
|
||||
"order": "ABC12",
|
||||
"positionid": 1,
|
||||
"canceled": false,
|
||||
"item": 1345,
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
@@ -1403,7 +1500,8 @@ Fetching individual positions
|
||||
"checkins": [
|
||||
{
|
||||
"list": 44,
|
||||
"datetime": "2017-12-25T12:45:23Z"
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": false
|
||||
}
|
||||
],
|
||||
"answers": [
|
||||
@@ -1426,11 +1524,14 @@ Fetching individual positions
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:param id: The ``id`` field of the order position to fetch
|
||||
:query include_canceled_positions: If set to ``true``, canceled positions may be returned (otherwise, they return 404).
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
:statuscode 404: The requested order position does not exist.
|
||||
|
||||
.. _`order-position-ticket-download`:
|
||||
|
||||
Order position ticket download
|
||||
------------------------------
|
||||
|
||||
@@ -1440,6 +1541,11 @@ Order position ticket download
|
||||
Depending on the chosen output, the response might be a ZIP file, PDF file or something else. The order details
|
||||
response contains a list of output options for this particular order position.
|
||||
|
||||
Be aware that the output does not have to be a file, but can also be a regular HTTP response with a ``Content-Type``
|
||||
set to ``text/uri-list``. In this case, the user is expected to navigate to that URL in order to access their ticket.
|
||||
The referenced URL can provide a download or a regular, human-viewable website - so it is advised to open this URL
|
||||
in a webbrowser and leave it up to the user to handle the result.
|
||||
|
||||
Tickets can be only downloaded if the order is paid and if ticket downloads are active. Also, depending on event
|
||||
configuration downloads might be only unavailable for add-on products or non-admission products.
|
||||
Note that in some cases the ticket file might not yet have been created. In that case, you will receive a status
|
||||
@@ -1546,6 +1652,8 @@ Order payment endpoints
|
||||
"amount": "23.00",
|
||||
"created": "2017-12-01T10:00:00Z",
|
||||
"payment_date": "2017-12-04T12:13:12Z",
|
||||
"payment_url": null,
|
||||
"details": {},
|
||||
"provider": "banktransfer"
|
||||
}
|
||||
]
|
||||
@@ -1586,6 +1694,8 @@ Order payment endpoints
|
||||
"amount": "23.00",
|
||||
"created": "2017-12-01T10:00:00Z",
|
||||
"payment_date": "2017-12-04T12:13:12Z",
|
||||
"payment_url": null,
|
||||
"details": {},
|
||||
"provider": "banktransfer"
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the question
|
||||
question multi-lingual string The field label shown to the customer
|
||||
help_text multi-lingual string The help text shown to the customer
|
||||
type string The expected type of answer. Valid options:
|
||||
|
||||
* ``N`` – number
|
||||
@@ -31,6 +32,7 @@ type string The expected ty
|
||||
* ``H`` – time
|
||||
* ``W`` – date and time
|
||||
* ``CC`` – country code (ISO 3666-1 alpha-2)
|
||||
* ``TEL`` – telephone number
|
||||
required boolean If ``true``, the question needs to be filled out.
|
||||
position integer An integer, used for sorting
|
||||
items list of integers List of item IDs this question is assigned to.
|
||||
@@ -41,6 +43,8 @@ ask_during_checkin boolean If ``true``, th
|
||||
the ticket instead.
|
||||
hidden boolean If ``true``, the question will only be shown in the
|
||||
backend.
|
||||
print_on_invoice boolean If ``true``, the question will only be shown on
|
||||
invoices.
|
||||
options list of objects In case of question type ``C`` or ``M``, this lists the
|
||||
available objects. Only writable during creation,
|
||||
use separate endpoint to modify this later.
|
||||
@@ -80,6 +84,14 @@ dependency_value string An old version
|
||||
|
||||
The attribute ``dependency_values`` has been added.
|
||||
|
||||
.. versionchanged:: 3.1
|
||||
|
||||
The attribute ``print_on_invoice`` has been added.
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
The attribute ``help_text`` has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -116,6 +128,7 @@ Endpoints
|
||||
{
|
||||
"id": 1,
|
||||
"question": {"en": "T-Shirt size"},
|
||||
"help_text": {"en": "Choose your preferred t-shirt-size"},
|
||||
"type": "C",
|
||||
"required": false,
|
||||
"items": [1, 2],
|
||||
@@ -123,6 +136,7 @@ Endpoints
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": false,
|
||||
"hidden": false,
|
||||
"print_on_invoice": false,
|
||||
"dependency_question": null,
|
||||
"dependency_value": null,
|
||||
"dependency_values": [],
|
||||
@@ -185,6 +199,7 @@ Endpoints
|
||||
{
|
||||
"id": 1,
|
||||
"question": {"en": "T-Shirt size"},
|
||||
"help_text": {"en": "Choose your preferred t-shirt-size"},
|
||||
"type": "C",
|
||||
"required": false,
|
||||
"items": [1, 2],
|
||||
@@ -192,6 +207,7 @@ Endpoints
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": false,
|
||||
"hidden": false,
|
||||
"print_on_invoice": false,
|
||||
"dependency_question": null,
|
||||
"dependency_value": null,
|
||||
"dependency_values": [],
|
||||
@@ -239,12 +255,14 @@ Endpoints
|
||||
|
||||
{
|
||||
"question": {"en": "T-Shirt size"},
|
||||
"help_text": {"en": "Choose your preferred t-shirt-size"},
|
||||
"type": "C",
|
||||
"required": false,
|
||||
"items": [1, 2],
|
||||
"position": 1,
|
||||
"ask_during_checkin": false,
|
||||
"hidden": false,
|
||||
"print_on_invoice": false,
|
||||
"dependency_question": null,
|
||||
"dependency_values": [],
|
||||
"options": [
|
||||
@@ -272,6 +290,7 @@ Endpoints
|
||||
{
|
||||
"id": 1,
|
||||
"question": {"en": "T-Shirt size"},
|
||||
"help_text": {"en": "Choose your preferred t-shirt-size"},
|
||||
"type": "C",
|
||||
"required": false,
|
||||
"items": [1, 2],
|
||||
@@ -279,6 +298,7 @@ Endpoints
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": false,
|
||||
"hidden": false,
|
||||
"print_on_invoice": false,
|
||||
"dependency_question": null,
|
||||
"dependency_value": null,
|
||||
"dependency_values": [],
|
||||
@@ -345,6 +365,7 @@ Endpoints
|
||||
{
|
||||
"id": 1,
|
||||
"question": {"en": "T-Shirt size"},
|
||||
"help_text": {"en": "Choose your preferred t-shirt-size"},
|
||||
"type": "C",
|
||||
"required": false,
|
||||
"items": [1, 2],
|
||||
@@ -352,6 +373,7 @@ Endpoints
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": false,
|
||||
"hidden": false,
|
||||
"print_on_invoice": false,
|
||||
"dependency_question": null,
|
||||
"dependency_value": null,
|
||||
"dependency_values": [],
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
.. spelling::
|
||||
|
||||
geo
|
||||
lat
|
||||
lon
|
||||
|
||||
.. _rest-subevents:
|
||||
|
||||
Event series dates / Sub-events
|
||||
@@ -28,6 +34,8 @@ date_admission datetime The sub-event's
|
||||
presale_start datetime The sub-date at which the ticket shop opens (or ``null``)
|
||||
presale_end datetime The sub-date at which the ticket shop closes (or ``null``)
|
||||
location multi-lingual string The sub-event location (or ``null``)
|
||||
geo_lat float Latitude of the location (or ``null``)
|
||||
geo_lon float Longitude of the location (or ``null``)
|
||||
item_price_overrides list of objects List of items for which this sub-event overrides the
|
||||
default price
|
||||
├ item integer The internal item ID
|
||||
@@ -62,9 +70,17 @@ seat_category_mapping object An object mappi
|
||||
|
||||
The attributes ``seating_plan`` and ``seat_category_mapping`` have been added.
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
|
||||
The attributes ``geo_lat`` and ``geo_lon`` have been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
|
||||
The sub-events resource can now be filtered by meta data attributes.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/
|
||||
|
||||
Returns a list of all sub-events of an event.
|
||||
@@ -104,6 +120,8 @@ Endpoints
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": 2,
|
||||
@@ -123,6 +141,11 @@ Endpoints
|
||||
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned.
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of the main event
|
||||
:query array attr[meta_data_key]: By providing the key and value of a meta data attribute, the list of sub-events
|
||||
will only contain the sub-events matching the set criteria. Providing ``?attr[Format]=Seminar`` would return
|
||||
only those sub-events having set their ``Format`` meta data to ``Seminar``, ``?attr[Format]=`` only those, that
|
||||
have no value set. Please note that this filter will respect default values set on
|
||||
organizer or event level.
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||
@@ -152,6 +175,8 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"item_price_overrides": [
|
||||
@@ -184,6 +209,8 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"item_price_overrides": [
|
||||
@@ -237,6 +264,8 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"item_price_overrides": [
|
||||
@@ -303,6 +332,8 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"item_price_overrides": [
|
||||
@@ -389,6 +420,8 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"geo_lat": null,
|
||||
"geo_lon": null,
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"item_price_overrides": [
|
||||
|
||||
@@ -38,6 +38,7 @@ quota integer An ID of a quot
|
||||
attached either to a specific product or to all
|
||||
products within one quota or it can be available
|
||||
for all items without restriction.
|
||||
seat string ``seat_guid`` attribute of a specific seat (or ``null``)
|
||||
tag string A string that is used for grouping vouchers
|
||||
comment string An internal comment on the voucher
|
||||
subevent integer ID of the date inside an event series this voucher belongs to (or ``null``).
|
||||
@@ -53,6 +54,10 @@ show_hidden_items boolean Only if set to
|
||||
|
||||
The attribute ``show_hidden_items`` has been added.
|
||||
|
||||
.. versionchanged:: 3.4
|
||||
|
||||
The attribute ``seat`` has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -96,7 +101,8 @@ Endpoints
|
||||
"quota": null,
|
||||
"tag": "testvoucher",
|
||||
"comment": "",
|
||||
"subevent": null
|
||||
"seat": null,
|
||||
"subevent": null,
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -162,6 +168,7 @@ Endpoints
|
||||
"quota": null,
|
||||
"tag": "testvoucher",
|
||||
"comment": "",
|
||||
"seat": null,
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
@@ -225,6 +232,7 @@ Endpoints
|
||||
"quota": null,
|
||||
"tag": "testvoucher",
|
||||
"comment": "",
|
||||
"seat": null,
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
@@ -352,6 +360,7 @@ Endpoints
|
||||
"quota": null,
|
||||
"tag": "testvoucher",
|
||||
"comment": "",
|
||||
"seat": null,
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
|
||||
12
doc/contents.rst
Normal file
12
doc/contents.rst
Normal file
@@ -0,0 +1,12 @@
|
||||
Table of contents
|
||||
=================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
user/index
|
||||
admin/index
|
||||
api/index
|
||||
development/index
|
||||
plugins/index
|
||||
|
||||
70
doc/development/api/auth.rst
Normal file
70
doc/development/api/auth.rst
Normal file
@@ -0,0 +1,70 @@
|
||||
.. highlight:: python
|
||||
:linenothreshold: 5
|
||||
|
||||
Pluggable authentication backends
|
||||
=================================
|
||||
|
||||
Plugins can supply additional authentication backends. This is mainly useful in self-hosted installations
|
||||
and allows you to use company-wide login mechanisms such as LDAP or OAuth for accessing pretix' backend.
|
||||
|
||||
Every authentication backend contains an implementation of the interface defined in ``pretix.base.auth.BaseAuthBackend``
|
||||
(see below). Note that pretix authentication backends work differently than plain Django authentication backends.
|
||||
Basically, three pre-defined flows are supported:
|
||||
|
||||
* Authentication mechanisms that rely on a **set of input parameters**, e.g. a username and a password. These can be
|
||||
implemented by supplying the ``login_form_fields`` property and a ``form_authenticate`` method.
|
||||
|
||||
* Authentication mechanisms that rely on **external sessions**, e.g. a cookie or a proxy HTTP header. These can be
|
||||
implemented by supplying a ``request_authenticate`` method.
|
||||
|
||||
* Authentication mechanisms that rely on **redirection**, e.g. to an OAuth provider. These can be implemented by
|
||||
supplying a ``authentication_url`` method and implementing a custom return view.
|
||||
|
||||
Authentication backends are *not* collected through a signal. Instead, they must explicitly be set through the
|
||||
``auth_backends`` directive in the ``pretix.cfg`` :ref:`configuration file <config>`.
|
||||
|
||||
In each of these methods (``form_authenticate``, ``request_authenticate`` or your custom view) you are supposed to
|
||||
either get an existing :py:class:`pretix.base.models.User` object from the database or create a new one. There are a
|
||||
few rules you need to follow:
|
||||
|
||||
* You **MUST** only return users with the ``auth_backend`` attribute set to the ``identifier`` value of your backend.
|
||||
|
||||
* You **MUST** create new users with the ``auth_backend`` attribute set to the ``identifier`` value of your backend.
|
||||
|
||||
* Every user object **MUST** have an email address. Email addresses are globally unique. If the email address is
|
||||
already registered to a user who signs in through a different backend, you **SHOULD** refuse the login.
|
||||
|
||||
The backend interface
|
||||
---------------------
|
||||
|
||||
.. class:: pretix.base.auth.BaseAuthBackend
|
||||
|
||||
The central object of each backend is the subclass of ``BaseAuthBackend``.
|
||||
|
||||
.. autoattribute:: identifier
|
||||
|
||||
This is an abstract attribute, you **must** override this!
|
||||
|
||||
.. autoattribute:: verbose_name
|
||||
|
||||
This is an abstract attribute, you **must** override this!
|
||||
|
||||
.. autoattribute:: login_form_fields
|
||||
|
||||
.. autoattribute:: visible
|
||||
|
||||
.. automethod:: form_authenticate
|
||||
|
||||
.. automethod:: request_authenticate
|
||||
|
||||
.. automethod:: authentication_url
|
||||
|
||||
Logging users in
|
||||
----------------
|
||||
|
||||
If you return a user from ``form_authenticate`` or ``request_authenticate``, the system will handle everything else
|
||||
for you correctly. However, if you use a redirection method and build a custom view to verify the login, we strongly
|
||||
recommend that you use the following utility method to correctly set session values and enforce two-factor
|
||||
authentication (if activated):
|
||||
|
||||
.. autofunction:: pretix.control.views.auth.process_login
|
||||
@@ -12,7 +12,7 @@ Core
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types,
|
||||
item_copy_data, register_sales_channels, register_global_settings, quota_availability
|
||||
item_copy_data, register_sales_channels, register_global_settings, quota_availability, global_email_filter
|
||||
|
||||
Order events
|
||||
""""""""""""
|
||||
@@ -20,13 +20,13 @@ Order events
|
||||
There are multiple signals that will be sent out in the ordering cycle:
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: validate_cart, validate_cart_addons, validate_order, order_fee_calculation, order_paid, order_placed, order_canceled, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split
|
||||
:members: validate_cart, validate_cart_addons, validate_order, order_fee_calculation, order_paid, order_placed, order_canceled, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
|
||||
|
||||
Frontend
|
||||
--------
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info
|
||||
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info, item_description
|
||||
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
@@ -49,8 +49,8 @@ Backend
|
||||
|
||||
.. automodule:: pretix.control.signals
|
||||
:members: nav_event, html_head, html_page_start, quota_detail_html, nav_topbar, nav_global, nav_organizer, nav_event_settings,
|
||||
order_info, event_settings_widget, oauth_application_registered, order_position_buttons, subevent_forms, item_formsets
|
||||
|
||||
order_info, event_settings_widget, oauth_application_registered, order_position_buttons, subevent_forms,
|
||||
item_formsets, order_search_filter_q
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: logentry_display, logentry_object_link, requiredaction_display, timeline_events
|
||||
@@ -78,3 +78,6 @@ Ticket designs
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: layout_text_variables
|
||||
|
||||
.. automodule:: pretix.plugins.ticketoutputpdf.signals
|
||||
:members: override_layout
|
||||
|
||||
112
doc/development/api/import.rst
Normal file
112
doc/development/api/import.rst
Normal file
@@ -0,0 +1,112 @@
|
||||
.. highlight:: python
|
||||
:linenothreshold: 5
|
||||
|
||||
.. _`importcol`:
|
||||
|
||||
Extending the order import process
|
||||
==================================
|
||||
|
||||
It's possible through the backend to import orders into pretix, for example from a legacy ticketing system. If your
|
||||
plugins defines additional data structures around orders, it might be useful to make it possible to import them as well.
|
||||
|
||||
Import process
|
||||
--------------
|
||||
|
||||
Here's a short description of pretix' import process to show you where the system will need to interact with your plugin.
|
||||
You can find more detailed descriptions of the attributes and methods further below.
|
||||
|
||||
1. The user uploads a CSV file. The system tries to parse the CSV file and understand its column headers.
|
||||
|
||||
2. A preview of the file is shown to the user and the user is asked to assign the various different input parameters to
|
||||
columns of the file or static values. For example, the user either needs to manually select a product or specify a
|
||||
column that contains a product. For this purpose, a select field is rendered for every possible input column,
|
||||
allowing the user to choose between a default/empty value (defined by your ``default_value``/``default_label``)
|
||||
attributes, the columns of the uploaded file, or a static value (defined by your ``static_choices`` method).
|
||||
|
||||
3. The user submits its assignment and the system uses the ``resolve`` method of all columns to get the raw value for
|
||||
all columns.
|
||||
|
||||
4. The system uses the ``clean`` method of all columns to verify that all input fields are valid and transformed to the
|
||||
correct data type.
|
||||
|
||||
5. The system prepares internal model objects (``Order`` etc) and uses the ``assign`` method of all columns to assign
|
||||
these objects with actual values.
|
||||
|
||||
6. The system saves all of these model objects to the database in a database transaction. Plugins can create additional
|
||||
objects in this stage through their ``save`` method.
|
||||
|
||||
Column registration
|
||||
-------------------
|
||||
|
||||
The import API does not make a lot of usage from signals, however, it
|
||||
does use a signal to get a list of all available import columns. Your plugin
|
||||
should listen for this signal and return the subclass of ``pretix.base.orderimport.ImportColumn``
|
||||
that we'll provide in this plugin:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
from django.dispatch import receiver
|
||||
|
||||
from pretix.base.signals import order_import_columns
|
||||
|
||||
|
||||
@receiver(order_import_columns, dispatch_uid="custom_columns")
|
||||
def register_column(sender, **kwargs):
|
||||
return [
|
||||
EmailColumn(sender),
|
||||
]
|
||||
|
||||
The column class API
|
||||
--------------------
|
||||
|
||||
.. class:: pretix.base.orderimport.ImportColumn
|
||||
|
||||
The central object of each import extension is the subclass of ``ImportColumn``.
|
||||
|
||||
.. py:attribute:: ImportColumn.event
|
||||
|
||||
The default constructor sets this property to the event we are currently
|
||||
working for.
|
||||
|
||||
.. autoattribute:: identifier
|
||||
|
||||
This is an abstract attribute, you **must** override this!
|
||||
|
||||
.. autoattribute:: verbose_name
|
||||
|
||||
This is an abstract attribute, you **must** override this!
|
||||
|
||||
.. autoattribute:: default_value
|
||||
|
||||
.. autoattribute:: default_label
|
||||
|
||||
.. autoattribute:: initial
|
||||
|
||||
.. automethod:: static_choices
|
||||
|
||||
.. automethod:: resolve
|
||||
|
||||
.. automethod:: clean
|
||||
|
||||
.. automethod:: assign
|
||||
|
||||
.. automethod:: save
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
For example, the import column responsible for assigning email addresses looks like this:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
class EmailColumn(ImportColumn):
|
||||
identifier = 'email'
|
||||
verbose_name = _('E-mail address')
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if value:
|
||||
EmailValidator()(value)
|
||||
return value
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
order.email = value
|
||||
@@ -12,8 +12,11 @@ Contents:
|
||||
payment
|
||||
payment_2.0
|
||||
email
|
||||
placeholder
|
||||
invoice
|
||||
shredder
|
||||
import
|
||||
customview
|
||||
auth
|
||||
general
|
||||
quality
|
||||
|
||||
@@ -62,6 +62,8 @@ The provider class
|
||||
|
||||
.. autoattribute:: is_enabled
|
||||
|
||||
.. autoattribute:: priority
|
||||
|
||||
.. autoattribute:: settings_form_fields
|
||||
|
||||
.. automethod:: settings_form_clean
|
||||
@@ -108,8 +110,14 @@ The provider class
|
||||
|
||||
.. automethod:: execute_refund
|
||||
|
||||
.. automethod:: refund_control_render
|
||||
|
||||
.. automethod:: api_payment_details
|
||||
|
||||
.. automethod:: shred_payment_info
|
||||
|
||||
.. automethod:: cancel_payment
|
||||
|
||||
.. autoattribute:: is_implicit
|
||||
|
||||
.. autoattribute:: is_meta
|
||||
|
||||
79
doc/development/api/placeholder.rst
Normal file
79
doc/development/api/placeholder.rst
Normal file
@@ -0,0 +1,79 @@
|
||||
.. highlight:: python
|
||||
:linenothreshold: 5
|
||||
|
||||
Writing an e-mail placeholder plugin
|
||||
====================================
|
||||
|
||||
An email placeholder is a dynamic value that pretix users can use in their email templates.
|
||||
|
||||
Please read :ref:`Creating a plugin <pluginsetup>` first, if you haven't already.
|
||||
|
||||
Placeholder registration
|
||||
------------------------
|
||||
|
||||
The placeholder API does not make a lot of usage from signals, however, it
|
||||
does use a signal to get a list of all available email placeholders. Your plugin
|
||||
should listen for this signal and return an instance of a subclass of ``pretix.base.email.BaseMailTextPlaceholder``::
|
||||
|
||||
from django.dispatch import receiver
|
||||
|
||||
from pretix.base.signals import register_mail_placeholders
|
||||
|
||||
|
||||
@receiver(register_mail_placeholders, dispatch_uid="placeholder_custom")
|
||||
def register_mail_renderers(sender, **kwargs):
|
||||
from .email import MyPlaceholderClass
|
||||
return MyPlaceholder()
|
||||
|
||||
|
||||
Context mechanism
|
||||
-----------------
|
||||
|
||||
Emails are sent in different "contexts" within pretix. For example, many emails are sent in the
|
||||
the context of an order, but some are not, such as the notification of a waiting list voucher.
|
||||
|
||||
Not all placeholders make sense in every email, and placeholders usually depend some parameters
|
||||
themselves, such as the ``Order`` object. Therefore, placeholders are expected to explicitly declare
|
||||
what values they depend on and they will only be available in an email if all those dependencies are
|
||||
met. Currently, placeholders can depend on the following context parameters:
|
||||
|
||||
* ``event``
|
||||
* ``order``
|
||||
* ``position``
|
||||
* ``waiting_list_entry``
|
||||
* ``invoice_address``
|
||||
* ``payment``
|
||||
|
||||
There are a few more that are only to be used internally but not by plugins.
|
||||
|
||||
The placeholder class
|
||||
---------------------
|
||||
|
||||
.. class:: pretix.base.email.BaseMailTextPlaceholder
|
||||
|
||||
.. autoattribute:: identifier
|
||||
|
||||
This is an abstract attribute, you **must** override this!
|
||||
|
||||
.. autoattribute:: required_context
|
||||
|
||||
This is an abstract attribute, you **must** override this!
|
||||
|
||||
.. automethod:: render
|
||||
|
||||
This is an abstract method, you **must** implement this!
|
||||
|
||||
.. automethod:: render_sample
|
||||
|
||||
This is an abstract method, you **must** implement this!
|
||||
|
||||
Helper class for simple placeholders
|
||||
------------------------------------
|
||||
|
||||
pretix ships with a helper class that makes it easy to provide placeholders based on simple
|
||||
functions::
|
||||
|
||||
placeholder = SimpleFunctionalMailTextPlaceholder(
|
||||
'code', ['order'], lambda order: order.code, sample='F8VVL'
|
||||
)
|
||||
|
||||
@@ -35,9 +35,9 @@ The shredder class
|
||||
|
||||
.. class:: pretix.base.shredder.BaseDataShredder
|
||||
|
||||
The central object of each invoice renderer is the subclass of ``BaseInvoiceRenderer``.
|
||||
The central object of each data shredder is the subclass of ``BaseDataShredder``.
|
||||
|
||||
.. py:attribute:: BaseInvoiceRenderer.event
|
||||
.. py:attribute:: BaseDataShredder.event
|
||||
|
||||
The default constructor sets this property to the event we are currently
|
||||
working for.
|
||||
|
||||
@@ -69,3 +69,9 @@ The output class
|
||||
.. automethod:: generate_order
|
||||
|
||||
.. autoattribute:: download_button_text
|
||||
|
||||
.. autoattribute:: download_button_icon
|
||||
|
||||
.. autoattribute:: preview_allowed
|
||||
|
||||
.. autoattribute:: javascript_required
|
||||
|
||||
@@ -65,7 +65,7 @@ Then, create the local database::
|
||||
python manage.py migrate
|
||||
|
||||
A first user with username ``admin@localhost`` and password ``admin`` will be automatically
|
||||
created.
|
||||
created.
|
||||
|
||||
If you want to see pretix in a different language than English, you have to compile our language
|
||||
files::
|
||||
@@ -81,8 +81,7 @@ To run the local development webserver, execute::
|
||||
and head to http://localhost:8000/
|
||||
|
||||
As we did not implement an overall front page yet, you need to go directly to
|
||||
http://localhost:8000/control/ for the admin view or, if you imported the test
|
||||
data as suggested above, to the event page at http://localhost:8000/bigevents/2019/
|
||||
http://localhost:8000/control/ for the admin view.
|
||||
|
||||
.. note:: If you want the development server to listen on a different interface or
|
||||
port (for example because you develop on `pretixdroid`_), you can check
|
||||
|
||||
@@ -1,12 +1 @@
|
||||
Table of contents
|
||||
=================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
user/index
|
||||
admin/index
|
||||
api/index
|
||||
development/index
|
||||
plugins/index
|
||||
|
||||
.. include:: contents.rst
|
||||
@@ -31,6 +31,7 @@ deprovision
|
||||
discoverable
|
||||
django
|
||||
dockerfile
|
||||
downloadable
|
||||
durations
|
||||
eu
|
||||
filename
|
||||
@@ -41,11 +42,13 @@ formsets
|
||||
frontend
|
||||
frontpage
|
||||
gettext
|
||||
giftcard
|
||||
gunicorn
|
||||
guid
|
||||
hardcoded
|
||||
hostname
|
||||
idempotency
|
||||
iframe
|
||||
incrementing
|
||||
inofficial
|
||||
invalidations
|
||||
@@ -54,6 +57,7 @@ Jimdo
|
||||
libpretixprint
|
||||
libsass
|
||||
linters
|
||||
login
|
||||
memcached
|
||||
metadata
|
||||
middleware
|
||||
@@ -75,6 +79,7 @@ overpayment
|
||||
param
|
||||
passphrase
|
||||
percental
|
||||
pluggable
|
||||
positionid
|
||||
pre
|
||||
prepend
|
||||
@@ -104,6 +109,7 @@ screenshot
|
||||
scss
|
||||
searchable
|
||||
selectable
|
||||
serializable
|
||||
serializers
|
||||
serializers
|
||||
sexualized
|
||||
@@ -135,6 +141,7 @@ username
|
||||
url
|
||||
versa
|
||||
versioning
|
||||
viewable
|
||||
viewset
|
||||
viewsets
|
||||
waitinglist
|
||||
|
||||
71
doc/user/events/giftcards.rst
Normal file
71
doc/user/events/giftcards.rst
Normal file
@@ -0,0 +1,71 @@
|
||||
.. spelling::
|
||||
|
||||
Warengutschein
|
||||
Wertgutschein
|
||||
|
||||
Gift cards
|
||||
==========
|
||||
|
||||
Gift cards, also known as "gift coupons" or "gift certificates" are a mechanism that allows you to sell tokens that
|
||||
can later be used to pay for tickets.
|
||||
|
||||
Gift cards are very different feature than **vouchers**. The difference is:
|
||||
|
||||
* Vouchers can be used to give a discount. When a voucher is used, the price of a ticket is reduced by the configured
|
||||
discount and sold at a lower price. They therefore reduce both revenue as well as taxes. Vouchers (in pretix) are
|
||||
always specific to a certain product in an order. Vouchers are usually not sold but given out as part of a
|
||||
marketing campaign or to specific groups of people. Vouchers in pretix are bound to a specific event.
|
||||
|
||||
* Gift cards are not a discount, but rather a means of payment. If you buy a €20 ticket with a €10 gift card, it is
|
||||
still a €20 ticket and will still count towards your revenue with €20. Gift cards are usually bought for the money
|
||||
that they are worth. Gift cards in pretix can be used across events (and even organizers).
|
||||
|
||||
Selling gift cards
|
||||
------------------
|
||||
|
||||
Selling gift cards works like selling every other type of product in pretix: Create a new product, then head to
|
||||
"Additional settings" and select the option "This product is a gift card". Whenever someone buys this product and
|
||||
pays for it, a new gift card will be created.
|
||||
|
||||
In this case, the gift card code corresponds to the "ticket secret" in the PDF ticket. Therefore, if selling gift cards,
|
||||
you can use ticket downloads just as with normal tickets and use our ticket editor to create beautiful gift certificates
|
||||
people can give to their loved ones.
|
||||
|
||||
Of course, you can use pretix' flexible options to modify your product. For example, you can configure that the customer
|
||||
can freely choose the price of the gift card.
|
||||
|
||||
.. note::
|
||||
|
||||
pretix currently does not support charging sales tax or VAT when selling gift cards, but instead charges VAT on
|
||||
the full price when the gift card is redeemed. This is the correct behavior in Germany and some other countries for
|
||||
gift cards which are not bound to a very specific service ("Warengutschein"), but instead to a monetary amount
|
||||
("Wertgutschein").
|
||||
|
||||
.. note::
|
||||
|
||||
The ticket PDF will not contain the correct gift card code before the order has been paid, so we recommend not
|
||||
selling gift cards in events where tickets are issued before payments arrive.
|
||||
|
||||
|
||||
Accepting gift cards
|
||||
--------------------
|
||||
|
||||
All your events have have the payment provider "Gift card" enabled by default, but it will only show up in the ticket
|
||||
shop once the very first gift card has been issued on your organizer account. Of course, you can turn off gift card
|
||||
payments if you do not want them for a specific event.
|
||||
|
||||
If gift card payments are enabled, buyers will be able to select "Gift card" as a payment method during checkout. If
|
||||
a gift card with a value less than the order total is used, the buyer will be asked to select a second payment method
|
||||
for the remaining payment. If a gift card with a value greater than the order total is used, the surplus amount
|
||||
remains on the gift card and can be used in a different purchase.
|
||||
|
||||
If it possible to accept gift cards across organizer accounts. To do so, you need to have access to both organizer
|
||||
accounts. Then, you will see a configuration section at the bottom of the "Gift cards" page of your organizer settings
|
||||
where you can specify which gift cards should be accepted.
|
||||
|
||||
Manually issuing or using gift cards
|
||||
------------------------------------
|
||||
|
||||
Of course, you can also issue or redeem gift cards manually through our backend using the "Gift cards" menu item in your
|
||||
organizer profile or using our API. These gift cards will be tracked by pretix, but do not correspond to any purchase
|
||||
within pretix. You will therefore need to account for them in your books separately.
|
||||
@@ -206,6 +206,21 @@ If you want, you can suppress us loading the widget and/or modify the user data
|
||||
|
||||
If you then later want to trigger loading the widgets, just call ``window.PretixWidget.buildWidgets()``.
|
||||
|
||||
Waiting for the widget to load
|
||||
------------------------------
|
||||
|
||||
If you want to run custom JavaScript once the widget is fully loaded, you can register a callback function. Note that
|
||||
this function might be run multiple times, for example if you have multiple widgets on a page or if the user switches
|
||||
e.g. from an event list to an event detail view::
|
||||
|
||||
<script type="text/javascript">
|
||||
window.pretixWidgetCallback = function () {
|
||||
window.PretixWidget.addLoadListener(function () {
|
||||
console.log("Widget has loaded!");
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
Passing user data to the widget
|
||||
-------------------------------
|
||||
@@ -241,6 +256,11 @@ This works for the pretix Button as well. Currently, the following attributes ar
|
||||
naming scheme such as ``name-title`` or ``name-given-name`` (see above). ``country`` expects a two-character
|
||||
country code.
|
||||
|
||||
* If ``data-fix="true"`` is given, the user will not be able to change the other given values later. This currently
|
||||
only works for the order email address as well as the invoice address. Attendee-level fields and questions can
|
||||
always be modified. Note that this is not a security feature and can easily be overridden by users, so do not rely
|
||||
on this for authentication.
|
||||
|
||||
Any configured pretix plugins might understand more data fields. For example, if the appropriate plugins on pretix
|
||||
Hosted or pretix Enterprise are active, you can pass the following fields:
|
||||
|
||||
@@ -274,6 +294,9 @@ Hosted or pretix Enterprise are active, you can pass the following fields:
|
||||
};
|
||||
</script>
|
||||
|
||||
In some combinations with Google Tag Manager, the widget does not load this way. In this case, try replacing
|
||||
``tracker.get('clientId')`` with ``ga.getAll()[0].get('clientId')``.
|
||||
|
||||
|
||||
.. versionchanged:: 2.3
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -12,5 +12,6 @@ wanting to use pretix to sell tickets.
|
||||
events/settings
|
||||
events/structureguide
|
||||
events/widget
|
||||
events/giftcards
|
||||
faq
|
||||
markdown
|
||||
markdown
|
||||
|
||||
@@ -22,3 +22,5 @@ recursive-include pretix/plugins/ticketoutputpdf/templates *
|
||||
recursive-include pretix/plugins/ticketoutputpdf/static *
|
||||
recursive-include pretix/plugins/badges/templates *
|
||||
recursive-include pretix/plugins/badges/static *
|
||||
recursive-include pretix/plugins/returnurl/templates *
|
||||
recursive-include pretix/plugins/returnurl/static *
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "3.0.0"
|
||||
__version__ = "3.5.0"
|
||||
|
||||
18
src/pretix/api/migrations/0005_auto_20191028_1541.py
Normal file
18
src/pretix/api/migrations/0005_auto_20191028_1541.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.2.1 on 2019-10-28 15:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixapi', '0004_auto_20190405_1048'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='oauthgrant',
|
||||
name='redirect_uri',
|
||||
field=models.CharField(max_length=2500),
|
||||
),
|
||||
]
|
||||
@@ -43,6 +43,7 @@ class OAuthGrant(AbstractGrant):
|
||||
OAuthApplication, on_delete=models.CASCADE
|
||||
)
|
||||
organizers = models.ManyToManyField('pretixbase.Organizer')
|
||||
redirect_uri = models.CharField(max_length=2500) # Only 255 in AbstractGrant, which caused problems
|
||||
|
||||
|
||||
class OAuthAccessToken(AbstractAccessToken):
|
||||
|
||||
@@ -30,11 +30,12 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
expires = serializers.DateTimeField(required=False)
|
||||
attendee_name = serializers.CharField(required=False, allow_null=True)
|
||||
seat = serializers.CharField(required=False, allow_null=True)
|
||||
sales_channel = serializers.CharField(required=False, default='sales_channel')
|
||||
|
||||
class Meta:
|
||||
model = CartPosition
|
||||
fields = ('cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||
'subevent', 'expires', 'includes_tax', 'answers', 'seat')
|
||||
'subevent', 'expires', 'includes_tax', 'answers', 'seat', 'sales_channel')
|
||||
|
||||
def create(self, validated_data):
|
||||
answers_data = validated_data.pop('answers')
|
||||
@@ -82,13 +83,16 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
seat = self.context['event'].seats.get(seat_guid=validated_data['seat'], subevent=validated_data.get('subevent'))
|
||||
except Seat.DoesNotExist:
|
||||
raise ValidationError('The specified seat does not exist.')
|
||||
except Seat.MultipleObjectsReturned:
|
||||
raise ValidationError('The specified seat ID is not unique.')
|
||||
else:
|
||||
validated_data['seat'] = seat
|
||||
if not seat.is_available():
|
||||
if not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web')):
|
||||
raise ValidationError(ugettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
|
||||
elif seated:
|
||||
raise ValidationError('The specified product requires to choose a seat.')
|
||||
|
||||
validated_data.pop('sales_channel')
|
||||
cp = CartPosition.objects.create(event=self.context['event'], **validated_data)
|
||||
|
||||
for answ_data in answers_data:
|
||||
|
||||
@@ -3,6 +3,7 @@ from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.models import CheckinList
|
||||
|
||||
|
||||
@@ -13,7 +14,7 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = CheckinList
|
||||
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
|
||||
'include_pending')
|
||||
'include_pending', 'auto_checkin_sales_channels')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -35,4 +36,8 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
if full_data.get('subevent'):
|
||||
raise ValidationError(_('The subevent does not belong to this event.'))
|
||||
|
||||
for channel in full_data.get('auto_checkin_sales_channels') or []:
|
||||
if channel not in get_all_sales_channels():
|
||||
raise ValidationError(_('Unknown sales channel.'))
|
||||
|
||||
return data
|
||||
|
||||
@@ -4,7 +4,8 @@ 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
|
||||
from pytz import common_timezones
|
||||
from rest_framework.fields import ChoiceField, Field
|
||||
from rest_framework.relations import SlugRelatedField
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
@@ -61,17 +62,27 @@ class PluginsField(Field):
|
||||
}
|
||||
|
||||
|
||||
class TimeZoneField(ChoiceField):
|
||||
def get_attribute(self, instance):
|
||||
return instance.cache.get_or_set(
|
||||
'timezone_name',
|
||||
lambda: instance.settings.timezone,
|
||||
3600
|
||||
)
|
||||
|
||||
|
||||
class EventSerializer(I18nAwareModelSerializer):
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
plugins = PluginsField(required=False, source='*')
|
||||
seat_category_mapping = SeatCategoryMappingField(source='*', required=False)
|
||||
timezone = TimeZoneField(required=False, choices=[(a, a) for a in common_timezones])
|
||||
|
||||
class Meta:
|
||||
model = Event
|
||||
fields = ('name', 'slug', 'live', 'testmode', 'currency', 'date_from',
|
||||
'date_to', 'date_admission', 'is_public', 'presale_start',
|
||||
'presale_end', 'location', 'has_subevents', 'meta_data', 'seating_plan',
|
||||
'plugins', 'seat_category_mapping')
|
||||
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
|
||||
'plugins', 'seat_category_mapping', 'timezone')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -156,8 +167,12 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
validated_data.pop('seat_category_mapping', None)
|
||||
plugins = validated_data.pop('plugins', settings.PRETIX_PLUGINS_DEFAULT.split(','))
|
||||
tz = validated_data.pop('timezone', None)
|
||||
event = super().create(validated_data)
|
||||
|
||||
if tz:
|
||||
event.settings.timezone = tz
|
||||
|
||||
# Meta data
|
||||
if meta_data is not None:
|
||||
for key, value in meta_data.items():
|
||||
@@ -182,8 +197,12 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
plugins = validated_data.pop('plugins', None)
|
||||
seat_category_mapping = validated_data.pop('seat_category_mapping', None)
|
||||
tz = validated_data.pop('timezone', None)
|
||||
event = super().update(instance, validated_data)
|
||||
|
||||
if tz:
|
||||
event.settings.timezone = tz
|
||||
|
||||
# Meta data
|
||||
if meta_data is not None:
|
||||
current = {mv.property: mv for mv in event.meta_values.select_related('property')}
|
||||
@@ -239,6 +258,8 @@ class CloneEventSerializer(EventSerializer):
|
||||
plugins = validated_data.pop('plugins', None)
|
||||
is_public = validated_data.pop('is_public', None)
|
||||
testmode = validated_data.pop('testmode', None)
|
||||
has_subevents = validated_data.pop('has_subevents', None)
|
||||
tz = validated_data.pop('timezone', None)
|
||||
new_event = super().create(validated_data)
|
||||
|
||||
event = Event.objects.filter(slug=self.context['event'], organizer=self.context['organizer'].pk).first()
|
||||
@@ -250,7 +271,11 @@ class CloneEventSerializer(EventSerializer):
|
||||
new_event.is_public = is_public
|
||||
if testmode is not None:
|
||||
new_event.testmode = testmode
|
||||
if has_subevents is not None:
|
||||
new_event.has_subevents = has_subevents
|
||||
new_event.save()
|
||||
if tz:
|
||||
new_event.settings.timezone = tz
|
||||
|
||||
return new_event
|
||||
|
||||
@@ -277,8 +302,9 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = SubEvent
|
||||
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
|
||||
'presale_start', 'presale_end', 'location', 'event', 'is_public', 'seating_plan',
|
||||
'item_price_overrides', 'variation_price_overrides', 'meta_data', 'seat_category_mapping')
|
||||
'presale_start', 'presale_end', 'location', 'geo_lat', 'geo_lon', 'event', 'is_public',
|
||||
'seating_plan', 'item_price_overrides', 'variation_price_overrides', 'meta_data',
|
||||
'seat_category_mapping')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
@@ -119,12 +119,9 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
||||
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
|
||||
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
|
||||
'show_quota_left', 'hidden_if_available', 'allow_waitinglist')
|
||||
'show_quota_left', 'hidden_if_available', 'allow_waitinglist', 'issue_giftcard')
|
||||
read_only_fields = ('has_variations', 'picture')
|
||||
|
||||
def get_serializer_context(self):
|
||||
return {"has_variations": self.kwargs['has_variations']}
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
if self.instance and ('addons' in data or 'variations' in data or 'bundles' in data):
|
||||
@@ -134,6 +131,17 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
Item.clean_per_order(data.get('min_per_order'), data.get('max_per_order'))
|
||||
Item.clean_available(data.get('available_from'), data.get('available_until'))
|
||||
|
||||
if data.get('issue_giftcard'):
|
||||
if data.get('tax_rule') and data.get('tax_rule').rate > 0:
|
||||
raise ValidationError(
|
||||
_("Gift card products should not be associated with non-zero tax rates since sales tax will be "
|
||||
"applied when the gift card is redeemed.")
|
||||
)
|
||||
if data.get('admission'):
|
||||
raise ValidationError(_(
|
||||
"Gift card products should not be admission products at the same time."
|
||||
))
|
||||
|
||||
return data
|
||||
|
||||
def validate_category(self, value):
|
||||
@@ -219,7 +227,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
model = Question
|
||||
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
|
||||
'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
|
||||
'hidden', 'dependency_value')
|
||||
'hidden', 'dependency_value', 'print_on_invoice', 'help_text')
|
||||
|
||||
def validate_identifier(self, value):
|
||||
Question._clean_identifier(self.context['event'], value, self.instance)
|
||||
@@ -249,6 +257,9 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
|
||||
dep = full_data.get('dependency_question')
|
||||
if dep:
|
||||
if dep.ask_during_checkin:
|
||||
raise ValidationError(_('Question cannot depend on a question asked during check-in.'))
|
||||
|
||||
seen_ids = {self.instance.pk} if self.instance else set()
|
||||
while dep:
|
||||
if dep.pk in seen_ids:
|
||||
@@ -256,6 +267,9 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
seen_ids.add(dep.pk)
|
||||
dep = dep.dependency_question
|
||||
|
||||
if full_data.get('ask_during_checkin') and full_data.get('type') in Question.ASK_DURING_CHECKIN_UNSUPPORTED:
|
||||
raise ValidationError(_('This type of question cannot be asked during check-in.'))
|
||||
|
||||
Question.clean_items(event, full_data.get('items'))
|
||||
return data
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import json
|
||||
from collections import Counter, defaultdict
|
||||
from decimal import Decimal
|
||||
|
||||
import pycountry
|
||||
from django.db.models import F, Q
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy
|
||||
from django_countries.fields import Country
|
||||
@@ -11,16 +14,21 @@ from rest_framework.reverse import reverse
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
Checkin, Invoice, InvoiceAddress, InvoiceLine, Item, ItemVariation, Order,
|
||||
OrderPosition, Question, QuestionAnswer, Seat, SubEvent,
|
||||
OrderPosition, Question, QuestionAnswer, Seat, SubEvent, TaxRule, Voucher,
|
||||
)
|
||||
from pretix.base.models.orders import (
|
||||
CartPosition, OrderFee, OrderPayment, OrderRefund,
|
||||
)
|
||||
from pretix.base.pdf import get_variables
|
||||
from pretix.base.services.cart import error_messages
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
||||
from pretix.base.signals import register_ticket_outputs
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
|
||||
|
||||
class CompatibleCountryField(serializers.Field):
|
||||
@@ -41,8 +49,8 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = InvoiceAddress
|
||||
fields = ('last_modified', 'is_business', 'company', 'name', 'name_parts', 'street', 'zipcode', 'city', 'country',
|
||||
'vat_id', 'vat_id_validated', 'internal_reference')
|
||||
read_only_fields = ('last_modified', 'vat_id_validated')
|
||||
'state', 'vat_id', 'vat_id_validated', 'internal_reference')
|
||||
read_only_fields = ('last_modified',)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -57,6 +65,24 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
||||
)
|
||||
if data.get('name_parts') and '_scheme' not in data.get('name_parts'):
|
||||
data['name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
|
||||
|
||||
if data.get('country'):
|
||||
if not pycountry.countries.get(alpha_2=data.get('country')):
|
||||
raise ValidationError(
|
||||
{'country': ['Invalid country code.']}
|
||||
)
|
||||
|
||||
if data.get('state'):
|
||||
cc = str(data.get('country') or self.instance.country or '')
|
||||
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||
raise ValidationError(
|
||||
{'state': ['States are not supported in country "{}".'.format(cc)]}
|
||||
)
|
||||
if not pycountry.subdivisions.get(code=cc + '-' + data.get('state')):
|
||||
raise ValidationError(
|
||||
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -89,7 +115,7 @@ class AnswerSerializer(I18nAwareModelSerializer):
|
||||
class CheckinSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = Checkin
|
||||
fields = ('datetime', 'list')
|
||||
fields = ('datetime', 'list', 'auto_checked_in')
|
||||
|
||||
|
||||
class OrderDownloadsField(serializers.Field):
|
||||
@@ -178,7 +204,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
model = OrderPosition
|
||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat')
|
||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -258,13 +284,36 @@ class OrderPaymentDateField(serializers.DateField):
|
||||
class OrderFeeSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = OrderFee
|
||||
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule')
|
||||
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule', 'canceled')
|
||||
|
||||
|
||||
class PaymentURLField(serializers.URLField):
|
||||
def to_representation(self, instance: OrderPayment):
|
||||
if instance.state != OrderPayment.PAYMENT_STATE_CREATED:
|
||||
return None
|
||||
return build_absolute_uri(self.context['event'], 'presale:event.order.pay', kwargs={
|
||||
'order': instance.order.code,
|
||||
'secret': instance.order.secret,
|
||||
'payment': instance.pk,
|
||||
})
|
||||
|
||||
|
||||
class PaymentDetailsField(serializers.Field):
|
||||
def to_representation(self, value: OrderPayment):
|
||||
pp = value.payment_provider
|
||||
if not pp:
|
||||
return {}
|
||||
return pp.api_payment_details(value)
|
||||
|
||||
|
||||
class OrderPaymentSerializer(I18nAwareModelSerializer):
|
||||
payment_url = PaymentURLField(source='*', allow_null=True, read_only=True)
|
||||
details = PaymentDetailsField(source='*', allow_null=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = OrderPayment
|
||||
fields = ('local_id', 'state', 'amount', 'created', 'payment_date', 'provider')
|
||||
fields = ('local_id', 'state', 'amount', 'created', 'payment_date', 'provider', 'payment_url',
|
||||
'details')
|
||||
|
||||
|
||||
class OrderRefundSerializer(I18nAwareModelSerializer):
|
||||
@@ -275,6 +324,14 @@ class OrderRefundSerializer(I18nAwareModelSerializer):
|
||||
fields = ('local_id', 'state', 'source', 'amount', 'payment', 'created', 'execution_date', 'provider')
|
||||
|
||||
|
||||
class OrderURLField(serializers.URLField):
|
||||
def to_representation(self, instance: Order):
|
||||
return build_absolute_uri(self.context['event'], 'presale:event.order', kwargs={
|
||||
'order': instance.code,
|
||||
'secret': instance.secret,
|
||||
})
|
||||
|
||||
|
||||
class OrderSerializer(I18nAwareModelSerializer):
|
||||
invoice_address = InvoiceAddressSerializer(allow_null=True)
|
||||
positions = OrderPositionSerializer(many=True, read_only=True)
|
||||
@@ -284,13 +341,15 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
refunds = OrderRefundSerializer(many=True, read_only=True)
|
||||
payment_date = OrderPaymentDateField(source='*', read_only=True)
|
||||
payment_provider = OrderPaymentTypeField(source='*', read_only=True)
|
||||
url = OrderURLField(source='*', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = (
|
||||
'code', 'status', 'testmode', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
|
||||
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
|
||||
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel'
|
||||
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
|
||||
'url'
|
||||
)
|
||||
read_only_fields = (
|
||||
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
|
||||
@@ -329,7 +388,7 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
}
|
||||
try:
|
||||
ia = instance.invoice_address
|
||||
if iadata.get('vat_id') != ia.vat_id:
|
||||
if iadata.get('vat_id') != ia.vat_id and 'vat_id_validated' not in iadata:
|
||||
ia.vat_id_validated = False
|
||||
self.fields['invoice_address'].update(ia, iadata)
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
@@ -419,9 +478,13 @@ class AnswerCreateSerializer(I18nAwareModelSerializer):
|
||||
|
||||
|
||||
class OrderFeeCreateSerializer(I18nAwareModelSerializer):
|
||||
_treat_value_as_percentage = serializers.BooleanField(default=False, required=False)
|
||||
_split_taxes_like_products = serializers.BooleanField(default=False, required=False)
|
||||
|
||||
class Meta:
|
||||
model = OrderFee
|
||||
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rule')
|
||||
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rule',
|
||||
'_treat_value_as_percentage', '_split_taxes_like_products')
|
||||
|
||||
def validate_tax_rule(self, tr):
|
||||
if tr and tr.event != self.context['event']:
|
||||
@@ -437,11 +500,15 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
secret = serializers.CharField(required=False)
|
||||
attendee_name = serializers.CharField(required=False, allow_null=True)
|
||||
seat = serializers.CharField(required=False, allow_null=True)
|
||||
price = serializers.DecimalField(required=False, allow_null=True, decimal_places=2,
|
||||
max_digits=10)
|
||||
voucher = serializers.SlugRelatedField(slug_field='code', queryset=Voucher.objects.none(),
|
||||
required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||
'secret', 'addon_to', 'subevent', 'answers', 'seat')
|
||||
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher')
|
||||
|
||||
def validate_secret(self, secret):
|
||||
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
|
||||
@@ -515,7 +582,7 @@ class CompatibleJSONField(serializers.JSONField):
|
||||
|
||||
class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
invoice_address = InvoiceAddressSerializer(required=False)
|
||||
positions = OrderPositionCreateSerializer(many=True, required=False)
|
||||
positions = OrderPositionCreateSerializer(many=True, required=True)
|
||||
fees = OrderFeeCreateSerializer(many=True, required=False)
|
||||
status = serializers.ChoiceField(choices=(
|
||||
('n', Order.STATUS_PENDING),
|
||||
@@ -527,18 +594,26 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
min_length=5
|
||||
)
|
||||
comment = serializers.CharField(required=False, allow_blank=True)
|
||||
payment_provider = serializers.CharField(required=True)
|
||||
payment_provider = serializers.CharField(required=False, allow_null=True)
|
||||
payment_info = CompatibleJSONField(required=False)
|
||||
consume_carts = serializers.ListField(child=serializers.CharField(), required=False)
|
||||
force = serializers.BooleanField(default=False, required=False)
|
||||
payment_date = serializers.DateTimeField(required=False, allow_null=True)
|
||||
send_mail = serializers.BooleanField(default=False, required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['positions'].child.fields['voucher'].queryset = self.context['event'].vouchers.all()
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ('code', 'status', 'testmode', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
||||
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts', 'force')
|
||||
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts',
|
||||
'force', 'send_mail')
|
||||
|
||||
def validate_payment_provider(self, pp):
|
||||
if pp is None:
|
||||
return None
|
||||
if pp not in self.context['event'].get_payment_providers():
|
||||
raise ValidationError('The given payment provider is not known.')
|
||||
return pp
|
||||
@@ -605,13 +680,28 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
raise ValidationError(errs)
|
||||
return data
|
||||
|
||||
def validate_testmode(self, testmode):
|
||||
if 'sales_channel' in self.initial_data:
|
||||
try:
|
||||
sales_channel = get_all_sales_channels()[self.initial_data['sales_channel']]
|
||||
|
||||
if testmode and not sales_channel.testmode_supported:
|
||||
raise ValidationError('This sales channel does not provide support for testmode.')
|
||||
except KeyError:
|
||||
# We do not need to raise a ValidationError here, since there is another check to validate the
|
||||
# sales_channel
|
||||
pass
|
||||
|
||||
return testmode
|
||||
|
||||
def create(self, validated_data):
|
||||
fees_data = validated_data.pop('fees') if 'fees' in validated_data else []
|
||||
positions_data = validated_data.pop('positions') if 'positions' in validated_data else []
|
||||
payment_provider = validated_data.pop('payment_provider')
|
||||
payment_provider = validated_data.pop('payment_provider', None)
|
||||
payment_info = validated_data.pop('payment_info', '{}')
|
||||
payment_date = validated_data.pop('payment_date', now())
|
||||
force = validated_data.pop('force', False)
|
||||
self._send_mail = validated_data.pop('send_mail', False)
|
||||
|
||||
if 'invoice_address' in validated_data:
|
||||
iadata = validated_data.pop('invoice_address')
|
||||
@@ -630,8 +720,12 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
consume_carts = validated_data.pop('consume_carts', [])
|
||||
delete_cps = []
|
||||
quota_avail_cache = {}
|
||||
v_budget = {}
|
||||
voucher_usage = Counter()
|
||||
if consume_carts:
|
||||
for cp in CartPosition.objects.filter(event=self.context['event'], cart_id__in=consume_carts):
|
||||
for cp in CartPosition.objects.filter(
|
||||
event=self.context['event'], cart_id__in=consume_carts, expires__gt=now()
|
||||
):
|
||||
quotas = (cp.variation.quotas.filter(subevent=cp.subevent)
|
||||
if cp.variation else cp.item.quotas.filter(subevent=cp.subevent))
|
||||
for quota in quotas:
|
||||
@@ -639,6 +733,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
quota_avail_cache[quota] = list(quota.availability())
|
||||
if quota_avail_cache[quota][1] is not None:
|
||||
quota_avail_cache[quota][1] += 1
|
||||
if cp.voucher:
|
||||
voucher_usage[cp.voucher] -= 1
|
||||
if cp.expires > now_dt:
|
||||
if cp.seat:
|
||||
free_seats.add(cp.seat)
|
||||
@@ -646,8 +742,98 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
|
||||
errs = [{} for p in positions_data]
|
||||
|
||||
for i, pos_data in enumerate(positions_data):
|
||||
|
||||
if pos_data.get('voucher'):
|
||||
v = pos_data['voucher']
|
||||
|
||||
if pos_data.get('addon_to'):
|
||||
errs[i]['voucher'] = ['Vouchers are currently not supported for add-on products.']
|
||||
continue
|
||||
|
||||
if not v.applies_to(pos_data['item'], pos_data.get('variation')):
|
||||
errs[i]['voucher'] = [error_messages['voucher_invalid_item']]
|
||||
continue
|
||||
|
||||
if v.subevent_id and pos_data.get('subevent').pk != v.subevent_id:
|
||||
errs[i]['voucher'] = [error_messages['voucher_invalid_subevent']]
|
||||
continue
|
||||
|
||||
if v.valid_until is not None and v.valid_until < now_dt:
|
||||
errs[i]['voucher'] = [error_messages['voucher_expired']]
|
||||
continue
|
||||
|
||||
voucher_usage[v] += 1
|
||||
if voucher_usage[v] > 0:
|
||||
redeemed_in_carts = CartPosition.objects.filter(
|
||||
Q(voucher=pos_data['voucher']) & Q(event=self.context['event']) & Q(expires__gte=now_dt)
|
||||
).exclude(pk__in=[cp.pk for cp in delete_cps])
|
||||
v_avail = v.max_usages - v.redeemed - redeemed_in_carts.count()
|
||||
if v_avail < voucher_usage[v]:
|
||||
errs[i]['voucher'] = [
|
||||
'The voucher has already been used the maximum number of times.'
|
||||
]
|
||||
|
||||
if v.budget is not None:
|
||||
price = pos_data.get('price')
|
||||
if price is None:
|
||||
price = get_price(
|
||||
item=pos_data.get('item'),
|
||||
variation=pos_data.get('variation'),
|
||||
voucher=v,
|
||||
custom_price=None,
|
||||
subevent=pos_data.get('subevent'),
|
||||
addon_to=pos_data.get('addon_to'),
|
||||
invoice_address=ia,
|
||||
).gross
|
||||
pbv = get_price(
|
||||
item=pos_data['item'],
|
||||
variation=pos_data.get('variation'),
|
||||
voucher=None,
|
||||
custom_price=None,
|
||||
subevent=pos_data.get('subevent'),
|
||||
addon_to=pos_data.get('addon_to'),
|
||||
invoice_address=ia,
|
||||
)
|
||||
|
||||
if v not in v_budget:
|
||||
v_budget[v] = v.budget - v.budget_used()
|
||||
disc = pbv.gross - price
|
||||
if disc > v_budget[v]:
|
||||
new_disc = v_budget[v]
|
||||
v_budget[v] -= new_disc
|
||||
if new_disc == Decimal('0.00') or pos_data.get('price') is not None:
|
||||
errs[i]['voucher'] = [
|
||||
'The voucher has a remaining budget of {}, therefore a discount of {} can not be '
|
||||
'given.'.format(v_budget[v] + new_disc, disc)
|
||||
]
|
||||
continue
|
||||
pos_data['price'] = price + (disc - new_disc)
|
||||
else:
|
||||
v_budget[v] -= disc
|
||||
|
||||
seated = pos_data.get('item').seat_category_mappings.filter(subevent=pos_data.get('subevent')).exists()
|
||||
if pos_data.get('seat'):
|
||||
if not seated:
|
||||
errs[i]['seat'] = ['The specified product does not allow to choose a seat.']
|
||||
try:
|
||||
seat = self.context['event'].seats.get(seat_guid=pos_data['seat'], subevent=pos_data.get('subevent'))
|
||||
except Seat.DoesNotExist:
|
||||
errs[i]['seat'] = ['The specified seat does not exist.']
|
||||
else:
|
||||
pos_data['seat'] = seat
|
||||
if (seat not in free_seats and not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web'))) or seat in seats_seen:
|
||||
errs[i]['seat'] = [ugettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
|
||||
seats_seen.add(seat)
|
||||
elif seated:
|
||||
errs[i]['seat'] = ['The specified product requires to choose a seat.']
|
||||
|
||||
if not force:
|
||||
for i, pos_data in enumerate(positions_data):
|
||||
if pos_data.get('voucher'):
|
||||
if pos_data['voucher'].allow_ignore_quota or pos_data['voucher'].block_quota:
|
||||
continue
|
||||
|
||||
new_quotas = (pos_data.get('variation').quotas.filter(subevent=pos_data.get('subevent'))
|
||||
if pos_data.get('variation')
|
||||
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
|
||||
@@ -669,23 +855,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
)
|
||||
]
|
||||
|
||||
for i, pos_data in enumerate(positions_data):
|
||||
seated = pos_data.get('item').seat_category_mappings.filter(subevent=pos_data.get('subevent')).exists()
|
||||
if pos_data.get('seat'):
|
||||
if not seated:
|
||||
errs[i]['seat'] = ['The specified product does not allow to choose a seat.']
|
||||
try:
|
||||
seat = self.context['event'].seats.get(seat_guid=pos_data['seat'], subevent=pos_data.get('subevent'))
|
||||
except Seat.DoesNotExist:
|
||||
errs[i]['seat'] = ['The specified seat does not exist.']
|
||||
else:
|
||||
pos_data['seat'] = seat
|
||||
if (seat not in free_seats and not seat.is_available()) or seat in seats_seen:
|
||||
errs[i]['seat'] = [ugettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
|
||||
seats_seen.add(seat)
|
||||
elif seated:
|
||||
errs[i]['seat'] = ['The specified product requires to choose a seat.']
|
||||
|
||||
if any(errs):
|
||||
raise ValidationError({'positions': errs})
|
||||
|
||||
@@ -693,38 +862,14 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
validated_data['locale'] = self.context['event'].settings.locale
|
||||
order = Order(event=self.context['event'], **validated_data)
|
||||
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
|
||||
order.total = sum([p['price'] for p in positions_data]) + sum([f['value'] for f in fees_data], Decimal('0.00'))
|
||||
order.meta_info = "{}"
|
||||
order.total = Decimal('0.00')
|
||||
order.save()
|
||||
|
||||
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID:
|
||||
order.status = Order.STATUS_PAID
|
||||
order.save()
|
||||
order.payments.create(
|
||||
amount=order.total, provider='free', state=OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||||
payment_date=now()
|
||||
)
|
||||
elif payment_provider == "free" and order.total != Decimal('0.00'):
|
||||
raise ValidationError('You cannot use the "free" payment provider for non-free orders.')
|
||||
elif validated_data.get('status') == Order.STATUS_PAID:
|
||||
order.payments.create(
|
||||
amount=order.total,
|
||||
provider=payment_provider,
|
||||
info=payment_info,
|
||||
payment_date=payment_date,
|
||||
state=OrderPayment.PAYMENT_STATE_CONFIRMED
|
||||
)
|
||||
elif payment_provider:
|
||||
order.payments.create(
|
||||
amount=order.total,
|
||||
provider=payment_provider,
|
||||
info=payment_info,
|
||||
state=OrderPayment.PAYMENT_STATE_CREATED
|
||||
)
|
||||
|
||||
if ia:
|
||||
ia.order = order
|
||||
ia.save()
|
||||
|
||||
pos_map = {}
|
||||
for pos_data in positions_data:
|
||||
answers_data = pos_data.pop('answers', [])
|
||||
@@ -736,9 +881,38 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
}
|
||||
pos = OrderPosition(**pos_data)
|
||||
pos.order = order
|
||||
pos._calculate_tax()
|
||||
if addon_to:
|
||||
pos.addon_to = pos_map[addon_to]
|
||||
|
||||
if pos.price is None:
|
||||
price = get_price(
|
||||
item=pos.item,
|
||||
variation=pos.variation,
|
||||
voucher=pos.voucher,
|
||||
custom_price=None,
|
||||
subevent=pos.subevent,
|
||||
addon_to=pos.addon_to,
|
||||
invoice_address=ia,
|
||||
)
|
||||
pos.price = price.gross
|
||||
pos.tax_rate = price.rate
|
||||
pos.tax_value = price.tax
|
||||
pos.tax_rule = pos.item.tax_rule
|
||||
else:
|
||||
pos._calculate_tax()
|
||||
|
||||
pos.price_before_voucher = get_price(
|
||||
item=pos.item,
|
||||
variation=pos.variation,
|
||||
voucher=None,
|
||||
custom_price=None,
|
||||
subevent=pos.subevent,
|
||||
addon_to=pos.addon_to,
|
||||
invoice_address=ia,
|
||||
).gross
|
||||
|
||||
if pos.voucher:
|
||||
Voucher.objects.filter(pk=pos.voucher.pk).update(redeemed=F('redeemed') + 1)
|
||||
pos.save()
|
||||
pos_map[pos.positionid] = pos
|
||||
for answ_data in answers_data:
|
||||
@@ -748,19 +922,103 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
|
||||
for cp in delete_cps:
|
||||
cp.delete()
|
||||
|
||||
order.total = sum([p.price for p in order.positions.all()])
|
||||
for fee_data in fees_data:
|
||||
f = OrderFee(**fee_data)
|
||||
f.order = order
|
||||
f._calculate_tax()
|
||||
f.save()
|
||||
is_percentage = fee_data.pop('_treat_value_as_percentage', False)
|
||||
if is_percentage:
|
||||
fee_data['value'] = round_decimal(order.total * (fee_data['value'] / Decimal('100.00')),
|
||||
self.context['event'].currency)
|
||||
is_split_taxes = fee_data.pop('_split_taxes_like_products', False)
|
||||
|
||||
if is_split_taxes:
|
||||
d = defaultdict(lambda: Decimal('0.00'))
|
||||
trz = TaxRule.zero()
|
||||
for p in pos_map.values():
|
||||
tr = p.tax_rule
|
||||
d[tr] += p.price - p.tax_value
|
||||
|
||||
base_values = sorted([tuple(t) for t in d.items()], key=lambda t: (t[0] or trz).rate)
|
||||
sum_base = sum(t[1] for t in base_values)
|
||||
fee_values = [(t[0], round_decimal(fee_data['value'] * t[1] / sum_base, self.context['event'].currency))
|
||||
for t in base_values]
|
||||
sum_fee = sum(t[1] for t in fee_values)
|
||||
|
||||
# If there are rounding differences, we fix them up, but always leaning to the benefit of the tax
|
||||
# authorities
|
||||
if sum_fee > fee_data['value']:
|
||||
fee_values[0] = (fee_values[0][0], fee_values[0][1] + (fee_data['value'] - sum_fee))
|
||||
elif sum_fee < fee_data['value']:
|
||||
fee_values[-1] = (fee_values[-1][0], fee_values[-1][1] + (fee_data['value'] - sum_fee))
|
||||
|
||||
for tr, val in fee_values:
|
||||
fee_data['tax_rule'] = tr
|
||||
fee_data['value'] = val
|
||||
f = OrderFee(**fee_data)
|
||||
f.order = order
|
||||
f._calculate_tax()
|
||||
f.save()
|
||||
else:
|
||||
f = OrderFee(**fee_data)
|
||||
f.order = order
|
||||
f._calculate_tax()
|
||||
f.save()
|
||||
|
||||
order.total += sum([f.value for f in order.fees.all()])
|
||||
order.save(update_fields=['total'])
|
||||
|
||||
if order.total == Decimal('0.00') and validated_data.get('status') == Order.STATUS_PAID and not payment_provider:
|
||||
payment_provider = 'free'
|
||||
|
||||
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID:
|
||||
order.status = Order.STATUS_PAID
|
||||
order.save()
|
||||
order.payments.create(
|
||||
amount=order.total, provider='free', state=OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||||
payment_date=now()
|
||||
)
|
||||
elif payment_provider == "free" and order.total != Decimal('0.00'):
|
||||
raise ValidationError('You cannot use the "free" payment provider for non-free orders.')
|
||||
elif validated_data.get('status') == Order.STATUS_PAID:
|
||||
if not payment_provider:
|
||||
raise ValidationError('You cannot create a paid order without a payment provider.')
|
||||
order.payments.create(
|
||||
amount=order.total,
|
||||
provider=payment_provider,
|
||||
info=payment_info,
|
||||
payment_date=payment_date,
|
||||
state=OrderPayment.PAYMENT_STATE_CONFIRMED
|
||||
)
|
||||
elif payment_provider:
|
||||
order.payments.create(
|
||||
amount=order.total,
|
||||
provider=payment_provider,
|
||||
info=payment_info,
|
||||
state=OrderPayment.PAYMENT_STATE_CREATED
|
||||
)
|
||||
|
||||
return order
|
||||
|
||||
|
||||
class LinePositionField(serializers.IntegerField):
|
||||
"""
|
||||
Internally, the position field is stored starting at 0, but for the API, starting at 1 makes it
|
||||
more consistent with other models
|
||||
"""
|
||||
|
||||
def to_representation(self, value):
|
||||
return super().to_representation(value) + 1
|
||||
|
||||
def to_internal_value(self, data):
|
||||
return super().to_internal_value(data) - 1
|
||||
|
||||
|
||||
class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
||||
position = LinePositionField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = InvoiceLine
|
||||
fields = ('description', 'gross_value', 'tax_value', 'tax_rate', 'tax_name')
|
||||
fields = ('position', 'description', 'gross_value', 'tax_value', 'tax_rate', 'tax_name')
|
||||
|
||||
|
||||
class InvoiceSerializer(I18nAwareModelSerializer):
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.order import CompatibleJSONField
|
||||
from pretix.base.models import Organizer, SeatingPlan
|
||||
from pretix.base.models import GiftCard, Organizer, SeatingPlan
|
||||
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
||||
|
||||
|
||||
@@ -18,3 +25,27 @@ class SeatingPlanSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = SeatingPlan
|
||||
fields = ('id', 'name', 'layout')
|
||||
|
||||
|
||||
class GiftCardSerializer(I18nAwareModelSerializer):
|
||||
value = serializers.DecimalField(max_digits=10, decimal_places=2, min_value=Decimal('0.00'))
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
s = data['secret']
|
||||
qs = GiftCard.objects.filter(
|
||||
secret=s
|
||||
).filter(
|
||||
Q(issuer=self.context["organizer"]) | Q(issuer__gift_card_collector_acceptance__collector=self.context["organizer"])
|
||||
)
|
||||
if self.instance:
|
||||
qs = qs.exclude(pk=self.instance.pk)
|
||||
if qs.exists():
|
||||
raise ValidationError(
|
||||
{'secret': _('A gift card with the same secret already exists in your or an affiliated organizer account.')}
|
||||
)
|
||||
return data
|
||||
|
||||
class Meta:
|
||||
model = GiftCard
|
||||
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode')
|
||||
|
||||
@@ -2,15 +2,23 @@ from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.models import Voucher
|
||||
from pretix.base.models import Seat, Voucher
|
||||
|
||||
|
||||
class VoucherListSerializer(serializers.ListSerializer):
|
||||
def create(self, validated_data):
|
||||
codes = set()
|
||||
seats = set()
|
||||
errs = []
|
||||
err = False
|
||||
for voucher_data in validated_data:
|
||||
if voucher_data.get('seat') and (voucher_data.get('seat'), voucher_data.get('subevent')) in seats:
|
||||
err = True
|
||||
errs.append({'code': ['Duplicate seat ID in request.']})
|
||||
continue
|
||||
else:
|
||||
seats.add((voucher_data.get('seat'), voucher_data.get('subevent')))
|
||||
|
||||
if voucher_data['code'] in codes:
|
||||
err = True
|
||||
errs.append({'code': ['Duplicate voucher code in request.']})
|
||||
@@ -22,12 +30,19 @@ class VoucherListSerializer(serializers.ListSerializer):
|
||||
return super().create(validated_data)
|
||||
|
||||
|
||||
class SeatGuidField(serializers.CharField):
|
||||
def to_representation(self, val: Seat):
|
||||
return val.seat_guid
|
||||
|
||||
|
||||
class VoucherSerializer(I18nAwareModelSerializer):
|
||||
seat = SeatGuidField(allow_null=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = Voucher
|
||||
fields = ('id', 'code', 'max_usages', 'redeemed', 'valid_until', 'block_quota',
|
||||
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
|
||||
'tag', 'comment', 'subevent', 'show_hidden_items')
|
||||
'tag', 'comment', 'subevent', 'show_hidden_items', 'seat')
|
||||
read_only_fields = ('id', 'redeemed')
|
||||
list_serializer_class = VoucherListSerializer
|
||||
|
||||
@@ -39,7 +54,8 @@ class VoucherSerializer(I18nAwareModelSerializer):
|
||||
|
||||
Voucher.clean_item_properties(
|
||||
full_data, self.context.get('event'),
|
||||
full_data.get('quota'), full_data.get('item'), full_data.get('variation')
|
||||
full_data.get('quota'), full_data.get('item'), full_data.get('variation'),
|
||||
block_quota=full_data.get('block_quota')
|
||||
)
|
||||
Voucher.clean_subevent(
|
||||
full_data, self.context.get('event')
|
||||
@@ -61,4 +77,10 @@ class VoucherSerializer(I18nAwareModelSerializer):
|
||||
)
|
||||
Voucher.clean_voucher_code(full_data, self.context.get('event'), self.instance.pk if self.instance else None)
|
||||
|
||||
if full_data.get('seat'):
|
||||
data['seat'] = Voucher.clean_seat_id(
|
||||
full_data, full_data.get('item'), full_data.get('quota'), self.context.get('event'),
|
||||
self.instance.pk if self.instance else None
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
@@ -19,6 +19,7 @@ orga_router.register(r'events', event.EventViewSet)
|
||||
orga_router.register(r'subevents', event.SubEventViewSet)
|
||||
orga_router.register(r'webhooks', webhooks.WebHookViewSet)
|
||||
orga_router.register(r'seatingplans', organizer.SeatingPlanViewSet)
|
||||
orga_router.register(r'giftcards', organizer.GiftCardViewSet)
|
||||
|
||||
event_router = routers.DefaultRouter()
|
||||
event_router.register(r'subevents', event.SubEventViewSet)
|
||||
|
||||
@@ -44,7 +44,6 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
qs = self.request.event.checkin_lists.prefetch_related(
|
||||
'limit_products',
|
||||
)
|
||||
qs = CheckinList.annotate_with_numbers(qs, self.request.event)
|
||||
return qs
|
||||
|
||||
def perform_create(self, serializer):
|
||||
|
||||
@@ -18,6 +18,7 @@ from pretix.base.models import (
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
from pretix.presale.views.organizer import filter_qs_by_attr
|
||||
|
||||
with scopes_disabled():
|
||||
class EventFilter(FilterSet):
|
||||
@@ -85,6 +86,8 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
organizer=self.request.organizer
|
||||
)
|
||||
|
||||
qs = filter_qs_by_attr(qs, self.request)
|
||||
|
||||
return qs.prefetch_related(
|
||||
'meta_values', 'meta_values__property', 'seat_category_mappings'
|
||||
)
|
||||
@@ -130,6 +133,7 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(organizer=self.request.organizer)
|
||||
serializer.instance.set_defaults()
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.added',
|
||||
user=self.request.user,
|
||||
@@ -241,6 +245,9 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
event__organizer=self.request.organizer,
|
||||
event__in=self.request.user.get_events_with_any_permission()
|
||||
)
|
||||
|
||||
qs = filter_qs_by_attr(qs, self.request)
|
||||
|
||||
return qs.prefetch_related(
|
||||
'subeventitem_set', 'subeventitemvariation_set', 'seat_category_mappings'
|
||||
)
|
||||
|
||||
@@ -62,7 +62,6 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['event'] = self.request.event
|
||||
ctx['has_variations'] = self.request.data.get('has_variations')
|
||||
return ctx
|
||||
|
||||
def perform_update(self, serializer):
|
||||
|
||||
@@ -6,7 +6,7 @@ import pytz
|
||||
from django.db import transaction
|
||||
from django.db.models import F, Prefetch, Q
|
||||
from django.db.models.functions import Coalesce, Concat
|
||||
from django.http import FileResponse
|
||||
from django.http import FileResponse, HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import ugettext as _
|
||||
@@ -30,8 +30,8 @@ from pretix.api.serializers.order import (
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedCombinedTicket, CachedTicket, Device, Event, Invoice, InvoiceAddress,
|
||||
Order, OrderPayment, OrderPosition, OrderRefund, Quota, TeamAPIToken,
|
||||
generate_position_secret, generate_secret,
|
||||
Order, OrderFee, OrderPayment, OrderPosition, OrderRefund, Quota,
|
||||
TeamAPIToken, generate_position_secret, generate_secret,
|
||||
)
|
||||
from pretix.base.payment import PaymentException
|
||||
from pretix.base.services import tickets
|
||||
@@ -41,13 +41,14 @@ from pretix.base.services.invoices import (
|
||||
)
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.services.orders import (
|
||||
OrderChangeManager, OrderError, approve_order, cancel_order, deny_order,
|
||||
OrderChangeManager, OrderError, _order_placed_email,
|
||||
_order_placed_email_attendee, approve_order, cancel_order, deny_order,
|
||||
extend_order, mark_order_expired, mark_order_refunded,
|
||||
)
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.base.services.tickets import generate
|
||||
from pretix.base.signals import (
|
||||
order_modified, order_placed, register_ticket_outputs,
|
||||
order_modified, order_paid, order_placed, register_ticket_outputs,
|
||||
)
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
|
||||
@@ -81,20 +82,29 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
return ctx
|
||||
|
||||
def get_queryset(self):
|
||||
if self.request.query_params.get('include_canceled_fees', 'false') == 'true':
|
||||
fqs = OrderFee.all
|
||||
else:
|
||||
fqs = OrderFee.objects
|
||||
qs = self.request.event.orders.prefetch_related(
|
||||
'fees', 'payments', 'refunds', 'refunds__payment'
|
||||
Prefetch('fees', queryset=fqs.all()),
|
||||
'payments', 'refunds', 'refunds__payment'
|
||||
).select_related(
|
||||
'invoice_address'
|
||||
)
|
||||
|
||||
if self.request.query_params.get('include_canceled_positions', 'false') == 'true':
|
||||
opq = OrderPosition.all
|
||||
else:
|
||||
opq = OrderPosition.objects
|
||||
if self.request.query_params.get('pdf_data', 'false') == 'true':
|
||||
qs = qs.prefetch_related(
|
||||
Prefetch(
|
||||
'positions',
|
||||
OrderPosition.objects.all().prefetch_related(
|
||||
opq.all().prefetch_related(
|
||||
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||
'item__category', 'addon_to', 'seat',
|
||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation', 'seat'))
|
||||
Prefetch('addons', opq.select_related('item', 'variation', 'seat'))
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -102,7 +112,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
qs = qs.prefetch_related(
|
||||
Prefetch(
|
||||
'positions',
|
||||
OrderPosition.objects.all().prefetch_related(
|
||||
opq.all().prefetch_related(
|
||||
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question', 'seat',
|
||||
)
|
||||
)
|
||||
@@ -147,12 +157,16 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
generate.apply_async(args=('order', order.pk, provider.identifier))
|
||||
raise RetryException()
|
||||
else:
|
||||
resp = FileResponse(ct.file.file, content_type=ct.type)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
|
||||
self.request.event.slug.upper(), order.code,
|
||||
provider.identifier, ct.extension
|
||||
)
|
||||
return resp
|
||||
if ct.type == 'text/uri-list':
|
||||
resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list')
|
||||
return resp
|
||||
else:
|
||||
resp = FileResponse(ct.file.file, content_type=ct.type)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
|
||||
self.request.event.slug.upper(), order.code,
|
||||
provider.identifier, ct.extension
|
||||
)
|
||||
return resp
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
def mark_paid(self, request, **kwargs):
|
||||
@@ -168,9 +182,26 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
amount=ps
|
||||
)
|
||||
except OrderPayment.DoesNotExist:
|
||||
order.payments.filter(state__in=(OrderPayment.PAYMENT_STATE_PENDING,
|
||||
OrderPayment.PAYMENT_STATE_CREATED)) \
|
||||
.update(state=OrderPayment.PAYMENT_STATE_CANCELED)
|
||||
for p in order.payments.filter(state__in=(OrderPayment.PAYMENT_STATE_PENDING,
|
||||
OrderPayment.PAYMENT_STATE_CREATED)):
|
||||
try:
|
||||
with transaction.atomic():
|
||||
p.payment_provider.cancel_payment(p)
|
||||
order.log_action('pretix.event.order.payment.canceled', {
|
||||
'local_id': p.local_id,
|
||||
'provider': p.provider,
|
||||
}, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
|
||||
except PaymentException as e:
|
||||
order.log_action(
|
||||
'pretix.event.order.payment.canceled.failed',
|
||||
{
|
||||
'local_id': p.local_id,
|
||||
'provider': p.provider,
|
||||
'error': str(e)
|
||||
},
|
||||
user=self.request.user if self.request.user.is_authenticated else None,
|
||||
auth=self.request.auth
|
||||
)
|
||||
p = order.payments.create(
|
||||
state=OrderPayment.PAYMENT_STATE_CREATED,
|
||||
provider='manual',
|
||||
@@ -431,6 +462,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
with transaction.atomic():
|
||||
self.perform_create(serializer)
|
||||
send_mail = serializer._send_mail
|
||||
order = serializer.instance
|
||||
serializer = OrderSerializer(order, context=serializer.context)
|
||||
|
||||
@@ -439,14 +471,52 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
auth=request.auth,
|
||||
)
|
||||
order_placed.send(self.request.event, order=order)
|
||||
|
||||
gen_invoice = invoice_qualified(order) and (
|
||||
(order.event.settings.get('invoice_generate') == 'True') or
|
||||
(order.event.settings.get('invoice_generate') == 'paid' and order.status == Order.STATUS_PAID)
|
||||
) and not order.invoices.last()
|
||||
if gen_invoice:
|
||||
generate_invoice(order, trigger_pdf=True)
|
||||
with language(order.locale):
|
||||
order_placed.send(self.request.event, order=order)
|
||||
if order.status == Order.STATUS_PAID:
|
||||
order_paid.send(self.request.event, order=order)
|
||||
|
||||
gen_invoice = invoice_qualified(order) and (
|
||||
(order.event.settings.get('invoice_generate') == 'True') or
|
||||
(order.event.settings.get('invoice_generate') == 'paid' and order.status == Order.STATUS_PAID)
|
||||
) and not order.invoices.last()
|
||||
invoice = None
|
||||
if gen_invoice:
|
||||
invoice = generate_invoice(order, trigger_pdf=True)
|
||||
|
||||
if send_mail:
|
||||
payment = order.payments.last()
|
||||
free_flow = (
|
||||
payment and order.total == Decimal('0.00') and order.status == Order.STATUS_PAID and
|
||||
not order.require_approval and payment.provider == "free"
|
||||
)
|
||||
if free_flow:
|
||||
email_template = request.event.settings.mail_text_order_free
|
||||
log_entry = 'pretix.event.order.email.order_free'
|
||||
email_attendees = request.event.settings.mail_send_order_free_attendee
|
||||
email_attendees_template = request.event.settings.mail_text_order_free_attendee
|
||||
else:
|
||||
email_template = request.event.settings.mail_text_order_placed
|
||||
log_entry = 'pretix.event.order.email.order_placed'
|
||||
email_attendees = request.event.settings.mail_send_order_placed_attendee
|
||||
email_attendees_template = request.event.settings.mail_text_order_placed_attendee
|
||||
|
||||
_order_placed_email(
|
||||
request.event, order, payment.payment_provider if payment else None, email_template,
|
||||
log_entry, invoice, payment
|
||||
)
|
||||
if email_attendees:
|
||||
for p in order.positions.all():
|
||||
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
|
||||
_order_placed_email_attendee(request.event, order, p, email_attendees_template, log_entry)
|
||||
|
||||
if not free_flow and order.status == Order.STATUS_PAID and payment:
|
||||
payment._send_paid_mail(invoice, None, '')
|
||||
if self.request.event.settings.mail_send_order_paid_attendee:
|
||||
for p in order.positions.all():
|
||||
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
|
||||
payment._send_paid_mail_attendee(p, None)
|
||||
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
@@ -593,11 +663,16 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
qs = OrderPosition.objects.filter(order__event=self.request.event)
|
||||
if self.request.query_params.get('include_canceled_positions', 'false') == 'true':
|
||||
qs = OrderPosition.all
|
||||
else:
|
||||
qs = OrderPosition.objects
|
||||
|
||||
qs = qs.filter(order__event=self.request.event)
|
||||
if self.request.query_params.get('pdf_data', 'false') == 'true':
|
||||
qs = qs.prefetch_related(
|
||||
'checkins', 'answers', 'answers__options', 'answers__question',
|
||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
|
||||
Prefetch('addons', qs.select_related('item', 'variation')),
|
||||
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
|
||||
Prefetch(
|
||||
'event',
|
||||
@@ -605,7 +680,7 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
|
||||
),
|
||||
Prefetch(
|
||||
'positions',
|
||||
OrderPosition.objects.prefetch_related(
|
||||
qs.prefetch_related(
|
||||
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||
)
|
||||
)
|
||||
@@ -615,7 +690,7 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
|
||||
)
|
||||
else:
|
||||
qs = qs.prefetch_related(
|
||||
'checkins', 'answers', 'answers__options', 'answers__question'
|
||||
'checkins', 'answers', 'answers__options', 'answers__question',
|
||||
).select_related(
|
||||
'item', 'order', 'order__event', 'order__event__organizer', 'seat'
|
||||
)
|
||||
@@ -723,12 +798,16 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
|
||||
generate.apply_async(args=('orderposition', pos.pk, provider.identifier))
|
||||
raise RetryException()
|
||||
else:
|
||||
resp = FileResponse(ct.file.file, content_type=ct.type)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
|
||||
self.request.event.slug.upper(), pos.order.code, pos.positionid,
|
||||
provider.identifier, ct.extension
|
||||
)
|
||||
return resp
|
||||
if ct.type == 'text/uri-list':
|
||||
resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list')
|
||||
return resp
|
||||
else:
|
||||
resp = FileResponse(ct.file.file, content_type=ct.type)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
|
||||
self.request.event.slug.upper(), pos.order.code, pos.positionid,
|
||||
provider.identifier, ct.extension
|
||||
)
|
||||
return resp
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
try:
|
||||
@@ -850,13 +929,16 @@ class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
if payment.state not in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED):
|
||||
return Response({'detail': 'Invalid state of payment'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
with transaction.atomic():
|
||||
payment.state = OrderPayment.PAYMENT_STATE_CANCELED
|
||||
payment.save()
|
||||
payment.order.log_action('pretix.event.order.payment.canceled', {
|
||||
'local_id': payment.local_id,
|
||||
'provider': payment.provider,
|
||||
}, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
|
||||
try:
|
||||
with transaction.atomic():
|
||||
payment.payment_provider.cancel_payment(payment)
|
||||
payment.order.log_action('pretix.event.order.payment.canceled', {
|
||||
'local_id': payment.local_id,
|
||||
'provider': payment.provider,
|
||||
}, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
|
||||
except PaymentException as e:
|
||||
return Response({'detail': 'External error: {}'.format(str(e))},
|
||||
status=status.HTTP_400_BAD_REQUEST)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
from rest_framework import filters, viewsets
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db import transaction
|
||||
from rest_framework import filters, serializers, status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import MethodNotAllowed, PermissionDenied
|
||||
from rest_framework.response import Response
|
||||
|
||||
from pretix.api.models import OAuthAccessToken
|
||||
from pretix.api.serializers.organizer import (
|
||||
OrganizerSerializer, SeatingPlanSerializer,
|
||||
GiftCardSerializer, OrganizerSerializer, SeatingPlanSerializer,
|
||||
)
|
||||
from pretix.base.models import Organizer, SeatingPlan
|
||||
from pretix.base.models import GiftCard, Organizer, SeatingPlan
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
|
||||
|
||||
@@ -81,3 +86,70 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
|
||||
data={'id': instance.pk}
|
||||
)
|
||||
instance.delete()
|
||||
|
||||
|
||||
class GiftCardViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = GiftCardSerializer
|
||||
queryset = GiftCard.objects.none()
|
||||
permission = 'can_manage_gift_cards'
|
||||
write_permission = 'can_manage_gift_cards'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.organizer.issued_gift_cards.all()
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['organizer'] = self.request.organizer
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
value = serializer.validated_data.pop('value')
|
||||
inst = serializer.save(issuer=self.request.organizer)
|
||||
inst.transactions.create(value=value)
|
||||
inst.log_action(
|
||||
'pretix.giftcards.transaction.manual',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=merge_dicts(self.request.data, {'id': inst.pk})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
GiftCard.objects.select_for_update().get(pk=self.get_object().pk)
|
||||
old_value = serializer.instance.value
|
||||
value = serializer.validated_data.pop('value')
|
||||
inst = serializer.save(secret=serializer.instance.secret, currency=serializer.instance.currency,
|
||||
testmode=serializer.instance.testmode)
|
||||
diff = value - old_value
|
||||
inst.transactions.create(value=diff)
|
||||
inst.log_action(
|
||||
'pretix.giftcards.transaction.manual',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={'value': diff}
|
||||
)
|
||||
return inst
|
||||
|
||||
@action(detail=True, methods=["POST"])
|
||||
@transaction.atomic()
|
||||
def transact(self, request, **kwargs):
|
||||
gc = GiftCard.objects.select_for_update().get(pk=self.get_object().pk)
|
||||
value = serializers.DecimalField(max_digits=10, decimal_places=2).to_internal_value(
|
||||
request.data.get('value')
|
||||
)
|
||||
if gc.value + value < Decimal('0.00'):
|
||||
return Response({
|
||||
'value': ['The gift card does not have sufficient credit for this operation.']
|
||||
}, status=status.HTTP_409_CONFLICT)
|
||||
gc.transactions.create(value=value)
|
||||
gc.log_action(
|
||||
'pretix.giftcards.transaction.manual',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={'value': value}
|
||||
)
|
||||
return Response(GiftCardSerializer(gc).data, status=status.HTTP_200_OK)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
raise MethodNotAllowed("Gift cards cannot be deleted.")
|
||||
|
||||
@@ -45,7 +45,7 @@ class VoucherViewSet(viewsets.ModelViewSet):
|
||||
write_permission = 'can_change_vouchers'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.vouchers.all()
|
||||
return self.request.event.vouchers.select_related('seat').all()
|
||||
|
||||
def _predict_quota_check(self, data, instance):
|
||||
# This method predicts if Voucher.clean_quota_needs_checking
|
||||
|
||||
@@ -13,7 +13,7 @@ class PretixBaseConfig(AppConfig):
|
||||
from . import invoice # NOQA
|
||||
from . import notifications # NOQA
|
||||
from . import email # NOQA
|
||||
from .services import auth, export, mail, tickets, cart, orders, invoices, cleanup, update_check, quotas, notifications # NOQA
|
||||
from .services import auth, checkin, export, mail, tickets, cart, orderimport, orders, invoices, cleanup, update_check, quotas, notifications, vouchers # NOQA
|
||||
|
||||
try:
|
||||
from .celery_app import app as celery_app # NOQA
|
||||
|
||||
109
src/pretix/base/auth.py
Normal file
109
src/pretix/base/auth.py
Normal file
@@ -0,0 +1,109 @@
|
||||
from collections import OrderedDict
|
||||
from importlib import import_module
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import authenticate
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
def get_auth_backends():
|
||||
backends = {}
|
||||
for b in settings.PRETIX_AUTH_BACKENDS:
|
||||
mod, name = b.rsplit('.', 1)
|
||||
b = getattr(import_module(mod), name)()
|
||||
backends[b.identifier] = b
|
||||
return backends
|
||||
|
||||
|
||||
class BaseAuthBackend:
|
||||
"""
|
||||
This base class defines the interface that needs to be implemented by every class that supplies
|
||||
an authentication method to pretix. Please note that pretix authentication backends are different
|
||||
from plain Django authentication backends! Be sure to read the documentation chapter on authentication
|
||||
backends before you implement one.
|
||||
"""
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
"""
|
||||
A short and unique identifier for this authentication backend.
|
||||
This should only contain lowercase letters and in most cases will
|
||||
be the same as your package name.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
"""
|
||||
A human-readable name of this authentication backend.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def visible(self):
|
||||
"""
|
||||
Whether or not this backend can be selected by users actively. Set this to ``False``
|
||||
if you only implement ``request_authenticate``.
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def login_form_fields(self) -> dict:
|
||||
"""
|
||||
This property may return form fields that the user needs to fill in to log in.
|
||||
"""
|
||||
return {}
|
||||
|
||||
def form_authenticate(self, request, form_data):
|
||||
"""
|
||||
This method will be called after the user filled in the login form. ``request`` will contain
|
||||
the current request and ``form_data`` the input for the form fields defined in ``login_form_fields``.
|
||||
You are expected to either return a ``User`` object (if login was successful) or ``None``.
|
||||
"""
|
||||
return
|
||||
|
||||
def request_authenticate(self, request):
|
||||
"""
|
||||
This method will be called when the user opens the login form. If the user already has a valid session
|
||||
according to your login mechanism, for example a cookie set by a different system or HTTP header set by a
|
||||
reverse proxy, you can directly return a ``User`` object that will be logged in.
|
||||
|
||||
``request`` will contain the current request.
|
||||
You are expected to either return a ``User`` object (if login was successful) or ``None``.
|
||||
"""
|
||||
return
|
||||
|
||||
def authentication_url(self, request):
|
||||
"""
|
||||
This method will be called to populate the URL for your authentication method's tab on the login page.
|
||||
For example, if your method works through OAuth, you could return the URL of the OAuth authorization URL the
|
||||
user needs to visit.
|
||||
|
||||
If you return ``None`` (the default), the link will point to a page that shows the form defined by
|
||||
``login_form_fields``.
|
||||
"""
|
||||
return
|
||||
|
||||
|
||||
class NativeAuthBackend(BaseAuthBackend):
|
||||
identifier = 'native'
|
||||
verbose_name = _('pretix User')
|
||||
|
||||
@property
|
||||
def login_form_fields(self) -> dict:
|
||||
"""
|
||||
This property may return form fields that the user needs to fill in
|
||||
to log in.
|
||||
"""
|
||||
d = OrderedDict([
|
||||
('email', forms.EmailField(label=_("E-mail"), max_length=254,
|
||||
widget=forms.EmailInput(attrs={'autofocus': 'autofocus'}))),
|
||||
('password', forms.CharField(label=_("Password"), widget=forms.PasswordInput)),
|
||||
])
|
||||
return d
|
||||
|
||||
def form_authenticate(self, request, form_data):
|
||||
u = authenticate(request=request, email=form_data['email'].lower(), password=form_data['password'])
|
||||
if u and u.auth_backend == self.identifier:
|
||||
return u
|
||||
80
src/pretix/base/banlist.py
Normal file
80
src/pretix/base/banlist.py
Normal file
@@ -0,0 +1,80 @@
|
||||
import re
|
||||
|
||||
# banlist based on http://www.bannedwordlist.com/lists/swearWords.txt
|
||||
banlist = [
|
||||
"anal",
|
||||
"anus",
|
||||
"arse",
|
||||
"ass",
|
||||
"balls",
|
||||
"bastard",
|
||||
"bitch",
|
||||
"biatch",
|
||||
"bloody",
|
||||
"blowjob",
|
||||
"bollock",
|
||||
"bollok",
|
||||
"boner",
|
||||
"boob",
|
||||
"bugger",
|
||||
"bum",
|
||||
"butt",
|
||||
"clitoris",
|
||||
"cock",
|
||||
"coon",
|
||||
"crap",
|
||||
"cunt",
|
||||
"damn",
|
||||
"dick",
|
||||
"dildo",
|
||||
"dyke",
|
||||
"fag",
|
||||
"feck",
|
||||
"fellate",
|
||||
"fellatio",
|
||||
"felching",
|
||||
"fuck",
|
||||
"fudgepacker",
|
||||
"flange",
|
||||
"goddamn",
|
||||
"hell",
|
||||
"homo",
|
||||
"jerk",
|
||||
"jizz",
|
||||
"knobend",
|
||||
"labia",
|
||||
"lmao",
|
||||
"lmfao",
|
||||
"muff",
|
||||
"nigger",
|
||||
"nigga",
|
||||
"omg",
|
||||
"penis",
|
||||
"piss",
|
||||
"poop",
|
||||
"prick",
|
||||
"pube",
|
||||
"pussy",
|
||||
"queer",
|
||||
"scrotum",
|
||||
"sex",
|
||||
"shit",
|
||||
"sh1t",
|
||||
"slut",
|
||||
"smegma",
|
||||
"spunk",
|
||||
"tit",
|
||||
"tosser",
|
||||
"turd",
|
||||
"twat",
|
||||
"vagina",
|
||||
"wank",
|
||||
"whore",
|
||||
"wtf"
|
||||
]
|
||||
|
||||
blacklist_regex = re.compile('(' + '|'.join(banlist) + ')')
|
||||
|
||||
|
||||
def banned(string):
|
||||
return bool(blacklist_regex.search(string.lower()))
|
||||
@@ -35,6 +35,32 @@ class SalesChannel:
|
||||
"""
|
||||
return "circle"
|
||||
|
||||
@property
|
||||
def testmode_supported(self) -> bool:
|
||||
"""
|
||||
Indication, if a saleschannels supports test mode orders
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def payment_restrictions_supported(self) -> bool:
|
||||
"""
|
||||
If this property is ``True``, organizers can restrict the usage of payment providers to this sales channel.
|
||||
|
||||
Example: pretixPOS provides its own sales channel, ignores the configured payment providers completely and
|
||||
handles payments locally. Therefor, this property should be set to ``False`` for the pretixPOS sales channel as
|
||||
the event organizer cannot restrict the usage of any payment provider through the backend.
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def unlimited_items_per_order(self) -> bool:
|
||||
"""
|
||||
If this property is ``True``, purchases made using this sales channel are not limited to the maximum amount of
|
||||
items defined in the event settings.
|
||||
"""
|
||||
return False
|
||||
|
||||
|
||||
def get_all_sales_channels():
|
||||
global _ALL_CHANNELS
|
||||
|
||||
15
src/pretix/base/context.py
Normal file
15
src/pretix/base/context.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import sys
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def contextprocessor(request):
|
||||
ctx = {
|
||||
'rtl': getattr(request, 'LANGUAGE_CODE', 'en') in settings.LANGUAGES_RTL,
|
||||
}
|
||||
if settings.DEBUG and 'runserver' not in sys.argv:
|
||||
ctx['debug_warning'] = True
|
||||
elif 'runserver' in sys.argv:
|
||||
ctx['development_warning'] = True
|
||||
|
||||
return ctx
|
||||
@@ -1,15 +1,23 @@
|
||||
import inspect
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
from smtplib import SMTPResponseException
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.mail.backends.smtp import EmailBackend
|
||||
from django.dispatch import receiver
|
||||
from django.template.loader import get_template
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import get_language, ugettext_lazy as _
|
||||
from inlinestyler.utils import inline_css
|
||||
|
||||
from pretix.base.models import Event, Order, OrderPosition
|
||||
from pretix.base.signals import register_html_mail_renderers
|
||||
from pretix.base.i18n import LazyCurrencyNumber, LazyDate, LazyNumber
|
||||
from pretix.base.models import Event
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.base.signals import (
|
||||
register_html_mail_renderers, register_mail_placeholders,
|
||||
)
|
||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||
|
||||
logger = logging.getLogger('pretix.base.email')
|
||||
@@ -44,8 +52,8 @@ class BaseHTMLMailRenderer:
|
||||
def __str__(self):
|
||||
return self.identifier
|
||||
|
||||
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order=None,
|
||||
position: OrderPosition=None) -> str:
|
||||
def render(self, plain_body: str, plain_signature: str, subject: str, order=None,
|
||||
position=None) -> str:
|
||||
"""
|
||||
This method should generate the HTML part of the email.
|
||||
|
||||
@@ -97,14 +105,15 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
||||
def template_name(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order, position: OrderPosition) -> str:
|
||||
def render(self, plain_body: str, plain_signature: str, subject: str, order, position) -> str:
|
||||
body_md = markdown_compile_email(plain_body)
|
||||
htmlctx = {
|
||||
'site': settings.PRETIX_INSTANCE_NAME,
|
||||
'site_url': settings.SITE_URL,
|
||||
'body': body_md,
|
||||
'subject': str(subject),
|
||||
'color': '#8E44B3'
|
||||
'color': '#8E44B3',
|
||||
'rtl': get_language() in settings.LANGUAGES_RTL
|
||||
}
|
||||
if self.event:
|
||||
htmlctx['event'] = self.event
|
||||
@@ -136,3 +145,305 @@ class ClassicMailRenderer(TemplateBasedMailRenderer):
|
||||
@receiver(register_html_mail_renderers, dispatch_uid="pretixbase_email_renderers")
|
||||
def base_renderers(sender, **kwargs):
|
||||
return [ClassicMailRenderer]
|
||||
|
||||
|
||||
class BaseMailTextPlaceholder:
|
||||
"""
|
||||
This is the base class for for all email text placeholders.
|
||||
"""
|
||||
|
||||
@property
|
||||
def required_context(self):
|
||||
"""
|
||||
This property should return a list of all attribute names that need to be
|
||||
contained in the base context so that this placeholder is available. By default,
|
||||
it returns a list containing the string "event".
|
||||
"""
|
||||
return ["event"]
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
"""
|
||||
This should return the identifier of this placeholder in the email.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def render(self, context):
|
||||
"""
|
||||
This method is called to generate the actual text that is being
|
||||
used in the email. You will be passed a context dictionary with the
|
||||
base context attributes specified in ``required_context``. You are
|
||||
expected to return a plain-text string.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def render_sample(self, event):
|
||||
"""
|
||||
This method is called to generate a text to be used in email previews.
|
||||
This may only depend on the event.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class SimpleFunctionalMailTextPlaceholder(BaseMailTextPlaceholder):
|
||||
def __init__(self, identifier, args, func, sample):
|
||||
self._identifier = identifier
|
||||
self._args = args
|
||||
self._func = func
|
||||
self._sample = sample
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
return self._identifier
|
||||
|
||||
@property
|
||||
def required_context(self):
|
||||
return self._args
|
||||
|
||||
def render(self, context):
|
||||
return self._func(**{k: context[k] for k in self._args})
|
||||
|
||||
def render_sample(self, event):
|
||||
if callable(self._sample):
|
||||
return self._sample(event)
|
||||
else:
|
||||
return self._sample
|
||||
|
||||
|
||||
def get_available_placeholders(event, base_parameters):
|
||||
if 'order' in base_parameters:
|
||||
base_parameters.append('invoice_address')
|
||||
params = {}
|
||||
for r, val in register_mail_placeholders.send(sender=event):
|
||||
if not isinstance(val, (list, tuple)):
|
||||
val = [val]
|
||||
for v in val:
|
||||
if all(rp in base_parameters for rp in v.required_context):
|
||||
params[v.identifier] = v
|
||||
return params
|
||||
|
||||
|
||||
def get_email_context(**kwargs):
|
||||
from pretix.base.models import InvoiceAddress
|
||||
|
||||
event = kwargs['event']
|
||||
if 'order' in kwargs:
|
||||
try:
|
||||
kwargs['invoice_address'] = kwargs['order'].invoice_address
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
kwargs['invoice_address'] = InvoiceAddress()
|
||||
ctx = {}
|
||||
for r, val in register_mail_placeholders.send(sender=event):
|
||||
if not isinstance(val, (list, tuple)):
|
||||
val = [val]
|
||||
for v in val:
|
||||
if all(rp in kwargs for rp in v.required_context):
|
||||
ctx[v.identifier] = v.render(kwargs)
|
||||
return ctx
|
||||
|
||||
|
||||
def _placeholder_payment(order, payment):
|
||||
if not payment:
|
||||
return None
|
||||
if 'payment' in inspect.signature(payment.payment_provider.order_pending_mail_render).parameters:
|
||||
return str(payment.payment_provider.order_pending_mail_render(order, payment))
|
||||
else:
|
||||
return str(payment.payment_provider.order_pending_mail_render(order))
|
||||
|
||||
|
||||
@receiver(register_mail_placeholders, dispatch_uid="pretixbase_register_mail_placeholders")
|
||||
def base_placeholders(sender, **kwargs):
|
||||
from pretix.base.models import InvoiceAddress
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
|
||||
ph = [
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'event', ['event'], lambda event: event.name, lambda event: event.name
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'event_slug', ['event'], lambda event: event.slug, lambda event: event.slug
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'code', ['order'], lambda order: order.code, 'F8VVL'
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'total', ['order'], lambda order: LazyNumber(order.total), lambda event: LazyNumber(Decimal('42.23'))
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'currency', ['event'], lambda event: event.currency, lambda event: event.currency
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'total_with_currency', ['event', 'order'], lambda event, order: LazyCurrencyNumber(order.total,
|
||||
event.currency),
|
||||
lambda event: LazyCurrencyNumber(Decimal('42.23'), event.currency)
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'expire_date', ['event', 'order'], lambda event, order: LazyDate(order.expires.astimezone(event.timezone)),
|
||||
lambda event: LazyDate(now() + timedelta(days=15))
|
||||
# TODO: This used to be "date" in some placeholders, add a migration!
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'url', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.open', kwargs={
|
||||
'order': order.code,
|
||||
'secret': order.secret,
|
||||
'hash': order.email_confirm_hash()
|
||||
}
|
||||
), lambda event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.open', kwargs={
|
||||
'order': 'F8VVL',
|
||||
'secret': '6zzjnumtsx136ddy',
|
||||
'hash': '98kusd8ofsj8dnkd'
|
||||
}
|
||||
),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'url', ['event', 'position'], lambda event, position: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.position',
|
||||
kwargs={
|
||||
'order': position.order.code,
|
||||
'secret': position.web_secret,
|
||||
'position': position.positionid
|
||||
}
|
||||
),
|
||||
lambda event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.position', kwargs={
|
||||
'order': 'F8VVL',
|
||||
'secret': '6zzjnumtsx136ddy',
|
||||
'position': '123'
|
||||
}
|
||||
),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'url', ['waiting_list_entry', 'event'],
|
||||
lambda waiting_list_entry, event: build_absolute_uri(
|
||||
event, 'presale:event.redeem'
|
||||
) + '?voucher=' + waiting_list_entry.voucher.code,
|
||||
lambda event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.redeem',
|
||||
) + '?voucher=68CYU2H6ZTP3WLK5',
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'invoice_name', ['invoice_address'], lambda invoice_address: invoice_address.name or '',
|
||||
_('John Doe')
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'invoice_company', ['invoice_address'], lambda invoice_address: invoice_address.company or '',
|
||||
_('Sample Corporation')
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
|
||||
'* {} - {}'.format(
|
||||
order.full_code,
|
||||
build_absolute_uri(event, 'presale:event.order', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'order': order.code,
|
||||
'secret': order.secret
|
||||
}),
|
||||
)
|
||||
for order in orders
|
||||
), lambda event: '\n' + '\n\n'.join(
|
||||
'* {} - {}'.format(
|
||||
'{}-{}'.format(event.slug.upper(), order['code']),
|
||||
build_absolute_uri(event, 'presale:event.order', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'order': order['code'],
|
||||
'secret': order['secret']
|
||||
}),
|
||||
)
|
||||
for order in [
|
||||
{'code': 'F8VVL', 'secret': '6zzjnumtsx136ddy'},
|
||||
{'code': 'HIDHK', 'secret': '98kusd8ofsj8dnkd'},
|
||||
{'code': 'OPKSB', 'secret': '09pjdksflosk3njd'}
|
||||
]
|
||||
),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'hours', ['event', 'waiting_list_entry'], lambda event, waiting_list_entry:
|
||||
event.settings.waiting_list_hours,
|
||||
lambda event: event.settings.waiting_list_hours
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'product', ['waiting_list_entry'], lambda waiting_list_entry: waiting_list_entry.item.name,
|
||||
_('Sample Admission Ticket')
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'code', ['waiting_list_entry'], lambda waiting_list_entry: waiting_list_entry.voucher.code,
|
||||
'68CYU2H6ZTP3WLK5'
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'voucher_list', ['voucher_list'], lambda voucher_list: '\n'.join(voucher_list),
|
||||
' 68CYU2H6ZTP3WLK5\n 7MB94KKPVEPSMVF2'
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'url', ['event', 'voucher_list'], lambda event, voucher_list: build_absolute_uri(event, 'presale:event.index', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
}), lambda event: build_absolute_uri(event, 'presale:event.index', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
})
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'name', ['name'], lambda name: name,
|
||||
_('John Doe')
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'comment', ['comment'], lambda comment: comment,
|
||||
_('An individual text with a reason can be inserted here.'),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'payment_info', ['order', 'payment'], _placeholder_payment,
|
||||
_('The amount has been charged to your card.'),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'payment_info', ['payment_info'], lambda payment_info: payment_info,
|
||||
_('Please transfer money to this bank account: 9999-9999-9999-9999'),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'attendee_name', ['position'], lambda position: position.attendee_name,
|
||||
_('John Doe'),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'name', ['position_or_address'],
|
||||
lambda position_or_address: (
|
||||
position_or_address.name
|
||||
if isinstance(position_or_address, InvoiceAddress)
|
||||
else position_or_address.attendee_name
|
||||
),
|
||||
_('John Doe'),
|
||||
),
|
||||
]
|
||||
|
||||
name_scheme = PERSON_NAME_SCHEMES[sender.settings.name_scheme]
|
||||
for f, l, w in name_scheme['fields']:
|
||||
if f == 'full_name':
|
||||
continue
|
||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
||||
'attendee_name_%s' % f, ['position'], lambda position, f=f: position.attendee_name_parts.get(f, ''),
|
||||
name_scheme['sample'][f]
|
||||
))
|
||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
||||
'name_%s' % f, ['position_or_address'],
|
||||
lambda position_or_address, f=f: (
|
||||
position_or_address.name_parts.get(f, '')
|
||||
if isinstance(position_or_address, InvoiceAddress)
|
||||
else position_or_address.attendee_name_parts.get(f, '')
|
||||
),
|
||||
name_scheme['sample'][f]
|
||||
))
|
||||
|
||||
for k, v in sender.meta_data.items():
|
||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
||||
'meta_%s' % k, ['event'], lambda event, k=k: event.meta_data[k],
|
||||
v
|
||||
))
|
||||
|
||||
return ph
|
||||
|
||||
@@ -111,7 +111,7 @@ class ListExporter(BaseExporter):
|
||||
raise NotImplementedError() # noqa
|
||||
|
||||
def get_filename(self):
|
||||
return 'export.csv'
|
||||
return 'export'
|
||||
|
||||
def _render_csv(self, form_data, output_file=None, **kwargs):
|
||||
if output_file:
|
||||
@@ -128,7 +128,7 @@ class ListExporter(BaseExporter):
|
||||
|
||||
def _render_xlsx(self, form_data, output_file=None):
|
||||
wb = Workbook()
|
||||
ws = wb.get_active_sheet()
|
||||
ws = wb.active
|
||||
try:
|
||||
ws.title = str(self.verbose_name)
|
||||
except:
|
||||
@@ -207,7 +207,7 @@ class MultiSheetListExporter(ListExporter):
|
||||
|
||||
def _render_xlsx(self, form_data, output_file=None):
|
||||
wb = Workbook()
|
||||
ws = wb.get_active_sheet()
|
||||
ws = wb.active
|
||||
wb.remove(ws)
|
||||
for s, l in self.sheets:
|
||||
ws = wb.create_sheet(str(l))
|
||||
|
||||
@@ -129,8 +129,11 @@ class DekodiNREIExporter(BaseExporter):
|
||||
'DIDt': invoice.order.datetime.isoformat().replace('Z', '+00:00'),
|
||||
'DT': '30' if invoice.is_cancellation else '10',
|
||||
'EM': invoice.order.email,
|
||||
'FamN': invoice.invoice_to_name.rsplit(' ', 1)[-1],
|
||||
'FN': invoice.invoice_to_name.rsplit(' ', 1)[0] if ' ' in invoice.invoice_to_name else '',
|
||||
'FamN': invoice.invoice_to_name.rsplit(' ', 1)[-1] if invoice.invoice_to_name else '',
|
||||
'FN': (
|
||||
invoice.invoice_to_name.rsplit(' ', 1)[0]
|
||||
if invoice.invoice_to_name and ' ' in invoice.invoice_to_name else ''
|
||||
),
|
||||
'IDt': invoice.date.isoformat() + 'T08:00:00+01:00',
|
||||
'INo': invoice.full_invoice_no,
|
||||
'IsNet': invoice.reverse_charge,
|
||||
|
||||
@@ -9,7 +9,7 @@ from django.utils.formats import date_format, localize
|
||||
from django.utils.translation import pgettext, ugettext as _, ugettext_lazy
|
||||
|
||||
from pretix.base.models import (
|
||||
InvoiceAddress, InvoiceLine, Order, OrderPosition,
|
||||
InvoiceAddress, InvoiceLine, Order, OrderPosition, Question,
|
||||
)
|
||||
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
@@ -96,7 +96,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
for k, label, w in name_scheme['fields']:
|
||||
headers.append(label)
|
||||
headers += [
|
||||
_('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
|
||||
_('Address'), _('ZIP code'), _('City'), _('Country'), pgettext('address', 'State'), _('VAT ID'),
|
||||
_('Date of last payment'), _('Fees'), _('Order locale')
|
||||
]
|
||||
|
||||
@@ -109,6 +109,8 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
|
||||
headers.append(_('Invoice numbers'))
|
||||
headers.append(_('Sales channel'))
|
||||
headers.append(_('Requires special attention'))
|
||||
headers.append(_('Comment'))
|
||||
|
||||
yield headers
|
||||
|
||||
@@ -153,10 +155,11 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
order.invoice_address.city,
|
||||
order.invoice_address.country if order.invoice_address.country else
|
||||
order.invoice_address.country_old,
|
||||
order.invoice_address.state,
|
||||
order.invoice_address.vat_id,
|
||||
]
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
row += [''] * (7 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
|
||||
row += [''] * (8 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
|
||||
|
||||
row += [
|
||||
order.payment_date.astimezone(tz).strftime('%Y-%m-%d') if order.payment_date else '',
|
||||
@@ -178,6 +181,8 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
|
||||
row.append(', '.join([i.number for i in order.invoices.all()]))
|
||||
row.append(order.sales_channel)
|
||||
row.append(_('Yes') if order.checkin_attention else _('No'))
|
||||
row.append(order.comment or "")
|
||||
yield row
|
||||
|
||||
def iterate_fees(self, form_data: dict):
|
||||
@@ -208,7 +213,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
for k, label, w in name_scheme['fields']:
|
||||
headers.append(_('Invoice address name') + ': ' + str(label))
|
||||
headers += [
|
||||
_('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
|
||||
_('Address'), _('ZIP code'), _('City'), _('Country'), pgettext('address', 'State'), _('VAT ID'),
|
||||
]
|
||||
|
||||
yield headers
|
||||
@@ -243,10 +248,11 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
order.invoice_address.city,
|
||||
order.invoice_address.country if order.invoice_address.country else
|
||||
order.invoice_address.country_old,
|
||||
order.invoice_address.state,
|
||||
order.invoice_address.vat_id,
|
||||
]
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
row += [''] * (7 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
|
||||
row += [''] * (8 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
|
||||
yield row
|
||||
|
||||
def iterate_positions(self, form_data: dict):
|
||||
@@ -272,6 +278,8 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
]
|
||||
if self.event.has_subevents:
|
||||
headers.append(pgettext('subevent', 'Date'))
|
||||
headers.append(_('Start date'))
|
||||
headers.append(_('End date'))
|
||||
headers += [
|
||||
_('Product'),
|
||||
_('Variation'),
|
||||
@@ -301,7 +309,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
for k, label, w in name_scheme['fields']:
|
||||
headers.append(_('Invoice address name') + ': ' + str(label))
|
||||
headers += [
|
||||
_('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
|
||||
_('Address'), _('ZIP code'), _('City'), _('Country'), pgettext('address', 'State'), _('VAT ID'),
|
||||
]
|
||||
headers.append(_('Sales channel'))
|
||||
|
||||
@@ -317,7 +325,12 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
|
||||
]
|
||||
if self.event.has_subevents:
|
||||
row.append(op.subevent)
|
||||
row.append(op.subevent.name)
|
||||
row.append(op.subevent.date_from.astimezone(self.event.timezone).strftime('%Y-%m-%d %H:%M:%S'))
|
||||
if op.subevent.date_to:
|
||||
row.append(op.subevent.date_to.astimezone(self.event.timezone).strftime('%Y-%m-%d %H:%M:%S'))
|
||||
else:
|
||||
row.append('')
|
||||
row += [
|
||||
str(op.item),
|
||||
str(op.variation) if op.variation else '',
|
||||
@@ -339,7 +352,12 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
]
|
||||
acache = {}
|
||||
for a in op.answers.all():
|
||||
acache[a.question_id] = str(a)
|
||||
# We do not want to localize Date, Time and Datetime question answers, as those can lead
|
||||
# to difficulties parsing the data (for example 2019-02-01 may become Février, 2019 01 in French).
|
||||
if a.question.type in Question.UNLOCALIZED_TYPES:
|
||||
acache[a.question_id] = a.answer
|
||||
else:
|
||||
acache[a.question_id] = str(a)
|
||||
for q in questions:
|
||||
row.append(acache.get(q.pk, ''))
|
||||
try:
|
||||
@@ -358,10 +376,11 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
order.invoice_address.city,
|
||||
order.invoice_address.country if order.invoice_address.country else
|
||||
order.invoice_address.country_old,
|
||||
order.invoice_address.state,
|
||||
order.invoice_address.vat_id,
|
||||
]
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
row += [''] * (7 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
|
||||
row += [''] * (8 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
|
||||
row.append(order.sales_channel)
|
||||
yield row
|
||||
|
||||
@@ -503,6 +522,7 @@ class InvoiceDataExporter(MultiSheetListExporter):
|
||||
_('Invoice recipient:') + ' ' + _('ZIP code'),
|
||||
_('Invoice recipient:') + ' ' + _('City'),
|
||||
_('Invoice recipient:') + ' ' + _('Country'),
|
||||
_('Invoice recipient:') + ' ' + pgettext('address', 'State'),
|
||||
_('Invoice recipient:') + ' ' + _('VAT ID'),
|
||||
_('Invoice recipient:') + ' ' + _('Beneficiary'),
|
||||
_('Invoice recipient:') + ' ' + _('Internal reference'),
|
||||
@@ -552,6 +572,7 @@ class InvoiceDataExporter(MultiSheetListExporter):
|
||||
i.invoice_to_zipcode,
|
||||
i.invoice_to_city,
|
||||
i.invoice_to_country,
|
||||
i.invoice_to_state,
|
||||
i.invoice_to_vat_id,
|
||||
i.invoice_to_beneficiary,
|
||||
i.internal_reference,
|
||||
@@ -591,6 +612,7 @@ class InvoiceDataExporter(MultiSheetListExporter):
|
||||
_('Invoice recipient:') + ' ' + _('ZIP code'),
|
||||
_('Invoice recipient:') + ' ' + _('City'),
|
||||
_('Invoice recipient:') + ' ' + _('Country'),
|
||||
_('Invoice recipient:') + ' ' + pgettext('address', 'State'),
|
||||
_('Invoice recipient:') + ' ' + _('VAT ID'),
|
||||
_('Invoice recipient:') + ' ' + _('Beneficiary'),
|
||||
_('Invoice recipient:') + ' ' + _('Internal reference'),
|
||||
@@ -630,6 +652,7 @@ class InvoiceDataExporter(MultiSheetListExporter):
|
||||
i.invoice_to_zipcode,
|
||||
i.invoice_to_city,
|
||||
i.invoice_to_country,
|
||||
i.invoice_to_state,
|
||||
i.invoice_to_vat_id,
|
||||
i.invoice_to_beneficiary,
|
||||
i.internal_reference,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth.password_validation import (
|
||||
password_validators_help_texts, validate_password,
|
||||
)
|
||||
@@ -14,32 +13,34 @@ class LoginForm(forms.Form):
|
||||
Base class for authenticating users. Extend this to get a form that accepts
|
||||
username/password logins.
|
||||
"""
|
||||
email = forms.EmailField(label=_("E-mail"), max_length=254, widget=forms.EmailInput(attrs={'autofocus': 'autofocus'}))
|
||||
password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
|
||||
keep_logged_in = forms.BooleanField(label=_("Keep me logged in"), required=False)
|
||||
|
||||
error_messages = {
|
||||
'invalid_login': _("Please enter a correct email address and password."),
|
||||
'invalid_login': _("This combination of credentials is not known to our system."),
|
||||
'inactive': _("This account is inactive.")
|
||||
}
|
||||
|
||||
def __init__(self, request=None, *args, **kwargs):
|
||||
def __init__(self, backend, request=None, *args, **kwargs):
|
||||
"""
|
||||
The 'request' parameter is set for custom auth use by subclasses.
|
||||
The form data comes in via the standard 'data' kwarg.
|
||||
"""
|
||||
self.request = request
|
||||
self.user_cache = None
|
||||
self.backend = backend
|
||||
super().__init__(*args, **kwargs)
|
||||
if not settings.PRETIX_LONG_SESSIONS:
|
||||
for k, f in backend.login_form_fields.items():
|
||||
self.fields[k] = f
|
||||
|
||||
# Authentication backends which use urls cannot have long sessions.
|
||||
if not settings.PRETIX_LONG_SESSIONS or backend.url:
|
||||
del self.fields['keep_logged_in']
|
||||
else:
|
||||
self.fields.move_to_end('keep_logged_in')
|
||||
|
||||
def clean(self):
|
||||
email = self.cleaned_data.get('email')
|
||||
password = self.cleaned_data.get('password')
|
||||
|
||||
if email and password:
|
||||
self.user_cache = authenticate(request=self.request, email=email.lower(), password=password)
|
||||
if all(k in self.cleaned_data for k, f in self.fields.items() if f.required):
|
||||
self.user_cache = self.backend.form_authenticate(self.request, self.cleaned_data)
|
||||
if self.user_cache is None:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['invalid_login'],
|
||||
@@ -181,3 +182,45 @@ class PasswordForgotForm(forms.Form):
|
||||
|
||||
def clean_email(self):
|
||||
return self.cleaned_data['email']
|
||||
|
||||
|
||||
class ReauthForm(forms.Form):
|
||||
error_messages = {
|
||||
'invalid_login': _("This combination of credentials is not known to our system."),
|
||||
'inactive': _("This account is inactive.")
|
||||
}
|
||||
|
||||
def __init__(self, backend, user, request=None, *args, **kwargs):
|
||||
"""
|
||||
The 'request' parameter is set for custom auth use by subclasses.
|
||||
The form data comes in via the standard 'data' kwarg.
|
||||
"""
|
||||
self.request = request
|
||||
self.user = user
|
||||
self.backend = backend
|
||||
self.backend.url = backend.authentication_url(self.request)
|
||||
super().__init__(*args, **kwargs)
|
||||
for k, f in backend.login_form_fields.items():
|
||||
self.fields[k] = f
|
||||
if 'email' in self.fields:
|
||||
self.fields['email'].disabled = True
|
||||
|
||||
def clean(self):
|
||||
self.cleaned_data['email'] = self.user.email
|
||||
user_cache = self.backend.form_authenticate(self.request, self.cleaned_data)
|
||||
if user_cache != self.user:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['invalid_login'],
|
||||
code='invalid_login'
|
||||
)
|
||||
else:
|
||||
self.confirm_login_allowed(user_cache)
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
def confirm_login_allowed(self, user: User):
|
||||
if not user.is_active:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['inactive'],
|
||||
code='inactive',
|
||||
)
|
||||
|
||||
@@ -5,9 +5,11 @@ from decimal import Decimal
|
||||
from urllib.error import HTTPError
|
||||
|
||||
import dateutil.parser
|
||||
import pycountry
|
||||
import pytz
|
||||
import vat_moss.errors
|
||||
import vat_moss.id
|
||||
from babel import localedata
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import ValidationError
|
||||
@@ -15,17 +17,28 @@ from django.db.models import QuerySet
|
||||
from django.forms import Select
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import get_language, ugettext_lazy as _
|
||||
from django.utils.translation import (
|
||||
get_language, pgettext_lazy, ugettext_lazy as _,
|
||||
)
|
||||
from django_countries import countries
|
||||
from django_countries.fields import Country, CountryField
|
||||
from phonenumber_field.formfields import PhoneNumberField
|
||||
from phonenumber_field.phonenumber import PhoneNumber
|
||||
from phonenumber_field.widgets import PhoneNumberPrefixWidget
|
||||
from phonenumbers import NumberParseException
|
||||
from phonenumbers.data import _COUNTRY_CODE_TO_REGION_CODE
|
||||
|
||||
from pretix.base.forms.widgets import (
|
||||
BusinessBooleanRadio, DatePickerWidget, SplitDateTimePickerWidget,
|
||||
TimePickerWidget, UploadedFileWidget,
|
||||
)
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import InvoiceAddress, Question, QuestionOption
|
||||
from pretix.base.models.tax import EU_COUNTRIES
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS
|
||||
from pretix.base.models.tax import EU_COUNTRIES, cc_to_vat_prefix
|
||||
from pretix.base.settings import (
|
||||
COUNTRIES_WITH_STATE_IN_ADDRESS, PERSON_NAME_SCHEMES,
|
||||
PERSON_NAME_TITLE_GROUPS,
|
||||
)
|
||||
from pretix.base.templatetags.rich_text import rich_text
|
||||
from pretix.control.forms import SplitDateTimeField
|
||||
from pretix.helpers.escapejson import escapejson_attr
|
||||
@@ -35,8 +48,19 @@ from pretix.presale.signals import question_form_fields
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
REQUIRED_NAME_PARTS = ['given_name', 'family_name', 'full_name']
|
||||
|
||||
|
||||
class NamePartsWidget(forms.MultiWidget):
|
||||
widget = forms.TextInput
|
||||
autofill_map = {
|
||||
'given_name': 'given-name',
|
||||
'family_name': 'family-name',
|
||||
'middle_name': 'additional-name',
|
||||
'title': 'honorific-prefix',
|
||||
'full_name': 'name',
|
||||
'calling_name': 'nickname',
|
||||
}
|
||||
|
||||
def __init__(self, scheme: dict, field: forms.Field, attrs=None, titles: list=None):
|
||||
widgets = []
|
||||
@@ -77,14 +101,21 @@ class NamePartsWidget(forms.MultiWidget):
|
||||
except (IndexError, TypeError):
|
||||
widget_value = None
|
||||
if id_:
|
||||
final_attrs = dict(
|
||||
these_attrs = dict(
|
||||
final_attrs,
|
||||
id='%s_%s' % (id_, i),
|
||||
title=self.scheme['fields'][i][1],
|
||||
placeholder=self.scheme['fields'][i][1],
|
||||
)
|
||||
final_attrs['data-size'] = self.scheme['fields'][i][2]
|
||||
output.append(widget.render(name + '_%s' % i, widget_value, final_attrs, renderer=renderer))
|
||||
if self.scheme['fields'][i][0] in REQUIRED_NAME_PARTS:
|
||||
if self.field.required:
|
||||
these_attrs['required'] = 'required'
|
||||
these_attrs.pop('data-no-required-attr', None)
|
||||
these_attrs['autocomplete'] = (self.attrs.get('autocomplete', '') + ' ' + self.autofill_map.get(self.scheme['fields'][i][0], 'off')).strip()
|
||||
these_attrs['data-size'] = self.scheme['fields'][i][2]
|
||||
else:
|
||||
these_attrs = final_attrs
|
||||
output.append(widget.render(name + '_%s' % i, widget_value, these_attrs, renderer=renderer))
|
||||
return mark_safe(self.format_output(output))
|
||||
|
||||
def format_output(self, rendered_widgets) -> str:
|
||||
@@ -144,13 +175,45 @@ class NamePartsFormField(forms.MultiValueField):
|
||||
|
||||
def clean(self, value) -> dict:
|
||||
value = super().clean(value)
|
||||
if self.one_required and (not value or not any(v for v in value)):
|
||||
if self.one_required and (not value or not any(v for v in value.values())):
|
||||
raise forms.ValidationError(self.error_messages['required'], code='required')
|
||||
if self.one_required:
|
||||
for k, v in value.items():
|
||||
if k in REQUIRED_NAME_PARTS and not v:
|
||||
raise forms.ValidationError(self.error_messages['required'], code='required')
|
||||
if self.require_all_fields and not all(v for v in value):
|
||||
raise forms.ValidationError(self.error_messages['incomplete'], code='required')
|
||||
return value
|
||||
|
||||
|
||||
class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
|
||||
def render(self, name, value, attrs=None, renderer=None):
|
||||
output = super().render(name, value, attrs, renderer)
|
||||
return mark_safe(self.format_output(output))
|
||||
|
||||
def format_output(self, rendered_widgets) -> str:
|
||||
return '<div class="nameparts-form-group">%s</div>' % ''.join(rendered_widgets)
|
||||
|
||||
|
||||
def guess_country(event):
|
||||
# Try to guess the initial country from either the country of the merchant
|
||||
# or the locale. This will hopefully save at least some users some scrolling :)
|
||||
locale = get_language()
|
||||
country = event.settings.invoice_address_from_country
|
||||
if not country:
|
||||
valid_countries = countries.countries
|
||||
if '-' in locale:
|
||||
parts = locale.split('-')
|
||||
if parts[1].upper() in valid_countries:
|
||||
country = Country(parts[1].upper())
|
||||
elif parts[0].upper() in valid_countries:
|
||||
country = Country(parts[0].upper())
|
||||
else:
|
||||
if locale in valid_countries:
|
||||
country = Country(locale.upper())
|
||||
return country
|
||||
|
||||
|
||||
class BaseQuestionsForm(forms.Form):
|
||||
"""
|
||||
This form class is responsible for asking order-related questions. This includes
|
||||
@@ -188,7 +251,12 @@ class BaseQuestionsForm(forms.Form):
|
||||
self.fields['attendee_email'] = forms.EmailField(
|
||||
required=event.settings.attendee_emails_required,
|
||||
label=_('Attendee email'),
|
||||
initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email)
|
||||
initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email),
|
||||
widget=forms.EmailInput(
|
||||
attrs={
|
||||
'autocomplete': 'email'
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
for q in questions:
|
||||
@@ -295,6 +363,32 @@ class BaseQuestionsForm(forms.Form):
|
||||
initial=dateutil.parser.parse(initial.answer).astimezone(tz) if initial and initial.answer else None,
|
||||
widget=SplitDateTimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
|
||||
)
|
||||
elif q.type == Question.TYPE_PHONENUMBER:
|
||||
babel_locale = 'en'
|
||||
# Babel, and therefore django-phonenumberfield, do not support our custom locales such das de_Informal
|
||||
if localedata.exists(get_language()):
|
||||
babel_locale = get_language()
|
||||
elif localedata.exists(get_language()[:2]):
|
||||
babel_locale = get_language()[:2]
|
||||
with language(babel_locale):
|
||||
default_country = guess_country(event)
|
||||
default_prefix = None
|
||||
for prefix, values in _COUNTRY_CODE_TO_REGION_CODE.items():
|
||||
if str(default_country) in values:
|
||||
default_prefix = prefix
|
||||
try:
|
||||
initial = PhoneNumber().from_string(initial.answer) if initial else "+{}.".format(default_prefix)
|
||||
except NumberParseException:
|
||||
initial = None
|
||||
field = PhoneNumberField(
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
# We now exploit an implementation detail in PhoneNumberPrefixWidget to allow us to pass just
|
||||
# a country code but no number as an initial value. It's a bit hacky, but should be stable for
|
||||
# the future.
|
||||
initial=initial,
|
||||
widget=WrappedPhoneNumberPrefixWidget()
|
||||
)
|
||||
field.question = q
|
||||
if answers:
|
||||
# Cache the answer object for later use
|
||||
@@ -318,12 +412,18 @@ class BaseQuestionsForm(forms.Form):
|
||||
self.fields[key] = value
|
||||
value.initial = data.get('question_form_data', {}).get(key)
|
||||
|
||||
for k, v in self.fields.items():
|
||||
if v.widget.attrs.get('autocomplete') or k == 'attendee_name_parts':
|
||||
v.widget.attrs['autocomplete'] = 'section-{} '.format(self.prefix) + v.widget.attrs.get('autocomplete', '')
|
||||
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
|
||||
question_cache = {f.question.pk: f.question for f in self.fields.values() if getattr(f, 'question', None)}
|
||||
|
||||
def question_is_visible(parentid, qvals):
|
||||
if parentid not in question_cache:
|
||||
return False
|
||||
parentq = question_cache[parentid]
|
||||
if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id, parentq.dependency_values):
|
||||
return False
|
||||
@@ -356,13 +456,29 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = InvoiceAddress
|
||||
fields = ('is_business', 'company', 'name_parts', 'street', 'zipcode', 'city', 'country', 'vat_id',
|
||||
'internal_reference', 'beneficiary')
|
||||
fields = ('is_business', 'company', 'name_parts', 'street', 'zipcode', 'city', 'country', 'state',
|
||||
'vat_id', 'internal_reference', 'beneficiary')
|
||||
widgets = {
|
||||
'is_business': BusinessBooleanRadio,
|
||||
'street': forms.Textarea(attrs={'rows': 2, 'placeholder': _('Street and Number')}),
|
||||
'street': forms.Textarea(attrs={
|
||||
'rows': 2,
|
||||
'placeholder': _('Street and Number'),
|
||||
'autocomplete': 'street-address'
|
||||
}),
|
||||
'beneficiary': forms.Textarea(attrs={'rows': 3}),
|
||||
'company': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
|
||||
'country': forms.Select(attrs={
|
||||
'autocomplete': 'country',
|
||||
}),
|
||||
'zipcode': forms.TextInput(attrs={
|
||||
'autocomplete': 'postal-code',
|
||||
}),
|
||||
'city': forms.TextInput(attrs={
|
||||
'autocomplete': 'address-level2',
|
||||
}),
|
||||
'company': forms.TextInput(attrs={
|
||||
'data-display-dependency': '#id_is_business_1',
|
||||
'autocomplete': 'organization',
|
||||
}),
|
||||
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
|
||||
'internal_reference': forms.TextInput,
|
||||
}
|
||||
@@ -378,28 +494,39 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
|
||||
kwargs.setdefault('initial', {})
|
||||
if not kwargs.get('instance') or not kwargs['instance'].country:
|
||||
# Try to guess the initial country from either the country of the merchant
|
||||
# or the locale. This will hopefully save at least some users some scrolling :)
|
||||
locale = get_language()
|
||||
country = event.settings.invoice_address_from_country
|
||||
if not country:
|
||||
valid_countries = countries.countries
|
||||
if '-' in locale:
|
||||
parts = locale.split('-')
|
||||
if parts[1].upper() in valid_countries:
|
||||
country = Country(parts[1].upper())
|
||||
elif parts[0].upper() in valid_countries:
|
||||
country = Country(parts[0].upper())
|
||||
else:
|
||||
if locale in valid_countries:
|
||||
country = Country(locale.upper())
|
||||
|
||||
kwargs['initial']['country'] = country
|
||||
kwargs['initial']['country'] = guess_country(self.event)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
if not event.settings.invoice_address_vatid:
|
||||
del self.fields['vat_id']
|
||||
|
||||
c = [('', pgettext_lazy('address', 'Select state'))]
|
||||
fprefix = self.prefix + '-' if self.prefix else ''
|
||||
cc = None
|
||||
if fprefix + 'country' in self.data:
|
||||
cc = str(self.data[fprefix + 'country'])
|
||||
elif 'country' in self.initial:
|
||||
cc = str(self.initial['country'])
|
||||
elif self.instance and self.instance.country:
|
||||
cc = str(self.instance.country)
|
||||
if cc and cc in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc]
|
||||
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
|
||||
c += sorted([(s.code[3:], s.name) for s in statelist], key=lambda s: s[1])
|
||||
elif fprefix + 'state' in self.data:
|
||||
self.data = self.data.copy()
|
||||
del self.data[fprefix + 'state']
|
||||
|
||||
self.fields['state'] = forms.ChoiceField(
|
||||
label=pgettext_lazy('address', 'State'),
|
||||
required=False,
|
||||
choices=c,
|
||||
widget=forms.Select(attrs={
|
||||
'autocomplete': 'address-level1',
|
||||
}),
|
||||
)
|
||||
self.fields['state'].widget.is_required = True
|
||||
|
||||
if not event.settings.invoice_address_required or self.all_optional:
|
||||
for k, f in self.fields.items():
|
||||
f.required = False
|
||||
@@ -426,13 +553,18 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
initial=(self.instance.name_parts if self.instance else self.instance.name_parts),
|
||||
)
|
||||
if event.settings.invoice_address_required and not event.settings.invoice_address_company_required and not self.all_optional:
|
||||
self.fields['name_parts'].widget.attrs['data-required-if'] = '#id_is_business_0'
|
||||
if not event.settings.invoice_name_required:
|
||||
self.fields['name_parts'].widget.attrs['data-required-if'] = '#id_is_business_0'
|
||||
self.fields['name_parts'].widget.attrs['data-no-required-attr'] = '1'
|
||||
self.fields['company'].widget.attrs['data-required-if'] = '#id_is_business_1'
|
||||
|
||||
if not event.settings.invoice_address_beneficiary:
|
||||
del self.fields['beneficiary']
|
||||
|
||||
for k, v in self.fields.items():
|
||||
if v.widget.attrs.get('autocomplete') or k == 'name_parts':
|
||||
v.widget.attrs['autocomplete'] = 'section-invoice billing ' + v.widget.attrs.get('autocomplete', '')
|
||||
|
||||
def clean(self):
|
||||
data = self.cleaned_data
|
||||
if not data.get('is_business'):
|
||||
@@ -446,6 +578,10 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
if 'vat_id' in self.changed_data or not data.get('vat_id'):
|
||||
self.instance.vat_id_validated = False
|
||||
|
||||
if data.get('city') and data.get('country') and str(data['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||
if not data.get('state'):
|
||||
self.add_error('state', _('This field is required.'))
|
||||
|
||||
self.instance.name_parts = data.get('name_parts')
|
||||
|
||||
if all(
|
||||
@@ -457,7 +593,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
if self.validate_vat_id and self.instance.vat_id_validated and 'vat_id' not in self.changed_data:
|
||||
pass
|
||||
elif self.validate_vat_id and data.get('is_business') and data.get('country') in EU_COUNTRIES and data.get('vat_id'):
|
||||
if data.get('vat_id')[:2] != str(data.get('country')):
|
||||
if data.get('vat_id')[:2] != cc_to_vat_prefix(str(data.get('country'))):
|
||||
raise ValidationError(_('Your VAT ID does not match the selected country.'))
|
||||
try:
|
||||
result = vat_moss.id.validate(data.get('vat_id'))
|
||||
@@ -488,9 +624,8 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
|
||||
|
||||
class BaseInvoiceNameForm(BaseInvoiceAddressForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for f in list(self.fields.keys()):
|
||||
if f != 'name':
|
||||
if f != 'name_parts':
|
||||
del self.fields[f]
|
||||
|
||||
@@ -56,6 +56,11 @@ class UserSettingsForm(forms.ModelForm):
|
||||
self.user = kwargs.pop('user')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['email'].required = True
|
||||
if self.user.auth_backend != 'native':
|
||||
del self.fields['old_pw']
|
||||
del self.fields['new_pw']
|
||||
del self.fields['new_pw_repeat']
|
||||
self.fields['email'].disabled = True
|
||||
|
||||
def clean_old_pw(self):
|
||||
old_pw = self.cleaned_data.get('old_pw')
|
||||
@@ -115,5 +120,5 @@ class User2FADeviceAddForm(forms.Form):
|
||||
name = forms.CharField(label=_('Device name'), max_length=64)
|
||||
devicetype = forms.ChoiceField(label=_('Device type'), widget=forms.RadioSelect, choices=(
|
||||
('totp', _('Smartphone with the Authenticator application')),
|
||||
('u2f', _('U2F-compatible hardware token (e.g. Yubikey)')),
|
||||
('webauthn', _('WebAuthn-compatible hardware token (e.g. Yubikey)')),
|
||||
))
|
||||
|
||||
@@ -26,7 +26,7 @@ class PlaceholderValidator(BaseValidator):
|
||||
if value.count('{') != value.count('}'):
|
||||
raise ValidationError(
|
||||
_('Invalid placeholder syntax: You used a different number of "{" than of "}".'),
|
||||
code='invalid',
|
||||
code='invalid_placeholder_syntax',
|
||||
)
|
||||
|
||||
data_placeholders = list(re.findall(r'({[^}]*})', value, re.X))
|
||||
@@ -37,7 +37,7 @@ class PlaceholderValidator(BaseValidator):
|
||||
if invalid_placeholders:
|
||||
raise ValidationError(
|
||||
_('Invalid placeholder(s): %(value)s'),
|
||||
code='invalid',
|
||||
code='invalid_placeholders',
|
||||
params={'value': ", ".join(invalid_placeholders,)})
|
||||
|
||||
def clean(self, x):
|
||||
|
||||
@@ -46,6 +46,14 @@ class TimePickerWidget(forms.TimeInput):
|
||||
|
||||
class UploadedFileWidget(forms.ClearableFileInput):
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Browsers can't recognize that the server already has a file uploaded
|
||||
# Don't mark this input as being required if we already have an answer
|
||||
# (this needs to be done via the attrs, otherwise we wouldn't get the "required" star on the field label)
|
||||
attrs = kwargs.get('attrs', {})
|
||||
if kwargs.get('required') and kwargs.get('initial'):
|
||||
attrs.update({'required': None})
|
||||
kwargs.update({'attrs': attrs})
|
||||
|
||||
self.position = kwargs.pop('position')
|
||||
self.event = kwargs.pop('event')
|
||||
self.answer = kwargs.pop('answer')
|
||||
|
||||
@@ -457,19 +457,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
id='normal')
|
||||
]
|
||||
|
||||
def _get_story(self, doc):
|
||||
has_taxes = any(il.tax_value for il in self.invoice.lines.all())
|
||||
|
||||
story = [
|
||||
NextPageTemplate('FirstPage'),
|
||||
Paragraph(pgettext('invoice', 'Invoice')
|
||||
if not self.invoice.is_cancellation
|
||||
else pgettext('invoice', 'Cancellation'),
|
||||
self.stylesheet['Heading1']),
|
||||
Spacer(1, 5 * mm),
|
||||
NextPageTemplate('OtherPages'),
|
||||
]
|
||||
|
||||
def _get_intro(self):
|
||||
story = []
|
||||
if self.invoice.internal_reference:
|
||||
story.append(Paragraph(
|
||||
pgettext('invoice', 'Customer reference: {reference}').format(reference=self.invoice.internal_reference),
|
||||
@@ -478,7 +467,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
|
||||
if self.invoice.invoice_to_vat_id:
|
||||
story.append(Paragraph(
|
||||
pgettext('invoice', 'Customer VAT ID') + ':<br />' +
|
||||
pgettext('invoice', 'Customer VAT ID') + ': ' +
|
||||
bleach.clean(self.invoice.invoice_to_vat_id, tags=[]).replace("\n", "<br />\n"),
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
@@ -494,6 +483,25 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
story.append(Paragraph(self.invoice.introductory_text, self.stylesheet['Normal']))
|
||||
story.append(Spacer(1, 10 * mm))
|
||||
|
||||
return story
|
||||
|
||||
def _get_story(self, doc):
|
||||
has_taxes = any(il.tax_value for il in self.invoice.lines.all())
|
||||
|
||||
story = [
|
||||
NextPageTemplate('FirstPage'),
|
||||
Paragraph(
|
||||
(
|
||||
pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU'
|
||||
else pgettext('invoice', 'Invoice')
|
||||
) if not self.invoice.is_cancellation else pgettext('invoice', 'Cancellation'),
|
||||
self.stylesheet['Heading1']
|
||||
),
|
||||
Spacer(1, 5 * mm),
|
||||
NextPageTemplate('OtherPages'),
|
||||
]
|
||||
story += self._get_intro()
|
||||
|
||||
taxvalue_map = defaultdict(Decimal)
|
||||
grossvalue_map = defaultdict(Decimal)
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("default_manager_name")
|
||||
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("permissions")
|
||||
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("default_permissions")
|
||||
IGNORED_ATTRS = [
|
||||
# (field type, attribute name, blacklist of field sub-types)
|
||||
# (field type, attribute name, banlist of field sub-types)
|
||||
(models.Field, 'verbose_name', []),
|
||||
(models.Field, 'help_text', []),
|
||||
(models.Field, 'validators', []),
|
||||
@@ -38,8 +38,8 @@ original_deconstruct = models.Field.deconstruct
|
||||
|
||||
def new_deconstruct(self):
|
||||
name, path, args, kwargs = original_deconstruct(self)
|
||||
for ftype, attr, blacklist in IGNORED_ATTRS:
|
||||
if isinstance(self, ftype) and not any(isinstance(self, ft) for ft in blacklist):
|
||||
for ftype, attr, banlist in IGNORED_ATTRS:
|
||||
if isinstance(self, ftype) and not any(isinstance(self, ft) for ft in banlist):
|
||||
kwargs.pop(attr, None)
|
||||
return name, path, args, kwargs
|
||||
|
||||
|
||||
@@ -11,13 +11,13 @@ from django.core.management.commands.migrate import Command as Parent
|
||||
|
||||
|
||||
class OutputFilter(OutputWrapper):
|
||||
blacklist = (
|
||||
banlist = (
|
||||
"Your models have changes that are not yet reflected",
|
||||
"Run 'manage.py makemigrations' to make new "
|
||||
)
|
||||
|
||||
def write(self, msg, style_func=None, ending=None):
|
||||
if any(b in msg for b in self.blacklist):
|
||||
if any(b in msg for b in self.banlist):
|
||||
return
|
||||
super().write(msg, style_func, ending)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from django.utils.translation.trans_real import (
|
||||
parse_accept_lang_header,
|
||||
)
|
||||
|
||||
from pretix.base.settings import GlobalSettingsObject
|
||||
from pretix.multidomain.urlreverse import get_domain
|
||||
|
||||
_supported = None
|
||||
@@ -187,6 +188,11 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
# https://github.com/pretix/pretix/issues/765
|
||||
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
|
||||
|
||||
img_src = []
|
||||
gs = GlobalSettingsObject()
|
||||
if gs.settings.leaflet_tiles:
|
||||
img_src.append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*"))
|
||||
|
||||
h = {
|
||||
'default-src': ["{static}"],
|
||||
'script-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
|
||||
@@ -196,7 +202,7 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
'child-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
|
||||
'style-src': ["{static}", "{media}"],
|
||||
'connect-src': ["{dynamic}", "{media}", "https://checkout.stripe.com"],
|
||||
'img-src': ["{static}", "{media}", "data:", "https://*.stripe.com"],
|
||||
'img-src': ["{static}", "{media}", "data:", "https://*.stripe.com"] + img_src,
|
||||
'font-src': ["{static}"],
|
||||
'media-src': ["{static}", "data:"],
|
||||
# form-action is not only used to match on form actions, but also on URLs
|
||||
|
||||
@@ -182,12 +182,12 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='slug',
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Slug'),
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Slug'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='organizer',
|
||||
name='slug',
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Slug'),
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Slug'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voucher',
|
||||
|
||||
@@ -23,12 +23,12 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='slug',
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Slug'),
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Slug'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='organizer',
|
||||
name='slug',
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Slug'),
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Slug'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voucher',
|
||||
|
||||
@@ -124,7 +124,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='slug',
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This will be used in order codes, invoice numbers, links 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'),
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This will be used in order codes, invoice numbers, links and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='requiredaction',
|
||||
@@ -179,7 +179,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='organizer',
|
||||
name='slug',
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Short form'),
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=merge_names,
|
||||
|
||||
@@ -346,7 +346,7 @@ class Migration(migrations.Migration):
|
||||
help_text='Should be short, only contain lowercase letters and numbers, 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'),
|
||||
pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoice',
|
||||
|
||||
@@ -33,7 +33,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='slug',
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This will be used in order codes, invoice numbers, links 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'),
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This will be used in order codes, invoice numbers, links and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='requiredaction',
|
||||
|
||||
@@ -36,7 +36,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='organizer',
|
||||
name='slug',
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Short form'),
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
|
||||
),
|
||||
migrations.RunPython(merge_names, migrations.RunPython.noop)
|
||||
]
|
||||
|
||||
@@ -38,7 +38,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='slug',
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. 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'),
|
||||
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, 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.EventSlugBanlistValidator()], verbose_name='Short form'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoice',
|
||||
|
||||
@@ -44,7 +44,7 @@ class Migration(migrations.Migration):
|
||||
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'),
|
||||
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.EventSlugBanlistValidator()], verbose_name='Short form'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventmetaproperty',
|
||||
@@ -54,7 +54,7 @@ class Migration(migrations.Migration):
|
||||
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'),
|
||||
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.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CheckinList',
|
||||
|
||||
@@ -105,7 +105,7 @@ class Migration(migrations.Migration):
|
||||
'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'),
|
||||
pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventmetaproperty',
|
||||
@@ -125,7 +125,7 @@ class Migration(migrations.Migration):
|
||||
' 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'),
|
||||
pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CheckinList',
|
||||
|
||||
@@ -27,7 +27,7 @@ class Migration(migrations.Migration):
|
||||
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'),
|
||||
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.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='staffsession',
|
||||
|
||||
@@ -29,7 +29,7 @@ class Migration(migrations.Migration):
|
||||
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'),
|
||||
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.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='staffsession',
|
||||
|
||||
27
src/pretix/base/migrations/0132_auto_20190808_1253.py
Normal file
27
src/pretix/base/migrations/0132_auto_20190808_1253.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 2.2.1 on 2019-08-08 12:53
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0131_auto_20190729_1422'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='invoice_to_state',
|
||||
field=models.CharField(max_length=190, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoiceaddress',
|
||||
name='state',
|
||||
field=models.CharField(default='', max_length=255),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
21
src/pretix/base/migrations/0133_auto_20190830_1513.py
Normal file
21
src/pretix/base/migrations/0133_auto_20190830_1513.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 2.2.4 on 2019-08-30 15:13
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0132_auto_20190808_1253'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='question',
|
||||
name='print_on_invoice',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
35
src/pretix/base/migrations/0134_auto_20190909_1042.py
Normal file
35
src/pretix/base/migrations/0134_auto_20190909_1042.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# Generated by Django 2.2.4 on 2019-09-09 10:42
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0133_auto_20190830_1513'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='WebAuthnDevice',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=64)),
|
||||
('confirmed', models.BooleanField(default=True)),
|
||||
('credential_id', models.CharField(max_length=255, null=True)),
|
||||
('rp_id', models.CharField(max_length=255, null=True)),
|
||||
('icon_url', models.CharField(max_length=255, null=True)),
|
||||
('ukey', models.TextField(null=True)),
|
||||
('pub_key', models.TextField(null=True)),
|
||||
('sign_count', models.IntegerField(default=0)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
27
src/pretix/base/migrations/0135_auto_20191007_0803.py
Normal file
27
src/pretix/base/migrations/0135_auto_20191007_0803.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 2.2.4 on 2019-10-07 08:03
|
||||
from django.core.cache import cache
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def mail_migrator(app, schema_editor):
|
||||
Event_SettingsStore = app.get_model('pretixbase', 'Event_SettingsStore')
|
||||
|
||||
for ss in Event_SettingsStore.objects.filter(
|
||||
key__in=['mail_text_order_approved', 'mail_text_order_placed', 'mail_text_order_placed_require_approval']
|
||||
):
|
||||
chgd = ss.value.replace("{date}", "{expire_date}")
|
||||
if chgd != ss.value:
|
||||
ss.value = chgd
|
||||
ss.save()
|
||||
cache.delete('hierarkey_{}_{}'.format('event', ss.object_id))
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0134_auto_20190909_1042'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(mail_migrator, migrations.RunPython.noop)
|
||||
]
|
||||
25
src/pretix/base/migrations/0136_auto_20190918_1742.py
Normal file
25
src/pretix/base/migrations/0136_auto_20190918_1742.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 2.2 on 2019-09-18 17:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0135_auto_20191007_0803'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='checkin',
|
||||
name='auto_checked_in',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='checkinlist',
|
||||
name='auto_checkin_sales_channels',
|
||||
field=pretix.base.models.fields.MultiStringField(default=[]),
|
||||
)
|
||||
]
|
||||
17
src/pretix/base/migrations/0137_auto_20191015_1141.py
Normal file
17
src/pretix/base/migrations/0137_auto_20191015_1141.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 2.2.4 on 2019-10-15 11:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('pretixbase', '0136_auto_20190918_1742'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='auth_backend',
|
||||
field=models.CharField(default='native', max_length=255),
|
||||
),
|
||||
]
|
||||
107
src/pretix/base/migrations/0138_auto_20191017_1151.py
Normal file
107
src/pretix/base/migrations/0138_auto_20191017_1151.py
Normal file
@@ -0,0 +1,107 @@
|
||||
# Generated by Django 2.2.4 on 2019-10-17 11:51
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.base
|
||||
import pretix.base.models.fields
|
||||
import pretix.base.models.giftcards
|
||||
|
||||
|
||||
def fwd(app, schema_editor):
|
||||
Team = app.get_model('pretixbase', 'Team')
|
||||
Team.objects.filter(can_change_organizer_settings=True).update(can_manage_gift_cards=True)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('pretixbase', '0137_auto_20191015_1141'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='GiftCard',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('issuance', models.DateTimeField(auto_now_add=True)),
|
||||
('secret', models.CharField(db_index=True, default=pretix.base.models.giftcards.gen_giftcard_secret,
|
||||
max_length=190)),
|
||||
('currency', models.CharField(max_length=10)),
|
||||
('issued_in', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='issued_gift_cards', to='pretixbase.OrderPosition')),
|
||||
('issuer',
|
||||
models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='issued_gift_cards',
|
||||
to='pretixbase.Organizer')),
|
||||
('testmode', django.db.models.BooleanField(default=False)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('secret', 'issuer')},
|
||||
},
|
||||
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='issue_giftcard',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='team',
|
||||
name='can_manage_gift_cards',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='question',
|
||||
name='dependency_values',
|
||||
field=pretix.base.models.fields.MultiStringField(default=[]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voucher',
|
||||
name='item',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vouchers',
|
||||
to='pretixbase.Item'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voucher',
|
||||
name='quota',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vouchers',
|
||||
to='pretixbase.Quota'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voucher',
|
||||
name='variation',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vouchers',
|
||||
to='pretixbase.ItemVariation'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GiftCardTransaction',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('datetime', models.DateTimeField(auto_now_add=True)),
|
||||
('value', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
('card', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='transactions',
|
||||
to='pretixbase.GiftCard')),
|
||||
('order', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='gift_card_transactions', to='pretixbase.Order')),
|
||||
('payment', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='gift_card_transactions', to='pretixbase.OrderPayment')),
|
||||
('refund', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='gift_card_transactions', to='pretixbase.OrderRefund')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('datetime',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GiftCardAcceptance',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('collector', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='gift_card_issuer_acceptance', to='pretixbase.Organizer')),
|
||||
('issuer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='gift_card_collector_acceptance', to='pretixbase.Organizer')),
|
||||
],
|
||||
),
|
||||
migrations.RunPython(
|
||||
fwd, migrations.RunPython.noop
|
||||
),
|
||||
]
|
||||
33
src/pretix/base/migrations/0139_auto_20191019_1317.py
Normal file
33
src/pretix/base/migrations/0139_auto_20191019_1317.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 2.2.1 on 2019-10-19 13:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0138_auto_20191017_1151'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='geo_lat',
|
||||
field=models.FloatField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='geo_lon',
|
||||
field=models.FloatField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subevent',
|
||||
name='geo_lat',
|
||||
field=models.FloatField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subevent',
|
||||
name='geo_lon',
|
||||
field=models.FloatField(null=True),
|
||||
),
|
||||
]
|
||||
19
src/pretix/base/migrations/0140_voucher_seat.py
Normal file
19
src/pretix/base/migrations/0140_voucher_seat.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 2.2.4 on 2019-11-14 11:49
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0139_auto_20191019_1317'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='voucher',
|
||||
name='seat',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vouchers', to='pretixbase.Seat'),
|
||||
),
|
||||
]
|
||||
18
src/pretix/base/migrations/0141_seat_sorting_rank.py
Normal file
18
src/pretix/base/migrations/0141_seat_sorting_rank.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.2.4 on 2019-11-22 11:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0140_voucher_seat'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='seat',
|
||||
name='sorting_rank',
|
||||
field=models.BigIntegerField(default=0),
|
||||
),
|
||||
]
|
||||
30
src/pretix/base/migrations/0142_auto_20191215_1522.py
Normal file
30
src/pretix/base/migrations/0142_auto_20191215_1522.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 2.2.7 on 2019-12-15 15:22
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0141_seat_sorting_rank'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='price_before_voucher',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=10, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='price_before_voucher',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=10, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='voucher',
|
||||
name='budget',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=10, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,5 +1,5 @@
|
||||
from ..settings import GlobalSettingsObject_SettingsStore
|
||||
from .auth import U2FDevice, User
|
||||
from .auth import U2FDevice, User, WebAuthnDevice
|
||||
from .base import CachedFile, LoggedModel, cachedfile_name
|
||||
from .checkin import Checkin, CheckinList
|
||||
from .devices import Device
|
||||
@@ -7,6 +7,7 @@ from .event import (
|
||||
Event, Event_SettingsStore, EventLock, EventMetaProperty, EventMetaValue,
|
||||
RequiredAction, SubEvent, SubEventMetaValue, generate_invite_token,
|
||||
)
|
||||
from .giftcards import GiftCard, GiftCardAcceptance, GiftCardTransaction
|
||||
from .invoices import Invoice, InvoiceLine, invoice_filename
|
||||
from .items import (
|
||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation, Question,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import binascii
|
||||
import json
|
||||
from datetime import timedelta
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import webauthn
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import (
|
||||
AbstractBaseUser, BaseUserManager, PermissionsMixin,
|
||||
@@ -13,6 +17,9 @@ from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django_otp.models import Device
|
||||
from django_scopes import scopes_disabled
|
||||
from u2flib_server.utils import (
|
||||
pub_key_from_der, websafe_decode, websafe_encode,
|
||||
)
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.helpers.urls import build_absolute_uri
|
||||
@@ -102,6 +109,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
help_text=_('If turned off, you will not get any notifications.')
|
||||
)
|
||||
notifications_token = models.CharField(max_length=255, default=generate_notifications_token)
|
||||
auth_backend = models.CharField(max_length=255, default='native')
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
@@ -176,6 +184,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
'url': build_absolute_uri('control:user.settings')
|
||||
},
|
||||
event=None,
|
||||
user=self,
|
||||
locale=self.locale
|
||||
)
|
||||
except SendMailException:
|
||||
@@ -191,9 +200,13 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
'url': (build_absolute_uri('control:auth.forgot.recover')
|
||||
+ '?id=%d&token=%s' % (self.id, default_token_generator.make_token(self)))
|
||||
},
|
||||
None, locale=self.locale
|
||||
None, locale=self.locale, user=self
|
||||
)
|
||||
|
||||
@property
|
||||
def top_logentries(self):
|
||||
return self.all_logentries
|
||||
|
||||
@property
|
||||
def all_logentries(self):
|
||||
from pretix.base.models import LogEntry
|
||||
@@ -322,6 +335,25 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
| Q(id__in=self.teams.filter(**kwargs).values_list('limit_events__id', flat=True))
|
||||
)
|
||||
|
||||
@scopes_disabled()
|
||||
def get_organizers_with_permission(self, permission, request=None):
|
||||
"""
|
||||
Returns a queryset of organizers the user has a specific permissions to.
|
||||
|
||||
:param request: The current request (optional). Required to detect staff sessions properly.
|
||||
:return: Iterable of Organizers
|
||||
"""
|
||||
from .event import Organizer
|
||||
|
||||
if request and self.has_active_staff_session(request.session.session_key):
|
||||
return Organizer.objects.all()
|
||||
|
||||
kwargs = {permission: True}
|
||||
|
||||
return Organizer.objects.filter(
|
||||
id__in=self.teams.filter(**kwargs).values_list('organizer', flat=True)
|
||||
)
|
||||
|
||||
def has_active_staff_session(self, session_key=None):
|
||||
"""
|
||||
Returns whether or not a user has an active staff session (formerly known as superuser session)
|
||||
@@ -375,3 +407,49 @@ class StaffSessionAuditLog(models.Model):
|
||||
|
||||
class U2FDevice(Device):
|
||||
json_data = models.TextField()
|
||||
|
||||
@property
|
||||
def webauthnuser(self):
|
||||
d = json.loads(self.json_data)
|
||||
# We manually need to convert the pubkey from DER format (used in our
|
||||
# former U2F implementation) to the format required by webauthn. This
|
||||
# is based on the following example:
|
||||
# https://www.w3.org/TR/webauthn/#sctn-encoded-credPubKey-examples
|
||||
pub_key = pub_key_from_der(websafe_decode(d['publicKey'].replace('+', '-').replace('/', '_')))
|
||||
pub_key = binascii.unhexlify(
|
||||
'A5010203262001215820{:064x}225820{:064x}'.format(
|
||||
pub_key.public_numbers().x, pub_key.public_numbers().y
|
||||
)
|
||||
)
|
||||
return webauthn.WebAuthnUser(
|
||||
d['keyHandle'],
|
||||
self.user.email,
|
||||
str(self.user),
|
||||
settings.SITE_URL,
|
||||
d['keyHandle'],
|
||||
websafe_encode(pub_key),
|
||||
1,
|
||||
urlparse(settings.SITE_URL).netloc
|
||||
)
|
||||
|
||||
|
||||
class WebAuthnDevice(Device):
|
||||
credential_id = models.CharField(max_length=255, null=True, blank=True)
|
||||
rp_id = models.CharField(max_length=255, null=True, blank=True)
|
||||
icon_url = models.CharField(max_length=255, null=True, blank=True)
|
||||
ukey = models.TextField(null=True)
|
||||
pub_key = models.TextField(null=True)
|
||||
sign_count = models.IntegerField(default=0)
|
||||
|
||||
@property
|
||||
def webauthnuser(self):
|
||||
return webauthn.WebAuthnUser(
|
||||
self.ukey,
|
||||
self.user.email,
|
||||
str(self.user),
|
||||
settings.SITE_URL,
|
||||
self.credential_id,
|
||||
self.pub_key,
|
||||
self.sign_count,
|
||||
urlparse(settings.SITE_URL).netloc
|
||||
)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from django.db import models
|
||||
from django.db.models import Case, Count, F, OuterRef, Q, Subquery, When
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.db.models import Exists, OuterRef
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django_scopes import ScopedManager
|
||||
|
||||
from pretix.base.models import LoggedModel
|
||||
from pretix.base.models.fields import MultiStringField
|
||||
|
||||
|
||||
class CheckinList(LoggedModel):
|
||||
@@ -18,142 +18,69 @@ class CheckinList(LoggedModel):
|
||||
include_pending = models.BooleanField(verbose_name=pgettext_lazy('checkin', 'Include pending orders'),
|
||||
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.'))
|
||||
'order have not been paid.'))
|
||||
|
||||
auto_checkin_sales_channels = MultiStringField(
|
||||
default=[],
|
||||
blank=True,
|
||||
verbose_name=_('Sales channels to automatically check in'),
|
||||
help_text=_('All items on this check-in list will be automatically marked as checked-in when purchased through '
|
||||
'any of the selected sales channels. This option can be useful when tickets sold at the box office '
|
||||
'are not checked again before entry and should be considered validated directly upon purchase.')
|
||||
)
|
||||
|
||||
objects = ScopedManager(organizer='event__organizer')
|
||||
|
||||
class Meta:
|
||||
ordering = ('subevent__date_from', 'name')
|
||||
|
||||
@property
|
||||
def positions(self):
|
||||
from . import OrderPosition, Order
|
||||
|
||||
qs = OrderPosition.objects.filter(
|
||||
order__event=self.event,
|
||||
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.include_pending else [Order.STATUS_PAID],
|
||||
subevent=self.subevent
|
||||
)
|
||||
if not self.all_products:
|
||||
qs = qs.filter(item__in=self.limit_products.values_list('id', flat=True))
|
||||
return qs
|
||||
|
||||
@property
|
||||
def checkin_count(self):
|
||||
return self.event.cache.get_or_set(
|
||||
'checkin_list_{}_checkin_count'.format(self.pk),
|
||||
lambda: self.positions.annotate(
|
||||
checkedin=Exists(Checkin.objects.filter(list_id=self.pk, position=OuterRef('pk')))
|
||||
).filter(
|
||||
checkedin=True
|
||||
).count(),
|
||||
60
|
||||
)
|
||||
|
||||
@property
|
||||
def percent(self):
|
||||
pc = self.position_count
|
||||
return round(self.checkin_count * 100 / pc) if pc else 0
|
||||
|
||||
@property
|
||||
def position_count(self):
|
||||
return self.event.cache.get_or_set(
|
||||
'checkin_list_{}_position_count'.format(self.pk),
|
||||
lambda: self.positions.count(),
|
||||
60
|
||||
)
|
||||
|
||||
def touch(self):
|
||||
self.event.cache.delete('checkin_list_{}_position_count'.format(self.pk))
|
||||
self.event.cache.delete('checkin_list_{}_checkin_count'.format(self.pk))
|
||||
|
||||
@staticmethod
|
||||
def annotate_with_numbers(qs, event):
|
||||
"""
|
||||
Modifies a queryset of checkin lists by annotating it with the number of order positions and
|
||||
checkins associated with it.
|
||||
"""
|
||||
# Import here to prevent circular import
|
||||
from . import Order, OrderPosition, Item
|
||||
|
||||
# This is the mother of all subqueries. Sorry. I try to explain it, at least?
|
||||
# First, we prepare a subquery that for every check-in that belongs to a paid-order
|
||||
# position and to the list in question. Then, we check that it also belongs to the
|
||||
# correct subevent (just to be sure) and aggregate over lists (so, over everything,
|
||||
# since we filtered by lists).
|
||||
cqs_paid = Checkin.objects.filter(
|
||||
position__order__event=event,
|
||||
position__order__status=Order.STATUS_PAID,
|
||||
list=OuterRef('pk')
|
||||
).filter(
|
||||
# This assumes that in an event with subevents, *all* positions have subevents
|
||||
# and *all* checkin lists have a subevent assigned
|
||||
Q(position__subevent=OuterRef('subevent'))
|
||||
| (Q(position__subevent__isnull=True))
|
||||
).order_by().values('list').annotate(
|
||||
c=Count('*')
|
||||
).values('c')
|
||||
cqs_paid_and_pending = Checkin.objects.filter(
|
||||
position__order__event=event,
|
||||
position__order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING],
|
||||
list=OuterRef('pk')
|
||||
).filter(
|
||||
# This assumes that in an event with subevents, *all* positions have subevents
|
||||
# and *all* checkin lists have a subevent assigned
|
||||
Q(position__subevent=OuterRef('subevent'))
|
||||
| (Q(position__subevent__isnull=True))
|
||||
).order_by().values('list').annotate(
|
||||
c=Count('*')
|
||||
).values('c')
|
||||
|
||||
# Now for the hard part: getting all order positions that contribute to this list. This
|
||||
# requires us to use TWO subqueries. The first one, pqs_all, will only be used for check-in
|
||||
# lists that contain all the products of the event. This is the simpler one, it basically
|
||||
# looks like the check-in counter above.
|
||||
pqs_all_paid = OrderPosition.objects.filter(
|
||||
order__event=event,
|
||||
order__status=Order.STATUS_PAID,
|
||||
).filter(
|
||||
# This assumes that in an event with subevents, *all* positions have subevents
|
||||
# and *all* checkin lists have a subevent assigned
|
||||
Q(subevent=OuterRef('subevent'))
|
||||
| (Q(subevent__isnull=True))
|
||||
).order_by().values('order__event').annotate(
|
||||
c=Count('*')
|
||||
).values('c')
|
||||
pqs_all_paid_and_pending = OrderPosition.objects.filter(
|
||||
order__event=event,
|
||||
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING]
|
||||
).filter(
|
||||
# This assumes that in an event with subevents, *all* positions have subevents
|
||||
# and *all* checkin lists have a subevent assigned
|
||||
Q(subevent=OuterRef('subevent'))
|
||||
| (Q(subevent__isnull=True))
|
||||
).order_by().values('order__event').annotate(
|
||||
c=Count('*')
|
||||
).values('c')
|
||||
|
||||
# Now we need a subquery for the case of checkin lists that are limited to certain
|
||||
# products. We cannot use OuterRef("limit_products") since that would do a cross-product
|
||||
# with the products table and we'd get duplicate rows in the output with different annotations
|
||||
# on them, which isn't useful at all. Therefore, we need to add a second layer of subqueries
|
||||
# to retrieve all of those items and then check if the item_id is IN this subquery result.
|
||||
pqs_limited_paid = OrderPosition.objects.filter(
|
||||
order__event=event,
|
||||
order__status=Order.STATUS_PAID,
|
||||
item_id__in=Subquery(Item.objects.filter(checkinlist__pk=OuterRef(OuterRef('pk'))).values('pk'))
|
||||
).filter(
|
||||
# This assumes that in an event with subevents, *all* positions have subevents
|
||||
# and *all* checkin lists have a subevent assigned
|
||||
Q(subevent=OuterRef('subevent'))
|
||||
| (Q(subevent__isnull=True))
|
||||
).order_by().values('order__event').annotate(
|
||||
c=Count('*')
|
||||
).values('c')
|
||||
pqs_limited_paid_and_pending = OrderPosition.objects.filter(
|
||||
order__event=event,
|
||||
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING],
|
||||
item_id__in=Subquery(Item.objects.filter(checkinlist__pk=OuterRef(OuterRef('pk'))).values('pk'))
|
||||
).filter(
|
||||
# This assumes that in an event with subevents, *all* positions have subevents
|
||||
# and *all* checkin lists have a subevent assigned
|
||||
Q(subevent=OuterRef('subevent'))
|
||||
| (Q(subevent__isnull=True))
|
||||
).order_by().values('order__event').annotate(
|
||||
c=Count('*')
|
||||
).values('c')
|
||||
|
||||
# Finally, we put all of this together. We force empty subquery aggregates to 0 by using Coalesce()
|
||||
# and decide which subquery to use for this row. In the end, we compute an integer percentage in case
|
||||
# we want to display a progress bar.
|
||||
return qs.annotate(
|
||||
checkin_count=Coalesce(
|
||||
Case(
|
||||
When(include_pending=True, then=Subquery(cqs_paid_and_pending, output_field=models.IntegerField())),
|
||||
default=Subquery(cqs_paid, output_field=models.IntegerField()),
|
||||
output_field=models.IntegerField()
|
||||
),
|
||||
0
|
||||
),
|
||||
position_count=Coalesce(
|
||||
Case(
|
||||
When(all_products=True, include_pending=False,
|
||||
then=Subquery(pqs_all_paid, output_field=models.IntegerField())),
|
||||
When(all_products=True, include_pending=True,
|
||||
then=Subquery(pqs_all_paid_and_pending, output_field=models.IntegerField())),
|
||||
When(all_products=False, include_pending=False,
|
||||
then=Subquery(pqs_limited_paid, output_field=models.IntegerField())),
|
||||
default=Subquery(pqs_limited_paid_and_pending, output_field=models.IntegerField()),
|
||||
output_field=models.IntegerField()
|
||||
),
|
||||
0
|
||||
)
|
||||
).annotate(
|
||||
percent=Case(
|
||||
When(position_count__gt=0, then=F('checkin_count') * 100 / F('position_count')),
|
||||
default=0,
|
||||
output_field=models.IntegerField()
|
||||
)
|
||||
)
|
||||
# This is only kept for backwards-compatibility reasons. This method used to precompute .position_count
|
||||
# and .checkin_count through a huge subquery chain, but was dropped for performance reasons.
|
||||
return qs
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -169,6 +96,7 @@ class Checkin(models.Model):
|
||||
list = models.ForeignKey(
|
||||
'pretixbase.CheckinList', related_name='checkins', on_delete=models.PROTECT,
|
||||
)
|
||||
auto_checked_in = models.BooleanField(default=False)
|
||||
|
||||
objects = ScopedManager(organizer='position__order__event__organizer')
|
||||
|
||||
@@ -182,8 +110,11 @@ class Checkin(models.Model):
|
||||
|
||||
def save(self, **kwargs):
|
||||
self.position.order.touch()
|
||||
self.list.event.cache.delete('checkin_count')
|
||||
self.list.touch()
|
||||
super().save(**kwargs)
|
||||
|
||||
def delete(self, **kwargs):
|
||||
self.position.order.touch()
|
||||
super().delete(**kwargs)
|
||||
self.list.touch()
|
||||
|
||||
@@ -3,6 +3,7 @@ import uuid
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime, time, timedelta
|
||||
from operator import attrgetter
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
@@ -11,7 +12,7 @@ from django.core.files.storage import default_storage
|
||||
from django.core.mail import get_connection
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import Exists, OuterRef, Prefetch, Q, Subquery
|
||||
from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery
|
||||
from django.template.defaultfilters import date as _date
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.functional import cached_property
|
||||
@@ -22,10 +23,11 @@ from i18nfield.fields import I18nCharField, I18nTextField
|
||||
|
||||
from pretix.base.models.base import LoggedModel
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
from pretix.base.validators import EventSlugBlacklistValidator
|
||||
from pretix.base.validators import EventSlugBanlistValidator
|
||||
from pretix.helpers.database import GroupConcat
|
||||
from pretix.helpers.daterange import daterange
|
||||
from pretix.helpers.json import safe_string
|
||||
from pretix.helpers.thumb import get_thumbnail
|
||||
|
||||
from ..settings import settings_hierarkey
|
||||
from .organizer import Organizer, Team
|
||||
@@ -65,7 +67,7 @@ class EventMixin:
|
||||
"SHORT_DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT"
|
||||
)
|
||||
|
||||
def get_date_from_display(self, tz=None, show_times=True) -> str:
|
||||
def get_date_from_display(self, tz=None, show_times=True, short=False) -> str:
|
||||
"""
|
||||
Returns a formatted string containing the start date of the event with respect
|
||||
to the current locale and to the ``show_times`` setting.
|
||||
@@ -73,7 +75,7 @@ class EventMixin:
|
||||
tz = tz or self.timezone
|
||||
return _date(
|
||||
self.date_from.astimezone(tz),
|
||||
"DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT"
|
||||
("SHORT_" if short else "") + ("DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT")
|
||||
)
|
||||
|
||||
def get_time_from_display(self, tz=None) -> str:
|
||||
@@ -86,7 +88,7 @@ class EventMixin:
|
||||
self.date_from.astimezone(tz), "TIME_FORMAT"
|
||||
)
|
||||
|
||||
def get_date_to_display(self, tz=None) -> str:
|
||||
def get_date_to_display(self, tz=None, short=False) -> str:
|
||||
"""
|
||||
Returns a formatted string containing the start date of the event with respect
|
||||
to the current locale and to the ``show_times`` setting. Returns an empty string
|
||||
@@ -97,7 +99,7 @@ class EventMixin:
|
||||
return ""
|
||||
return _date(
|
||||
self.date_to.astimezone(tz),
|
||||
"DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT"
|
||||
("SHORT_" if short else "") + ("DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT")
|
||||
)
|
||||
|
||||
def get_date_range_display(self, tz=None, force_show_end=False) -> str:
|
||||
@@ -145,10 +147,13 @@ class EventMixin:
|
||||
"@context": "http://schema.org",
|
||||
"@type": "Event", "location": {
|
||||
"@type": "Place",
|
||||
"address": str(self.location)
|
||||
"address": str(self.location),
|
||||
},
|
||||
"name": str(self.name)
|
||||
"name": str(self.name),
|
||||
}
|
||||
img = getattr(self, 'event', self).social_image
|
||||
if img:
|
||||
eventdict['image'] = img
|
||||
|
||||
if self.settings.show_times:
|
||||
eventdict["startDate"] = self.date_from.isoformat()
|
||||
@@ -291,7 +296,7 @@ class Event(EventMixin, LoggedModel):
|
||||
regex="^[a-zA-Z0-9.-]+$",
|
||||
message=_("The slug may only contain letters, numbers, dots and dashes."),
|
||||
),
|
||||
EventSlugBlacklistValidator()
|
||||
EventSlugBanlistValidator()
|
||||
],
|
||||
verbose_name=_("Short form"),
|
||||
)
|
||||
@@ -324,6 +329,14 @@ class Event(EventMixin, LoggedModel):
|
||||
max_length=200,
|
||||
verbose_name=_("Location"),
|
||||
)
|
||||
geo_lat = models.FloatField(
|
||||
verbose_name=_("Latitude"),
|
||||
null=True, blank=True,
|
||||
)
|
||||
geo_lon = models.FloatField(
|
||||
verbose_name=_("Longitude"),
|
||||
null=True, blank=True,
|
||||
)
|
||||
plugins = models.TextField(
|
||||
null=False, blank=True,
|
||||
verbose_name=_("Plugins"),
|
||||
@@ -350,10 +363,41 @@ class Event(EventMixin, LoggedModel):
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
|
||||
def set_defaults(self):
|
||||
"""
|
||||
This will be called after event creation, but only if the event was not created by copying an existing one.
|
||||
This way, we can use this to introduce new default settings to pretix that do not affect existing events.
|
||||
"""
|
||||
self.settings.invoice_renderer = 'modern1'
|
||||
self.settings.invoice_include_expire_date = True
|
||||
|
||||
@property
|
||||
def free_seats(self):
|
||||
def social_image(self):
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
|
||||
img = None
|
||||
logo_file = self.settings.get('logo_image', as_type=str, default='')[7:]
|
||||
og_file = self.settings.get('og_image', as_type=str, default='')[7:]
|
||||
if og_file:
|
||||
img = get_thumbnail(og_file, '1200').thumb.url
|
||||
elif logo_file:
|
||||
img = get_thumbnail(logo_file, '5000x120').thumb.url
|
||||
if img:
|
||||
return urljoin(build_absolute_uri(self, 'presale:event.index'), img)
|
||||
|
||||
def free_seats(self, ignore_voucher=None, sales_channel='web'):
|
||||
from .orders import CartPosition, Order, OrderPosition
|
||||
return self.seats.annotate(
|
||||
from .vouchers import Voucher
|
||||
vqs = Voucher.objects.filter(
|
||||
event=self,
|
||||
seat_id=OuterRef('pk'),
|
||||
redeemed__lt=F('max_usages'),
|
||||
).filter(
|
||||
Q(valid_until__isnull=True) | Q(valid_until__gte=now())
|
||||
)
|
||||
if ignore_voucher:
|
||||
vqs = vqs.exclude(pk=ignore_voucher.pk)
|
||||
qs = self.seats.annotate(
|
||||
has_order=Exists(
|
||||
OrderPosition.objects.filter(
|
||||
order__event=self,
|
||||
@@ -367,8 +411,14 @@ class Event(EventMixin, LoggedModel):
|
||||
seat_id=OuterRef('pk'),
|
||||
expires__gte=now()
|
||||
)
|
||||
),
|
||||
has_voucher=Exists(
|
||||
vqs
|
||||
)
|
||||
).filter(has_order=False, has_cart=False, blocked=False)
|
||||
).filter(has_order=False, has_cart=False, has_voucher=False)
|
||||
if sales_channel not in self.settings.seating_allow_blocked_seats_for_channel:
|
||||
qs = qs.filter(blocked=False)
|
||||
return qs
|
||||
|
||||
@property
|
||||
def presale_has_ended(self):
|
||||
@@ -472,6 +522,7 @@ class Event(EventMixin, LoggedModel):
|
||||
self.is_public = other.is_public
|
||||
self.testmode = other.testmode
|
||||
self.save()
|
||||
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
|
||||
|
||||
tax_map = {}
|
||||
for t in other.tax_rules.all():
|
||||
@@ -479,6 +530,7 @@ class Event(EventMixin, LoggedModel):
|
||||
t.pk = None
|
||||
t.event = self
|
||||
t.save()
|
||||
t.log_action('pretix.object.cloned')
|
||||
|
||||
category_map = {}
|
||||
for c in ItemCategory.objects.filter(event=other):
|
||||
@@ -486,6 +538,7 @@ class Event(EventMixin, LoggedModel):
|
||||
c.pk = None
|
||||
c.event = self
|
||||
c.save()
|
||||
c.log_action('pretix.object.cloned')
|
||||
|
||||
item_map = {}
|
||||
variation_map = {}
|
||||
@@ -501,6 +554,7 @@ class Event(EventMixin, LoggedModel):
|
||||
if i.tax_rule_id:
|
||||
i.tax_rule = tax_map[i.tax_rule_id]
|
||||
i.save()
|
||||
i.log_action('pretix.object.cloned')
|
||||
for v in vars:
|
||||
variation_map[v.pk] = v
|
||||
v.pk = None
|
||||
@@ -525,6 +579,7 @@ class Event(EventMixin, LoggedModel):
|
||||
q.cached_availability_time = None
|
||||
q.closed = False
|
||||
q.save()
|
||||
q.log_action('pretix.object.cloned')
|
||||
for i in items:
|
||||
if i.pk in item_map:
|
||||
q.items.add(item_map[i.pk])
|
||||
@@ -540,6 +595,7 @@ class Event(EventMixin, LoggedModel):
|
||||
q.pk = None
|
||||
q.event = self
|
||||
q.save()
|
||||
q.log_action('pretix.object.cloned')
|
||||
|
||||
for i in items:
|
||||
q.items.add(item_map[i.pk])
|
||||
@@ -557,6 +613,7 @@ class Event(EventMixin, LoggedModel):
|
||||
cl.pk = None
|
||||
cl.event = self
|
||||
cl.save()
|
||||
cl.log_action('pretix.object.cloned')
|
||||
for i in items:
|
||||
cl.limit_products.add(item_map[i.pk])
|
||||
|
||||
@@ -608,22 +665,26 @@ class Event(EventMixin, LoggedModel):
|
||||
question_map=question_map
|
||||
)
|
||||
|
||||
def get_payment_providers(self) -> dict:
|
||||
def get_payment_providers(self, cached=False) -> dict:
|
||||
"""
|
||||
Returns a dictionary of initialized payment providers mapped by their identifiers.
|
||||
"""
|
||||
from ..signals import register_payment_providers
|
||||
|
||||
responses = register_payment_providers.send(self)
|
||||
providers = {}
|
||||
for receiver, response in responses:
|
||||
if not isinstance(response, list):
|
||||
response = [response]
|
||||
for p in response:
|
||||
pp = p(self)
|
||||
providers[pp.identifier] = pp
|
||||
if not cached or not hasattr(self, '_cached_payment_providers'):
|
||||
responses = register_payment_providers.send(self)
|
||||
providers = {}
|
||||
for receiver, response in responses:
|
||||
if not isinstance(response, list):
|
||||
response = [response]
|
||||
for p in response:
|
||||
pp = p(self)
|
||||
providers[pp.identifier] = pp
|
||||
|
||||
return OrderedDict(sorted(providers.items(), key=lambda v: str(v[1].verbose_name)))
|
||||
self._cached_payment_providers = OrderedDict(sorted(
|
||||
providers.items(), key=lambda v: (-v[1].priority, str(v[1].verbose_name))
|
||||
))
|
||||
return self._cached_payment_providers
|
||||
|
||||
def get_html_mail_renderer(self):
|
||||
"""
|
||||
@@ -728,7 +789,7 @@ class Event(EventMixin, LoggedModel):
|
||||
def has_payment_provider(self):
|
||||
result = False
|
||||
for provider in self.get_payment_providers().values():
|
||||
if provider.is_enabled and provider.identifier not in ('free', 'boxoffice', 'offsetting'):
|
||||
if provider.is_enabled and provider.identifier not in ('free', 'boxoffice', 'offsetting', 'giftcard'):
|
||||
result = True
|
||||
break
|
||||
return result
|
||||
@@ -824,18 +885,24 @@ class Event(EventMixin, LoggedModel):
|
||||
|
||||
def enable_plugin(self, module, allow_restricted=False):
|
||||
plugins_active = self.get_plugins()
|
||||
from pretix.presale.style import regenerate_css
|
||||
|
||||
if module not in plugins_active:
|
||||
plugins_active.append(module)
|
||||
self.set_active_plugins(plugins_active, allow_restricted=allow_restricted)
|
||||
|
||||
regenerate_css.apply_async(args=(self.pk,))
|
||||
|
||||
def disable_plugin(self, module):
|
||||
plugins_active = self.get_plugins()
|
||||
from pretix.presale.style import regenerate_css
|
||||
|
||||
if module in plugins_active:
|
||||
plugins_active.remove(module)
|
||||
self.set_active_plugins(plugins_active)
|
||||
|
||||
regenerate_css.apply_async(args=(self.pk,))
|
||||
|
||||
@staticmethod
|
||||
def clean_has_subevents(event, has_subevents):
|
||||
if event is not None and event.has_subevents is not None:
|
||||
@@ -921,6 +988,14 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
max_length=200,
|
||||
verbose_name=_("Location"),
|
||||
)
|
||||
geo_lat = models.FloatField(
|
||||
verbose_name=_("Latitude"),
|
||||
null=True, blank=True,
|
||||
)
|
||||
geo_lon = models.FloatField(
|
||||
verbose_name=_("Longitude"),
|
||||
null=True, blank=True
|
||||
)
|
||||
frontpage_text = I18nTextField(
|
||||
null=True, blank=True,
|
||||
verbose_name=_("Frontpage text")
|
||||
@@ -941,10 +1016,20 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
def __str__(self):
|
||||
return '{} - {}'.format(self.name, self.get_date_range_display())
|
||||
|
||||
@property
|
||||
def free_seats(self):
|
||||
def free_seats(self, ignore_voucher=None, sales_channel='web'):
|
||||
from .orders import CartPosition, Order, OrderPosition
|
||||
return self.seats.annotate(
|
||||
from .vouchers import Voucher
|
||||
vqs = Voucher.objects.filter(
|
||||
event_id=self.event_id,
|
||||
subevent=self,
|
||||
seat_id=OuterRef('pk'),
|
||||
redeemed__lt=F('max_usages'),
|
||||
).filter(
|
||||
Q(valid_until__isnull=True) | Q(valid_until__gte=now())
|
||||
)
|
||||
if ignore_voucher:
|
||||
vqs = vqs.exclude(pk=ignore_voucher.pk)
|
||||
qs = self.seats.annotate(
|
||||
has_order=Exists(
|
||||
OrderPosition.objects.filter(
|
||||
order__event_id=self.event_id,
|
||||
@@ -960,8 +1045,14 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
seat_id=OuterRef('pk'),
|
||||
expires__gte=now()
|
||||
)
|
||||
),
|
||||
has_voucher=Exists(
|
||||
vqs
|
||||
)
|
||||
).filter(has_order=False, has_cart=False, blocked=False)
|
||||
).filter(has_order=False, has_cart=False, has_voucher=False)
|
||||
if sales_channel not in self.settings.seating_allow_blocked_seats_for_channel:
|
||||
qs = qs.filter(blocked=False)
|
||||
return qs
|
||||
|
||||
@cached_property
|
||||
def settings(self):
|
||||
|
||||
117
src/pretix/base/models/giftcards.py
Normal file
117
src/pretix/base/models/giftcards.py
Normal file
@@ -0,0 +1,117 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.db.models import Sum
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.banlist import banned
|
||||
from pretix.base.models import LoggedModel
|
||||
|
||||
|
||||
def gen_giftcard_secret(length):
|
||||
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
|
||||
while True:
|
||||
code = get_random_string(length=length, allowed_chars=charset)
|
||||
if not banned(code) and not GiftCard.objects.filter(secret=code).exists():
|
||||
return code
|
||||
|
||||
|
||||
class GiftCardAcceptance(models.Model):
|
||||
issuer = models.ForeignKey(
|
||||
'Organizer',
|
||||
related_name='gift_card_collector_acceptance',
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
collector = models.ForeignKey(
|
||||
'Organizer',
|
||||
related_name='gift_card_issuer_acceptance',
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
|
||||
class GiftCard(LoggedModel):
|
||||
issuer = models.ForeignKey(
|
||||
'Organizer',
|
||||
related_name='issued_gift_cards',
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
issued_in = models.ForeignKey(
|
||||
'OrderPosition',
|
||||
related_name='issued_gift_cards',
|
||||
on_delete=models.PROTECT,
|
||||
null=True, blank=True
|
||||
)
|
||||
issuance = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
)
|
||||
secret = models.CharField(
|
||||
max_length=190,
|
||||
db_index=True,
|
||||
verbose_name=_('Gift card code'),
|
||||
)
|
||||
testmode = models.BooleanField(
|
||||
verbose_name=_('Test mode card'),
|
||||
default=False
|
||||
)
|
||||
CURRENCY_CHOICES = [(c.alpha_3, c.alpha_3 + " - " + c.name) for c in settings.CURRENCIES]
|
||||
currency = models.CharField(max_length=10, choices=CURRENCY_CHOICES)
|
||||
|
||||
def __str__(self):
|
||||
return self.secret
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self.transactions.aggregate(s=Sum('value'))['s'] or Decimal('0.00')
|
||||
|
||||
def accepted_by(self, organizer):
|
||||
return self.issuer == organizer or GiftCardAcceptance.objects.filter(issuer=self.issuer, collector=organizer).exists()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.secret:
|
||||
self.secret = gen_giftcard_secret(self.issuer.settings.giftcard_length)
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
unique_together = (('secret', 'issuer'),)
|
||||
|
||||
|
||||
class GiftCardTransaction(models.Model):
|
||||
card = models.ForeignKey(
|
||||
'GiftCard',
|
||||
related_name='transactions',
|
||||
on_delete=models.PROTECT
|
||||
)
|
||||
datetime = models.DateTimeField(
|
||||
auto_now_add=True
|
||||
)
|
||||
value = models.DecimalField(
|
||||
decimal_places=2,
|
||||
max_digits=10
|
||||
)
|
||||
order = models.ForeignKey(
|
||||
'Order',
|
||||
related_name='gift_card_transactions',
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.PROTECT
|
||||
)
|
||||
payment = models.ForeignKey(
|
||||
'OrderPayment',
|
||||
related_name='gift_card_transactions',
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.PROTECT
|
||||
)
|
||||
refund = models.ForeignKey(
|
||||
'OrderRefund',
|
||||
related_name='gift_card_transactions',
|
||||
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