Compare commits
702 Commits
v3.7.0
...
release/3.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c621c5f3a | ||
|
|
df2998dd33 | ||
|
|
de98206abc | ||
|
|
dd95e807ae | ||
|
|
f08bc6c679 | ||
|
|
a0631ffba5 | ||
|
|
d7a12cc1ee | ||
|
|
2239128971 | ||
|
|
b3fd515652 | ||
|
|
6ee54f6cc1 | ||
|
|
403dc9cf98 | ||
|
|
eb462b1950 | ||
|
|
cb6d50e570 | ||
|
|
32a2a99b7b | ||
|
|
7859985ff0 | ||
|
|
7b7cf76028 | ||
|
|
4fda9b2205 | ||
|
|
ede93f1669 | ||
|
|
4af4dbd9cf | ||
|
|
820f6d52a5 | ||
|
|
8d04974caa | ||
|
|
8021c9e865 | ||
|
|
874ea6750b | ||
|
|
8f2c125435 | ||
|
|
2f21dc8c3c | ||
|
|
05135c779c | ||
|
|
f08f06ddff | ||
|
|
9c28537d1c | ||
|
|
95b1a4a434 | ||
|
|
019801e9dc | ||
|
|
3e97fd87d1 | ||
|
|
8bcc0e4641 | ||
|
|
878becfee9 | ||
|
|
273c34999c | ||
|
|
3f4f9d98de | ||
|
|
d703eeb770 | ||
|
|
b7754d8737 | ||
|
|
ed62ecaccb | ||
|
|
5c3ef3f2b9 | ||
|
|
f3282807e2 | ||
|
|
52944ff3a3 | ||
|
|
23a9018988 | ||
|
|
936c771d5e | ||
|
|
d064a7affa | ||
|
|
4b894eb433 | ||
|
|
6c7ef89779 | ||
|
|
e8f3a66a8e | ||
|
|
d999971249 | ||
|
|
fb701f25f4 | ||
|
|
913596459a | ||
|
|
04e9ea1ae7 | ||
|
|
5dc09019ff | ||
|
|
7c3671b383 | ||
|
|
b91c16538e | ||
|
|
9ba517109d | ||
|
|
5f86fbc21d | ||
|
|
fae35cc56f | ||
|
|
78c5eb4516 | ||
|
|
860f4c36a4 | ||
|
|
311dcfaab0 | ||
|
|
83a6041a32 | ||
|
|
a2f9bb73ad | ||
|
|
fb92c9dd64 | ||
|
|
aa1910fd70 | ||
|
|
f66c266ff7 | ||
|
|
7cc5179e85 | ||
|
|
f633cc3103 | ||
|
|
5e212c83e4 | ||
|
|
2d5768aa20 | ||
|
|
8ea66bc05b | ||
|
|
eba17e22fb | ||
|
|
620c956ef8 | ||
|
|
35debba865 | ||
|
|
a635ea527e | ||
|
|
6e76db40ed | ||
|
|
7956074d8b | ||
|
|
7a3418e32f | ||
|
|
0bfc436970 | ||
|
|
b6fc02255d | ||
|
|
a06f94fde1 | ||
|
|
d5073f416c | ||
|
|
b32ea0dec4 | ||
|
|
932851cf96 | ||
|
|
4513cd7ec3 | ||
|
|
261878b3fe | ||
|
|
31590f7e6c | ||
|
|
ebe7560f14 | ||
|
|
c5b722ebc1 | ||
|
|
5ea961819d | ||
|
|
983d734c6a | ||
|
|
c1bca2f207 | ||
|
|
118f0f55e9 | ||
|
|
d1146add38 | ||
|
|
fc18788cb8 | ||
|
|
a2eb4444b4 | ||
|
|
606d13e303 | ||
|
|
d90fcee5e1 | ||
|
|
e9a4c3845a | ||
|
|
018fac2361 | ||
|
|
41dd71879e | ||
|
|
738e5d07aa | ||
|
|
a22451140b | ||
|
|
6759506838 | ||
|
|
82bb3f3b6e | ||
|
|
cdb8a92a47 | ||
|
|
7597344897 | ||
|
|
c94d384e86 | ||
|
|
b2357b7e29 | ||
|
|
c7d1e5d069 | ||
|
|
754d498938 | ||
|
|
ec7fc05108 | ||
|
|
bbba0df6c4 | ||
|
|
65e87455ec | ||
|
|
e6d09baacc | ||
|
|
fbd38fef58 | ||
|
|
253f944951 | ||
|
|
7f30f753d7 | ||
|
|
8789a42dc1 | ||
|
|
e7740b1735 | ||
|
|
586da71a64 | ||
|
|
68697f0c6a | ||
|
|
a2e1bc9c20 | ||
|
|
89a82d75a9 | ||
|
|
a2414081af | ||
|
|
c812d39b39 | ||
|
|
c6f3fdd8e4 | ||
|
|
30e0f5ebc7 | ||
|
|
f767f2f644 | ||
|
|
750c3c5201 | ||
|
|
7d9220ae3e | ||
|
|
69879bdae0 | ||
|
|
0e245b41ee | ||
|
|
2839ee1ffd | ||
|
|
d72a03c434 | ||
|
|
b6f47f6f4a | ||
|
|
ca2dd0d6b6 | ||
|
|
c4415beb8c | ||
|
|
35c8684cd4 | ||
|
|
9bb5c57792 | ||
|
|
1c8699662d | ||
|
|
9b367cb28b | ||
|
|
896ba5b06b | ||
|
|
8f3dbba859 | ||
|
|
bf5b92c465 | ||
|
|
aef09003d9 | ||
|
|
9d22e833a6 | ||
|
|
1e121c0f75 | ||
|
|
373755a502 | ||
|
|
6f694b60ca | ||
|
|
77f76195c8 | ||
|
|
355dd4463b | ||
|
|
c0c39223aa | ||
|
|
db7f8d9658 | ||
|
|
c15344ced2 | ||
|
|
0f3f15a736 | ||
|
|
478f6e3029 | ||
|
|
4c77e2f16e | ||
|
|
80b6a3d27d | ||
|
|
89e8d3d12f | ||
|
|
baf8a4ae18 | ||
|
|
2cdaf07c46 | ||
|
|
cf76a2e24d | ||
|
|
559b4a8e66 | ||
|
|
59bf11b98d | ||
|
|
5b3551fb60 | ||
|
|
72a5008513 | ||
|
|
c5ace8447d | ||
|
|
28b82841c2 | ||
|
|
fbe10a981b | ||
|
|
21c22aa63a | ||
|
|
fb4116012f | ||
|
|
53fe4a32cd | ||
|
|
ff066898d4 | ||
|
|
cbb848b3fa | ||
|
|
98dfdd8b01 | ||
|
|
0e95a7863f | ||
|
|
0913f5bc18 | ||
|
|
d1eb4c4cce | ||
|
|
4a0a3aff59 | ||
|
|
83908fde45 | ||
|
|
143ac10991 | ||
|
|
413cbec4b9 | ||
|
|
b168516d78 | ||
|
|
d0ccc42aff | ||
|
|
7aa793f4f7 | ||
|
|
1b48b519e3 | ||
|
|
5f502776b1 | ||
|
|
985e1ac9bf | ||
|
|
df1014d62f | ||
|
|
062afc42d3 | ||
|
|
1fb861a117 | ||
|
|
0a2346778d | ||
|
|
605a21a0cf | ||
|
|
1cfec9cc99 | ||
|
|
0a97b0ce67 | ||
|
|
60eee25cd1 | ||
|
|
779ec6c3f6 | ||
|
|
988eb85c05 | ||
|
|
556cb7c46d | ||
|
|
86e3c30633 | ||
|
|
6276f213b9 | ||
|
|
524f6c9975 | ||
|
|
125a14c8e9 | ||
|
|
c7f0e6f652 | ||
|
|
1e58ef6f9e | ||
|
|
41127ce978 | ||
|
|
99b42d201e | ||
|
|
265da6c746 | ||
|
|
d58c8559fc | ||
|
|
b5dca762f0 | ||
|
|
a310c33497 | ||
|
|
fc5c3caf66 | ||
|
|
bff1041878 | ||
|
|
2626259492 | ||
|
|
18415c62bb | ||
|
|
85f546a3a6 | ||
|
|
829b0041fc | ||
|
|
4968a6d995 | ||
|
|
033deb7cf2 | ||
|
|
e23e88f5c3 | ||
|
|
c3745e792b | ||
|
|
735d4564f8 | ||
|
|
b305ac012c | ||
|
|
7bd9a01f5e | ||
|
|
8bebea61f1 | ||
|
|
6714ab24ee | ||
|
|
a54dbc0110 | ||
|
|
19fa2fb016 | ||
|
|
12b5d6663e | ||
|
|
ca4db5f628 | ||
|
|
b6a343a623 | ||
|
|
dc451cdeea | ||
|
|
6732d13439 | ||
|
|
5bf67ba613 | ||
|
|
8885b50972 | ||
|
|
940566ab93 | ||
|
|
e7b9c49620 | ||
|
|
c8ef825de5 | ||
|
|
5b25a68599 | ||
|
|
e26a07d44d | ||
|
|
6842127802 | ||
|
|
3c5948d2e0 | ||
|
|
ed3542e219 | ||
|
|
e439b20618 | ||
|
|
5c1fe6f68c | ||
|
|
e4e91523a0 | ||
|
|
00827700de | ||
|
|
0a87225a9a | ||
|
|
9371d221bf | ||
|
|
08a3c846b6 | ||
|
|
1c84de9ab2 | ||
|
|
980f4012bc | ||
|
|
591d70eabe | ||
|
|
2c4609604d | ||
|
|
30c2b8b03f | ||
|
|
a685af6433 | ||
|
|
f179a220bc | ||
|
|
b61893e3b1 | ||
|
|
d3282a1acb | ||
|
|
c585946e72 | ||
|
|
b6245b97ca | ||
|
|
51720c3afe | ||
|
|
4746b8e456 | ||
|
|
b2401f7641 | ||
|
|
b4cd11ef94 | ||
|
|
33682e1b38 | ||
|
|
e10e3300ba | ||
|
|
4f0eadfd6e | ||
|
|
0f9ec2ce7d | ||
|
|
93e3cf1d99 | ||
|
|
3affaa8c85 | ||
|
|
fddf134755 | ||
|
|
568398e4e7 | ||
|
|
fc08531639 | ||
|
|
bd25ce238d | ||
|
|
c88ce8a9a8 | ||
|
|
004403e2c8 | ||
|
|
0cde5288ac | ||
|
|
e585da2901 | ||
|
|
f369fca091 | ||
|
|
e0e638ac8c | ||
|
|
4abe906511 | ||
|
|
855a47776f | ||
|
|
c63c499a95 | ||
|
|
f06a23ae95 | ||
|
|
2b68a22aad | ||
|
|
86d42cc524 | ||
|
|
6e8040ac9d | ||
|
|
c742c9979b | ||
|
|
15f3880fcd | ||
|
|
6bf5d8cb5e | ||
|
|
27d772f52f | ||
|
|
6e9d921af6 | ||
|
|
1c9a1b5e02 | ||
|
|
640c05729b | ||
|
|
fc9e5166da | ||
|
|
b1eb5bb3df | ||
|
|
f690d74be7 | ||
|
|
c52fdc95a7 | ||
|
|
039ca36233 | ||
|
|
9920a47580 | ||
|
|
74acfbe2fd | ||
|
|
b0b8f32cb9 | ||
|
|
066cf510e3 | ||
|
|
aca963d960 | ||
|
|
582c7b50f7 | ||
|
|
d6b185193e | ||
|
|
fb92d500be | ||
|
|
27ed9ae4fd | ||
|
|
06fbf56c04 | ||
|
|
d843fc1545 | ||
|
|
cf2af3c94d | ||
|
|
5f50aa95eb | ||
|
|
626e332886 | ||
|
|
507e1a5b83 | ||
|
|
0d1aa2f96e | ||
|
|
df6038e39b | ||
|
|
922f12f55e | ||
|
|
fdea190d72 | ||
|
|
34fe34d50a | ||
|
|
3412c1d2a9 | ||
|
|
6f58f30d92 | ||
|
|
19b5610503 | ||
|
|
55670b92a8 | ||
|
|
e5cc15ffac | ||
|
|
249e6978ea | ||
|
|
a223d57124 | ||
|
|
2a5c24482e | ||
|
|
868292f9b3 | ||
|
|
5c24fd966a | ||
|
|
61490a9ee8 | ||
|
|
3c3333c485 | ||
|
|
4c19002be6 | ||
|
|
2c49eaeef8 | ||
|
|
481e29c3b2 | ||
|
|
0aebde62eb | ||
|
|
49e44f68ba | ||
|
|
84dc9f241d | ||
|
|
07b05f4a44 | ||
|
|
d1c1aed1f2 | ||
|
|
de9f7248cc | ||
|
|
0d45706608 | ||
|
|
016dd88e8b | ||
|
|
6362c27cba | ||
|
|
7396f29b82 | ||
|
|
ff9f6b6a36 | ||
|
|
7359b5543d | ||
|
|
74a0cafa0f | ||
|
|
16472e915d | ||
|
|
ec6844f900 | ||
|
|
e6455f8204 | ||
|
|
4c48fcd861 | ||
|
|
adfd7834fb | ||
|
|
a4b8315487 | ||
|
|
8594fecad4 | ||
|
|
61979f0c40 | ||
|
|
df5f8a340b | ||
|
|
651e797264 | ||
|
|
d40010fab6 | ||
|
|
89c3d59e6d | ||
|
|
b30569a941 | ||
|
|
fe230fe56d | ||
|
|
0b20d3f6f8 | ||
|
|
e895c13b28 | ||
|
|
5cc0bd5d36 | ||
|
|
569379e508 | ||
|
|
d975a68641 | ||
|
|
c992de341f | ||
|
|
11cc27dbd6 | ||
|
|
90e70eae25 | ||
|
|
9eacd38ec7 | ||
|
|
d1c96aa77c | ||
|
|
99e02bde36 | ||
|
|
e7da2aec53 | ||
|
|
d0c6f0f0e9 | ||
|
|
3ae148956f | ||
|
|
e0436039d2 | ||
|
|
29510b8617 | ||
|
|
2d88da3a67 | ||
|
|
fbb88602d4 | ||
|
|
7d7820a4ee | ||
|
|
8001063347 | ||
|
|
0c3a200355 | ||
|
|
ebb1cc1be7 | ||
|
|
45f120b0c3 | ||
|
|
bae0e45d00 | ||
|
|
057fd95706 | ||
|
|
597d4aa206 | ||
|
|
7f9b245eb5 | ||
|
|
42490c6dec | ||
|
|
60c0b7da12 | ||
|
|
7d41922274 | ||
|
|
fc7fbf31c5 | ||
|
|
da5433325c | ||
|
|
939a38d53b | ||
|
|
a57280004e | ||
|
|
ce896cec8f | ||
|
|
effc9723f1 | ||
|
|
cd5f6b66a1 | ||
|
|
0d35064d21 | ||
|
|
314ce5467f | ||
|
|
d97ef380a4 | ||
|
|
d3498c419d | ||
|
|
0d90ae9d53 | ||
|
|
796d6f92ef | ||
|
|
73faecaa39 | ||
|
|
e04e8e8ca6 | ||
|
|
89a9148073 | ||
|
|
205faafc57 | ||
|
|
db8ed065df | ||
|
|
c84ae364ba | ||
|
|
55a57dced2 | ||
|
|
62db01e353 | ||
|
|
e435e7140c | ||
|
|
f4d38965cc | ||
|
|
5d080a4ab2 | ||
|
|
7895729c38 | ||
|
|
3b003d0baa | ||
|
|
69a4a16793 | ||
|
|
cf3412d54d | ||
|
|
03bcfc7c5a | ||
|
|
a6c1c85591 | ||
|
|
1c8468c21b | ||
|
|
dcc54a0204 | ||
|
|
435b32a6b8 | ||
|
|
9e93560f7c | ||
|
|
e12e7a5dd3 | ||
|
|
e822ba5430 | ||
|
|
b1ee355663 | ||
|
|
ca40ddc39b | ||
|
|
1b85911a76 | ||
|
|
27b56b5aea | ||
|
|
5147508ef9 | ||
|
|
39ae22b8b2 | ||
|
|
65b612eabd | ||
|
|
48d2f98815 | ||
|
|
5dd5ff8a7c | ||
|
|
059bdc629e | ||
|
|
36532e5bbb | ||
|
|
ed318791fb | ||
|
|
4d61ec4d86 | ||
|
|
cc2f8ac3da | ||
|
|
770293e8ec | ||
|
|
0ab11a8134 | ||
|
|
7e56100c21 | ||
|
|
6fecb42e26 | ||
|
|
4c063272d4 | ||
|
|
029f113b06 | ||
|
|
7827b026fd | ||
|
|
bd4ccf4507 | ||
|
|
f95d2972d3 | ||
|
|
e642098a5f | ||
|
|
bf11dea798 | ||
|
|
9e47f37097 | ||
|
|
149c25e609 | ||
|
|
fccd7a5499 | ||
|
|
d16acf0bfd | ||
|
|
71c915f5f7 | ||
|
|
f1c29daa42 | ||
|
|
fec31823ee | ||
|
|
c056db46b6 | ||
|
|
640b9c876d | ||
|
|
25ad2ea475 | ||
|
|
d2dfbca913 | ||
|
|
7287a16711 | ||
|
|
c6e969b7fe | ||
|
|
c03c278ecd | ||
|
|
140f041cc1 | ||
|
|
de9c450648 | ||
|
|
6a4c81ff3c | ||
|
|
e117545b3f | ||
|
|
feb7f419d3 | ||
|
|
ea04c85486 | ||
|
|
094450564a | ||
|
|
66d3c4516f | ||
|
|
4b01c42f31 | ||
|
|
c0972ef39d | ||
|
|
4d1cbd8248 | ||
|
|
07eb71400a | ||
|
|
f6f72d4b7f | ||
|
|
08d89acffd | ||
|
|
28831d674f | ||
|
|
65f1c20af0 | ||
|
|
5b2b3d71ec | ||
|
|
15bdcb9973 | ||
|
|
1a1afcddc6 | ||
|
|
219c82b028 | ||
|
|
19e2158f19 | ||
|
|
23055cfe09 | ||
|
|
790f248388 | ||
|
|
ef8d9bdb93 | ||
|
|
0d9b534cee | ||
|
|
b1b303e598 | ||
|
|
b4f3a665af | ||
|
|
f71153db4a | ||
|
|
1643005a4b | ||
|
|
0ee2f674bc | ||
|
|
d142a09ad3 | ||
|
|
68c13aaa3c | ||
|
|
556c77a54b | ||
|
|
5b689e5fd2 | ||
|
|
57f5fbc131 | ||
|
|
7e80ec93d5 | ||
|
|
ee1928aeed | ||
|
|
5c62f2b852 | ||
|
|
ad8be705fd | ||
|
|
4635d9b5f7 | ||
|
|
ace32f3fc4 | ||
|
|
88a235da30 | ||
|
|
4ec24fc884 | ||
|
|
e2b9fe8e71 | ||
|
|
6f1c29581d | ||
|
|
9bf0a6dfcd | ||
|
|
3e6e324027 | ||
|
|
912e6816df | ||
|
|
d00aa2f3ad | ||
|
|
41fecf366c | ||
|
|
f7f751bda7 | ||
|
|
7b68614de3 | ||
|
|
c0fc259d17 | ||
|
|
d6cad265fc | ||
|
|
bb1882aca5 | ||
|
|
b887c2c43d | ||
|
|
c05bf779bd | ||
|
|
68ff001bb6 | ||
|
|
3c6fdd15af | ||
|
|
16957eec33 | ||
|
|
1bcf1ec26a | ||
|
|
d224b5387d | ||
|
|
27d6e49c4a | ||
|
|
50bb66a32b | ||
|
|
207149f681 | ||
|
|
e0ed25a1d3 | ||
|
|
c0c177d755 | ||
|
|
f2844ac686 | ||
|
|
d9fd4b33a0 | ||
|
|
d2415f46df | ||
|
|
ad7c745465 | ||
|
|
b260cca412 | ||
|
|
82c84a0fdc | ||
|
|
d703c4de7a | ||
|
|
ff5fbf1c15 | ||
|
|
41ff4bca7f | ||
|
|
37f45de8a5 | ||
|
|
71f01c17bd | ||
|
|
43b1df572f | ||
|
|
ecda905ceb | ||
|
|
4e59b02bb1 | ||
|
|
234bf093ff | ||
|
|
ae9bd1b6ba | ||
|
|
b84b51250f | ||
|
|
d0dd2116ca | ||
|
|
945218035e | ||
|
|
89f8436e9a | ||
|
|
4992b17966 | ||
|
|
f7d057f9fa | ||
|
|
96ba32b1bb | ||
|
|
02792e3ae8 | ||
|
|
a87fe3ef41 | ||
|
|
bb673e0cc9 | ||
|
|
0b02bcea8b | ||
|
|
1d8668aaf1 | ||
|
|
0783add3b9 | ||
|
|
2030aaf12e | ||
|
|
7ce4c30922 | ||
|
|
f1e772c829 | ||
|
|
31d1fc31cd | ||
|
|
597211d83a | ||
|
|
17679d4304 | ||
|
|
0fb70c78a9 | ||
|
|
e254e90e49 | ||
|
|
9c6e5f025d | ||
|
|
3c86532218 | ||
|
|
3834ae566f | ||
|
|
6766b2b19e | ||
|
|
b6d2f67c7c | ||
|
|
e70f593a94 | ||
|
|
ed5726fc0c | ||
|
|
5400d26c60 | ||
|
|
0bb6104532 | ||
|
|
16aa403735 | ||
|
|
1c279a92a7 | ||
|
|
35985dcb11 | ||
|
|
b0dcbe31fa | ||
|
|
b3c3ee3b22 | ||
|
|
aab340fd87 | ||
|
|
1871324ef4 | ||
|
|
d799d560b7 | ||
|
|
01e2851a76 | ||
|
|
ef2a4244ed | ||
|
|
55539dc8e5 | ||
|
|
ef303bfcc4 | ||
|
|
fff9ac04a9 | ||
|
|
76d27fbfaa | ||
|
|
2b1123b487 | ||
|
|
3607d8706d | ||
|
|
31fdf8721b | ||
|
|
128a1f349a | ||
|
|
7d432f0639 | ||
|
|
1ffc799c4d | ||
|
|
25dd8f2e2f | ||
|
|
b121596e4b | ||
|
|
cf835df62e | ||
|
|
7a3b7d4f02 | ||
|
|
b151d8f455 | ||
|
|
06de74d877 | ||
|
|
2ae9e3e0d9 | ||
|
|
0c0fe58bbf | ||
|
|
7b1e1a48ef | ||
|
|
c7dd50de0d | ||
|
|
a1caa65776 | ||
|
|
260973345d | ||
|
|
2c9b2620ea | ||
|
|
909c80e710 | ||
|
|
5a218ae6a9 | ||
|
|
b498d45621 | ||
|
|
b02196434b | ||
|
|
c0edce7760 | ||
|
|
cc46d55f5e | ||
|
|
ea8abb8dab | ||
|
|
f765d094b4 | ||
|
|
86f222870d | ||
|
|
19b5270d76 | ||
|
|
db76b9b0ef | ||
|
|
d23e53873f | ||
|
|
c116a4b998 | ||
|
|
2471d4bca5 | ||
|
|
8e04dbdcca | ||
|
|
0928358396 | ||
|
|
23f783c15c | ||
|
|
edae96c84f | ||
|
|
242ebdfae9 | ||
|
|
0ee502abec | ||
|
|
29cb1e93d8 | ||
|
|
c89242855c | ||
|
|
61a1368ed2 | ||
|
|
ac3e00fa03 | ||
|
|
d9d0f7b6f3 | ||
|
|
ad5e2df3be | ||
|
|
ec34561815 | ||
|
|
e1540b1648 | ||
|
|
a6b265455d | ||
|
|
8a6334bd86 | ||
|
|
173a23722a | ||
|
|
ab8eb2a34d | ||
|
|
30dcda616b | ||
|
|
3eafec9d6e | ||
|
|
a5910016fd | ||
|
|
0a49b93b26 | ||
|
|
7449bea836 | ||
|
|
0fc4478332 | ||
|
|
0df4a6e7ed | ||
|
|
a37cd380c8 | ||
|
|
11b2bd8887 | ||
|
|
8986db0975 | ||
|
|
2921611cb1 | ||
|
|
785fb29513 | ||
|
|
81c3d7fa17 | ||
|
|
8ff963698d | ||
|
|
6da63e0169 | ||
|
|
f84903ae27 | ||
|
|
a0a7859b33 | ||
|
|
af23d6e4bf | ||
|
|
7e9c9beace | ||
|
|
ac2fc2de5c | ||
|
|
45e548873e | ||
|
|
f484eb65df | ||
|
|
027a785ab5 | ||
|
|
25b80cbb57 | ||
|
|
589fa0f9de | ||
|
|
6d2989d15a | ||
|
|
5bb27b29ae | ||
|
|
d17f8a71e6 | ||
|
|
b664cc712a | ||
|
|
d61e8a9204 | ||
|
|
f00012a63e | ||
|
|
bd238f76ce | ||
|
|
703ae97820 | ||
|
|
1a60c5ea64 | ||
|
|
1d3ac5f02f | ||
|
|
8d23d75dfd | ||
|
|
9a32668ee1 | ||
|
|
ca0407a133 | ||
|
|
1de77b0784 | ||
|
|
d0907d3dcf | ||
|
|
81cc4bd768 | ||
|
|
262639e063 | ||
|
|
dedd93fb89 | ||
|
|
45f94aee03 | ||
|
|
d36e7d033f | ||
|
|
b94bd277bf | ||
|
|
e5095185d9 | ||
|
|
d76ce47597 | ||
|
|
58717850c2 | ||
|
|
29d52d4fe5 | ||
|
|
34c9c40ddc | ||
|
|
39d05a6c40 | ||
|
|
b664222c62 | ||
|
|
1ee48a10b5 | ||
|
|
2431a8b767 | ||
|
|
af84354e51 | ||
|
|
b04de880fc |
42
.github/workflows/docs.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- 'src/pretix/locale/**'
|
||||
- 'src/pretix/static/**'
|
||||
- 'src/tests/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- 'src/pretix/locale/**'
|
||||
- 'src/pretix/static/**'
|
||||
- 'src/tests/**'
|
||||
|
||||
jobs:
|
||||
spelling:
|
||||
name: Spellcheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install system packages
|
||||
run: sudo apt update && sudo apt install enchant hunspell aspell-en
|
||||
- name: Install Dependencies
|
||||
run: pip3 install --no-use-pep517 -Ur doc/requirements.txt
|
||||
- name: Spellcheck docs
|
||||
run: make spelling
|
||||
working-directory: ./doc
|
||||
- name:
|
||||
run: '[ ! -s _build/spelling/output.txt ]'
|
||||
working-directory: ./doc
|
||||
62
.github/workflows/strings.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
name: Strings
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'doc/**'
|
||||
- 'src/pretix/locale/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'doc/**'
|
||||
- 'src/pretix/locale/**'
|
||||
|
||||
jobs:
|
||||
compile:
|
||||
runs-on: ubuntu-latest
|
||||
name: Check gettext syntax
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install system packages
|
||||
run: sudo apt update && sudo apt install gettext
|
||||
- name: Install Dependencies
|
||||
run: pip3 install --no-use-pep517 -Ur src/requirements.txt
|
||||
- name: Compile messages
|
||||
run: python manage.py compilemessages
|
||||
working-directory: ./src
|
||||
- name: Compile jsi18n
|
||||
run: python manage.py compilejsi18n
|
||||
working-directory: ./src
|
||||
spelling:
|
||||
runs-on: ubuntu-latest
|
||||
name: Spellcheck
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install system packages
|
||||
run: sudo apt update && sudo apt install enchant hunspell hunspell-de-de aspell-en aspell-de
|
||||
- name: Install Dependencies
|
||||
run: pip3 install --no-use-pep517 -Ur src/requirements/dev.txt
|
||||
- name: Spellcheck translations
|
||||
run: potypo
|
||||
working-directory: ./src
|
||||
55
.github/workflows/style.yml
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
name: Code Style
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- 'src/pretix/locale/**'
|
||||
- 'src/pretix/static/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- 'src/pretix/locale/**'
|
||||
- 'src/pretix/static/**'
|
||||
|
||||
jobs:
|
||||
isort:
|
||||
name: isort
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install Dependencies
|
||||
run: pip3 install --no-use-pep517 -Ur src/requirements/dev.txt
|
||||
- name: Run isort
|
||||
run: isort -c .
|
||||
working-directory: ./src
|
||||
flake:
|
||||
name: flake8
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install Dependencies
|
||||
run: pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt
|
||||
- name: Run flake8
|
||||
run: flake8 .
|
||||
working-directory: ./src
|
||||
75
.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- 'doc/**'
|
||||
- 'src/pretix/locale/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- 'doc/**'
|
||||
- 'src/pretix/locale/**'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
name: Tests
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.6, 3.7, 3.8]
|
||||
database: [sqlite, postgres, mysql]
|
||||
exclude:
|
||||
- database: mysql
|
||||
python-version: 3.7
|
||||
- database: sqlite
|
||||
python-version: 3.7
|
||||
- database: mysql
|
||||
python-version: 3.6
|
||||
- database: sqlite
|
||||
python-version: 3.6
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: getong/mariadb-action@v1.1
|
||||
with:
|
||||
mariadb version: '10.4'
|
||||
mysql database: 'pretix'
|
||||
mysql root password: ''
|
||||
if: matrix.database == 'mysql'
|
||||
- uses: harmon758/postgresql-action@v1
|
||||
with:
|
||||
postgresql version: '11'
|
||||
postgresql db: 'pretix'
|
||||
postgresql user: 'postgres'
|
||||
postgresql password: 'postgres'
|
||||
if: matrix.database == 'postgres'
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install system dependencies
|
||||
run: sudo apt update && sudo apt install gettext mysql-client
|
||||
- name: Install Python dependencies
|
||||
run: pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt mysqlclient psycopg2-binary
|
||||
- name: Run checks
|
||||
run: python manage.py check
|
||||
working-directory: ./src
|
||||
- name: Compile
|
||||
working-directory: ./src
|
||||
run: make all compress
|
||||
- name: Run tests
|
||||
working-directory: ./src
|
||||
run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test -n 3 -p no:sugar --cov=./ --cov-report=xml --reruns 3 tests --maxfail=100
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
file: src/coverage.xml
|
||||
fail_ci_if_error: true
|
||||
if: matrix.database == 'postgres' && matrix.python-version == '3.8'
|
||||
@@ -5,7 +5,11 @@ tests:
|
||||
- virtualenv env
|
||||
- source env/bin/activate
|
||||
- pip install -U pip wheel setuptools
|
||||
- XDG_CACHE_HOME=/cache bash .travis.sh tests
|
||||
- XDG_CACHE_HOME=/cache pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt
|
||||
- cd src
|
||||
- python manage.py check
|
||||
- make all compress
|
||||
- py.test --reruns 3 -n 3 tests
|
||||
tags:
|
||||
- python3
|
||||
except:
|
||||
@@ -16,15 +20,17 @@ pypi:
|
||||
- cp /keys/.pypirc ~/.pypirc
|
||||
- virtualenv env
|
||||
- source env/bin/activate
|
||||
- pip install -U pip wheel setuptools
|
||||
- pip install -U pip wheel setuptools check-manifest twine
|
||||
- XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements.txt -r src/requirements/dev.txt
|
||||
- cd src
|
||||
- python setup.py sdist
|
||||
- pip install dist/pretix-*.tar.gz
|
||||
- python -m pretix migrate
|
||||
- python -m pretix check
|
||||
- python setup.py sdist upload
|
||||
- python setup.py bdist_wheel upload
|
||||
- check-manifest
|
||||
- python setup.py sdist bdist_wheel
|
||||
- twine check dist/*
|
||||
- twine upload dist/*
|
||||
tags:
|
||||
- python3
|
||||
only:
|
||||
|
||||
68
.travis.sh
@@ -1,68 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
set -x
|
||||
|
||||
echo "Executing job $1"
|
||||
|
||||
if [ "$PRETIX_CONFIG_FILE" == "tests/travis_mysql.cfg" ]; then
|
||||
mysql -u root -e 'CREATE DATABASE pretix DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;'
|
||||
pip3 install -Ur src/requirements/mysql.txt
|
||||
fi
|
||||
|
||||
if [ "$PRETIX_CONFIG_FILE" == "tests/travis_postgres.cfg" ]; then
|
||||
psql -c 'create database travis_ci_test;' -U postgres
|
||||
fi
|
||||
|
||||
if [ "$1" == "style" ]; then
|
||||
XDG_CACHE_HOME=/cache pip3 install --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 --no-use-pep517 -Ur doc/requirements.txt
|
||||
cd doc
|
||||
make doctest
|
||||
fi
|
||||
if [ "$1" == "doc-spelling" ]; then
|
||||
XDG_CACHE_HOME=/cache pip3 install --no-use-pep517 -Ur doc/requirements.txt
|
||||
cd doc
|
||||
make spelling
|
||||
if [ -s _build/spelling/output.txt ]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
if [ "$1" == "translation-spelling" ]; then
|
||||
XDG_CACHE_HOME=/cache pip3 install --no-use-pep517 -Ur src/requirements/dev.txt
|
||||
cd src
|
||||
potypo
|
||||
fi
|
||||
if [ "$1" == "tests" ]; then
|
||||
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 --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 --no-use-pep517 -Ur src/requirements/dev.txt
|
||||
cd src
|
||||
python setup.py develop
|
||||
make all compress
|
||||
|
||||
pushd ~
|
||||
git clone --depth 1 https://github.com/pretix/pretix-cartshare.git
|
||||
cd pretix-cartshare
|
||||
python setup.py develop
|
||||
make
|
||||
py.test --reruns 5 tests
|
||||
popd
|
||||
|
||||
fi
|
||||
45
.travis.yml
@@ -1,45 +0,0 @@
|
||||
language: python
|
||||
dist: xenial
|
||||
sudo: false
|
||||
install:
|
||||
- pip install -U pip wheel setuptools
|
||||
script:
|
||||
- bash .travis.sh $JOB
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/pip
|
||||
services:
|
||||
- mysql
|
||||
- postgresql
|
||||
matrix:
|
||||
include:
|
||||
- python: 3.7
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
|
||||
- python: 3.7
|
||||
env: JOB=tests-cov PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||
- python: 3.7
|
||||
env: JOB=style
|
||||
- python: 3.7
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
|
||||
- python: 3.7
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||
- python: 3.5
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||
- python: 3.7
|
||||
env: JOB=doc-spelling
|
||||
- python: 3.7
|
||||
env: JOB=translation-spelling
|
||||
addons:
|
||||
postgresql: "9.4"
|
||||
mariadb: '10.3'
|
||||
apt:
|
||||
packages:
|
||||
- enchant
|
||||
- myspell-de-de
|
||||
- aspell-en
|
||||
- sqlite3
|
||||
sources:
|
||||
- travis-ci/sqlite3
|
||||
branches:
|
||||
except:
|
||||
- /^weblate-.*/
|
||||
@@ -29,7 +29,7 @@ RUN apt-get update && \
|
||||
mkdir /etc/pretix && \
|
||||
mkdir /data && \
|
||||
useradd -ms /bin/bash -d /pretix -u 15371 pretixuser && \
|
||||
echo 'pretixuser ALL=(ALL) NOPASSWD: /usr/bin/supervisord' >> /etc/sudoers && \
|
||||
echo 'pretixuser ALL=(ALL) NOPASSWD:SETENV: /usr/bin/supervisord' >> /etc/sudoers && \
|
||||
mkdir /static
|
||||
|
||||
ENV LC_ALL=C.UTF-8 \
|
||||
|
||||
24
README.rst
@@ -4,11 +4,10 @@ pretix
|
||||
.. image:: https://img.shields.io/pypi/v/pretix.svg
|
||||
:target: https://pypi.python.org/pypi/pretix
|
||||
|
||||
.. image:: https://readthedocs.org/projects/pretix/badge/?version=latest
|
||||
.. image:: https://github.com/pretix/pretix/workflows/Documentation/badge.svg
|
||||
:target: https://docs.pretix.eu/en/latest/
|
||||
|
||||
.. image:: https://travis-ci.org/pretix/pretix.svg?branch=master
|
||||
:target: https://travis-ci.org/pretix/pretix
|
||||
.. image:: https://github.com/pretix/pretix/workflows/Tests/badge.svg
|
||||
|
||||
.. image:: https://codecov.io/gh/pretix/pretix/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/pretix/pretix
|
||||
@@ -20,9 +19,8 @@ Reinventing ticket presales, one ticket at a time.
|
||||
Project status & release cycle
|
||||
------------------------------
|
||||
|
||||
While there is always a lot to do and improve on, pretix by now has been in use for more than a dozen
|
||||
conferences that sold over ten thousand tickets combined without major problems. We therefore think of
|
||||
pretix as being stable and ready to use.
|
||||
While there is always a lot to do and improve on, pretix by now has been in use for thousands of events
|
||||
conferences that sold millions of tickets combined. We therefore think of pretix as being stable and ready to use.
|
||||
|
||||
If you want to use or extend pretix, we strongly recommend to follow our `blog`_. We will announce all
|
||||
releases there. You can always find the latest stable version on PyPI or in the ``release/X.Y`` branch of
|
||||
@@ -31,9 +29,13 @@ the sense that it does not break your data, but its APIs might change without p
|
||||
|
||||
To get started using pretix on your own server, look at the `installation guide`_ in our documentation.
|
||||
|
||||
This project is 100 percent free and open source software. If you are interested in commercial support,
|
||||
hosting services or supporting this project financially, please go to `pretix.eu`_ or contact us at
|
||||
support@pretix.eu.
|
||||
Support
|
||||
-------
|
||||
|
||||
This project is 100 percent free and open source software. You are welcome to ask questions in the GitHub
|
||||
repository. Private support via email or phone is only offered to customers of our pretix Hosted or pretix
|
||||
Enterprise offerings. If you are interested in commercial support, hosting services or supporting this project
|
||||
financially, please go to `pretix.eu`_ or contact us at support@pretix.eu.
|
||||
|
||||
Contributing
|
||||
------------
|
||||
@@ -53,8 +55,8 @@ License
|
||||
The code in this repository is published under the terms of the Apache License.
|
||||
See the LICENSE file for the complete license text.
|
||||
|
||||
This project is maintained by Raphael Michel <mail@raphaelmichel.de>. See the
|
||||
AUTHORS file for a list of all the awesome folks who contributed to this project.
|
||||
This project is maintained by Raphael Michel. See the AUTHORS file for a list of all
|
||||
the awesome folks who contributed to this project.
|
||||
|
||||
.. _installation guide: https://docs.pretix.eu/en/latest/admin/installation/index.html
|
||||
.. _developer documentation: https://docs.pretix.eu/en/latest/development/index.html
|
||||
|
||||
@@ -19,7 +19,7 @@ fi
|
||||
python3 -m pretix migrate --noinput
|
||||
|
||||
if [ "$1" == "all" ]; then
|
||||
exec sudo /usr/bin/supervisord -n -c /etc/supervisord.conf
|
||||
exec sudo -E /usr/bin/supervisord -n -c /etc/supervisord.conf
|
||||
fi
|
||||
|
||||
if [ "$1" == "webworker" ]; then
|
||||
|
||||
@@ -92,9 +92,11 @@ Example::
|
||||
|
||||
``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.
|
||||
proxy that actively removes and re-adds the header to make sure the correct value is set.
|
||||
Defaults to ``off``.
|
||||
|
||||
``csp_log``
|
||||
Log violations of the Content Security Policy (CSP). Defaults to ``on``.
|
||||
|
||||
Locale settings
|
||||
---------------
|
||||
@@ -337,6 +339,15 @@ application. If you want to use sentry, you need to set a DSN in the configurati
|
||||
You will be given this value by your sentry installation.
|
||||
|
||||
|
||||
Caching
|
||||
-------
|
||||
|
||||
You can adjust some caching settings to control how much storage pretix uses::
|
||||
|
||||
[cache]
|
||||
tickets=48 ; Number of hours tickets (PDF, passbook, …) are cached
|
||||
|
||||
|
||||
Secret length
|
||||
-------------
|
||||
|
||||
|
||||
@@ -12,3 +12,4 @@ This documentation is for everyone who wants to install pretix on a server.
|
||||
config
|
||||
maintainance
|
||||
scaling
|
||||
indexes
|
||||
|
||||
73
doc/admin/indexes.rst
Normal file
@@ -0,0 +1,73 @@
|
||||
Additional database indices
|
||||
===========================
|
||||
|
||||
If you have a large pretix database, some features such as search for orders or events might turn pretty slow.
|
||||
For PostgreSQL, we have compiled a list of additional database indexes that you can add to speed things up.
|
||||
Just like any index, they in turn make write operations insignificantly slower and cause the database to use
|
||||
more disk space.
|
||||
|
||||
The indexes aren't automatically created by pretix since Django does not allow us to do so only on PostgreSQL
|
||||
(and they won't work on other databases). Also, they're really not necessary if you're not having tens of
|
||||
thousands of records in your database.
|
||||
|
||||
However, this also means they won't automatically adapt if some of the referred fields change in future updates of pretix
|
||||
and you might need to re-check this page and change them manually.
|
||||
|
||||
Here is the currently recommended set of commands::
|
||||
|
||||
CREATE EXTENSION pg_trgm;
|
||||
|
||||
CREATE INDEX CONCURRENTLY pretix_addidx_event_slug
|
||||
ON pretixbase_event
|
||||
USING gin (upper("slug") gin_trgm_ops);
|
||||
CREATE INDEX CONCURRENTLY pretix_addidx_event_name
|
||||
ON pretixbase_event
|
||||
USING gin (upper("name") gin_trgm_ops);
|
||||
CREATE INDEX CONCURRENTLY pretix_addidx_order_code
|
||||
ON pretixbase_order
|
||||
USING gin (upper("code") gin_trgm_ops);
|
||||
CREATE INDEX CONCURRENTLY pretix_addidx_voucher_code
|
||||
ON pretixbase_voucher
|
||||
USING gin (upper("code") gin_trgm_ops);
|
||||
CREATE INDEX CONCURRENTLY pretix_addidx_invoice_nu1
|
||||
ON "pretixbase_invoice" (UPPER("invoice_no"));
|
||||
CREATE INDEX CONCURRENTLY pretix_addidx_invoice_nu2
|
||||
ON "pretixbase_invoice" (UPPER("full_invoice_no"));
|
||||
CREATE INDEX CONCURRENTLY pretix_addidx_organizer_name
|
||||
ON pretixbase_organizer
|
||||
USING gin (upper("name") gin_trgm_ops);
|
||||
CREATE INDEX CONCURRENTLY pretix_addidx_organizer_slug
|
||||
ON pretixbase_organizer
|
||||
USING gin (upper("slug") gin_trgm_ops);
|
||||
CREATE INDEX CONCURRENTLY pretix_addidx_order_email
|
||||
ON pretixbase_order
|
||||
USING gin (upper("email") gin_trgm_ops);
|
||||
CREATE INDEX CONCURRENTLY pretix_addidx_order_comment
|
||||
ON pretixbase_order
|
||||
USING gin (upper("comment") gin_trgm_ops);
|
||||
CREATE INDEX CONCURRENTLY pretix_addidx_orderpos_name
|
||||
ON pretixbase_orderposition
|
||||
USING gin (upper("attendee_name_cached") gin_trgm_ops);
|
||||
CREATE INDEX CONCURRENTLY pretix_addidx_orderpos_scret
|
||||
ON pretixbase_orderposition
|
||||
USING gin (upper("secret") gin_trgm_ops);
|
||||
CREATE INDEX CONCURRENTLY pretix_addidx_orderpos_email
|
||||
ON pretixbase_orderposition
|
||||
USING gin (upper("attendee_email") gin_trgm_ops);
|
||||
CREATE INDEX CONCURRENTLY pretix_addidx_ia_name
|
||||
ON pretixbase_invoiceaddress
|
||||
USING gin (upper("name_cached") gin_trgm_ops);
|
||||
CREATE INDEX CONCURRENTLY pretix_addidx_ia_company
|
||||
ON pretixbase_invoiceaddress
|
||||
USING gin (upper("company") gin_trgm_ops);
|
||||
|
||||
|
||||
Also, if you use our ``pretix-shipping`` plugin::
|
||||
|
||||
CREATE INDEX CONCURRENTLY pretix_addidx_sa_name
|
||||
ON pretix_shipping_shippingaddress
|
||||
USING gin (upper("name") gin_trgm_ops);
|
||||
CREATE INDEX CONCURRENTLY pretix_addidx_sa_company
|
||||
ON pretix_shipping_shippingaddress
|
||||
USING gin (upper("company") gin_trgm_ops);
|
||||
|
||||
@@ -26,7 +26,7 @@ installation guides):
|
||||
* `Docker`_
|
||||
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for
|
||||
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
|
||||
* A `PostgreSQL`_, `MySQL`_ 5.7+, or MariaDB 10.2.7+ database server
|
||||
* A `PostgreSQL`_ 9.5+, `MySQL`_ 5.7+, or MariaDB 10.2.7+ database server
|
||||
* A `redis`_ server
|
||||
|
||||
We also recommend that you use a firewall, although this is not a pretix-specific recommendation. If you're new to
|
||||
@@ -182,7 +182,7 @@ named ``/etc/systemd/system/pretix.service`` with the following content::
|
||||
-v /var/pretix-data:/data \
|
||||
-v /etc/pretix:/etc/pretix \
|
||||
-v /var/run/redis:/var/run/redis \
|
||||
--sysctl net.core.somaxconn=4096
|
||||
--sysctl net.core.somaxconn=4096 \
|
||||
pretix/standalone:stable all
|
||||
ExecStop=/usr/bin/docker stop %n
|
||||
|
||||
@@ -290,7 +290,7 @@ to re-build your custom image after you pulled ``pretix/standalone`` if you want
|
||||
.. _Let's Encrypt: https://letsencrypt.org/
|
||||
.. _pretix.eu: https://pretix.eu/
|
||||
.. _MySQL: https://dev.mysql.com/doc/refman/5.7/en/linux-installation-apt-repo.html
|
||||
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-9-4-on-debian-8
|
||||
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-20-04
|
||||
.. _redis: https://blog.programster.org/debian-8-install-redis-server/
|
||||
.. _ufw: https://en.wikipedia.org/wiki/Uncomplicated_Firewall
|
||||
.. _redis website: https://redis.io/topics/security
|
||||
|
||||
@@ -12,7 +12,7 @@ solution with many things readily set-up, look at :ref:`dockersmallscale`.
|
||||
get it right. If you're not feeling comfortable managing a Linux server, check out our hosting and service
|
||||
offers at `pretix.eu`_.
|
||||
|
||||
We tested this guide on the Linux distribution **Debian 8.0** but it should work very similar on other
|
||||
We tested this guide on the Linux distribution **Debian 10.0** but it should work very similar on other
|
||||
modern distributions, especially on all systemd-based ones.
|
||||
|
||||
Requirements
|
||||
@@ -23,7 +23,7 @@ installation guides):
|
||||
|
||||
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for
|
||||
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
|
||||
* A `PostgreSQL`_, `MySQL`_ 5.7+, or MariaDB 10.2.7+ database server
|
||||
* A `PostgreSQL`_ 9.5+, `MySQL`_ 5.7+, or MariaDB 10.2.7+ database server
|
||||
* A `redis`_ server
|
||||
|
||||
We also recommend that you use a firewall, although this is not a pretix-specific recommendation. If you're new to
|
||||
@@ -133,7 +133,7 @@ command if you're running MySQL::
|
||||
|
||||
(venv)$ pip3 install "pretix[postgres]" gunicorn
|
||||
|
||||
Note that you need Python 3.5 or newer. You can find out your Python version using ``python -V``.
|
||||
Note that you need Python 3.6 or newer. You can find out your Python version using ``python -V``.
|
||||
|
||||
We also need to create a data directory::
|
||||
|
||||
@@ -308,7 +308,7 @@ example::
|
||||
.. _Let's Encrypt: https://letsencrypt.org/
|
||||
.. _pretix.eu: https://pretix.eu/
|
||||
.. _MySQL: https://dev.mysql.com/doc/refman/5.7/en/linux-installation-apt-repo.html
|
||||
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-9-4-on-debian-8
|
||||
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-20-04
|
||||
.. _redis: https://blog.programster.org/debian-8-install-redis-server/
|
||||
.. _ufw: https://en.wikipedia.org/wiki/Uncomplicated_Firewall
|
||||
.. _strong encryption settings: https://mozilla.github.io/server-side-tls/ssl-config-generator/
|
||||
|
||||
@@ -92,7 +92,8 @@ pretix_task_duration_seconds
|
||||
|
||||
pretix_model_instances
|
||||
Gauge. Measures number of instances of a certain model within the database, labeled with
|
||||
the ``model`` name.
|
||||
the ``model`` name. Starting with pretix 3.11, these numbers might only be approximate for
|
||||
most tables when running on PostgreSQL to mitigate performance impact.
|
||||
|
||||
.. _metric types: https://prometheus.io/docs/concepts/metric_types/
|
||||
.. _Prometheus: https://prometheus.io/
|
||||
|
||||
@@ -7,9 +7,6 @@ This part of the documentation contains information about the REST-style API
|
||||
exposed by pretix since version 1.5 that can be used by third-party programs
|
||||
to interact with pretix and its data structures.
|
||||
|
||||
Currently, the API provides mostly read-only capabilities, but it will be extended
|
||||
in functionality over time.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
@@ -30,6 +30,9 @@ position_count integer Number of ticke
|
||||
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.
|
||||
allow_multiple_entries boolean If ``true``, subsequent scans of a ticket on this list should not show a warning but instead be stored as an additional check-in.
|
||||
allow_entry_after_exit boolean If ``true``, subsequent scans of a ticket on this list are valid if the last scan of the ticket was an exit scan.
|
||||
rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.10
|
||||
@@ -48,6 +51,15 @@ auto_checkin_sales_channels list of strings All items on th
|
||||
|
||||
The ``auto_checkin_sales_channels`` field has been added.
|
||||
|
||||
.. versionchanged:: 3.9
|
||||
|
||||
The ``subevent`` attribute may now be ``null`` inside event series. The ``allow_multiple_entries``,
|
||||
``allow_entry_after_exit``, and ``rules`` attributes have been added.
|
||||
|
||||
.. versionchanged:: 3.11
|
||||
|
||||
The ``subevent_match`` and ``exclude`` query parameters have been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -89,6 +101,9 @@ Endpoints
|
||||
"limit_products": [],
|
||||
"include_pending": false,
|
||||
"subevent": null,
|
||||
"allow_multiple_entries": false,
|
||||
"allow_entry_after_exit": true,
|
||||
"rules": {},
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
@@ -98,6 +113,8 @@ Endpoints
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:query integer subevent: Only return check-in lists of the sub-event with the given ID
|
||||
:query integer subevent_match: Only return check-in lists that are valid for the sub-event with the given ID (i.e. also lists valid for all subevents)
|
||||
:query string exclude: Exclude a field from the output, e.g. ``checkin_count``. Can be used as a performance optimization. Can be passed multiple times.
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:statuscode 200: no error
|
||||
@@ -133,6 +150,9 @@ Endpoints
|
||||
"limit_products": [],
|
||||
"include_pending": false,
|
||||
"subevent": null,
|
||||
"allow_multiple_entries": false,
|
||||
"allow_entry_after_exit": true,
|
||||
"rules": {},
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
@@ -229,6 +249,8 @@ Endpoints
|
||||
"all_products": false,
|
||||
"limit_products": [1, 2],
|
||||
"subevent": null,
|
||||
"allow_multiple_entries": false,
|
||||
"allow_entry_after_exit": true,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
@@ -251,6 +273,8 @@ Endpoints
|
||||
"limit_products": [1, 2],
|
||||
"include_pending": false,
|
||||
"subevent": null,
|
||||
"allow_multiple_entries": false,
|
||||
"allow_entry_after_exit": true,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
@@ -303,6 +327,8 @@ Endpoints
|
||||
"limit_products": [1, 2],
|
||||
"include_pending": false,
|
||||
"subevent": null,
|
||||
"allow_multiple_entries": false,
|
||||
"allow_entry_after_exit": true,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
@@ -582,6 +608,7 @@ Order position endpoints
|
||||
:<json datetime datetime: Specifies the datetime of the check-in. If not supplied, the current time will be used.
|
||||
:<json boolean force: Specifies that the check-in should succeed regardless of previous check-ins or required
|
||||
questions that have not been filled. Defaults to ``false``.
|
||||
:<json string type: Send ``"exit"`` for an exit and ``"entry"`` (default) for an entry.
|
||||
:<json boolean ignore_unpaid: Specifies that the check-in should succeed even if the order is in pending state.
|
||||
Defaults to ``false`` and only works when ``include_pending`` is set on the check-in
|
||||
list.
|
||||
@@ -696,6 +723,7 @@ Order position endpoints
|
||||
``canceled_supported`` to ``true``, otherwise these orders return ``unpaid``.
|
||||
* ``already_redeemed`` - Ticket already has been redeemed
|
||||
* ``product`` - Tickets with this product may not be scanned at this device
|
||||
* ``rules`` - Check-in prevented by a user-defined rule
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
|
||||
@@ -526,7 +526,7 @@ information about the properties.
|
||||
|
||||
Get current values of event settings.
|
||||
|
||||
Permission required: "Can change event settings"
|
||||
Permission required: "Can change event settings" (Exception: with device auth, *some* settings can always be *read*.)
|
||||
|
||||
**Example request**:
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ secret string Gift card code
|
||||
value money (string) Current gift card value
|
||||
currency string Currency of the value (can not be modified later)
|
||||
testmode boolean Whether this is a test gift card
|
||||
expires datetime Expiry date (or ``null``)
|
||||
conditions string Special terms and conditions for this card (or ``null``)
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
Endpoints
|
||||
@@ -53,6 +55,8 @@ Endpoints
|
||||
"secret": "HLBYVELFRC77NCQY",
|
||||
"currency": "EUR",
|
||||
"testmode": false,
|
||||
"expires": null,
|
||||
"conditions": null,
|
||||
"value": "13.37"
|
||||
}
|
||||
]
|
||||
@@ -92,6 +96,8 @@ Endpoints
|
||||
"secret": "HLBYVELFRC77NCQY",
|
||||
"currency": "EUR",
|
||||
"testmode": false,
|
||||
"expires": null,
|
||||
"conditions": null,
|
||||
"value": "13.37"
|
||||
}
|
||||
|
||||
@@ -134,6 +140,8 @@ Endpoints
|
||||
"secret": "HLBYVELFRC77NCQY",
|
||||
"testmode": false,
|
||||
"currency": "EUR",
|
||||
"expires": null,
|
||||
"conditions": null,
|
||||
"value": "13.37"
|
||||
}
|
||||
|
||||
@@ -180,6 +188,8 @@ Endpoints
|
||||
"secret": "HLBYVELFRC77NCQY",
|
||||
"testmode": false,
|
||||
"currency": "EUR",
|
||||
"expires": null,
|
||||
"conditions": null,
|
||||
"value": "14.00"
|
||||
}
|
||||
|
||||
@@ -222,6 +232,8 @@ Endpoints
|
||||
"secret": "HLBYVELFRC77NCQY",
|
||||
"currency": "EUR",
|
||||
"testmode": false,
|
||||
"expires": null,
|
||||
"conditions": null,
|
||||
"value": "15.37"
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ addon_category integer Internal ID of
|
||||
min_count integer The minimal number of add-ons that need to be chosen.
|
||||
max_count integer The maximal number of add-ons that can be chosen.
|
||||
position integer An integer, used for sorting
|
||||
multi_allowed boolean Adding the same item multiple times is allowed
|
||||
price_included boolean Adding this add-on to the item is free
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
@@ -65,6 +66,7 @@ Endpoints
|
||||
"min_count": 0,
|
||||
"max_count": 10,
|
||||
"position": 0,
|
||||
"multi_allowed": false,
|
||||
"price_included": true
|
||||
},
|
||||
{
|
||||
@@ -73,6 +75,7 @@ Endpoints
|
||||
"min_count": 0,
|
||||
"max_count": 10,
|
||||
"position": 1,
|
||||
"multi_allowed": false,
|
||||
"price_included": true
|
||||
}
|
||||
]
|
||||
@@ -112,6 +115,7 @@ Endpoints
|
||||
"min_count": 0,
|
||||
"max_count": 10,
|
||||
"position": 1,
|
||||
"multi_allowed": false,
|
||||
"price_included": true
|
||||
}
|
||||
|
||||
@@ -141,6 +145,7 @@ Endpoints
|
||||
"min_count": 0,
|
||||
"max_count": 10,
|
||||
"position": 1,
|
||||
"multi_allowed": false,
|
||||
"price_included": true
|
||||
}
|
||||
|
||||
@@ -158,6 +163,7 @@ Endpoints
|
||||
"min_count": 0,
|
||||
"max_count": 10,
|
||||
"position": 1,
|
||||
"multi_allowed": false,
|
||||
"price_included": true
|
||||
}
|
||||
|
||||
@@ -206,6 +212,7 @@ Endpoints
|
||||
"min_count": 0,
|
||||
"max_count": 10,
|
||||
"position": 1,
|
||||
"multi_allowed": false,
|
||||
"price_included": true
|
||||
}
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@ addons list of objects Definition of a
|
||||
├ min_count integer The minimal number of add-ons that need to be chosen.
|
||||
├ max_count integer The maximal number of add-ons that can be chosen.
|
||||
├ position integer An integer, used for sorting
|
||||
├ multi_allowed boolean Adding the same item multiple times is allowed
|
||||
└ price_included boolean Adding this add-on to the item is free
|
||||
bundles list of objects Definition of bundles that are included in this item.
|
||||
Only writable during creation,
|
||||
@@ -159,6 +160,10 @@ meta_data object Values set for
|
||||
|
||||
The attribute ``meta_data`` has been added.
|
||||
|
||||
.. versionchanged:: 3.10
|
||||
|
||||
The attribute ``multi_allowed`` has been added to ``addons``.
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
|
||||
@@ -151,6 +151,18 @@ last_modified datetime Last modificati
|
||||
|
||||
The ``order.fees.canceled`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 3.8
|
||||
|
||||
The ``reactivate`` operation has been added.
|
||||
|
||||
.. versionchanged:: 3.10
|
||||
|
||||
The ``search`` query parameter has been added.
|
||||
|
||||
.. versionchanged:: 3.11
|
||||
|
||||
The ``exclude`` and ``subevent_after`` query parameter has been added.
|
||||
|
||||
|
||||
.. _order-position-resource:
|
||||
|
||||
@@ -173,6 +185,13 @@ price money (string) Price of this p
|
||||
attendee_name string Specified attendee name for this position (or ``null``)
|
||||
attendee_name_parts object of strings Decomposition of attendee name (i.e. given name, family name)
|
||||
attendee_email string Specified attendee email address for this position (or ``null``)
|
||||
company string Attendee company name (or ``null``)
|
||||
street string Attendee street (or ``null``)
|
||||
zipcode string Attendee ZIP code (or ``null``)
|
||||
city string Attendee city (or ``null``)
|
||||
country string Attendee country code (or ``null``)
|
||||
state string Attendee state (ISO 3166-2 code). Only supported in
|
||||
AU, BR, CA, CN, MY, MX, and US, otherwise ``null``.
|
||||
voucher integer Internal ID of the voucher used for this position (or ``null``)
|
||||
tax_rate decimal (string) VAT rate applied for this position
|
||||
tax_value money (string) VAT included in this position
|
||||
@@ -184,6 +203,7 @@ pseudonymization_id string A random ID, e.
|
||||
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
|
||||
├ type string Type of scan (defaults to ``entry``)
|
||||
└ 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``)
|
||||
@@ -236,6 +256,14 @@ pdf_data object Data object req
|
||||
|
||||
The attribute ``canceled`` has been added.
|
||||
|
||||
.. versionchanged:: 3.8
|
||||
|
||||
The attributes ``company``, ``street``, ``zipcode``, ``city``, ``country``, and ``state`` have been added.
|
||||
|
||||
.. versionchanged:: 3.9
|
||||
|
||||
The ``checkin.type`` attribute has been added.
|
||||
|
||||
.. _order-payment-resource:
|
||||
|
||||
Order payment resource
|
||||
@@ -380,6 +408,12 @@ List of all orders
|
||||
"full_name": "Peter",
|
||||
},
|
||||
"attendee_email": null,
|
||||
"company": "Sample company",
|
||||
"street": "Test street 12",
|
||||
"zipcode": "12345",
|
||||
"city": "Testington",
|
||||
"country": "DE",
|
||||
"state": null,
|
||||
"voucher": null,
|
||||
"tax_rate": "0.00",
|
||||
"tax_value": "0.00",
|
||||
@@ -392,6 +426,7 @@ List of all orders
|
||||
"checkins": [
|
||||
{
|
||||
"list": 44,
|
||||
"type": "entry",
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": false
|
||||
}
|
||||
@@ -441,6 +476,7 @@ List of all orders
|
||||
``last_modified``, and ``status``. Default: ``datetime``
|
||||
:query string code: Only return orders that match the given order code
|
||||
:query string status: Only return orders in the given order status (see above)
|
||||
:query string search: Only return orders matching a given search query
|
||||
: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.
|
||||
@@ -453,6 +489,8 @@ List of all orders
|
||||
recommend using this in combination with ``testmode=false``, since test mode orders can vanish at any time and
|
||||
you will not notice it using this method.
|
||||
:query datetime created_since: Only return orders that have been created since the given date.
|
||||
:query datetime subevent_after: Only return orders that contain a ticket for a subevent taking place after the given date.
|
||||
:query string exclude: Exclude a field from the output, e.g. ``fees`` or ``positions.downloads``. Can be used as a performance optimization. Can be passed multiple times.
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:resheader X-Page-Generated: The server time at the beginning of the operation. If you're using this API to fetch
|
||||
@@ -536,6 +574,12 @@ Fetching individual orders
|
||||
"full_name": "Peter",
|
||||
},
|
||||
"attendee_email": null,
|
||||
"company": "Sample company",
|
||||
"street": "Test street 12",
|
||||
"zipcode": "12345",
|
||||
"city": "Testington",
|
||||
"country": "DE",
|
||||
"state": null,
|
||||
"voucher": null,
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": null,
|
||||
@@ -548,6 +592,7 @@ Fetching individual orders
|
||||
"checkins": [
|
||||
{
|
||||
"list": 44,
|
||||
"type": "entry",
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": false
|
||||
}
|
||||
@@ -816,9 +861,9 @@ Creating orders
|
||||
* ``consume_carts`` (optional) – A list of cart IDs. All cart positions with these IDs will be deleted if the
|
||||
order creation is successful. Any quotas or seats that become free by this operation will be credited to your order
|
||||
creation.
|
||||
* ``email``
|
||||
* ``email`` (optional)
|
||||
* ``locale``
|
||||
* ``sales_channel``
|
||||
* ``sales_channel`` (optional)
|
||||
* ``payment_provider`` (optional) – The identifier of the payment provider set for this order. This needs to be an
|
||||
existing payment provider. You should use ``"free"`` for free orders, and we strongly advise to use ``"manual"``
|
||||
for all orders you create as paid. This field is optional when the order status is ``"n"`` or the order total is
|
||||
@@ -851,15 +896,21 @@ Creating orders
|
||||
|
||||
* ``positionid`` (optional, see below)
|
||||
* ``item``
|
||||
* ``variation``
|
||||
* ``variation`` (optional)
|
||||
* ``price`` (optional, if set to ``null`` or missing the price will be computed from the given product)
|
||||
* ``seat`` (The ``seat_guid`` attribute of a seat. Required when the specified ``item`` requires a seat, otherwise must be ``null``.)
|
||||
* ``attendee_name`` **or** ``attendee_name_parts``
|
||||
* ``attendee_name`` **or** ``attendee_name_parts`` (optional)
|
||||
* ``voucher`` (optional, the ``code`` attribute of a valid voucher)
|
||||
* ``attendee_email``
|
||||
* ``attendee_email`` (optional)
|
||||
* ``company`` (optional)
|
||||
* ``street`` (optional)
|
||||
* ``zipcode`` (optional)
|
||||
* ``city`` (optional)
|
||||
* ``country`` (optional)
|
||||
* ``state`` (optional)
|
||||
* ``secret`` (optional)
|
||||
* ``addon_to`` (optional, see below)
|
||||
* ``subevent``
|
||||
* ``subevent`` (optional)
|
||||
* ``answers``
|
||||
|
||||
* ``question``
|
||||
@@ -883,7 +934,8 @@ Creating orders
|
||||
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
|
||||
* ``send_mail`` (optional). If set to ``true``, the same emails will be sent as for a regular order, regardless of
|
||||
whether these emails are enabled for certain sales channels. Defaults to
|
||||
``false``.
|
||||
|
||||
If you want to use add-on products, you need to set the ``positionid`` fields of all positions manually
|
||||
@@ -1057,6 +1109,42 @@ Order state operations
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
:statuscode 404: The requested order does not exist.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/reactivate/
|
||||
|
||||
Reactivates a canceled order. This will set the order to pending or paid state. Only possible if all products are
|
||||
still available.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/reactivate/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"code": "ABC12",
|
||||
"status": "n",
|
||||
...
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param code: The ``code`` field of the order to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The order cannot be reactivated
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
:statuscode 404: The requested order does not exist.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_pending/
|
||||
|
||||
Marks a paid order as unpaid.
|
||||
@@ -1402,6 +1490,7 @@ List of all order positions
|
||||
"checkins": [
|
||||
{
|
||||
"list": 44,
|
||||
"type": "entry",
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": false
|
||||
}
|
||||
@@ -1507,6 +1596,7 @@ Fetching individual positions
|
||||
"checkins": [
|
||||
{
|
||||
"list": 44,
|
||||
"type": "entry",
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": false
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ close_when_sold_out boolean If ``true``, th
|
||||
again.
|
||||
closed boolean Whether the quota is currently closed (see above
|
||||
field).
|
||||
release_after_exit boolean Whether the quota regains capacity as soon as some tickets
|
||||
have been scanned at an exit.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.10
|
||||
@@ -36,6 +38,10 @@ closed boolean Whether the quo
|
||||
|
||||
The attributes ``close_when_sold_out`` and ``closed`` have been added.
|
||||
|
||||
.. versionchanged:: 3.10
|
||||
|
||||
The attribute ``release_after_exit`` has been added.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
@@ -283,6 +289,7 @@ Endpoints
|
||||
"total_size": 1000,
|
||||
"pending_orders": 25,
|
||||
"paid_orders": 423,
|
||||
"exited_orders": 0,
|
||||
"cart_positions": 7,
|
||||
"blocking_vouchers": 126,
|
||||
"waiting_list": 0
|
||||
|
||||
@@ -39,10 +39,12 @@ geo_lon float Longitude of th
|
||||
item_price_overrides list of objects List of items for which this sub-event overrides the
|
||||
default price
|
||||
├ item integer The internal item ID
|
||||
├ disabled boolean If ``true``, item should not be available for this sub-event
|
||||
└ price money (string) The price or ``null`` for the default price
|
||||
variation_price_overrides list of objects List of variations for which this sub-event overrides
|
||||
the default price
|
||||
├ variation integer The internal variation ID
|
||||
├ disabled boolean If ``true``, variation should not be available for this sub-event
|
||||
└ price money (string) The price or ``null`` for the default price
|
||||
meta_data object Values set for organizer-specific meta data parameters.
|
||||
seating_plan integer If reserved seating is in use, the ID of a seating
|
||||
@@ -74,6 +76,10 @@ seat_category_mapping object An object mappi
|
||||
|
||||
The attributes ``geo_lat`` and ``geo_lon`` have been added.
|
||||
|
||||
.. versionchanged:: 3.10
|
||||
|
||||
The ``disabled`` attribute has been added to ``item_price_overrides`` and ``variation_price_overrides``.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -125,6 +131,7 @@ Endpoints
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": 2,
|
||||
"disabled": false,
|
||||
"price": "12.00"
|
||||
}
|
||||
],
|
||||
@@ -182,6 +189,7 @@ Endpoints
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": 2,
|
||||
"disabled": false,
|
||||
"price": "12.00"
|
||||
}
|
||||
],
|
||||
@@ -216,6 +224,7 @@ Endpoints
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": 2,
|
||||
"disabled": false,
|
||||
"price": "12.00"
|
||||
}
|
||||
],
|
||||
@@ -271,6 +280,7 @@ Endpoints
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": 2,
|
||||
"disabled": false,
|
||||
"price": "12.00"
|
||||
}
|
||||
],
|
||||
@@ -307,6 +317,7 @@ Endpoints
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": 2,
|
||||
"disabled": false,
|
||||
"price": "23.42"
|
||||
}
|
||||
],
|
||||
@@ -339,6 +350,7 @@ Endpoints
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": 2,
|
||||
"disabled": false,
|
||||
"price": "23.42"
|
||||
}
|
||||
],
|
||||
@@ -427,6 +439,7 @@ Endpoints
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": 2,
|
||||
"disabled": false,
|
||||
"price": "12.00"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -70,6 +70,9 @@ and ``checkin_list``.
|
||||
only include the minimum amount of data necessary for you to fetch the changed objects from our
|
||||
:ref:`rest-api` in an authenticated way.
|
||||
|
||||
.. warning:: In very rare cases, you could receive the same webhook notification twice. We try to avoid it, but we
|
||||
prefer it over missing a notification.
|
||||
|
||||
If you want to further prevent others from accessing your webhook URL, you can also use `Basic authentication`_ and
|
||||
supply the URL to us in the format of ``https://username:password@domain.com/path/``.
|
||||
We recommend that you use HTTPS for your webhook URL and might require it in the future. If HTTPS is used, we require
|
||||
|
||||
@@ -66,7 +66,7 @@ event-related views, there is also a signal that allows you to add the view to t
|
||||
|
||||
from django.urls import resolve, reverse
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from pretix.control.signals import nav_event
|
||||
|
||||
|
||||
|
||||
@@ -29,6 +29,22 @@ that we'll provide in this plugin::
|
||||
from .exporter import MyExporter
|
||||
return MyExporter
|
||||
|
||||
Some exporters might also prove to be useful, when provided on an organizer-level. In order to declare your
|
||||
exporter as capable of providing exports spanning multiple events, your plugin should listen for this signal
|
||||
and return the subclass of ``pretix.base.exporter.BaseExporter`` that we'll provide in this plugin::
|
||||
|
||||
from django.dispatch import receiver
|
||||
|
||||
from pretix.base.signals import register_multievent_data_exporters
|
||||
|
||||
|
||||
@receiver(register_multievent_data_exporters, dispatch_uid="multieventexporter_myexporter")
|
||||
def register_multievent_data_exporter(sender, **kwargs):
|
||||
from .exporter import MyExporter
|
||||
return MyExporter
|
||||
|
||||
If your exporter supports both event-level and multi-event level exports, you will need to listen for both
|
||||
signals.
|
||||
|
||||
The exporter class
|
||||
------------------
|
||||
|
||||
@@ -20,7 +20,7 @@ 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, order_gracefully_delete, invoice_line_text
|
||||
:members: validate_cart, validate_cart_addons, validate_order, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
|
||||
|
||||
Check-ins
|
||||
"""""""""
|
||||
@@ -33,11 +33,11 @@ Frontend
|
||||
--------
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
: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
|
||||
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header
|
||||
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
:members: order_info, order_meta_from_request
|
||||
:members: order_info, order_info_top, order_meta_from_request
|
||||
|
||||
Request flow
|
||||
""""""""""""
|
||||
@@ -66,19 +66,13 @@ Vouchers
|
||||
""""""""
|
||||
|
||||
.. automodule:: pretix.control.signals
|
||||
:members: item_forms
|
||||
|
||||
Vouchers
|
||||
""""""""
|
||||
|
||||
.. automodule:: pretix.control.signals
|
||||
:members: voucher_form_class, voucher_form_html, voucher_form_validation
|
||||
:members: item_forms, voucher_form_class, voucher_form_html, voucher_form_validation
|
||||
|
||||
Dashboards
|
||||
""""""""""
|
||||
|
||||
.. automodule:: pretix.control.signals
|
||||
:members: event_dashboard_widgets, user_dashboard_widgets
|
||||
:members: event_dashboard_widgets, user_dashboard_widgets, event_dashboard_top
|
||||
|
||||
Ticket designs
|
||||
""""""""""""""
|
||||
|
||||
@@ -126,6 +126,8 @@ The provider class
|
||||
|
||||
.. autoattribute:: test_mode_message
|
||||
|
||||
.. autoattribute:: requires_invoice_immediately
|
||||
|
||||
|
||||
Additional views
|
||||
----------------
|
||||
|
||||
@@ -61,7 +61,7 @@ A working example would be::
|
||||
from pretix.base.plugins import PluginConfig
|
||||
except ImportError:
|
||||
raise RuntimeError("Please use pretix 2.7 or above to run this plugin!")
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class PaypalApp(PluginConfig):
|
||||
|
||||
@@ -72,6 +72,10 @@ The output class
|
||||
|
||||
.. autoattribute:: download_button_icon
|
||||
|
||||
.. autoattribute:: multi_download_button_text
|
||||
|
||||
.. autoattribute:: long_download_button_text
|
||||
|
||||
.. autoattribute:: preview_allowed
|
||||
|
||||
.. autoattribute:: javascript_required
|
||||
|
||||
@@ -7,7 +7,7 @@ Coding style and quality
|
||||
for more information. Use four spaces for indentation.
|
||||
|
||||
* We sort our imports by a certain schema, but you don't have to do this by hand. Again, ``setup.cfg`` contains
|
||||
some definitions that allow the command ``isort -rc <directory>`` to automatically sort the imports in your source
|
||||
some definitions that allow the command ``isort <directory>`` to automatically sort the imports in your source
|
||||
files.
|
||||
|
||||
* For templates and models, please take a look at the `Django Coding Style`_. We like Django's `class-based views`_ and
|
||||
@@ -18,7 +18,7 @@ Coding style and quality
|
||||
* We expect all new code to come with proper tests. When writing new tests, please write them using `pytest-style`_
|
||||
test functions and raw ``assert`` statements. Use `fixtures`_ to prevent repetitive code. Some old parts of pretix'
|
||||
test suite are in the style of Python's unit test module. If you extend those files, you might continue in this style,
|
||||
but please use pytest style for any new test files.
|
||||
but please use ``pytest`` style for any new test files.
|
||||
|
||||
* Please keep the first line of your commit messages short. When referencing an issue, please phrase it like
|
||||
``Fix #123 -- Problems with order creation`` or ``Refs #123 -- Fix this part of that bug``.
|
||||
|
||||
@@ -69,7 +69,7 @@ We now need a way to translate the action codes like ``pretix.event.changed`` in
|
||||
strings. The :py:attr:`pretix.base.signals.logentry_display` signals allows you to do so. A simple
|
||||
implementation could look like::
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from pretix.base.signals import logentry_display
|
||||
|
||||
@receiver(signal=logentry_display)
|
||||
|
||||
@@ -98,7 +98,7 @@ pull request nevertheless and ask us for help, we are happy to assist you.
|
||||
Execute the following commands to check for code style errors::
|
||||
|
||||
flake8 .
|
||||
isort -c -rc .
|
||||
isort -c .
|
||||
python manage.py check
|
||||
|
||||
Execute the following command to run pretix' test suite (might take a couple of minutes)::
|
||||
@@ -121,7 +121,7 @@ for example, to check for any errors in any staged files when committing::
|
||||
do
|
||||
echo $file
|
||||
git show ":$file" | flake8 - --stdin-display-name="$file" || exit 1 # we only want to lint the staged changes, not any un-staged changes
|
||||
git show ":$file" | isort -df --check-only - | grep ERROR && exit 1 || true
|
||||
git show ":$file" | isort -c - | grep ERROR && exit 1 || true
|
||||
done
|
||||
|
||||
|
||||
|
||||
356
doc/plugins/digital.rst
Normal file
@@ -0,0 +1,356 @@
|
||||
Digital content
|
||||
===============
|
||||
|
||||
URL interpolation and JWT authentication
|
||||
----------------------------------------
|
||||
|
||||
In the simplest case, you can use the digital content module to point users to a specific piece of content on some
|
||||
platform after their ticket purchase, or show them an embedded video or live stream. However, the full power of the
|
||||
module can be utilized by passing additional information to the target system to automatically authenticate the user
|
||||
or pre-fill some fields with their data. For example, you could use an URL like this::
|
||||
|
||||
https://webinars.example.com/join?as={attendee_name}&userid={order_code}-{positionid}
|
||||
|
||||
While this is already useful, it does not provide much security – anyone could guess a valid combination for that URL.
|
||||
Therefore, the module allows you to pass information as a `JSON Web Token`_, which isn't encrypted, but signed with a
|
||||
shared secret such that nobody can create their own tokens or modify the contents. To use a token, set up a URL like this::
|
||||
|
||||
https://webinars.example.com/join?with_token={token}
|
||||
|
||||
Additionally, you will need to set a JWT secret and a token template, either through the pretix interface or through the
|
||||
API (see below). pretix currently only supports tokens signed with ``HMAC-SHA256`` (``HS256``). Your token template can contain
|
||||
whatever JSON you'd like to pass on based on the same variables, for example::
|
||||
|
||||
{
|
||||
"iss": "pretix.eu",
|
||||
"aud": "webinars.example.com",
|
||||
"user": {
|
||||
"id": "{order_code}-{positionid}",
|
||||
"product": "{product_id}",
|
||||
"variation": "{variation_id}",
|
||||
"name": "{attendee_name}"
|
||||
}
|
||||
}
|
||||
|
||||
Variables can only be used in strings inside the JSON structure.
|
||||
pretix will automatically add an ``iat`` claim with the current timestamp and an ``exp`` claim with an expiration timestamp
|
||||
based on your configuration.
|
||||
|
||||
|
||||
List of variables
|
||||
"""""""""""""""""
|
||||
|
||||
The following variables are currently supported:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
=================================== ====================================================================
|
||||
Variable Description
|
||||
=================================== ====================================================================
|
||||
``order_code`` Order code (alphanumerical, unique per order, not per ticket)
|
||||
``positionid`` ID of the ticket within the order (integer, starting at 1)
|
||||
``order_email`` E-mail address of the ticket purchaser
|
||||
``product_id`` Internal ID of the purchased product
|
||||
``product_variation`` Internal ID of the purchased product variation (or empty)
|
||||
``attendee_name`` Full name of the ticket holder (or empty)
|
||||
``attendee_name_*`` Name parts of the ticket holder, depending on configuration, e.g. ``attendee_name_given_name`` or ``attendee_name_family_name``
|
||||
``attendee_email`` E-mail address of the ticket holder (or empty)
|
||||
``attendee_company`` Company of the ticket holder (or empty)
|
||||
``attendee_street`` Street of the ticket holder's address (or empty)
|
||||
``attendee_zipcode`` ZIP code of the ticket holder's address (or empty)
|
||||
``attendee_city`` City of the ticket holder's address (or empty)
|
||||
``attendee_country`` Country code of the ticket holder's address (or empty)
|
||||
``attendee_state`` State of the ticket holder's address (or empty)
|
||||
``answer[XYZ]`` Answer to the custom question with identifier ``XYZ``
|
||||
``invoice_name`` Full name of the invoice address (or empty)
|
||||
``invoice_name_*`` Name parts of the invoice address, depending on configuration, e.g. ``invoice_name_given_name`` or ``invoice_name_family_name``
|
||||
``invoice_company`` Company of the invoice address (or empty)
|
||||
``invoice_street`` Street of the invoice address (or empty)
|
||||
``invoice_zipcode`` ZIP code of the invoice address (or empty)
|
||||
``invoice_city`` City of the invoice address (or empty)
|
||||
``invoice_country`` Country code of the invoice address (or empty)
|
||||
``invoice_state`` State of the invoice address (or empty)
|
||||
``meta_XYZ`` Value of the event's ``XYZ`` meta property
|
||||
``token`` Signed JWT (only to be used in URLs, not in tokens)
|
||||
=================================== ====================================================================
|
||||
|
||||
|
||||
API Resource description
|
||||
-------------------------
|
||||
|
||||
The digital content plugin provides a HTTP API that allows you to create new digital content for your ticket holders,
|
||||
such as live streams, videos, or material downloads.
|
||||
|
||||
The digital content resource contains the following public fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal content ID
|
||||
title multi-lingual string The content title (required)
|
||||
content_type string The type of content, valid values are ``webinar``, ``video``, ``livestream``, ``link``, ``file``
|
||||
url string The location of the digital content
|
||||
description multi-lingual string A public description of the item. May contain Markdown
|
||||
syntax and is not required.
|
||||
available_from datetime The first date time at which this content will be shown
|
||||
(or ``null``).
|
||||
available_until datetime The last date time at which this content will b e shown
|
||||
(or ``null``).
|
||||
all_products boolean If ``true``, the content is available to all buyers of tickets for this event. The ``limit_products`` field is ignored in this case.
|
||||
limit_products list of integers List of product/item IDs. This content is only shown to buyers of these ticket types.
|
||||
position integer An integer, used for sorting
|
||||
subevent integer Date in an event series this content should be shown for. Should be ``null`` if this is not an event series or if this should be shown to all customers.
|
||||
jwt_template string Template for JWT token generation
|
||||
jwt_secret string Secret for JWT token generation
|
||||
jwt_validity integer JWT validity in days
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
API Endpoints
|
||||
-------------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/
|
||||
|
||||
Returns a list of all digital content configured for an event.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"subevent": null,
|
||||
"title": {
|
||||
"en": "Concert livestream"
|
||||
},
|
||||
"content_type": "link",
|
||||
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"description": {
|
||||
"en": "Watch our event live here on YouTube!"
|
||||
},
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"available_from": "2020-03-22T23:00:00Z",
|
||||
"available_until": null,
|
||||
"position": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query page: The page number in case of a multi-page result set, default is 1
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to view it.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/(id)/
|
||||
|
||||
Returns information on one content item, identified by its ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"subevent": null,
|
||||
"title": {
|
||||
"en": "Concert livestream"
|
||||
},
|
||||
"content_type": "link",
|
||||
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"description": {
|
||||
"en": "Watch our event live here on YouTube!"
|
||||
},
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"available_from": "2020-03-22T23:00:00Z",
|
||||
"available_until": null,
|
||||
"position": 1
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:param id: The ``id`` field of the content to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event/content does not exist **or** you have no permission to view it.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/
|
||||
|
||||
Create a new digital content.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 166
|
||||
|
||||
{
|
||||
"subevent": null,
|
||||
"title": {
|
||||
"en": "Concert livestream"
|
||||
},
|
||||
"content_type": "link",
|
||||
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"description": {
|
||||
"en": "Watch our event live here on YouTube!"
|
||||
},
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"available_from": "2020-03-22T23:00:00Z",
|
||||
"available_until": null,
|
||||
"position": 1
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 2,
|
||||
"subevent": null,
|
||||
"title": {
|
||||
"en": "Concert livestream"
|
||||
},
|
||||
"content_type": "link",
|
||||
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"description": {
|
||||
"en": "Watch our event live here on YouTube!"
|
||||
},
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"available_from": "2020-03-22T23:00:00Z",
|
||||
"available_until": null,
|
||||
"position": 1
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to create new content for
|
||||
:param event: The ``slug`` field of the event to create new content for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The content could not be created due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create digital contents.
|
||||
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/(id)/
|
||||
|
||||
Update a content. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||
want to change.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 34
|
||||
|
||||
{
|
||||
"url": "https://mywebsite.com"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 2,
|
||||
"subevent": null,
|
||||
"title": {
|
||||
"en": "Concert livestream"
|
||||
},
|
||||
"content_type": "link",
|
||||
"url": "https://mywebsite.com",
|
||||
"description": {
|
||||
"en": "Watch our event live here on YouTube!"
|
||||
},
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"available_from": "2020-03-22T23:00:00Z",
|
||||
"available_until": null,
|
||||
"position": 1
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param id: The ``id`` field of the content to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The content could not be modified due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event/content does not exist **or** you have no permission to change it.
|
||||
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/(id)/
|
||||
|
||||
Delete a digital content.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204 No Content
|
||||
Vary: Accept
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param id: The ``id`` field of the content to delete
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event/content does not exist **or** you have no permission to change it
|
||||
|
||||
.. _JSON Web Token: https://en.wikipedia.org/wiki/JSON_Web_Token
|
||||
@@ -15,3 +15,5 @@ If you want to **create** a plugin, please go to the
|
||||
ticketoutputpdf
|
||||
badges
|
||||
campaigns
|
||||
digital
|
||||
webinar
|
||||
|
||||
43
doc/plugins/webinar.rst
Normal file
@@ -0,0 +1,43 @@
|
||||
pretix Webinar
|
||||
==============
|
||||
|
||||
Fetch host URLs
|
||||
---------------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/webinars/
|
||||
|
||||
Returns a list of all currently available webinar calls configured for an event.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/webinars/ 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
|
||||
|
||||
[
|
||||
{
|
||||
"name": "Webinar B – Sept. 8th, 2020",
|
||||
"hosturl": "http://pretix.eu/demo/museum/webinar/host/a9aded3d7bd4df60/30611a34f9fee5d3/"
|
||||
},
|
||||
{
|
||||
"name": "Webinar A – Sept. 8, 2020",
|
||||
"hosturl": "http://pretix.eu/demo/museum/webinar/host/e714x7d4a4a36a04/b9cc444665xxx757/"
|
||||
}
|
||||
]
|
||||
|
||||
:query subevent: Limit the result to the webinar(s) for a specific subevent.
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to view it.
|
||||
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 70 KiB |
BIN
doc/screens/event/tax_add.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 53 KiB |
BIN
doc/screens/event/timeslots_checkinlists.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
doc/screens/event/timeslots_create.png
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
doc/screens/event/timeslots_create_2.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
doc/screens/event/timeslots_create_3.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
doc/screens/event/timeslots_presale.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
doc/screens/event/timeslots_settings_1.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 55 KiB |
@@ -47,6 +47,7 @@ gunicorn
|
||||
guid
|
||||
hardcoded
|
||||
hostname
|
||||
ics
|
||||
idempotency
|
||||
iframe
|
||||
incrementing
|
||||
@@ -54,6 +55,8 @@ inofficial
|
||||
invalidations
|
||||
iterable
|
||||
Jimdo
|
||||
jwt
|
||||
JWT
|
||||
libpretixprint
|
||||
libsass
|
||||
linters
|
||||
|
||||
@@ -26,6 +26,9 @@ Sender address
|
||||
we strongly recommend to use the SMTP settings below as well, otherwise your e-mails might be detected as spam
|
||||
due to the `Sender Policy Framework`_ and similar mechanisms.
|
||||
|
||||
Sender name
|
||||
This is the name associated with the sender address. By default, this is your event name.
|
||||
|
||||
Signature
|
||||
This text will be appended to all e-mails in form of a signature. This might be useful e.g. to add your contact
|
||||
details or any legal information that needs to be included with the e-mails.
|
||||
@@ -33,6 +36,15 @@ Signature
|
||||
Bcc address
|
||||
This email address will receive a copy of every event-related email.
|
||||
|
||||
Attach calendar files
|
||||
With this option, every order confirmation mail will include an ics file with name, date and location of
|
||||
your event. It can be imported into many digital calendars.
|
||||
|
||||
Sales Channels for Checkout Emails
|
||||
When you are using multiple sales channel, you may want to decide that mails for order and payment confirmation
|
||||
are only to be sent for some sales channels. For orders created through the default online shop, these emails
|
||||
must always be send. A similar option is available for ticket download reminders.
|
||||
|
||||
E-mail design
|
||||
-------------
|
||||
|
||||
|
||||
94
doc/user/events/guides/timeslots.rst
Normal file
@@ -0,0 +1,94 @@
|
||||
.. _timeslots:
|
||||
|
||||
Use case: Time slots
|
||||
====================
|
||||
|
||||
A more advanced use case of pretix is using pretix for time-slot-based access to an area with a limited visitor
|
||||
capacity, such as a museum or other attraction. This guide will show you the quickest way to set up such an event
|
||||
with pretix.
|
||||
|
||||
First of all, when creating your event, you need to select that your event represents an "event series":
|
||||
|
||||
|
||||
.. thumbnail:: ../../../screens/event/create_step1.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
You can click :ref:`here <subevents>` for a more general description of event series with pretix, but everything you
|
||||
need to know is in this chapter as well.
|
||||
|
||||
General event setup
|
||||
-------------------
|
||||
|
||||
Before you go further, set up your products that you want to sell for each time slot, such as different types of entry.
|
||||
|
||||
Creating slots
|
||||
--------------
|
||||
|
||||
To create the time slots, you need to create a number of "dates" in the event series. Select "Dates" in the navigation
|
||||
menu on the left side and click "Create many new dates". Then, first enter the pattern of your opening days. In the
|
||||
example, the museum is open week Tuesday to Sunday. We recommend to create the slots for a few weeks at a time, but not
|
||||
e.g. for a full year, since it will be more complicated to change things later.
|
||||
|
||||
.. thumbnail:: ../../../screens/event/timeslots_create.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
Then, scroll to the times section and create your time slots. You can do any interval you like. If you have different
|
||||
opening times on different week days, you will need to go through the creation process multiple times.
|
||||
|
||||
.. thumbnail:: ../../../screens/event/timeslots_create_2.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
Scroll further down and create one or multiple quotas that define how many people can book a ticket for that time slot.
|
||||
In this example, 50 people in total are allowed to enter within every slot:
|
||||
|
||||
.. thumbnail:: ../../../screens/event/timeslots_create_3.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
Do **not** create a check-in list at this point. We will deal with this further below in the guide.
|
||||
Now, press "Save" to create your slots.
|
||||
|
||||
.. warning:: If you create a lot of time slots at once, the server might need a few minutes to create them all in our
|
||||
system. If you receive an error page because it took too long, please do not try again immediately but wait
|
||||
for a few minutes. Most likely, the slots will be created successfully even though you saw an error.
|
||||
|
||||
Event settings
|
||||
--------------
|
||||
|
||||
We recommend that you navigate to "Settings" > "General" > "Display" and set the settings "Default overview style"
|
||||
to "Week calendar":
|
||||
|
||||
.. thumbnail:: ../../../screens/event/timeslots_settings_1.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
Now, your ticket shop should give users a nice weekly overview over all time slots and their availability:
|
||||
|
||||
.. thumbnail:: ../../../screens/event/timeslots_presale.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
Check-in
|
||||
--------
|
||||
|
||||
If you want to scan people at the entrance of your event and only admit them at their designated time, we recommend
|
||||
the following setup: Go to "Check-in" in the main navigation on the left and create a new check-in list. Give it a name
|
||||
and do *not* choose a specific data. We will use one check-in list for all dates. Then, go to the "Advanced" tab at
|
||||
the top and set up two restrictions to make sure people can only get in during the time slot they registered for.
|
||||
You can create the rules exactly like shown in the following screenshot:
|
||||
|
||||
.. thumbnail:: ../../../screens/event/timeslots_checkinlists.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
If you want, you can enter a tolerance of e.g. "10" if you want to be a little bit more relaxed and admit people up to
|
||||
10 minutes before or after their time slot.
|
||||
|
||||
Now, download our `Android or Desktop app`_ and register it to your account. The app will ask you to select one the
|
||||
time slots, but it does not matter, you can select any one of them and then select your newly created check-in list.
|
||||
That's it, you're good to go!
|
||||
|
||||
.. _Android or Desktop app: https://pretix.eu/about/en/scan
|
||||
@@ -292,6 +292,8 @@ Flexible group sizes
|
||||
|
||||
If you want to give out discounted tickets to groups starting at a given size, but still billed per person, you can do so by creating a special **Group ticket** at the per-person price and set the **Minimum amount per order** option of the ticket to the minimal group size.
|
||||
|
||||
For more complex use cases, you can also use add-on products that can be chosen multiple times.
|
||||
|
||||
This way, your ticket can be bought an arbitrary number of times – but no less than the given minimal amount per order.
|
||||
|
||||
Fixed group sizes
|
||||
@@ -344,3 +346,13 @@ In addition to your normal conference quota, you need to create an unlimited quo
|
||||
Then, head to the **Bundled products** tab of the "conference ticket" and add the "conference food" as a bundled product with a **designated price** of € 150.
|
||||
|
||||
Once a customer tries to buy the € 450 conference ticket, a sub-product will be added and the price will automatically be split into the two components, leading to a correct computation of taxes.
|
||||
|
||||
You can find more use cases in these specialized guides:
|
||||
|
||||
More use cases
|
||||
--------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
guides/timeslots
|
||||
|
||||
@@ -136,10 +136,15 @@ If you want to include all your public events, you can just reference your organ
|
||||
|
||||
<pretix-widget event="https://pretix.eu/demo/"></pretix-widget>
|
||||
|
||||
There is an optional ``style`` parameter that let's you choose between a calendar view and a list view. If you do not set it, the choice will be taken from your organizer settings::
|
||||
There is an optional ``style`` parameter that let's you choose between a monthly calendar view, a week view and a list
|
||||
view. If you do not set it, the choice will be taken from your organizer settings::
|
||||
|
||||
<pretix-widget event="https://pretix.eu/demo/series/" style="list"></pretix-widget>
|
||||
<pretix-widget event="https://pretix.eu/demo/series/" style="calendar"></pretix-widget>
|
||||
<pretix-widget event="https://pretix.eu/demo/series/" style="week"></pretix-widget>
|
||||
|
||||
If you have more than 100 events, the system might refuse to show a list view and always show a calendar for performance
|
||||
reasons instead.
|
||||
|
||||
You can see an example here:
|
||||
|
||||
|
||||
@@ -58,28 +58,6 @@ method without creating a new order. If payment deadlines were dependent on the
|
||||
forth could either allow someone to extend their deadline forever, or render someones order invalid by moving the date
|
||||
back in the past.
|
||||
|
||||
How can I revert a check-in?
|
||||
----------------------------
|
||||
|
||||
Neither our apps nor our web interface can currently undo the check-in of a tickets. We know that this is
|
||||
inconvenient for some of you, but we have a good reason for it:
|
||||
|
||||
Our Desktop and Android apps both support an asynchronous mode in which they can scan tickets while staying
|
||||
independent of their internet connection. When scanning with multiple devices, it can of course happen that two
|
||||
devices scan the same ticket without knowing of the other scan. As soon as one of the devices regains connectivity, it
|
||||
will upload its activity and the server marks the ticket as checked in -- regardless of the order in which the two
|
||||
scans were made and uploaded (which could be two different orders).
|
||||
|
||||
If we'd provide a "check out" feature, it would not only be used to fix an accidental scan, but scan at entry and
|
||||
exit to count the current number of people inside etc. In this case, the order of operations matters very much for them
|
||||
to make sense and provide useful results. This makes implementing an asynchronous mode much more complicated.
|
||||
|
||||
In this trade off, we chose offline-capabilities over the check out feature. We plan on solving this problem in the
|
||||
future, but we're not there yet.
|
||||
|
||||
If you're just *testing* the check-in capabilities and want to clean out everything for the real process, you can just
|
||||
delete and re-create the check-in list.
|
||||
|
||||
Why does pretix not support any 1D (linear) bar codes?
|
||||
------------------------------------------------------
|
||||
|
||||
|
||||
@@ -14,30 +14,23 @@ and with pretix, you can do this. On this page, you find out the necessary steps
|
||||
With the pretix.eu hosted service
|
||||
---------------------------------
|
||||
|
||||
Step 1: DNS Configuration
|
||||
#########################
|
||||
Go to "Organizers" in the backend and select your organizer account. Then, go to "Settings" and "Custom Domain".
|
||||
|
||||
This page will show you instructions on how to set up your own domain. Basically, it works like this:
|
||||
|
||||
Go to the website of the provider you registered your domain name with. Look for the "DNS" settings page in their
|
||||
interface. Unfortunately, we can't tell you exactly how that is named and how it looks, since it is different for every
|
||||
domain provider.
|
||||
|
||||
Use this interface to add a new subdomain record, e.g. ``tickets`` of the type ``CNAME`` (might also be called "alias").
|
||||
The value of the record should be ``www.pretix.eu``.
|
||||
|
||||
Step 2: Wait for the DNS entry to propagate
|
||||
###########################################
|
||||
The value of the record should be the one shown on the "Custom Domain" page in pretix' backend.
|
||||
|
||||
Submit your changes and wait a bit, it can regularly take up to three hours for DNS changes to propagate to the caches
|
||||
of all DNS servers. You can try checking by accessing your new subdomain, ``http://tickets.awesomepartycorp.com``.
|
||||
If DNS was changed successfully, you should see a SSL certificate error. If you ignore the error and access the page
|
||||
anyways, you should get a pretix-themed error page with the headline "Unknown domain".
|
||||
|
||||
Step 3: Tell us
|
||||
###############
|
||||
|
||||
Write an email to support@pretix.eu, naming your new domain and your organizer account. We will then generate a SSL
|
||||
certificate for you (for free!) and configure the domain.
|
||||
|
||||
Now, tell us about your domain on the "Custom Domain" page to get started.
|
||||
|
||||
With a custom pretix installation
|
||||
---------------------------------
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
General settings
|
||||
================
|
||||
|
||||
At "Settings" → "Payment", you can configure every aspect related to the payments you want to accept. The upper part
|
||||
of the page shows a number of general settings that affect all payment methods:
|
||||
At "Settings" → "Payment", you can configure every aspect related to the payments you want to accept. The "Deadline"
|
||||
and "Advanced" tabs of the page show a number of general settings that affect all payment methods:
|
||||
|
||||
.. thumbnail:: ../../screens/event/settings_payment.png
|
||||
:align: center
|
||||
|
||||
@@ -3,6 +3,7 @@ include README.rst
|
||||
recursive-include pretix/static *
|
||||
recursive-include pretix/static.dist *
|
||||
recursive-include pretix/locale *
|
||||
recursive-include pretix/helpers/locale *
|
||||
recursive-include pretix/base/templates *
|
||||
recursive-include pretix/control/templates *
|
||||
recursive-include pretix/presale/templates *
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "3.7.0"
|
||||
__version__ = "3.11.1"
|
||||
|
||||
@@ -3,7 +3,7 @@ from datetime import timedelta
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from oauth2_provider.generators import (
|
||||
generate_client_id, generate_client_secret,
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ from datetime import timedelta
|
||||
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy
|
||||
from django.utils.translation import gettext_lazy
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
@@ -56,7 +56,7 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
else validated_data.get('item').quotas.filter(subevent=validated_data.get('subevent')))
|
||||
if len(new_quotas) == 0:
|
||||
raise ValidationError(
|
||||
ugettext_lazy('The product "{}" is not assigned to a quota.').format(
|
||||
gettext_lazy('The product "{}" is not assigned to a quota.').format(
|
||||
str(validated_data.get('item'))
|
||||
)
|
||||
)
|
||||
@@ -64,8 +64,8 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
avail = quota.availability()
|
||||
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < 1):
|
||||
raise ValidationError(
|
||||
ugettext_lazy('There is not enough quota available on quota "{}" to perform '
|
||||
'the operation.').format(
|
||||
gettext_lazy('There is not enough quota available on quota "{}" to perform '
|
||||
'the operation.').format(
|
||||
quota.name
|
||||
)
|
||||
)
|
||||
@@ -88,7 +88,7 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
else:
|
||||
validated_data['seat'] = seat
|
||||
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))
|
||||
raise ValidationError(gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
|
||||
elif seated:
|
||||
raise ValidationError('The specified product requires to choose a seat.')
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
@@ -14,7 +14,19 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = CheckinList
|
||||
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
|
||||
'include_pending', 'auto_checkin_sales_channels')
|
||||
'include_pending', 'auto_checkin_sales_channels', 'allow_multiple_entries', 'allow_entry_after_exit',
|
||||
'rules')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
for exclude_field in self.context['request'].query_params.getlist('exclude'):
|
||||
p = exclude_field.split('.')
|
||||
if p[0] in self.fields:
|
||||
if len(p) == 1:
|
||||
del self.fields[p[0]]
|
||||
elif len(p) == 2:
|
||||
self.fields[p[0]].child.fields.pop(p[1])
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -28,9 +40,7 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
raise ValidationError(_('One or more items do not belong to this event.'))
|
||||
|
||||
if event.has_subevents:
|
||||
if not full_data.get('subevent'):
|
||||
raise ValidationError(_('Subevent cannot be null for event series.'))
|
||||
if event != full_data.get('subevent').event:
|
||||
if full_data.get('subevent') and event != full_data.get('subevent').event:
|
||||
raise ValidationError(_('The subevent does not belong to this event.'))
|
||||
else:
|
||||
if full_data.get('subevent'):
|
||||
|
||||
@@ -2,7 +2,7 @@ from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from django_countries.serializers import CountryFieldMixin
|
||||
from hierarkey.proxy import HierarkeyProxy
|
||||
from pytz import common_timezones
|
||||
@@ -29,6 +29,9 @@ class MetaDataField(Field):
|
||||
}
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if not isinstance(data, dict) or not all(isinstance(k, str) for k in data.keys()):
|
||||
raise ValidationError('meta_data needs to be an object (str -> str).')
|
||||
|
||||
return {
|
||||
'meta_data': data
|
||||
}
|
||||
@@ -42,6 +45,8 @@ class MetaPropertyField(Field):
|
||||
}
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if not isinstance(data, dict) or not all(isinstance(k, str) for k in data.keys()) or not all(isinstance(k, str) for k in data.values()):
|
||||
raise ValidationError('item_meta_properties needs to be an object (str -> str).')
|
||||
return {
|
||||
'item_meta_properties': data
|
||||
}
|
||||
@@ -58,6 +63,8 @@ class SeatCategoryMappingField(Field):
|
||||
}
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if not isinstance(data, dict) or not all(isinstance(k, str) for k in data.keys()) or not all(isinstance(k, int) for k in data.values()):
|
||||
raise ValidationError('seat_category_mapping needs to be an object (str -> int).')
|
||||
return {
|
||||
'seat_category_mapping': data or {}
|
||||
}
|
||||
@@ -341,13 +348,13 @@ class CloneEventSerializer(EventSerializer):
|
||||
class SubEventItemSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = SubEventItem
|
||||
fields = ('item', 'price')
|
||||
fields = ('item', 'price', 'disabled')
|
||||
|
||||
|
||||
class SubEventItemVariationSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = SubEventItemVariation
|
||||
fields = ('variation', 'price')
|
||||
fields = ('variation', 'price', 'disabled')
|
||||
|
||||
|
||||
class SubEventSerializer(I18nAwareModelSerializer):
|
||||
@@ -452,27 +459,29 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
|
||||
@transaction.atomic
|
||||
def update(self, instance, validated_data):
|
||||
item_price_overrides_data = validated_data.pop('subeventitem_set') if 'subeventitem_set' in validated_data else {}
|
||||
variation_price_overrides_data = validated_data.pop('subeventitemvariation_set') if 'subeventitemvariation_set' in validated_data else {}
|
||||
item_price_overrides_data = validated_data.pop('subeventitem_set', None)
|
||||
variation_price_overrides_data = validated_data.pop('subeventitemvariation_set', None)
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
seat_category_mapping = validated_data.pop('seat_category_mapping', None)
|
||||
subevent = super().update(instance, validated_data)
|
||||
|
||||
existing_item_overrides = {item.item: item.id for item in SubEventItem.objects.filter(subevent=subevent)}
|
||||
if item_price_overrides_data is not None:
|
||||
existing_item_overrides = {item.item: item.id for item in SubEventItem.objects.filter(subevent=subevent)}
|
||||
|
||||
for item_price_override_data in item_price_overrides_data:
|
||||
id = existing_item_overrides.pop(item_price_override_data['item'], None)
|
||||
SubEventItem(id=id, subevent=subevent, **item_price_override_data).save()
|
||||
for item_price_override_data in item_price_overrides_data:
|
||||
id = existing_item_overrides.pop(item_price_override_data['item'], None)
|
||||
SubEventItem(id=id, subevent=subevent, **item_price_override_data).save()
|
||||
|
||||
SubEventItem.objects.filter(id__in=existing_item_overrides.values()).delete()
|
||||
SubEventItem.objects.filter(id__in=existing_item_overrides.values()).delete()
|
||||
|
||||
existing_variation_overrides = {item.variation: item.id for item in SubEventItemVariation.objects.filter(subevent=subevent)}
|
||||
if variation_price_overrides_data is not None:
|
||||
existing_variation_overrides = {item.variation: item.id for item in SubEventItemVariation.objects.filter(subevent=subevent)}
|
||||
|
||||
for variation_price_override_data in variation_price_overrides_data:
|
||||
id = existing_variation_overrides.pop(variation_price_override_data['variation'], None)
|
||||
SubEventItemVariation(id=id, subevent=subevent, **variation_price_override_data).save()
|
||||
for variation_price_override_data in variation_price_overrides_data:
|
||||
id = existing_variation_overrides.pop(variation_price_override_data['variation'], None)
|
||||
SubEventItemVariation(id=id, subevent=subevent, **variation_price_override_data).save()
|
||||
|
||||
SubEventItemVariation.objects.filter(id__in=existing_variation_overrides.values()).delete()
|
||||
SubEventItemVariation.objects.filter(id__in=existing_variation_overrides.values()).delete()
|
||||
|
||||
# Meta data
|
||||
if meta_data is not None:
|
||||
@@ -532,6 +541,9 @@ class EventSettingsSerializer(serializers.Serializer):
|
||||
'checkout_email_helptext',
|
||||
'presale_has_ended_text',
|
||||
'voucher_explanation_text',
|
||||
'banner_text',
|
||||
'banner_text_bottom',
|
||||
'show_dates_on_frontpage',
|
||||
'show_date_to',
|
||||
'show_times',
|
||||
'show_items_outside_presale_period',
|
||||
@@ -552,16 +564,23 @@ class EventSettingsSerializer(serializers.Serializer):
|
||||
'meta_noindex',
|
||||
'redirect_to_checkout_directly',
|
||||
'frontpage_subevent_ordering',
|
||||
'event_list_type',
|
||||
'frontpage_text',
|
||||
'attendee_names_asked',
|
||||
'attendee_names_required',
|
||||
'attendee_emails_asked',
|
||||
'attendee_emails_required',
|
||||
'confirm_text',
|
||||
'attendee_addresses_asked',
|
||||
'attendee_addresses_required',
|
||||
'attendee_company_asked',
|
||||
'attendee_company_required',
|
||||
'confirm_texts',
|
||||
'order_email_asked_twice',
|
||||
'payment_term_mode',
|
||||
'payment_term_days',
|
||||
'payment_term_last',
|
||||
'payment_term_weekdays',
|
||||
'payment_term_minutes',
|
||||
'payment_term_last',
|
||||
'payment_term_expire_automatically',
|
||||
'payment_term_accept_late',
|
||||
'payment_explanation',
|
||||
@@ -589,6 +608,7 @@ class EventSettingsSerializer(serializers.Serializer):
|
||||
'invoice_numbers_consecutive',
|
||||
'invoice_numbers_prefix',
|
||||
'invoice_numbers_prefix_cancellations',
|
||||
'invoice_numbers_counter_length',
|
||||
'invoice_attendee_name',
|
||||
'invoice_include_expire_date',
|
||||
'invoice_address_explanation_text',
|
||||
@@ -603,6 +623,7 @@ class EventSettingsSerializer(serializers.Serializer):
|
||||
'invoice_introductory_text',
|
||||
'invoice_additional_text',
|
||||
'invoice_footer_text',
|
||||
'invoice_eu_currencies',
|
||||
'cancel_allow_user',
|
||||
'cancel_allow_user_until',
|
||||
'cancel_allow_user_paid',
|
||||
@@ -610,6 +631,13 @@ class EventSettingsSerializer(serializers.Serializer):
|
||||
'cancel_allow_user_paid_keep',
|
||||
'cancel_allow_user_paid_keep_fees',
|
||||
'cancel_allow_user_paid_keep_percentage',
|
||||
'cancel_allow_user_paid_adjust_fees',
|
||||
'cancel_allow_user_paid_adjust_fees_explanation',
|
||||
'cancel_allow_user_paid_refund_as_giftcard',
|
||||
'cancel_allow_user_paid_require_approval',
|
||||
'change_allow_user_variation',
|
||||
'change_allow_user_until',
|
||||
'change_allow_user_price',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -617,9 +645,13 @@ class EventSettingsSerializer(serializers.Serializer):
|
||||
super().__init__(*args, **kwargs)
|
||||
for fname in self.default_fields:
|
||||
kwargs = DEFAULTS[fname].get('serializer_kwargs', {})
|
||||
if callable(kwargs):
|
||||
kwargs = kwargs()
|
||||
kwargs.setdefault('required', False)
|
||||
kwargs.setdefault('allow_null', True)
|
||||
form_kwargs = DEFAULTS[fname].get('form_kwargs', {})
|
||||
if callable(form_kwargs):
|
||||
form_kwargs = form_kwargs()
|
||||
if 'serializer_class' not in DEFAULTS[fname]:
|
||||
raise ValidationError('{} has no serializer class'.format(fname))
|
||||
f = DEFAULTS[fname]['serializer_class'](
|
||||
@@ -648,3 +680,40 @@ class EventSettingsSerializer(serializers.Serializer):
|
||||
settings_dict.update(data)
|
||||
validate_settings(self.event, settings_dict)
|
||||
return data
|
||||
|
||||
|
||||
class DeviceEventSettingsSerializer(EventSettingsSerializer):
|
||||
default_fields = [
|
||||
'locales',
|
||||
'locale',
|
||||
'last_order_modification_date',
|
||||
'show_quota_left',
|
||||
'max_items_per_order',
|
||||
'attendee_names_asked',
|
||||
'attendee_names_required',
|
||||
'attendee_emails_asked',
|
||||
'attendee_emails_required',
|
||||
'attendee_addresses_asked',
|
||||
'attendee_addresses_required',
|
||||
'attendee_company_asked',
|
||||
'attendee_company_required',
|
||||
'ticket_download',
|
||||
'ticket_download_addons',
|
||||
'ticket_download_nonadm',
|
||||
'ticket_download_pending',
|
||||
'invoice_address_asked',
|
||||
'invoice_address_required',
|
||||
'invoice_address_vatid',
|
||||
'invoice_address_company_required',
|
||||
'invoice_address_beneficiary',
|
||||
'invoice_address_custom_field',
|
||||
'invoice_name_required',
|
||||
'invoice_address_not_asked_free',
|
||||
'invoice_address_from_name',
|
||||
'invoice_address_from',
|
||||
'invoice_address_from_zipcode',
|
||||
'invoice_address_from_city',
|
||||
'invoice_address_from_country',
|
||||
'invoice_address_from_tax_id',
|
||||
'invoice_address_from_vat_id',
|
||||
]
|
||||
|
||||
29
src/pretix/api/serializers/fields.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
def remove_duplicates_from_list(data):
|
||||
return list(OrderedDict.fromkeys(data))
|
||||
|
||||
|
||||
class ListMultipleChoiceField(serializers.MultipleChoiceField):
|
||||
def to_internal_value(self, data):
|
||||
if isinstance(data, str) or not hasattr(data, '__iter__'):
|
||||
self.fail('not_a_list', input_type=type(data).__name__)
|
||||
if not self.allow_empty and len(data) == 0:
|
||||
self.fail('empty')
|
||||
|
||||
internal_value_data = [
|
||||
super(serializers.MultipleChoiceField, self).to_internal_value(item)
|
||||
for item in data
|
||||
]
|
||||
|
||||
return remove_duplicates_from_list(internal_value_data)
|
||||
|
||||
def to_representation(self, value):
|
||||
representation_data = [
|
||||
self.choice_strings_to_values.get(str(item), item) for item in value
|
||||
]
|
||||
|
||||
return remove_duplicates_from_list(representation_data)
|
||||
@@ -3,7 +3,7 @@ from decimal import Decimal
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from pretix.api.serializers.event import MetaDataField
|
||||
@@ -45,7 +45,7 @@ class InlineItemAddOnSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ItemAddOn
|
||||
fields = ('addon_category', 'min_count', 'max_count',
|
||||
'position', 'price_included')
|
||||
'position', 'price_included', 'multi_allowed')
|
||||
|
||||
|
||||
class ItemBundleSerializer(serializers.ModelSerializer):
|
||||
@@ -77,7 +77,7 @@ class ItemAddOnSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ItemAddOn
|
||||
fields = ('id', 'addon_category', 'min_count', 'max_count',
|
||||
'position', 'price_included')
|
||||
'position', 'price_included', 'multi_allowed')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -287,8 +287,8 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
if value:
|
||||
if value.type not in (Question.TYPE_CHOICE, Question.TYPE_BOOLEAN, Question.TYPE_CHOICE_MULTIPLE):
|
||||
raise ValidationError('Question dependencies can only be set to boolean or choice questions.')
|
||||
if value == self.instance:
|
||||
raise ValidationError('A question cannot depend on itself.')
|
||||
if value == self.instance:
|
||||
raise ValidationError('A question cannot depend on itself.')
|
||||
return value
|
||||
|
||||
def validate(self, data):
|
||||
@@ -349,7 +349,7 @@ class QuotaSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Quota
|
||||
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent', 'closed', 'close_when_sold_out')
|
||||
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent', 'closed', 'close_when_sold_out', 'release_after_exit')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
@@ -5,7 +5,7 @@ from decimal import Decimal
|
||||
import pycountry
|
||||
from django.db.models import F, Q
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy
|
||||
from django.utils.translation import gettext_lazy
|
||||
from django_countries.fields import Country
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
@@ -39,7 +39,7 @@ class CompatibleCountryField(serializers.Field):
|
||||
def to_representation(self, instance: InvoiceAddress):
|
||||
if instance.country:
|
||||
return str(instance.country)
|
||||
else:
|
||||
elif hasattr(instance, 'country_old'):
|
||||
return instance.country_old
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
||||
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')):
|
||||
if not pycountry.countries.get(alpha_2=data.get('country').code):
|
||||
raise ValidationError(
|
||||
{'country': ['Invalid country code.']}
|
||||
)
|
||||
@@ -122,7 +122,7 @@ class AnswerSerializer(I18nAwareModelSerializer):
|
||||
class CheckinSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = Checkin
|
||||
fields = ('datetime', 'list', 'auto_checked_in')
|
||||
fields = ('datetime', 'list', 'auto_checked_in', 'type')
|
||||
|
||||
|
||||
class OrderDownloadsField(serializers.Field):
|
||||
@@ -211,10 +211,12 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
||||
pdf_data = PdfDataSerializer(source='*')
|
||||
seat = InlineSeatSerializer(read_only=True)
|
||||
country = CompatibleCountryField(source='*')
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled')
|
||||
|
||||
@@ -268,8 +270,9 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'require_attention',
|
||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'require_attention',
|
||||
'order__status')
|
||||
|
||||
|
||||
@@ -374,6 +377,14 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
if not self.context['request'].query_params.get('pdf_data', 'false') == 'true':
|
||||
self.fields['positions'].child.fields.pop('pdf_data')
|
||||
|
||||
for exclude_field in self.context['request'].query_params.getlist('exclude'):
|
||||
p = exclude_field.split('.')
|
||||
if p[0] in self.fields:
|
||||
if len(p) == 1:
|
||||
del self.fields[p[0]]
|
||||
elif len(p) == 2:
|
||||
self.fields[p[0]].child.fields.pop(p[1])
|
||||
|
||||
def validate_locale(self, l):
|
||||
if l not in set(k for k in self.instance.event.settings.locales):
|
||||
raise ValidationError('"{}" is not a supported locale for this event.'.format(l))
|
||||
@@ -414,16 +425,26 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
return instance
|
||||
|
||||
|
||||
class SimulatedOrderPositionSerializer(OrderPositionSerializer):
|
||||
addon_to = serializers.SlugRelatedField(read_only=True, slug_field='positionid')
|
||||
|
||||
|
||||
class SimulatedOrderSerializer(OrderSerializer):
|
||||
positions = SimulatedOrderPositionSerializer(many=True, read_only=True)
|
||||
|
||||
|
||||
class PriceCalcSerializer(serializers.Serializer):
|
||||
item = serializers.PrimaryKeyRelatedField(queryset=Item.objects.none(), required=False, allow_null=True)
|
||||
variation = serializers.PrimaryKeyRelatedField(queryset=ItemVariation.objects.none(), required=False, allow_null=True)
|
||||
subevent = serializers.PrimaryKeyRelatedField(queryset=SubEvent.objects.none(), required=False, allow_null=True)
|
||||
tax_rule = serializers.PrimaryKeyRelatedField(queryset=TaxRule.objects.none(), required=False, allow_null=True)
|
||||
locale = serializers.CharField(allow_null=True, required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
event = kwargs.pop('event')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['item'].queryset = event.items.all()
|
||||
self.fields['tax_rule'].queryset = event.tax_rules.all()
|
||||
self.fields['variation'].queryset = ItemVariation.objects.filter(item__event=event)
|
||||
if event.has_subevents:
|
||||
self.fields['subevent'].queryset = event.subevents.all()
|
||||
@@ -516,12 +537,22 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
max_digits=10)
|
||||
voucher = serializers.SlugRelatedField(slug_field='code', queryset=Voucher.objects.none(),
|
||||
required=False, allow_null=True)
|
||||
country = CompatibleCountryField(source='*')
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for k, v in self.fields.items():
|
||||
if k in ('company', 'street', 'zipcode', 'city', 'country', 'state'):
|
||||
v.required = False
|
||||
v.allow_blank = True
|
||||
v.allow_null = True
|
||||
|
||||
def validate_secret(self, secret):
|
||||
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
|
||||
raise ValidationError(
|
||||
@@ -576,6 +607,24 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
)
|
||||
if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'):
|
||||
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
|
||||
|
||||
if data.get('country'):
|
||||
if not pycountry.countries.get(alpha_2=data.get('country').code):
|
||||
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
|
||||
|
||||
|
||||
@@ -721,7 +770,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
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.')
|
||||
raise ValidationError('This sales channel does not provide support for test mode.')
|
||||
except KeyError:
|
||||
# We do not need to raise a ValidationError here, since there is another check to validate the
|
||||
# sales_channel
|
||||
@@ -862,7 +911,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
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)]
|
||||
errs[i]['seat'] = [gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
|
||||
seats_seen.add(seat)
|
||||
elif seated:
|
||||
errs[i]['seat'] = ['The specified product requires to choose a seat.']
|
||||
@@ -873,11 +922,24 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
if pos_data['voucher'].allow_ignore_quota or pos_data['voucher'].block_quota:
|
||||
continue
|
||||
|
||||
if pos_data.get('subevent'):
|
||||
if pos_data.get('item').pk in pos_data['subevent'].item_overrides and pos_data['subevent'].item_overrides[pos_data['item'].pk].disabled:
|
||||
errs[i]['item'] = [gettext_lazy('The product "{}" is not available on this date.').format(
|
||||
str(pos_data.get('item'))
|
||||
)]
|
||||
if (
|
||||
pos_data.get('variation') and pos_data['variation'].pk in pos_data['subevent'].var_overrides and
|
||||
pos_data['subevent'].var_overrides[pos_data['variation'].pk].disabled
|
||||
):
|
||||
errs[i]['item'] = [gettext_lazy('The product "{}" is not available on this date.').format(
|
||||
str(pos_data.get('item'))
|
||||
)]
|
||||
|
||||
new_quotas = (pos_data.get('variation').quotas.filter(subevent=pos_data.get('subevent'))
|
||||
if pos_data.get('variation')
|
||||
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
|
||||
if len(new_quotas) == 0:
|
||||
errs[i]['item'] = [ugettext_lazy('The product "{}" is not assigned to a quota.').format(
|
||||
errs[i]['item'] = [gettext_lazy('The product "{}" is not assigned to a quota.').format(
|
||||
str(pos_data.get('item'))
|
||||
)]
|
||||
else:
|
||||
@@ -889,7 +951,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
quota_avail_cache[quota][1] -= 1
|
||||
if quota_avail_cache[quota][1] < 0:
|
||||
errs[i]['item'] = [
|
||||
ugettext_lazy('There is not enough quota available on quota "{}" to perform the operation.').format(
|
||||
gettext_lazy('There is not enough quota available on quota "{}" to perform the operation.').format(
|
||||
quota.name
|
||||
)
|
||||
]
|
||||
@@ -933,7 +995,10 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
else:
|
||||
pos.order = order
|
||||
if addon_to:
|
||||
pos.addon_to = pos_map[addon_to]
|
||||
if simulate:
|
||||
pos.addon_to = pos_map[addon_to]._wrapped
|
||||
else:
|
||||
pos.addon_to = pos_map[addon_to]
|
||||
|
||||
if pos.price is None:
|
||||
price = get_price(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import get_language, ugettext_lazy as _
|
||||
from django.utils.translation import get_language, gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
@@ -55,7 +55,7 @@ class GiftCardSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = GiftCard
|
||||
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode')
|
||||
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions')
|
||||
|
||||
|
||||
class EventSlugField(serializers.SlugRelatedField):
|
||||
|
||||
@@ -6,6 +6,7 @@ from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.api.models import ApiCall, WebHookCall
|
||||
from pretix.base.signals import periodic_task
|
||||
from pretix.helpers.periodic import minimum_interval
|
||||
|
||||
register_webhook_events = Signal(
|
||||
providing_args=[]
|
||||
@@ -19,11 +20,13 @@ instances.
|
||||
|
||||
@receiver(periodic_task)
|
||||
@scopes_disabled()
|
||||
@minimum_interval(minutes_after_success=12 * 60)
|
||||
def cleanup_webhook_logs(sender, **kwargs):
|
||||
WebHookCall.objects.filter(datetime__lte=now() - timedelta(days=30)).delete()
|
||||
|
||||
|
||||
@receiver(periodic_task)
|
||||
@scopes_disabled()
|
||||
@minimum_interval(minutes_after_success=12 * 60)
|
||||
def cleanup_api_logs(sender, **kwargs):
|
||||
ApiCall.objects.filter(created__lte=now() - timedelta(hours=24)).delete()
|
||||
|
||||
@@ -41,8 +41,8 @@ class ConditionalListView:
|
||||
return super().list(request, **kwargs)
|
||||
|
||||
lmd = request.event.logentry_set.filter(
|
||||
content_type__model=self.queryset.model._meta.model_name,
|
||||
content_type__app_label=self.queryset.model._meta.app_label,
|
||||
content_type__model=self.get_queryset().model._meta.model_name,
|
||||
content_type__app_label=self.get_queryset().model._meta.app_label,
|
||||
).aggregate(
|
||||
m=Max('datetime')
|
||||
)['m']
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import django_filters
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Count, F, Max, OuterRef, Prefetch, Q, Subquery
|
||||
from django.db.models.functions import Coalesce
|
||||
@@ -27,10 +28,17 @@ from pretix.helpers.database import FixedOrderBy
|
||||
|
||||
with scopes_disabled():
|
||||
class CheckinListFilter(FilterSet):
|
||||
subevent_match = django_filters.NumberFilter(method='subevent_match_qs')
|
||||
|
||||
class Meta:
|
||||
model = CheckinList
|
||||
fields = ['subevent']
|
||||
|
||||
def subevent_match_qs(self, qs, name, value):
|
||||
return qs.filter(
|
||||
Q(subevent_id=value) | Q(subevent_id__isnull=True)
|
||||
)
|
||||
|
||||
|
||||
class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = CheckinListSerializer
|
||||
@@ -88,8 +96,9 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
pqs = OrderPosition.objects.filter(
|
||||
order__event=clist.event,
|
||||
order__status__in=[Order.STATUS_PAID] + ([Order.STATUS_PENDING] if clist.include_pending else []),
|
||||
subevent=clist.subevent,
|
||||
)
|
||||
if clist.subevent:
|
||||
pqs = pqs.filter(subevent=clist.subevent)
|
||||
if not clist.all_products:
|
||||
pqs = pqs.filter(item__in=clist.limit_products.values_list('id', flat=True))
|
||||
cqs = cqs.filter(position__item__in=clist.limit_products.values_list('id', flat=True))
|
||||
@@ -191,7 +200,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
except ValueError:
|
||||
raise Http404()
|
||||
|
||||
def get_queryset(self, ignore_status=False):
|
||||
def get_queryset(self, ignore_status=False, ignore_products=False):
|
||||
cqs = Checkin.objects.filter(
|
||||
position_id=OuterRef('pk'),
|
||||
list_id=self.checkinlist.pk
|
||||
@@ -201,10 +210,13 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
|
||||
qs = OrderPosition.objects.filter(
|
||||
order__event=self.request.event,
|
||||
subevent=self.checkinlist.subevent
|
||||
).annotate(
|
||||
last_checked_in=Subquery(cqs)
|
||||
)
|
||||
if self.checkinlist.subevent:
|
||||
qs = qs.filter(
|
||||
subevent=self.checkinlist.subevent
|
||||
)
|
||||
|
||||
if self.request.query_params.get('ignore_status', 'false') != 'true' and not ignore_status:
|
||||
qs = qs.filter(
|
||||
@@ -243,23 +255,40 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
|
||||
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat')
|
||||
|
||||
if not self.checkinlist.all_products:
|
||||
if not self.checkinlist.all_products and not ignore_products:
|
||||
qs = qs.filter(item__in=self.checkinlist.limit_products.values_list('id', flat=True))
|
||||
|
||||
return qs
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
@action(detail=False, methods=['POST'], url_name='redeem', url_path='(?P<pk>[^/]+)/redeem')
|
||||
def redeem(self, *args, **kwargs):
|
||||
force = bool(self.request.data.get('force', False))
|
||||
type = self.request.data.get('type', None) or Checkin.TYPE_ENTRY
|
||||
if type not in dict(Checkin.CHECKIN_TYPES):
|
||||
raise ValidationError("Invalid check-in type.")
|
||||
ignore_unpaid = bool(self.request.data.get('ignore_unpaid', False))
|
||||
nonce = self.request.data.get('nonce')
|
||||
op = self.get_object(ignore_status=True)
|
||||
|
||||
if 'datetime' in self.request.data:
|
||||
dt = DateTimeField().to_internal_value(self.request.data.get('datetime'))
|
||||
else:
|
||||
dt = now()
|
||||
|
||||
try:
|
||||
queryset = self.get_queryset(ignore_status=True, ignore_products=True)
|
||||
if self.kwargs['pk'].isnumeric():
|
||||
op = queryset.get(Q(pk=self.kwargs['pk']) | Q(secret=self.kwargs['pk']))
|
||||
else:
|
||||
op = queryset.get(secret=self.kwargs['pk'])
|
||||
except OrderPosition.DoesNotExist:
|
||||
self.request.event.log_action('pretix.event.checkin.unknown', data={
|
||||
'datetime': dt,
|
||||
'type': type,
|
||||
'list': self.checkinlist.pk,
|
||||
'barcode': self.kwargs['pk']
|
||||
}, user=self.request.user, auth=self.request.auth)
|
||||
raise Http404()
|
||||
|
||||
given_answers = {}
|
||||
if 'answers' in self.request.data:
|
||||
aws = self.request.data.get('answers')
|
||||
@@ -283,6 +312,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
canceled_supported=self.request.data.get('canceled_supported', False),
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
type=type,
|
||||
)
|
||||
except RequiredQuestionsError as e:
|
||||
return Response({
|
||||
@@ -294,6 +324,14 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
]
|
||||
}, status=400)
|
||||
except CheckInError as e:
|
||||
op.order.log_action('pretix.event.checkin.denied', data={
|
||||
'position': op.id,
|
||||
'positionid': op.positionid,
|
||||
'errorcode': e.code,
|
||||
'datetime': dt,
|
||||
'type': type,
|
||||
'list': self.checkinlist.pk
|
||||
}, user=self.request.user, auth=self.request.auth)
|
||||
return Response({
|
||||
'status': 'error',
|
||||
'reason': e.code,
|
||||
@@ -306,11 +344,3 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
|
||||
'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data
|
||||
}, status=201)
|
||||
|
||||
def get_object(self, ignore_status=False):
|
||||
queryset = self.filter_queryset(self.get_queryset(ignore_status=ignore_status))
|
||||
if self.kwargs['pk'].isnumeric():
|
||||
obj = get_object_or_404(queryset, Q(pk=self.kwargs['pk']) | Q(secret=self.kwargs['pk']))
|
||||
else:
|
||||
obj = get_object_or_404(queryset, secret=self.kwargs['pk'])
|
||||
return obj
|
||||
|
||||
@@ -10,8 +10,8 @@ from rest_framework.response import Response
|
||||
|
||||
from pretix.api.auth.permission import EventCRUDPermission
|
||||
from pretix.api.serializers.event import (
|
||||
CloneEventSerializer, EventSerializer, EventSettingsSerializer,
|
||||
SubEventSerializer, TaxRuleSerializer,
|
||||
CloneEventSerializer, DeviceEventSettingsSerializer, EventSerializer,
|
||||
EventSettingsSerializer, SubEventSerializer, TaxRuleSerializer,
|
||||
)
|
||||
from pretix.api.views import ConditionalListView
|
||||
from pretix.base.models import (
|
||||
@@ -337,10 +337,16 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
|
||||
|
||||
class EventSettingsView(views.APIView):
|
||||
permission = 'can_change_event_settings'
|
||||
permission = None
|
||||
write_permission = 'can_change_event_settings'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
s = EventSettingsSerializer(instance=request.event.settings, event=request.event)
|
||||
if isinstance(request.auth, Device):
|
||||
s = DeviceEventSettingsSerializer(instance=request.event.settings, event=request.event)
|
||||
elif 'can_change_event_settings' in request.eventpermset:
|
||||
s = EventSettingsSerializer(instance=request.event.settings, event=request.event)
|
||||
else:
|
||||
raise PermissionDenied()
|
||||
if 'explain' in request.GET:
|
||||
return Response({
|
||||
fname: {
|
||||
|
||||
@@ -20,6 +20,7 @@ from pretix.base.models import (
|
||||
CartPosition, Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation,
|
||||
Question, QuestionOption, Quota,
|
||||
)
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
|
||||
with scopes_disabled():
|
||||
@@ -533,14 +534,18 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
def availability(self, request, *args, **kwargs):
|
||||
quota = self.get_object()
|
||||
|
||||
avail = quota.availability()
|
||||
qa = QuotaAvailability()
|
||||
qa.queue(quota)
|
||||
qa.compute()
|
||||
avail = qa.results[quota]
|
||||
|
||||
data = {
|
||||
'paid_orders': quota.count_paid_orders(),
|
||||
'pending_orders': quota.count_pending_orders(),
|
||||
'blocking_vouchers': quota.count_blocking_vouchers(),
|
||||
'cart_positions': quota.count_in_cart(),
|
||||
'waiting_list': quota.count_waiting_list_pending(),
|
||||
'paid_orders': qa.count_paid_orders[quota],
|
||||
'pending_orders': qa.count_pending_orders[quota],
|
||||
'exited_orders': qa.count_exited_orders[quota],
|
||||
'blocking_vouchers': qa.count_vouchers[quota],
|
||||
'cart_positions': qa.count_cart[quota],
|
||||
'waiting_list': qa.count_pending_orders[quota],
|
||||
'available_number': avail[1],
|
||||
'available': avail[0] == Quota.AVAILABILITY_OK,
|
||||
'total_size': quota.size,
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from oauth2_provider.exceptions import OAuthToolkitError
|
||||
from oauth2_provider.forms import AllowForm
|
||||
from oauth2_provider.views import (
|
||||
|
||||
@@ -4,12 +4,12 @@ from decimal import Decimal
|
||||
import django_filters
|
||||
import pytz
|
||||
from django.db import transaction
|
||||
from django.db.models import F, Prefetch, Q
|
||||
from django.db.models import Exists, F, OuterRef, Prefetch, Q
|
||||
from django.db.models.functions import Coalesce, Concat
|
||||
from django.http import FileResponse, HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import mixins, serializers, status, viewsets
|
||||
@@ -26,12 +26,12 @@ from pretix.api.serializers.order import (
|
||||
InvoiceSerializer, OrderCreateSerializer, OrderPaymentCreateSerializer,
|
||||
OrderPaymentSerializer, OrderPositionSerializer,
|
||||
OrderRefundCreateSerializer, OrderRefundSerializer, OrderSerializer,
|
||||
PriceCalcSerializer,
|
||||
PriceCalcSerializer, SimulatedOrderSerializer,
|
||||
)
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedCombinedTicket, CachedTicket, Device, Event, Invoice, InvoiceAddress,
|
||||
Order, OrderFee, OrderPayment, OrderPosition, OrderRefund, Quota,
|
||||
Order, OrderFee, OrderPayment, OrderPosition, OrderRefund, Quota, SubEvent,
|
||||
TeamAPIToken, generate_position_secret, generate_secret,
|
||||
)
|
||||
from pretix.base.payment import PaymentException
|
||||
@@ -44,7 +44,7 @@ from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.services.orders import (
|
||||
OrderChangeManager, OrderError, _order_placed_email,
|
||||
_order_placed_email_attendee, approve_order, cancel_order, deny_order,
|
||||
extend_order, mark_order_expired, mark_order_refunded,
|
||||
extend_order, mark_order_expired, mark_order_refunded, reactivate_order,
|
||||
)
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.base.services.tickets import generate
|
||||
@@ -52,6 +52,7 @@ from pretix.base.signals import (
|
||||
order_modified, order_paid, order_placed, register_ticket_outputs,
|
||||
)
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.control.signals import order_search_filter_q
|
||||
|
||||
with scopes_disabled():
|
||||
class OrderFilter(FilterSet):
|
||||
@@ -60,11 +61,62 @@ with scopes_disabled():
|
||||
status = django_filters.CharFilter(field_name='status', lookup_expr='iexact')
|
||||
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
|
||||
created_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte')
|
||||
subevent_after = django_filters.IsoDateTimeFilter(method='subevent_after_qs')
|
||||
search = django_filters.CharFilter(method='search_qs')
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ['code', 'status', 'email', 'locale', 'testmode', 'require_approval']
|
||||
|
||||
def subevent_after_qs(self, qs, name, value):
|
||||
qs = qs.annotate(
|
||||
has_se_after=Exists(
|
||||
OrderPosition.all.filter(
|
||||
subevent_id__in=SubEvent.objects.filter(
|
||||
Q(date_to__gt=value) | Q(date_from__gt=value, date_to__isnull=True), event=OuterRef(OuterRef('event_id'))
|
||||
).values_list('id'),
|
||||
order_id=OuterRef('pk'),
|
||||
)
|
||||
)
|
||||
).filter(has_se_after=True)
|
||||
return qs
|
||||
|
||||
def search_qs(self, qs, name, value):
|
||||
u = value
|
||||
if "-" in value:
|
||||
code = (Q(event__slug__icontains=u.rsplit("-", 1)[0])
|
||||
& Q(code__icontains=Order.normalize_code(u.rsplit("-", 1)[1])))
|
||||
else:
|
||||
code = Q(code__icontains=Order.normalize_code(u))
|
||||
|
||||
matching_invoices = Invoice.objects.filter(
|
||||
Q(invoice_no__iexact=u)
|
||||
| Q(invoice_no__iexact=u.zfill(5))
|
||||
| Q(full_invoice_no__iexact=u)
|
||||
).values_list('order_id', flat=True)
|
||||
|
||||
matching_positions = OrderPosition.objects.filter(
|
||||
Q(order=OuterRef('pk')) & Q(
|
||||
Q(attendee_name_cached__icontains=u) | Q(attendee_email__icontains=u)
|
||||
| Q(secret__istartswith=u) | Q(voucher__code__icontains=u)
|
||||
)
|
||||
).values('id')
|
||||
|
||||
mainq = (
|
||||
code
|
||||
| Q(email__icontains=u)
|
||||
| Q(invoice_address__name_cached__icontains=u)
|
||||
| Q(invoice_address__company__icontains=u)
|
||||
| Q(pk__in=matching_invoices)
|
||||
| Q(comment__icontains=u)
|
||||
| Q(has_pos=True)
|
||||
)
|
||||
for recv, q in order_search_filter_q.send(sender=getattr(self, 'event', None), query=u):
|
||||
mainq = mainq | q
|
||||
return qs.annotate(has_pos=Exists(matching_positions)).filter(
|
||||
mainq
|
||||
)
|
||||
|
||||
|
||||
class OrderViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = OrderSerializer
|
||||
@@ -83,16 +135,19 @@ 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(
|
||||
Prefetch('fees', queryset=fqs.all()),
|
||||
'payments', 'refunds', 'refunds__payment'
|
||||
).select_related(
|
||||
'invoice_address'
|
||||
)
|
||||
qs = self.request.event.orders
|
||||
if 'fees' not in self.request.GET.getlist('exclude'):
|
||||
if self.request.query_params.get('include_canceled_fees', 'false') == 'true':
|
||||
fqs = OrderFee.all
|
||||
else:
|
||||
fqs = OrderFee.objects
|
||||
qs = qs.prefetch_related(Prefetch('fees', queryset=fqs.all()))
|
||||
if 'payments' not in self.request.GET.getlist('exclude'):
|
||||
qs = qs.prefetch_related('payments')
|
||||
if 'refunds' not in self.request.GET.getlist('exclude'):
|
||||
qs = qs.prefetch_related('refunds', 'refunds__payment')
|
||||
if 'invoice_address' not in self.request.GET.getlist('exclude'):
|
||||
qs = qs.select_related('invoice_address')
|
||||
|
||||
if self.request.query_params.get('include_canceled_positions', 'false') == 'true':
|
||||
opq = OrderPosition.all
|
||||
@@ -129,6 +184,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
return prov
|
||||
raise NotFound('Unknown output provider.')
|
||||
|
||||
@scopes_disabled() # we are sure enough that get_queryset() is correct, so we save some perforamnce
|
||||
def list(self, request, **kwargs):
|
||||
date = serializers.DateTimeField().to_representation(now())
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
@@ -261,6 +317,29 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
def reactivate(self, request, **kwargs):
|
||||
|
||||
order = self.get_object()
|
||||
if order.status != Order.STATUS_CANCELED:
|
||||
return Response(
|
||||
{'detail': 'The order is not allowed to be reactivated.'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
try:
|
||||
reactivate_order(
|
||||
order,
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None,
|
||||
)
|
||||
except OrderError as e:
|
||||
return Response(
|
||||
{'detail': str(e)},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
def approve(self, request, **kwargs):
|
||||
send_mail = request.data.get('send_email', True)
|
||||
@@ -465,10 +544,12 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
self.perform_create(serializer)
|
||||
send_mail = serializer._send_mail
|
||||
order = serializer.instance
|
||||
serializer = OrderSerializer(order, context=serializer.context)
|
||||
if not order.pk:
|
||||
# Simulation
|
||||
serializer = SimulatedOrderSerializer(order, context=serializer.context)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
else:
|
||||
serializer = OrderSerializer(order, context=serializer.context)
|
||||
|
||||
order.log_action(
|
||||
'pretix.event.order.placed',
|
||||
@@ -720,7 +801,8 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
|
||||
{
|
||||
"item": 2,
|
||||
"variation": null,
|
||||
"subevent": 3
|
||||
"subevent": 3,
|
||||
"tax_rule": 4,
|
||||
}
|
||||
|
||||
Sample output:
|
||||
@@ -774,7 +856,11 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
|
||||
if data.get('subevent'):
|
||||
kwargs['subevent'] = data.get('subevent')
|
||||
|
||||
if data.get('tax_rule'):
|
||||
kwargs['tax_rule'] = data.get('tax_rule')
|
||||
|
||||
price = get_price(**kwargs)
|
||||
tr = kwargs.get('tax_rule', kwargs.get('item').tax_rule)
|
||||
with language(data.get('locale') or self.request.event.settings.locale):
|
||||
return Response({
|
||||
'gross': price.gross,
|
||||
@@ -783,6 +869,7 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
|
||||
'rate': price.rate,
|
||||
'name': str(price.name),
|
||||
'tax': price.tax,
|
||||
'tax_rule': tr.pk if tr else None,
|
||||
})
|
||||
|
||||
@action(detail=True, url_name='download', url_path='download/(?P<output>[^/]+)')
|
||||
|
||||
@@ -58,7 +58,7 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
|
||||
write_permission = 'can_change_organizer_settings'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.organizer.seating_plans.all()
|
||||
return self.request.organizer.seating_plans.order_by('name')
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
@@ -195,7 +195,7 @@ class TeamViewSet(viewsets.ModelViewSet):
|
||||
write_permission = 'can_change_teams'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.organizer.teams.all()
|
||||
return self.request.organizer.teams.order_by('pk')
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
@@ -268,7 +268,7 @@ class TeamInviteViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyMo
|
||||
return get_object_or_404(self.request.organizer.teams, pk=self.kwargs.get('team'))
|
||||
|
||||
def get_queryset(self):
|
||||
return self.team.invites.all()
|
||||
return self.team.invites.order_by('email')
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
@@ -305,7 +305,7 @@ class TeamAPITokenViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
|
||||
return get_object_or_404(self.request.organizer.teams, pk=self.kwargs.get('team'))
|
||||
|
||||
def get_queryset(self):
|
||||
return self.team.tokens.all()
|
||||
return self.team.tokens.order_by('name')
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
|
||||
@@ -7,7 +7,7 @@ import requests
|
||||
from celery.exceptions import MaxRetriesExceededError
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import scope, scopes_disabled
|
||||
from requests import RequestException
|
||||
|
||||
@@ -125,6 +125,10 @@ def register_default_webhook_events(sender, **kwargs):
|
||||
'pretix.event.order.canceled',
|
||||
_('Order canceled'),
|
||||
),
|
||||
ParametrizedOrderWebhookEvent(
|
||||
'pretix.event.order.reactivated',
|
||||
_('Order reactivated'),
|
||||
),
|
||||
ParametrizedOrderWebhookEvent(
|
||||
'pretix.event.order.expired',
|
||||
_('Order expired'),
|
||||
@@ -164,9 +168,9 @@ def register_default_webhook_events(sender, **kwargs):
|
||||
)
|
||||
|
||||
|
||||
@app.task(base=TransactionAwareTask)
|
||||
@app.task(base=TransactionAwareTask, acks_late=True)
|
||||
def notify_webhooks(logentry_id: int):
|
||||
logentry = LogEntry.all.get(id=logentry_id)
|
||||
logentry = LogEntry.all.select_related('event', 'event__organizer').get(id=logentry_id)
|
||||
|
||||
if not logentry.organizer:
|
||||
return # We need to know the organizer
|
||||
@@ -201,7 +205,7 @@ def notify_webhooks(logentry_id: int):
|
||||
send_webhook.apply_async(args=(logentry_id, notification_type.action_type, wh.pk))
|
||||
|
||||
|
||||
@app.task(base=ProfiledTask, bind=True, max_retries=9)
|
||||
@app.task(base=ProfiledTask, bind=True, max_retries=9, acks_late=True)
|
||||
def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int):
|
||||
# 9 retries with 2**(2*x) timing is roughly 72 hours
|
||||
with scopes_disabled():
|
||||
|
||||
@@ -85,6 +85,16 @@ class BaseAuthBackend:
|
||||
"""
|
||||
return
|
||||
|
||||
def get_next_url(self, request):
|
||||
"""
|
||||
This method will be called after a successful login to determine the next URL. Pretix in general uses the
|
||||
``'next'`` query parameter. However, external authentication methods could use custom attributes with hardcoded
|
||||
names for security purposes. For example, OAuth uses ``'state'`` for keeping track of application state.
|
||||
"""
|
||||
if "next" in request.GET:
|
||||
return request.GET.get("next")
|
||||
return None
|
||||
|
||||
|
||||
class NativeAuthBackend(BaseAuthBackend):
|
||||
identifier = 'native'
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.signals import register_sales_channels
|
||||
|
||||
|
||||
@@ -9,10 +9,12 @@ from django.core.mail.backends.smtp import EmailBackend
|
||||
from django.dispatch import receiver
|
||||
from django.template.loader import get_template
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import get_language, ugettext_lazy as _
|
||||
from django.utils.translation import get_language, gettext_lazy as _
|
||||
from inlinestyler.utils import inline_css
|
||||
|
||||
from pretix.base.i18n import LazyCurrencyNumber, LazyDate, LazyNumber
|
||||
from pretix.base.i18n import (
|
||||
LazyCurrencyNumber, LazyDate, LazyExpiresDate, LazyNumber,
|
||||
)
|
||||
from pretix.base.models import Event
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.base.signals import (
|
||||
@@ -258,9 +260,30 @@ def _placeholder_payment(order, payment):
|
||||
return str(payment.payment_provider.order_pending_mail_render(order))
|
||||
|
||||
|
||||
def get_best_name(position_or_address, parts=False):
|
||||
"""
|
||||
Return the best name we got for either an invoice address or an order position, falling back to the respective other
|
||||
"""
|
||||
from pretix.base.models import InvoiceAddress, OrderPosition
|
||||
if isinstance(position_or_address, InvoiceAddress):
|
||||
if position_or_address.name:
|
||||
return position_or_address.name_parts if parts else position_or_address.name
|
||||
position_or_address = position_or_address.order.positions.exclude(attendee_name_cached="").exclude(attendee_name_cached__isnull=True).first()
|
||||
|
||||
if isinstance(position_or_address, OrderPosition):
|
||||
if position_or_address.attendee_name:
|
||||
return position_or_address.attendee_name_parts if parts else position_or_address.attendee_name
|
||||
elif position_or_address.order:
|
||||
try:
|
||||
return position_or_address.order.invoice_address.name_parts if parts else position_or_address.order.invoice_address.name
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
pass
|
||||
|
||||
return {} if parts else ""
|
||||
|
||||
|
||||
@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 = [
|
||||
@@ -294,9 +317,8 @@ def base_placeholders(sender, **kwargs):
|
||||
lambda event: LazyCurrencyNumber(Decimal('42.23'), event.currency)
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'expire_date', ['event', 'order'], lambda event, order: LazyDate(order.expires.astimezone(event.timezone)),
|
||||
'expire_date', ['event', 'order'], lambda event, order: LazyExpiresDate(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(
|
||||
@@ -315,6 +337,51 @@ def base_placeholders(sender, **kwargs):
|
||||
}
|
||||
),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'url_info_change', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.modify', kwargs={
|
||||
'order': order.code,
|
||||
'secret': order.secret,
|
||||
}
|
||||
), lambda event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.modify', kwargs={
|
||||
'order': 'F8VVL',
|
||||
'secret': '6zzjnumtsx136ddy',
|
||||
}
|
||||
),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'url_products_change', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.change', kwargs={
|
||||
'order': order.code,
|
||||
'secret': order.secret,
|
||||
}
|
||||
), lambda event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.change', kwargs={
|
||||
'order': 'F8VVL',
|
||||
'secret': '6zzjnumtsx136ddy',
|
||||
}
|
||||
),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'url_cancel', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.cancel', kwargs={
|
||||
'order': order.code,
|
||||
'secret': order.secret,
|
||||
}
|
||||
), lambda event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.cancel', kwargs={
|
||||
'order': 'F8VVL',
|
||||
'secret': '6zzjnumtsx136ddy',
|
||||
}
|
||||
),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'url', ['event', 'position'], lambda event, position: build_absolute_uri(
|
||||
event,
|
||||
@@ -429,11 +496,7 @@ def base_placeholders(sender, **kwargs):
|
||||
),
|
||||
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
|
||||
),
|
||||
get_best_name,
|
||||
_('John Doe'),
|
||||
),
|
||||
]
|
||||
@@ -448,11 +511,7 @@ def base_placeholders(sender, **kwargs):
|
||||
))
|
||||
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, '')
|
||||
),
|
||||
lambda position_or_address, f=f: get_best_name(position_or_address, parts=True).get(f, ''),
|
||||
name_scheme['sample'][f]
|
||||
))
|
||||
|
||||
|
||||