Compare commits

..

265 Commits

Author SHA1 Message Date
Raphael Michel
b66a35df7a Bump to 2.8.0 2019-06-05 16:13:37 +02:00
Raphael Michel
2e1347cf9a Update from Weblate (#1311)
Update from Weblate
2019-06-05 13:39:11 +02:00
Raphael Michel
8d1c9e44fc Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3123 of 3123 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de_Informal/

powered by weblate
2019-06-05 11:38:38 +00:00
Raphael Michel
a187a02daa Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3123 of 3123 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de/

powered by weblate
2019-06-05 11:38:37 +00:00
Raphael Michel
b9d100b5a8 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-06-05 12:21:46 +02:00
Raphael Michel
f1e097c1b1 Update from Weblate (#1309)
Update from Weblate
2019-06-05 12:20:14 +02:00
ThanosTeste
91a5b1546a Translated on translate.pretix.eu (Greek)
Currently translated at 100.0% (99 of 99 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/el/

powered by weblate
2019-06-05 07:18:57 +00:00
ThanosTeste
3532f9c5a9 Translated on translate.pretix.eu (Greek)
Currently translated at 100.0% (3116 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-06-05 07:18:57 +00:00
ThanosTeste
884e54180a Translated on translate.pretix.eu (Greek)
Currently translated at 100.0% (99 of 99 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/el/

powered by weblate
2019-06-05 07:18:57 +00:00
ThanosTeste
e17ddb0cc8 Translated on translate.pretix.eu (Greek)
Currently translated at 100.0% (3116 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-06-05 07:18:57 +00:00
Raphael Michel
55edc8a3d6 Order change interface: Fix taxation edge case 2019-06-05 09:18:03 +02:00
Raphael Michel
bd79a93737 Fix spelling error 2019-06-03 11:53:26 +02:00
Raphael Michel
12ab260eb1 Add documentation for billing_invoices API 2019-06-03 11:29:54 +02:00
Raphael Michel
30f0318de6 API: Add stable and configurable filtering to events and organizers endpoints 2019-06-03 10:19:16 +02:00
Raphael Michel
52e072e68f Dekodi export: Deal with raw Stripe sources
Fix PRETIXEU-14B
2019-06-03 09:59:51 +02:00
Raphael Michel
f25bb571b9 Typeahead: Fix ValueError introduced in b3436c1
Fix PRETIXEU-14A
2019-06-03 09:57:35 +02:00
Martin Gross
ae71492902 Assign flag for Greek (el) 2019-05-31 12:41:33 +02:00
Raphael Michel
57375eb9b6 Update from Weblate (#1308)
Update from Weblate
2019-05-31 10:50:55 +02:00
Raphael Michel
0657ef2e0c Add missing log descriptions 2019-05-31 10:50:10 +02:00
Raphael Michel
f63907fb16 Do not show infinitely long logs in sidebars 2019-05-31 10:49:57 +02:00
ThanosTeste
e266d3808f Translated on translate.pretix.eu (Greek)
Currently translated at 100.0% (99 of 99 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/el/

powered by weblate
2019-05-30 16:39:05 +00:00
ThanosTeste
180f9a356f Translated on translate.pretix.eu (Greek)
Currently translated at 100.0% (3116 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-30 16:39:05 +00:00
mapostolopoulou
480b71bd50 Translated on translate.pretix.eu (Greek)
Currently translated at 93.4% (2910 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-30 16:39:05 +00:00
ThanosTeste
79839e3735 Translated on translate.pretix.eu (Greek)
Currently translated at 93.4% (2910 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-30 16:39:05 +00:00
markiousi
6ba5c58556 Translated on translate.pretix.eu (Greek)
Currently translated at 93.4% (2910 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-30 16:39:05 +00:00
Raphael Michel
a5e3bab107 Fix missing argument in HTML email preview 2019-05-30 18:37:18 +02:00
Raphael Michel
4dcce70ab3 Fix doc spelling 2019-05-29 16:32:22 +02:00
Raphael Michel
8a5332f415 Promote Greek to an inofficial language 2019-05-29 16:09:53 +02:00
Raphael Michel
58ce1cbab7 Allow to configure locale path and incubating languages 2019-05-29 16:09:53 +02:00
Raphael Michel
27c3e5d875 Update from Weblate (#1305)
Update from Weblate
2019-05-29 15:57:18 +02:00
ThanosTeste
caac517c0d Translated on translate.pretix.eu (Greek)
Currently translated at 100.0% (99 of 99 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/el/

powered by weblate
2019-05-29 13:24:30 +00:00
mapostolopoulou
58b9052164 Translated on translate.pretix.eu (Greek)
Currently translated at 100.0% (99 of 99 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/el/

powered by weblate
2019-05-29 13:24:30 +00:00
ThanosTeste
2d223a9e11 Translated on translate.pretix.eu (Greek)
Currently translated at 96.0% (95 of 99 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/el/

powered by weblate
2019-05-29 13:22:27 +00:00
mapostolopoulou
fe37ab9286 Translated on translate.pretix.eu (Greek)
Currently translated at 96.0% (95 of 99 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/el/

powered by weblate
2019-05-29 13:22:08 +00:00
ThanosTeste
95cc661a05 Translated on translate.pretix.eu (Greek)
Currently translated at 96.0% (95 of 99 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/el/

powered by weblate
2019-05-29 13:22:08 +00:00
mapostolopoulou
9a98d16949 Translated on translate.pretix.eu (Greek)
Currently translated at 90.9% (90 of 99 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/el/

powered by weblate
2019-05-29 13:21:26 +00:00
ThanosTeste
50ba019a07 Translated on translate.pretix.eu (Greek)
Currently translated at 90.9% (90 of 99 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/el/

powered by weblate
2019-05-29 13:21:25 +00:00
ThanosTeste
7d3e9b1777 Translated on translate.pretix.eu (Greek)
Currently translated at 86.9% (86 of 99 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/el/

powered by weblate
2019-05-29 13:21:07 +00:00
ThanosTeste
f82640d763 Translated on translate.pretix.eu (Greek)
Currently translated at 86.9% (86 of 99 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/el/

powered by weblate
2019-05-29 13:21:07 +00:00
markiousi
d84cd71a5c Translated on translate.pretix.eu (Greek)
Currently translated at 83.8% (83 of 99 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/el/

powered by weblate
2019-05-29 13:19:25 +00:00
ThanosTeste
74105ddd53 Translated on translate.pretix.eu (Greek)
Currently translated at 83.8% (83 of 99 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/el/

powered by weblate
2019-05-29 13:19:25 +00:00
Andrikopoulos-Giannis
3a2f915ac9 Translated on translate.pretix.eu (Greek)
Currently translated at 92.3% (2877 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 13:14:52 +00:00
ThanosTeste
9024a552a9 Translated on translate.pretix.eu (Greek)
Currently translated at 92.3% (2877 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 13:14:51 +00:00
mapostolopoulou
bae9fab2c4 Translated on translate.pretix.eu (Greek)
Currently translated at 92.3% (2877 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 13:14:51 +00:00
markiousi
ee3cd6d465 Translated on translate.pretix.eu (Greek)
Currently translated at 92.3% (2877 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 13:14:50 +00:00
ThanosTeste
ccdcd380fa Translated on translate.pretix.eu (Greek)
Currently translated at 91.8% (2859 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 13:12:11 +00:00
mapostolopoulou
3c0f0434cd Translated on translate.pretix.eu (Greek)
Currently translated at 91.8% (2859 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 13:12:10 +00:00
markiousi
58dba57bef Translated on translate.pretix.eu (Greek)
Currently translated at 91.1% (2840 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 13:09:49 +00:00
mapostolopoulou
9178aef323 Translated on translate.pretix.eu (Greek)
Currently translated at 91.1% (2840 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 13:09:49 +00:00
ThanosTeste
e9d696ea5e Translated on translate.pretix.eu (Greek)
Currently translated at 91.1% (2840 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 13:09:49 +00:00
markiousi
983ffdd8a8 Translated on translate.pretix.eu (Greek)
Currently translated at 89.7% (2794 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 13:05:26 +00:00
mapostolopoulou
294d47ccfc Translated on translate.pretix.eu (Greek)
Currently translated at 89.7% (2794 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 13:05:26 +00:00
ThanosTeste
a14b1a5a14 Translated on translate.pretix.eu (Greek)
Currently translated at 89.7% (2794 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 13:05:23 +00:00
Andrikopoulos-Giannis
a28c5f71c9 Translated on translate.pretix.eu (Greek)
Currently translated at 73.5% (2290 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 11:52:18 +00:00
ThanosTeste
35bd9d1c22 Translated on translate.pretix.eu (Greek)
Currently translated at 73.5% (2290 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 11:52:18 +00:00
mapostolopoulou
b070fc0297 Translated on translate.pretix.eu (Greek)
Currently translated at 73.5% (2290 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 11:52:15 +00:00
markiousi
f7fd3596a6 Translated on translate.pretix.eu (Greek)
Currently translated at 73.5% (2290 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 11:52:14 +00:00
markiousi
3b4f758c82 Translated on translate.pretix.eu (Greek)
Currently translated at 65.8% (2050 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 10:36:35 +00:00
Andrikopoulos-Giannis
df8c8f2063 Translated on translate.pretix.eu (Greek)
Currently translated at 65.8% (2050 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 10:36:35 +00:00
markiousi
ebb6b5b469 Translated on translate.pretix.eu (Greek)
Currently translated at 65.8% (2050 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 10:36:34 +00:00
ThanosTeste
16ad39bb16 Translated on translate.pretix.eu (Greek)
Currently translated at 65.8% (2050 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 10:36:32 +00:00
mapostolopoulou
6ca65edde9 Translated on translate.pretix.eu (Greek)
Currently translated at 65.8% (2050 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 10:36:31 +00:00
markiousi
02684a0fcd Translated on translate.pretix.eu (Greek)
Currently translated at 55.4% (1727 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 09:47:39 +00:00
mapostolopoulou
141ba6e50d Translated on translate.pretix.eu (Greek)
Currently translated at 55.4% (1727 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 09:47:38 +00:00
ThanosTeste
6681eb1a27 Translated on translate.pretix.eu (Greek)
Currently translated at 55.4% (1727 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 09:47:38 +00:00
mapostolopoulou
2b515ea30c Translated on translate.pretix.eu (Greek)
Currently translated at 54.2% (1690 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 09:44:07 +00:00
ThanosTeste
7997882e24 Translated on translate.pretix.eu (Greek)
Currently translated at 54.2% (1690 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 09:44:07 +00:00
markiousi
a8190258a4 Translated on translate.pretix.eu (Greek)
Currently translated at 53.8% (1676 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 09:42:12 +00:00
mapostolopoulou
9376a26709 Translated on translate.pretix.eu (Greek)
Currently translated at 53.8% (1676 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 09:42:12 +00:00
mapostolopoulou
d8e2e0e217 Translated on translate.pretix.eu (Greek)
Currently translated at 53.7% (1672 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 09:41:18 +00:00
GiorgosPap
f9c942bc6f Translated on translate.pretix.eu (Greek)
Currently translated at 53.7% (1672 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 09:41:16 +00:00
markiousi
f9b7696366 Translated on translate.pretix.eu (Greek)
Currently translated at 53.7% (1672 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 09:41:15 +00:00
ThanosTeste
2143135285 Translated on translate.pretix.eu (Greek)
Currently translated at 53.7% (1672 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 09:41:14 +00:00
ThanosTeste
54146bb9e8 Translated on translate.pretix.eu (Greek)
Currently translated at 36.7% (1143 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 08:09:15 +00:00
GiorgosPap
d1f702cafd Translated on translate.pretix.eu (Greek)
Currently translated at 36.7% (1143 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 08:09:15 +00:00
markiousi
54e7b8da89 Translated on translate.pretix.eu (Greek)
Currently translated at 36.7% (1143 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 08:09:14 +00:00
GiorgosPap
25af386d87 Translated on translate.pretix.eu (Greek)
Currently translated at 36.0% (1121 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 08:04:19 +00:00
markiousi
51fa9e78dd Translated on translate.pretix.eu (Greek)
Currently translated at 36.0% (1121 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 08:04:19 +00:00
ThanosTeste
6cf244bb4b Translated on translate.pretix.eu (Greek)
Currently translated at 36.0% (1121 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 08:04:19 +00:00
mapostolopoulou
6a0e3b1b46 Translated on translate.pretix.eu (Greek)
Currently translated at 36.0% (1121 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 08:04:17 +00:00
mapostolopoulou
571b0e9aa8 Translated on translate.pretix.eu (Greek)
Currently translated at 29.7% (924 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
ThanosTeste
8075d3e385 Translated on translate.pretix.eu (Greek)
Currently translated at 29.6% (922 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
mapostolopoulou
411f5c358f Translated on translate.pretix.eu (Greek)
Currently translated at 29.6% (922 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
ThanosTeste
94d3eff799 Translated on translate.pretix.eu (Greek)
Currently translated at 79.8% (79 of 99 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/el/

powered by weblate
2019-05-29 07:19:38 +00:00
markiousi
a073d66213 Translated on translate.pretix.eu (Greek)
Currently translated at 29.4% (916 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
mapostolopoulou
7b2dda9cd9 Translated on translate.pretix.eu (Greek)
Currently translated at 29.4% (916 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
ThanosTeste
e9c66f5bb1 Translated on translate.pretix.eu (Greek)
Currently translated at 29.4% (916 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
mapostolopoulou
24aa8fc033 Translated on translate.pretix.eu (Greek)
Currently translated at 16.8% (522 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
Chris Spy
ee74f75913 Translated on translate.pretix.eu (Greek)
Currently translated at 16.8% (522 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
ThanosTeste
b484675aeb Translated on translate.pretix.eu (Greek)
Currently translated at 16.8% (522 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
Chris Spy
88379d7c25 Translated on translate.pretix.eu (Greek)
Currently translated at 16.7% (519 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
ThanosTeste
4e8bdb4427 Translated on translate.pretix.eu (Greek)
Currently translated at 16.7% (519 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
Chris Spy
f68c29ca95 Translated on translate.pretix.eu (Greek)
Currently translated at 16.6% (517 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
ThanosTeste
eb7154a55b Translated on translate.pretix.eu (Greek)
Currently translated at 16.6% (517 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
Chris Spy
ed19cc99f3 Translated on translate.pretix.eu (Greek)
Currently translated at 16.6% (516 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
mapostolopoulou
51ae1e5e33 Translated on translate.pretix.eu (Greek)
Currently translated at 16.6% (516 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
ThanosTeste
132f8d8cb3 Translated on translate.pretix.eu (Greek)
Currently translated at 16.6% (516 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
mapostolopoulou
cd4b4b98b8 Translated on translate.pretix.eu (Greek)
Currently translated at 16.5% (513 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
Chris Spy
07e0ffd4f3 Translated on translate.pretix.eu (Greek)
Currently translated at 16.5% (513 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
ThanosTeste
1d2a6d55b9 Translated on translate.pretix.eu (Greek)
Currently translated at 16.5% (513 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
mapostolopoulou
33b893b0ba Translated on translate.pretix.eu (Greek)
Currently translated at 15.8% (492 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
Chris Spy
03ebe0e528 Translated on translate.pretix.eu (Greek)
Currently translated at 15.8% (492 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
ThanosTeste
28797b8cc6 Translated on translate.pretix.eu (Greek)
Currently translated at 15.8% (492 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
mapostolopoulou
691ba3a1a7 Translated on translate.pretix.eu (Greek)
Currently translated at 15.3% (477 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
Chris Spy
656673ccde Translated on translate.pretix.eu (Greek)
Currently translated at 15.3% (477 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
ThanosTeste
7073622ab3 Translated on translate.pretix.eu (Greek)
Currently translated at 15.3% (477 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
Chris Spy
b8f71d2428 Translated on translate.pretix.eu (Greek)
Currently translated at 15.2% (473 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
ThanosTeste
2a8bdc29f4 Translated on translate.pretix.eu (Greek)
Currently translated at 15.2% (473 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
Chris Spy
a6da1bb4e9 Translated on translate.pretix.eu (Greek)
Currently translated at 15.1% (471 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
ThanosTeste
d1e67d38d9 Translated on translate.pretix.eu (Greek)
Currently translated at 15.1% (471 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
mapostolopoulou
a5214d459c Translated on translate.pretix.eu (Greek)
Currently translated at 15.1% (471 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
markiousi
9ad2891d17 Translated on translate.pretix.eu (Greek)
Currently translated at 13.6% (424 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
Chris Spy
4b2d55a2fb Translated on translate.pretix.eu (Greek)
Currently translated at 13.6% (424 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
mapostolopoulou
5cfed32d61 Translated on translate.pretix.eu (Greek)
Currently translated at 13.6% (424 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
ThanosTeste
06ccd83921 Translated on translate.pretix.eu (Greek)
Currently translated at 13.6% (424 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
ThanosTeste
ba417b6e3c Translated on translate.pretix.eu (Greek)
Currently translated at 4.3% (134 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
markiousi
f7ed0236f3 Translated on translate.pretix.eu (Greek)
Currently translated at 4.0% (126 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
markiousi
4491b80786 Translated on translate.pretix.eu (Greek)
Currently translated at 4.0% (125 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
Chris Spy
37bcb520cc Translated on translate.pretix.eu (Greek)
Currently translated at 4.0% (125 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
Chris Spy
782e957c3a Translated on translate.pretix.eu (Greek)
Currently translated at 3.7% (116 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
Andrikopoulos-Giannis
953890c269 Translated on translate.pretix.eu (Greek)
Currently translated at 3.7% (116 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:38 +00:00
Martin Gross
60d9c1080a Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3116 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de_Informal/

powered by weblate
2019-05-29 07:19:38 +00:00
Raphael Michel
364e7cefda Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3116 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de_Informal/

powered by weblate
2019-05-29 07:19:38 +00:00
Martin Gross
33accf3250 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3116 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de/

powered by weblate
2019-05-29 07:19:37 +00:00
Raphael Michel
be4d9ac00e Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3116 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de/

powered by weblate
2019-05-29 07:19:37 +00:00
Andrikopoulos-Giannis
8ca5e4dd54 Translated on translate.pretix.eu (Greek)
Currently translated at 3.7% (114 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:37 +00:00
markiousi
1394cf3148 Translated on translate.pretix.eu (Greek)
Currently translated at 3.7% (114 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:37 +00:00
Andrikopoulos-Giannis
de58b35bf4 Translated on translate.pretix.eu (Greek)
Currently translated at 3.6% (111 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:37 +00:00
markiousi
490b421d53 Translated on translate.pretix.eu (Greek)
Currently translated at 3.6% (111 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:37 +00:00
markiousi
61d45f26dd Translated on translate.pretix.eu (Greek)
Currently translated at 3.4% (105 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:37 +00:00
Andrikopoulos-Giannis
a8b0475c6d Translated on translate.pretix.eu (Greek)
Currently translated at 3.4% (105 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:37 +00:00
Chris Spy
31cf94eb02 Translated on translate.pretix.eu (Greek)
Currently translated at 3.4% (105 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2019-05-29 07:19:37 +00:00
Raphael Michel
dc0590ea91 Fix grammar 2019-05-29 09:19:26 +02:00
Raphael Michel
bc5e5d0a27 Fix #1307 -- Document hierarchies in navigation signals 2019-05-29 09:17:43 +02:00
Raphael Michel
0fc448fbd3 Refs #1307 -- fix navigation hierarchies being broken by sorting 2019-05-29 09:17:43 +02:00
Raphael Michel
67d5c1ccad Refs #1307 -- Fix crash when assigning a navigation parent without a children list 2019-05-29 09:17:43 +02:00
Raphael Michel
779ad11640 Timeline: Do not show disabled payment providers 2019-05-29 09:17:43 +02:00
Martin Gross
70e9d9faad Add documentation change for cloning events with testmode-attribute 2019-05-28 12:42:03 +02:00
Martin Gross
51f5b0645a Respect testmode in CloneEventSerializer 2019-05-28 12:38:10 +02:00
Martin Gross
b3436c1a93 Prepend current organizer to typeahead 2019-05-28 12:29:54 +02:00
Raphael Michel
9eef5d5d6d Fix failing test after update 2019-05-28 12:07:57 +02:00
Martin Gross
e139de3c19 Create new events in Testmode by default 2019-05-28 11:14:03 +02:00
Martin Gross
74f861bd48 Respect Testmode when cloning events 2019-05-28 11:13:20 +02:00
Raphael Michel
35c02f35d7 Event selection: Fix typeahead query 2019-05-28 10:52:34 +02:00
Raphael Michel
d5c0b0f71d Event creation: Use select2 for event/organizer selection and properly support admin sessions 2019-05-28 10:42:14 +02:00
Raphael Michel
6c701d66b1 Removed setter for cached_property 2019-05-28 10:37:41 +02:00
Martin Gross
8d62b509a2 Fix #1292 -- Fix build of PDF documentation 2019-05-28 10:26:05 +02:00
Raphael Michel
9e0b97e88e Fix #601 -- provide setters for meta_info_data 2019-05-28 10:16:54 +02:00
Raphael Michel
28a5519881 Fix #1270 -- Provide preview for fonts in display settings 2019-05-28 10:07:42 +02:00
Raphael Michel
363826e294 Fix #1292 -- Use standard naming of sphinx document root 2019-05-28 09:50:29 +02:00
Raphael Michel
eb8ea6d477 Fix #1296 -- Show last_login in user admin 2019-05-28 09:49:28 +02:00
Raphael Michel
77be4d835b Fix #1301 -- Do not export empty files 2019-05-28 09:43:21 +02:00
Raphael Michel
c6390520a7 Warn about hidden product limitations 2019-05-28 09:25:05 +02:00
Raphael Michel
594803ec17 Make product.tax_rule required as soon as tax rules exist to avoid users from screwing up their taxes 2019-05-28 08:59:02 +02:00
Raphael Michel
32ce3a4319 Event list: Ignore invalid filter attributes 2019-05-27 23:04:37 +02:00
Raphael Michel
d3f01832fe Fix a bug during validation 2019-05-27 18:27:20 +02:00
lislis
bba702489d rm superfluous closing anchor
in templates/pretixpresale/events/index.html
2019-05-27 18:26:36 +02:00
Raphael Michel
85fe7e55be Guess and pre-fill invoice address country 2019-05-27 17:48:22 +02:00
Martin Gross
92c9216fbd Adding Greek to incubating languages 2019-05-27 17:02:26 +02:00
Raphael Michel
db63e20708 Optimize refresh_quota_caches for less long-running queries 2019-05-27 10:09:29 +02:00
Raphael Michel
e2ce35a85b Order details: Do not show empty cancellation section 2019-05-24 13:59:58 +02:00
Martin Gross
d39964b021 Show information, when item contained in product bundle is disabled 2019-05-24 12:30:31 +02:00
Martin Gross
59beba5069 Allow to unset QuestionAnswers 2019-05-24 11:15:35 +02:00
Raphael Michel
1bd3a63959 Update from Weblate (#1300)
Update from Weblate
2019-05-24 10:45:35 +02:00
Raphael Michel
1d644e90c9 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3116 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de_Informal/

powered by weblate
2019-05-24 08:34:07 +00:00
Raphael Michel
e0e66c903e Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3116 of 3116 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de/

powered by weblate
2019-05-24 08:34:06 +00:00
Raphael Michel
bc08bdebb5 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-05-24 09:42:41 +02:00
Raphael Michel
edd92ac34d Update from Weblate (#1297)
Update from Weblate
2019-05-24 09:42:04 +02:00
Raphael Michel
f1bce0c08b Allow to send e-mails to attendees individually (#1299)
* .

* Add a position detail page to the frontend

* Mail templates

* Send mails

* Send reminder email

* Add position support to sendmail plugin

* Add and fix some tests

* Fix failing test on real databases
2019-05-24 09:41:44 +02:00
Raphael Michel
68c24ebea3 Translated on translate.pretix.eu (German (informal))
Currently translated at 99.9% (3083 of 3084 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de_Informal/

powered by weblate
2019-05-23 12:33:05 +00:00
Raphael Michel
d22a7844ea Fix TypeError PRETIXEU-12W 2019-05-23 14:32:55 +02:00
Martin Gross
6238e1df98 Add Mail sender name option 2019-05-22 16:09:52 +02:00
Martin Gross
6acca4c4ba Show event time in event series cart 2019-05-22 16:09:25 +02:00
Raphael Michel
1a9f6e49d4 Refactor product list into own template 2019-05-22 09:48:07 +02:00
Raphael Michel
efa1d2683e rich_text: allow a[class] 2019-05-22 08:20:00 +02:00
Martin Gross
9b39d34f81 Fix style - again 2019-05-21 15:33:07 +02:00
Martin Gross
96c5c8c4ff Fix style 2019-05-21 14:30:38 +02:00
Martin Gross
3254ac36a2 Add option to exclude Sales Channels from invoice generation 2019-05-21 14:18:31 +02:00
Raphael Michel
52d10957a1 Timeline: Fix issues with relative dates 2019-05-19 14:42:19 +02:00
Raphael Michel
f9d4669423 Timeline: Fix padding 2019-05-19 14:18:23 +02:00
Raphael Michel
6e220cbbd8 Update djangojs.js to current Django 2019-05-19 14:10:22 +02:00
Raphael Michel
036a555374 Update from Weblate (#1295)
Update from Weblate
2019-05-19 13:50:18 +02:00
Raphael Michel
861a41c95f Translated on translate.pretix.eu (German (informal))
Currently translated at 99.9% (3083 of 3084 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de_Informal/

powered by weblate
2019-05-19 11:14:43 +00:00
Raphael Michel
e2abc19fe3 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3084 of 3084 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de/

powered by weblate
2019-05-19 11:13:59 +00:00
ligi
97fc226e07 Document development dependency libenchant1c2a (#1294) 2019-05-19 13:06:24 +02:00
Raphael Michel
d73c98bff0 Document Debian dependency python-venv (#1293)
Add external dependency python-venv
2019-05-19 13:02:58 +02:00
Raphael Michel
aa186f7a09 Update setup.rst 2019-05-19 13:02:05 +02:00
ligi
1b434b40d2 Add external dependency python-venv
otherwise in the next step I get
```
$ python3 -m venv env
The virtual environment was not created successfully because ensurepip is not
available.  On Debian/Ubuntu systems, you need to install the python3-venv
package using the following command.

    apt-get install python3-venv
```
2019-05-17 19:32:32 +02:00
Raphael Michel
71475c5863 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-05-17 17:41:30 +02:00
Raphael Michel
71b544d951 Update from Weblate (#1291)
Update from Weblate
2019-05-17 17:40:52 +02:00
pretix Translation Platform
b0685437f1 Merge branch 'master' of https://github.com/pretix/pretix 2019-05-17 17:39:03 +02:00
Raphael Michel
2d99828eab Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-05-17 17:33:45 +02:00
pretix translation bot
2ffc1b8eaf Update from Weblate (#1278)
* Translated on translate.pretix.eu (Slovenian)

Currently translated at 0.1% (1 of 3067 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/sl/

powered by weblate

* Translated on translate.pretix.eu (Slovenian)

Currently translated at 0.3% (9 of 3067 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/sl/

powered by weblate
2019-05-17 17:32:54 +02:00
Bostjan Marusic
893f47d365 Translated on translate.pretix.eu (Slovenian)
Currently translated at 0.3% (9 of 3067 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/sl/

powered by weblate
2019-05-17 15:32:43 +00:00
Bostjan Marusic
7de1fca2f4 Translated on translate.pretix.eu (Slovenian)
Currently translated at 0.1% (1 of 3067 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/sl/

powered by weblate
2019-05-17 15:32:43 +00:00
Raphael Michel
c6b18b31a1 Display a timeline on the dashboard (#1290)
* Timeline data model

* Display timeline

* …

* More events

* Plugin support

* Fix docs typo
2019-05-17 17:32:38 +02:00
Raphael Michel
ecc9c7f39f Try to fix a flaky test 2019-05-17 13:09:30 +02:00
Raphael Michel
b9aba9cf56 Fix issue in offset calculation of relative dates 2019-05-17 12:37:00 +02:00
Raphael Michel
33f0892052 Adjust tests to c7774dfdb 2019-05-16 12:02:49 +02:00
Raphael Michel
4bf3d48549 Open graph contents for organizer pages 2019-05-16 12:02:19 +02:00
Raphael Michel
32aa4b4f3e Fix #1286 -- Autofill quota names with products 2019-05-16 11:41:30 +02:00
Raphael Michel
e1992bb99f Refs #1286 -- Show variations in list of quotas 2019-05-16 11:35:19 +02:00
Raphael Michel
45e98546d6 Add open graph tags to event pages 2019-05-16 11:30:25 +02:00
Raphael Michel
c7774dfdb8 Allow to set a custom payment date for manual payments 2019-05-16 11:21:00 +02:00
Raphael Michel
6c582b8f8c Prefix notification emails with the event slug 2019-05-16 11:09:20 +02:00
Raphael Michel
5f82db3949 Fix #1205 -- Layout bug on order page if only some tickets can be downloaded 2019-05-16 10:52:11 +02:00
Raphael Michel
2b818f42cd Raise 404 on opening unknown order
PRETIXEU-12Q
2019-05-16 10:05:58 +02:00
Raphael Michel
b19df33dda Fix a bug during deletion of vouchers 2019-05-15 15:57:08 +02:00
Raphael Michel
dba8761bc5 Fix a bug around bundles and carts 2019-05-15 15:56:54 +02:00
Raphael Michel
0311c0251a Fix an unlogical comparison in a query 2019-05-15 15:23:02 +02:00
Raphael Michel
5b99bf3623 Fix missing encoding 2019-05-15 09:39:32 +02:00
Raphael Michel
4137e0fc1f Add new signal validate_order 2019-05-15 09:37:34 +02:00
Raphael Michel
b32c6033f1 Bump to 2.8.0.dev0 2019-05-15 09:37:34 +02:00
Raphael Michel
de0e700fec Store whether we know email addresses are working because links have been clicked 2019-05-15 08:22:53 +02:00
Raphael Michel
00bc5f4fae Fix another crash around original prices 2019-05-14 17:20:22 +02:00
Raphael Michel
6ef3603d9f Allow to add multiple Bcc addresses 2019-05-14 10:18:09 +02:00
Raphael Michel
2c7cefea35 Fix #1279 -- Incorrect initial price value in widget in German locale 2019-05-14 09:08:33 +02:00
Tobias Kunze
a10b31cacb Remove dead test fixtures (#1284) 2019-05-14 08:55:12 +02:00
Raphael Michel
4e9e925b32 Fix #1282 -- Work around issues in file backends 2019-05-12 14:49:45 +02:00
Raphael Michel
f4415cf906 Ensure deterministic test collection order 2019-05-10 09:13:18 +02:00
Raphael Michel
bf4fcfd914 Add tests to ensure that we documented all signals 2019-05-10 08:58:42 +02:00
Raphael Michel
7021c178ab Set original_price to TaxedPrice even with variations 2019-05-09 23:30:50 +02:00
Raphael Michel
5d8e3e28d6 Assign flag for zh_Hans 2019-05-09 17:00:30 +02:00
Raphael Michel
e89aaf4059 Bump to 2.7.0 2019-05-09 16:20:40 +02:00
Raphael Michel
db270b3bf2 Update from Weblate (#1276)
Update from Weblate
2019-05-09 14:48:51 +02:00
Raphael Michel
d8b78c3a7a Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3067 of 3067 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de/

powered by weblate
2019-05-09 12:48:23 +00:00
Raphael Michel
67c448a29e Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3067 of 3067 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de_Informal/

powered by weblate
2019-05-09 12:48:22 +00:00
Raphael Michel
5b7906f2a1 Translated on translate.pretix.eu (German (informal))
Currently translated at 99.8% (3060 of 3067 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de_Informal/

powered by weblate
2019-05-09 12:45:12 +00:00
Raphael Michel
0612d42607 Translated on translate.pretix.eu (German)
Currently translated at 99.8% (3060 of 3067 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de/

powered by weblate
2019-05-09 12:45:12 +00:00
Raphael Michel
83f866034a Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-05-09 14:45:03 +02:00
Raphael Michel
b1fa214869 Clarify a description 2019-05-09 14:44:06 +02:00
Raphael Michel
aa53b5235a Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-05-09 14:39:58 +02:00
Raphael Michel
61a13256a0 Update from Weblate (#1269)
Update from Weblate
2019-05-09 14:39:16 +02:00
Raphael Michel
64e2336014 Added translation on translate.pretix.eu (Slovenian) 2019-05-09 11:47:16 +00:00
Bostjan Marusic
3411abd1e6 Added translation on translate.pretix.eu (Slovenian) 2019-05-09 11:47:16 +00:00
Allan Nordhøy
2a34e54fae Added translation on translate.pretix.eu (Norwegian Bokmål) 2019-05-09 11:47:16 +00:00
Alvaro Enrique Ruano
9863dc35d6 Translated on translate.pretix.eu (Spanish)
Currently translated at 99.0% (3031 of 3063 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/es/

powered by weblate
2019-05-09 11:47:16 +00:00
Raphael Michel
690883a198 Fix #480 -- Allow plugins to specify a minimum pretix version 2019-05-09 13:46:54 +02:00
Raphael Michel
d8ded08a46 Checkin list PDF: Remove date from headline, it's in the page header now 2019-05-09 10:52:42 +02:00
Raphael Michel
4aab5daa57 Fixing import order 2019-05-09 10:22:09 +02:00
Raphael Michel
e87628c902 Ensure that we document all signals 2019-05-09 10:02:12 +02:00
Raphael Michel
3c7bf46268 Resolve requests/urllib versions 2019-05-09 10:02:12 +02:00
Raphael Michel
a1dacb1897 Remove outdated reference from docs 2019-05-09 10:02:12 +02:00
Raphael Michel
08d5626704 Simplify the future of our migration history 2019-05-09 10:02:12 +02:00
Raphael Michel
c8a1481f93 Fix #1154 -- Add country-typed questions 2019-05-09 10:02:12 +02:00
Raphael Michel
e7c4121745 Add hidden questions 2019-05-09 10:02:12 +02:00
Sohalt
35ddd8dd28 Typo (#1274) 2019-05-08 13:13:27 +02:00
Raphael Michel
e2ec6eb156 Dekodi: Change semantics of signs 2019-05-08 11:47:57 +02:00
Raphael Michel
42edc4c3aa money_filter: Ignore case of currency 2019-05-07 16:20:24 +02:00
Raphael Michel
1cb2f99f3a Tax calculation of "original prices" 2019-05-06 12:33:21 +02:00
Raphael Michel
d4146e08b1 Fix widget tests 2019-05-06 12:06:45 +02:00
Raphael Michel
79ae9b6501 Revert "updatestyles: Fix a TypeError"
This reverts commit 53053f19e4.
2019-05-06 11:43:40 +02:00
Raphael Michel
c23f71a19c Widget: Add voucher explanation text 2019-05-06 11:33:48 +02:00
Raphael Michel
53053f19e4 updatestyles: Fix a TypeError 2019-05-06 11:33:36 +02:00
Raphael Michel
a42b2d76f6 Add scalability to docs word list 2019-05-06 08:50:56 +02:00
Raphael Michel
51392f73a8 Locking optimizations 2019-05-05 17:31:08 +02:00
Raphael Michel
465a5b01b9 Offload more work to database replica 2019-05-05 17:31:08 +02:00
Raphael Michel
74a6004613 Documentation on scaling 2019-05-05 17:31:08 +02:00
Sohalt
f9fc33eba1 Fix #1266 -- Make references to plugin settings clickable links (#1268) 2019-05-02 09:18:42 +02:00
Raphael Michel
363dc74c31 Dekodi: Try to find correct PayPal ID 2019-05-02 09:12:57 +02:00
Raphael Michel
efb598e93a Update from Weblate (#1267)
Update from Weblate
2019-05-01 14:13:47 +02:00
Raphael Michel
bcfaf2801d Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3063 of 3063 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de/

powered by weblate
2019-05-01 12:13:28 +00:00
Raphael Michel
98db417fe6 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (99 of 99 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/de/

powered by weblate
2019-05-01 12:13:18 +00:00
Raphael Michel
a03ffd949e Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3063 of 3063 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de_Informal/

powered by weblate
2019-05-01 12:12:58 +00:00
Raphael Michel
88ef46dee9 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (99 of 99 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/de_Informal/

powered by weblate
2019-05-01 12:12:39 +00:00
191 changed files with 78406 additions and 32496 deletions

View File

@@ -273,6 +273,24 @@ to speed up various operations::
If redis is not configured, pretix will store sessions and locks in the database. If memcached
is configured, memcached will be used for caching instead of redis.
Translations
------------
pretix comes with a number of translations. Some of them are marked as "incubating", which means
they can usually only be selected in development mode. If you want to use them nevertheless, you
can activate them like this::
[languages]
allow_incubating=pt-br,da
You can also tell pretix about additional paths where it will search for translations::
[languages]
path=/path/to/my/translations
For a given language (e.g. ``pt-br``), pretix will then look in the
specific sub-folder, e.g. ``/path/to/my/translations/pt_BR/LC_MESSAGES/django.po``.
Celery task queue
-----------------

View File

@@ -192,7 +192,8 @@ that you have a deep understanding of the semantics of your replication mechanis
If you do have a replica, you *can* tell pretix about it :ref:`in your configuration <config-replica>`.
This way, pretix can offload complex read-only queries to the replica when it is safe to do so.
As of pretix 2.6, this is mainly used for search queries in the backend and therefore does not really accelerate sales throughput, but we plan on expanding this in the future.
As of pretix 2.7, this is mainly used for search queries in the backend and for rendering the
product list and event lists in the frontend, but we plan on expanding this in the future.
Therefore, for now our clear recommendation is: Try to scale your database vertically and put
it on the most powerful machine you have available.
@@ -207,30 +208,29 @@ Having some memory available is good in case of e.g. lots of tasks queuing up du
Feel free to set up a redis cluster for availability but you won't need it for performance in a long time.
The locking issue
-----------------
The limitations
---------------
Currently, there is one big issue with scaling pretix. We take the reliability and correctness
of pretix' core features very seriously and one part of this is that we want to make absolutely
sure that pretix never sells the wrong number of tickets or tickets it otherwise shouldn't sell.
Up to a certain point, pretix scales really well. However, there are a few things that we consider
even more important than scalability, and those are correctness and reliability. We want you to be
able to trust that pretix will not sell more tickets than you intended or run into similar error
cases.
However, due to pretix flexibility and complexity, ensuring this in a high-concurrency environment is really hard.
We're not just counting down integers, since quota availability in pretix depends on a multitude of factors and requires a handful of complex database queries to calculate.
We can't rely on database-level locking alone to get this right.
Combined with pretix' flexibility and complexity, especially around vouchers and quotas, this creates
some hard issues. In many cases, we need to fall back to event-global locking for some actions which
are likely to run with high concurrency and cause harm.
Therefore, we currently use a very drastic option:
During relevant actions that typically occur with high concurrency, such as creating carts and completing orders, we create a system-wide lock for the event and all other such
actions need to wait. In essence, this means **for a given event we're only ever selling
one ticket at a time**.
For every event, only one of these locking actions can be run at the same time. Examples for this are
adding products limited by a quota to a cart, adding items to a cart using a voucher or placing an order
consisting of cart positions that don't have a valid reservation for much longer. In these cases, it is
currently not realistically possible to exceed selling **approx. 500 orders per minute per event**, even
if you add more hardware.
If you have an unlimited number of tickets, we can apply fewer locking and we've reached **approx.
1500 orders per minute per event** in benchmarks, although even more should be possible.
Therefore, it is currently very unlikely that you will be able to exceed **300-400 tickets per minute per event**.
For events up to a few thousand tickets, this isn't a problem and it's not even desirable to be able to sell much faster: If all tickets are reserved in the first minute, the shop shows a big "sold out" and everyone else goes away. Fifteen to thirty minutes later, depending on your settings, the first shopping carts will expire because people didn't actually go through with the purchase and tickets will become available again. This leads to a much more frustrating shopping experience for those trying to get a ticket than if tickets are sold at a slower pace and the first reservations expire before the last reservations are made. Not selling tickets through quickly (e.g. through a queue system) can do a lot to smooth out this process.
That said, we still want to fix this of course and make it possible to achieve much higher
throughput rates. We have some plans on how to soften this limitation, but they require
lots of time and effort to be realized. If you want to use pretix for an event with
10,000+ tickets that will be sold out within minutes, get in touch, we will make it work ;)
We're working to reduce the number of cases in which this is relevant and thereby improve the possible
throughput. If you want to use pretix for an event with 10,000+ tickets that are likely to be sold out
within minutes, please get in touch to discuss possible solutions. We'll work something out for you!
.. _object storage cluster: https://behind.pretix.eu/2018/03/20/high-available-cdn/

View File

@@ -0,0 +1,131 @@
pretix Hosted billing invoices
==============================
This endpoint allows you to access invoices you received for pretix Hosted. It only contains invoices created starting
November 2017.
.. note:: Only available on pretix Hosted, not on self-hosted pretix instances.
Resource description
--------------------
The resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
invoice_number string Invoice number
date_issued date Invoice date
===================================== ========================== =======================================================
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/billing_invoices/
Returns a list of all invoices to a given organizer.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/billing_invoices/ 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": [
{
"invoice_number": "R2019002",
"date_issued": "2019-06-03"
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``date_issued`` and
its reverse, ``-date_issued``. Default: ``date_issued``.
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/billing_invoices/(invoice_number)/
Returns information on one invoice, identified by its invoice number.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/billing_invoices/R2019002/ 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
{
"invoice_number": "R2019002",
"date_issued": "2019-06-03"
}
:param organizer: The ``slug`` field of the organizer to fetch
:param invoice_number: The ``invoice_number`` field of the invoice to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/billing_invoices/(invoice_number)/download/
Download an invoice in PDF format.
.. warning:: After we created the invoices, they are placed in review with our accounting department. You will
already see them in the API at this point, but you are not able to download them until they completed
review and are sent to you via email. This usually takes a few hours. If you try to download them
in this time frame, you will receive a status code :http:statuscode:`423`.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/billing_invoices/R2019002/download/ 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/pdf
...
:param organizer: The ``slug`` field of the organizer to fetch
:param invoice_number: The ``invoice_number`` field of the invoice to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 423: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
seconds.

View File

@@ -50,6 +50,10 @@ plugins list A list of packa
The ``testmode`` attribute has been added.
.. versionchanged:: 2.8
When cloning events, the ``testmode`` attribute will now be cloned, too.
Endpoints
---------
@@ -112,6 +116,9 @@ Endpoints
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned. Event series are never (always) returned.
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned. Event series are never (always) returned.
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned. Event series are never returned.
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``date_from`` and
``slug``. Keep in mind that ``date_from`` of event series does not really tell you anything.
Default: ``slug``.
:param organizer: The ``slug`` field of a valid organizer
:statuscode 200: no error
:statuscode 401: Authentication failure
@@ -246,7 +253,7 @@ Endpoints
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/clone/
Creates a new event with properties as set in the request body. The properties that are copied are: 'is_public',
settings, plugin settings, items, variations, add-ons, quotas, categories, tax rules, questions.
`testmode`, settings, plugin settings, items, variations, add-ons, quotas, categories, tax rules, questions.
If the 'plugins' and/or 'is_public' fields are present in the post body this will determine their value. Otherwise
their value will be copied from the existing event.

View File

@@ -23,3 +23,4 @@ Resources and endpoints
waitinglist
carts
webhooks
billing_invoices

View File

@@ -56,6 +56,8 @@ Endpoints
}
:query page: The page number in case of a multi-page result set, default is 1
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``slug`` and
``name``. Default: ``slug``.
:statuscode 200: no error
:statuscode 401: Authentication failure

View File

@@ -30,6 +30,7 @@ type string The expected ty
* ``D`` date
* ``H`` time
* ``W`` date and time
* ``CC`` country code (ISO 3666-1 alpha-2)
required boolean If ``true``, the question needs to be filled out.
position integer An integer, used for sorting
items list of integers List of item IDs this question is assigned to.
@@ -38,6 +39,8 @@ identifier string An arbitrary st
ask_during_checkin boolean If ``true``, this question will not be asked while
buying the ticket, but will show up when redeeming
the ticket instead.
hidden boolean If ``true``, the question will only be shown in the
backend.
options list of objects In case of question type ``C`` or ``M``, this lists the
available objects. Only writable during creation,
use separate endpoint to modify this later.
@@ -68,6 +71,10 @@ dependency_value string The value ``dep
Write methods have been added. The attribute ``identifier`` has been added to both the resource itself and the
options resource. The ``position`` attribute has been added to the options resource.
.. versionchanged:: 2.7
The attribute ``hidden`` and the question type ``CC`` have been added.
Endpoints
---------
@@ -110,6 +117,7 @@ Endpoints
"position": 1,
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"hidden": false,
"dependency_question": null,
"dependency_value": null,
"options": [
@@ -177,6 +185,7 @@ Endpoints
"position": 1,
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"hidden": false,
"dependency_question": null,
"dependency_value": null,
"options": [
@@ -228,6 +237,7 @@ Endpoints
"items": [1, 2],
"position": 1,
"ask_during_checkin": false,
"hidden": false,
"dependency_question": null,
"dependency_value": null,
"options": [
@@ -261,6 +271,7 @@ Endpoints
"position": 1,
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"hidden": false,
"dependency_question": null,
"dependency_value": null,
"options": [
@@ -332,6 +343,7 @@ Endpoints
"position": 2,
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"hidden": false,
"dependency_question": null,
"dependency_value": null,
"options": [

View File

@@ -66,7 +66,7 @@ source_suffix = '.rst'
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'contents'
master_doc = 'index'
# General information about the project.
project = 'pretix'
@@ -234,7 +234,7 @@ latex_elements = {
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
('contents', 'pretix.tex', 'pretix Documentation',
('index', 'pretix.tex', 'pretix Documentation',
'Raphael Michel', 'manual'),
]

View File

@@ -12,7 +12,7 @@ Core
.. automodule:: pretix.base.signals
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types,
item_copy_data, register_sales_channels
item_copy_data, register_sales_channels, register_global_settings
Order events
""""""""""""
@@ -20,17 +20,13 @@ Order events
There are multiple signals that will be sent out in the ordering cycle:
.. automodule:: pretix.base.signals
:members: validate_cart, 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
:members: validate_cart, 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
Frontend
--------
.. automodule:: pretix.presale.signals
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble
.. automodule:: pretix.presale.signals
:members: order_info, order_meta_from_request
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, checkout_flow_steps, order_info, order_meta_from_request, position_info
Request flow
""""""""""""
@@ -53,7 +49,7 @@ Backend
.. automodule:: pretix.base.signals
:members: logentry_display, logentry_object_link, requiredaction_display
:members: logentry_display, logentry_object_link, requiredaction_display, timeline_events
Vouchers
""""""""

View File

@@ -49,15 +49,19 @@ description string A more verbose description of what your
visible boolean (optional) ``True`` by default, can hide a plugin so it cannot be normally activated.
restricted boolean (optional) ``False`` by default, restricts a plugin such that it can only be enabled
for an event by system administrators / superusers.
compatibility string Specifier for compatible pretix versions.
================== ==================== ===========================================================
A working example would be::
from django.apps import AppConfig
try:
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 _
class PaypalApp(AppConfig):
class PaypalApp(PluginConfig):
name = 'pretix_paypal'
verbose_name = _("PayPal")
@@ -68,6 +72,7 @@ A working example would be::
visible = True
restricted = False
description = _("This plugin allows you to receive payments via PayPal")
compatibility = "pretix>=2.7.0"
default_app_config = 'pretix_paypal.PaypalApp'

View File

@@ -23,7 +23,7 @@ Organizers and events
:members:
.. autoclass:: pretix.base.models.Event
:members: get_date_from_display, get_time_from_display, get_date_to_display, get_date_range_display, presale_has_ended, presale_is_running, cache, lock, get_plugins, get_mail_backend, payment_term_last, get_payment_providers, get_invoice_renderers, active_subevents, invoice_renderer, settings
:members: get_date_from_display, get_time_from_display, get_date_to_display, get_date_range_display, presale_has_ended, presale_is_running, cache, lock, get_plugins, get_mail_backend, payment_term_last, get_payment_providers, get_invoice_renderers, invoice_renderer, settings
.. autoclass:: pretix.base.models.SubEvent
:members: get_date_from_display, get_time_from_display, get_date_to_display, get_date_range_display, presale_has_ended, presale_is_running

View File

@@ -21,10 +21,12 @@ Your should install the following on your system:
* Python 3.5 or newer
* ``pip`` for Python 3 (Debian package: ``python3-pip``)
* ``python-dev`` for Python 3 (Debian package: ``python3-dev``)
* On Debian/Ubuntu: ``python-venv`` for Python 3 (Debian package: ``python3-venv``)
* ``libffi`` (Debian package: ``libffi-dev``)
* ``libssl`` (Debian package: ``libssl-dev``)
* ``libxml2`` (Debian package ``libxml2-dev``)
* ``libxslt`` (Debian package ``libxslt1-dev``)
* ``libenchant1c2a`` (Debian package ``libenchant1c2a``)
* ``msgfmt`` (Debian package ``gettext``)
* ``git``

View File

@@ -96,6 +96,7 @@ renderer
renderers
reportlab
SaaS
scalability
screenshot
scss
searchable

View File

@@ -1 +1 @@
__version__ = "2.6.0"
__version__ = "2.8.0"

View File

@@ -164,6 +164,7 @@ class CloneEventSerializer(EventSerializer):
def create(self, validated_data):
plugins = validated_data.pop('plugins', None)
is_public = validated_data.pop('is_public', None)
testmode = validated_data.pop('testmode', None)
new_event = super().create(validated_data)
event = Event.objects.filter(slug=self.context['event'], organizer=self.context['organizer'].pk).first()
@@ -173,6 +174,8 @@ class CloneEventSerializer(EventSerializer):
new_event.set_active_plugins(plugins)
if is_public is not None:
new_event.is_public = is_public
if testmode is not None:
new_event.testmode = testmode
new_event.save()
return new_event

View File

@@ -207,7 +207,8 @@ class QuestionSerializer(I18nAwareModelSerializer):
class Meta:
model = Question
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_value')
'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_value',
'hidden')
def validate_identifier(self, value):
Question._clean_identifier(self.context['event'], value, self.instance)

View File

@@ -72,6 +72,8 @@ class EventViewSet(viewsets.ModelViewSet):
lookup_url_kwarg = 'event'
permission_classes = (EventCRUDPermission,)
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
ordering = ('slug',)
ordering_fields = ('date_from', 'slug')
filterset_class = EventFilter
def get_queryset(self):

View File

@@ -482,6 +482,7 @@ class OrderViewSet(viewsets.ModelViewSet):
)
if 'email' in self.request.data and serializer.instance.email != self.request.data.get('email'):
serializer.instance.email_known_to_work = False
serializer.instance.log_action(
'pretix.event.order.contact.changed',
user=self.request.user,

View File

@@ -1,4 +1,4 @@
from rest_framework import viewsets
from rest_framework import filters, viewsets
from pretix.api.models import OAuthAccessToken
from pretix.api.serializers.organizer import OrganizerSerializer
@@ -10,6 +10,9 @@ class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Organizer.objects.none()
lookup_field = 'slug'
lookup_url_kwarg = 'organizer'
filter_backends = (filters.OrderingFilter,)
ordering = ('slug',)
ordering_fields = ('name', 'slug')
def get_queryset(self):
if self.request.user.is_authenticated:

View File

@@ -8,7 +8,7 @@ from django.template.loader import get_template
from django.utils.translation import ugettext_lazy as _
from inlinestyler.utils import inline_css
from pretix.base.models import Event, Order
from pretix.base.models import Event, Order, OrderPosition
from pretix.base.signals import register_html_mail_renderers
from pretix.base.templatetags.rich_text import markdown_compile_email
@@ -44,7 +44,8 @@ class BaseHTMLMailRenderer:
def __str__(self):
return self.identifier
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order=None) -> str:
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order=None,
position: OrderPosition=None) -> str:
"""
This method should generate the HTML part of the email.
@@ -52,6 +53,7 @@ class BaseHTMLMailRenderer:
:param plain_signature: The signature with event organizer contact details in plain text.
:param subject: The email subject.
:param order: The order if this email is connected to one, otherwise ``None``.
:param position: The order position if this email is connected to one, otherwise ``None``.
:return: An HTML string
"""
raise NotImplementedError()
@@ -95,7 +97,7 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
def template_name(self):
raise NotImplementedError()
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order) -> str:
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order, position: OrderPosition) -> str:
body_md = markdown_compile_email(plain_body)
htmlctx = {
'site': settings.PRETIX_INSTANCE_NAME,
@@ -116,6 +118,9 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
if order:
htmlctx['order'] = order
if position:
htmlctx['position'] = position
tpl = get_template(self.template_name)
body_html = inline_css(tpl.render(htmlctx))
return body_html

View File

@@ -40,6 +40,7 @@ class AnswerFilesExporter(BaseExporter):
if form_data.get('questions'):
qs = qs.filter(question__in=form_data['questions'])
with tempfile.TemporaryDirectory() as d:
any = False
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
for i in qs:
if i.file:
@@ -51,9 +52,12 @@ class AnswerFilesExporter(BaseExporter):
i.question.pk,
os.path.basename(i.file.name).split('.', 1)[1]
)
any = True
zipf.writestr(fname, i.file.read())
i.file.close()
if not any:
return None
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
return '{}_answers.zip'.format(self.event.slug), 'application/zip', zipf.read()

View File

@@ -30,13 +30,13 @@ class DekodiNREIExporter(BaseExporter):
for l in invoice.lines.all():
positions.append({
'ADes': l.description.replace("<br />", "\n"),
'ANetA': round(float(l.net_value), 2),
'ANetA': round(float((-1 if invoice.is_cancellation else 1) * l.net_value), 2),
'ANo': self.event.slug,
'AQ': -1 if invoice.is_cancellation else 1,
'AVatP': round(float(l.tax_rate), 2),
'DIDt': (l.subevent or invoice.order.event).date_from.isoformat().replace('Z', '+00:00'),
'PosGrossA': round(float((-1 if invoice.is_cancellation else 1) * l.gross_value), 2),
'PosNetA': round(float((-1 if invoice.is_cancellation else 1) * l.net_value), 2),
'PosGrossA': round(float(l.gross_value), 2),
'PosNetA': round(float(l.net_value), 2),
})
gross_total += l.gross_value
net_total += l.net_value
@@ -50,7 +50,7 @@ class DekodiNREIExporter(BaseExporter):
if p.provider == 'paypal':
paypal_email = p.info_data.get('payer', {}).get('payer_info', {}).get('email')
try:
ppid = p.info_data['transactions'][0]['related_resources']['sale']['id']
ppid = p.info_data['transactions'][0]['related_resources'][0]['sale']['id']
except:
ppid = p.info_data.get('id')
payments.append({
@@ -93,7 +93,7 @@ class DekodiNREIExporter(BaseExporter):
'PTNo15': p.full_id or '',
})
elif p.provider.startswith('stripe'):
src = p.info_data.get("source", "{}")
src = p.info_data.get("source", p.info_data)
payments.append({
'PTID': '81',
'PTN': 'Stripe',

View File

@@ -46,6 +46,7 @@ class InvoiceExporter(BaseExporter):
qs = qs.filter(date__lte=date_value)
with tempfile.TemporaryDirectory() as d:
any = False
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
for i in qs:
try:
@@ -54,14 +55,19 @@ class InvoiceExporter(BaseExporter):
i.refresh_from_db()
i.file.open('rb')
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
any = True
i.file.close()
except FileNotFoundError:
invoice_pdf_task.apply(args=(i.pk,))
i.refresh_from_db()
i.file.open('rb')
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
any = True
i.file.close()
if not any:
return None
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
return '{}_invoices.zip'.format(self.event.slug), 'application/zip', zipf.read()

View File

@@ -11,7 +11,9 @@ from django.contrib import messages
from django.core.exceptions import ValidationError
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import get_language, ugettext_lazy as _
from django_countries import countries
from django_countries.fields import Country, CountryField
from pretix.base.forms.widgets import (
BusinessBooleanRadio, DatePickerWidget, SplitDateTimePickerWidget,
@@ -213,6 +215,14 @@ class BaseQuestionsForm(forms.Form):
widget=forms.Textarea,
initial=initial.answer if initial else None,
)
elif q.type == Question.TYPE_COUNTRYCODE:
field = CountryField().formfield(
label=label, required=required,
help_text=help_text,
widget=forms.Select,
empty_label='',
initial=initial.answer if initial else None,
)
elif q.type == Question.TYPE_CHOICE:
field = forms.ModelChoiceField(
queryset=q.options,
@@ -342,6 +352,27 @@ class BaseInvoiceAddressForm(forms.ModelForm):
self.request = kwargs.pop('request', None)
self.validate_vat_id = kwargs.pop('validate_vat_id')
self.all_optional = kwargs.pop('all_optional', False)
kwargs.setdefault('initial', {})
if not kwargs.get('instance') or not kwargs['instance'].country:
# Try to guess the initial country from either the country of the merchant
# or the locale. This will hopefully save at least some users some scrolling :)
locale = get_language()
country = event.settings.invoice_address_from_country
if not country:
valid_countries = countries.countries
if '-' in locale:
parts = locale.split('-')
if parts[1].upper() in valid_countries:
country = Country(parts[1].upper())
elif parts[0].upper() in valid_countries:
country = Country(parts[0].upper())
else:
if locale in valid_countries:
country = Country(locale.upper())
kwargs['initial']['country'] = country
super().__init__(*args, **kwargs)
if not event.settings.invoice_address_vatid:
del self.fields['vat_id']
@@ -393,6 +424,12 @@ class BaseInvoiceAddressForm(forms.ModelForm):
self.instance.name_parts = data.get('name_parts')
if all(
not v for k, v in data.items() if k not in ('is_business', 'country', 'name_parts')
) and len(data.get('name_parts', {})) == 1:
# Do not save the country if it is the only field set -- we don't know the user even checked it!
self.cleaned_data['country'] = ''
if self.validate_vat_id and self.instance.vat_id_validated and 'vat_id' not in self.changed_data:
pass
elif self.validate_vat_id and data.get('is_business') and data.get('country') in EU_COUNTRIES and data.get('vat_id'):

View File

@@ -0,0 +1,51 @@
"""
Django, for theoretically very valid reasons, creates migrations for *every single thing*
we change on a model. Even the `help_text`! This makes sense, as we don't know if any
database backend unknown to us might actually use this information for its database schema.
However, pretix only supports PostgreSQL, MySQL, MariaDB and SQLite and we can be pretty
certain that some changes to models will never require a change to the database. In this case,
not creating a migration for certain changes will save us some performance while applying them
*and* allow for a cleaner git history. Win-win!
Only caveat is that we need to do some dirty monkeypatching to achieve it...
"""
from django.core.management.commands.makemigrations import Command as Parent
from django.db import models
from django.db.migrations.operations import models as modelops
from django_countries.fields import CountryField
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("verbose_name")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("verbose_name_plural")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("ordering")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("get_latest_by")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("default_manager_name")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("permissions")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("default_permissions")
IGNORED_ATTRS = [
# (field type, attribute name, blacklist of field sub-types)
(models.Field, 'verbose_name', []),
(models.Field, 'help_text', []),
(models.Field, 'validators', []),
(models.Field, 'editable', [models.DateField, models.DateTimeField, models.DateField, models.BinaryField]),
(models.Field, 'blank', [models.DateField, models.DateTimeField, models.AutoField, models.NullBooleanField,
models.TimeField]),
(models.CharField, 'choices', [CountryField])
]
original_deconstruct = models.Field.deconstruct
def new_deconstruct(self):
name, path, args, kwargs = original_deconstruct(self)
for ftype, attr, blacklist in IGNORED_ATTRS:
if isinstance(self, ftype) and not any(isinstance(self, ft) for ft in blacklist):
kwargs.pop(attr, None)
return name, path, args, kwargs
models.Field.deconstruct = new_deconstruct
class Command(Parent):
pass

View File

@@ -0,0 +1,28 @@
"""
Django tries to be helpful by suggesting to run "makemigrations" in red font on every "migrate"
run when there are things we have no migrations for. Usually, this is intended, and running
"makemigrations" can really screw up the environment of a user, so we want to prevent novice
users from doing that by going really dirty and fitlering it from the output.
"""
import sys
from django.core.management.base import OutputWrapper
from django.core.management.commands.migrate import Command as Parent
class OutputFilter(OutputWrapper):
blacklist = (
"Your models have changes that are not yet reflected",
"Run 'manage.py makemigrations' to make new "
)
def write(self, msg, style_func=None, ending=None):
if any(b in msg for b in self.blacklist):
return
super().write(msg, style_func, ending)
class Command(Parent):
def __init__(self, stdout=None, stderr=None, no_color=False, force_color=False):
super().__init__(stdout, stderr, no_color, force_color)
self.stdout = OutputFilter(stdout or sys.stdout)

View File

@@ -0,0 +1,22 @@
# Generated by Django 2.2 on 2019-05-09 06:54
import django.db.models.deletion
import jsonfallback.fields
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0118_auto_20190423_0839'),
]
operations = [
migrations.AddField(
model_name='question',
name='hidden',
field=models.BooleanField(default=False, help_text='This question will only show up in the backend.', verbose_name='Hidden question'),
),
]

View File

@@ -0,0 +1,77 @@
# Generated by Django 2.2 on 2019-05-09 07:36
import django.db.models.deletion
import jsonfallback.fields
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0119_auto_20190509_0654'),
]
operations = [
migrations.AlterField(
model_name='cartposition',
name='attendee_name_parts',
field=jsonfallback.fields.FallbackJSONField(default=dict),
),
migrations.AlterField(
model_name='cartposition',
name='subevent',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.SubEvent'),
),
migrations.AlterField(
model_name='cartposition',
name='voucher',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.Voucher'),
),
migrations.AlterField(
model_name='event',
name='is_public',
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name='invoiceaddress',
name='name_parts',
field=jsonfallback.fields.FallbackJSONField(default=dict),
),
migrations.AlterField(
model_name='item',
name='sales_channels',
field=pretix.base.models.fields.MultiStringField(default=['web']),
),
migrations.AlterField(
model_name='order',
name='sales_channel',
field=models.CharField(default='web', max_length=190),
),
migrations.AlterField(
model_name='orderposition',
name='attendee_name_parts',
field=jsonfallback.fields.FallbackJSONField(default=dict),
),
migrations.AlterField(
model_name='orderposition',
name='subevent',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.SubEvent'),
),
migrations.AlterField(
model_name='orderposition',
name='voucher',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.Voucher'),
),
migrations.AlterField(
model_name='staffsessionauditlog',
name='method',
field=models.CharField(max_length=255),
),
migrations.AlterField(
model_name='user',
name='email',
field=models.EmailField(db_index=True, max_length=190, null=True, unique=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.2.1 on 2019-05-15 05:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0120_auto_20190509_0736'),
]
operations = [
migrations.AddField(
model_name='order',
name='email_known_to_work',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 2.2.1 on 2019-05-15 13:23
from django.db import migrations, models
import pretix.base.models.orders
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0121_order_email_known_to_work'),
]
operations = [
migrations.AddField(
model_name='orderposition',
name='web_secret',
field=models.CharField(db_index=True, default=pretix.base.models.orders.generate_secret, max_length=32),
),
]

View File

@@ -6,7 +6,9 @@ from django.db import models
from django.db.models.constants import LOOKUP_SEP
from django.db.models.signals import post_delete
from django.dispatch import receiver
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from pretix.helpers.json import CustomJSONEncoder
@@ -113,6 +115,40 @@ class LoggedModel(models.Model, LoggingMixin):
class Meta:
abstract = True
@cached_property
def logs_content_type(self):
return ContentType.objects.get_for_model(type(self))
@cached_property
def all_logentries_link(self):
from pretix.base.models import Event
if isinstance(self, Event):
event = self
elif hasattr(self, 'event'):
event = self.event
else:
return None
return reverse(
'control:event.log',
kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
}
) + '?content_type={}&object={}'.format(
self.logs_content_type.pk,
self.pk
)
def top_logentries(self):
qs = self.all_logentries()
if self.all_logentries_link:
qs = qs[:25]
return qs
def top_logentries_has_more(self):
return self.all_logentries().count() > 25
def all_logentries(self):
"""
Returns all log entries that are attached to this object.
@@ -122,7 +158,7 @@ class LoggedModel(models.Model, LoggingMixin):
from .log import LogEntry
return LogEntry.objects.filter(
content_type=ContentType.objects.get_for_model(type(self)), object_id=self.pk
content_type=self.logs_content_type, object_id=self.pk
).select_related('user', 'event', 'oauth_application', 'api_token', 'device')

View File

@@ -164,7 +164,7 @@ class EventMixin:
def annotated(cls, qs, channel='web'):
from pretix.base.models import Item, ItemVariation, Quota
sq_active_item = Item.objects.filter_available(channel=channel).filter(
sq_active_item = Item.objects.using(settings.DATABASE_REPLICA).filter_available(channel=channel).filter(
Q(variations__isnull=True)
& Q(quotas__pk=OuterRef('pk'))
).order_by().values_list('quotas__pk').annotate(
@@ -186,7 +186,7 @@ class EventMixin:
Prefetch(
'quotas',
to_attr='active_quotas',
queryset=Quota.objects.annotate(
queryset=Quota.objects.using(settings.DATABASE_REPLICA).annotate(
active_items=Subquery(sq_active_item, output_field=models.TextField()),
active_variations=Subquery(sq_active_variation, output_field=models.TextField()),
).exclude(
@@ -445,6 +445,7 @@ class Event(EventMixin, LoggedModel):
self.plugins = other.plugins
self.is_public = other.is_public
self.testmode = other.testmode
self.save()
tax_map = {}

View File

@@ -16,6 +16,7 @@ from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.timezone import is_naive, make_aware, now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_countries.fields import Country
from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.models import fields
@@ -332,7 +333,9 @@ class Item(LoggedModel):
require_bundling = models.BooleanField(
verbose_name=_('Only sell this product as part of a bundle'),
default=False,
help_text=_('If this option is set, the product will only be sold as part of bundle products.')
help_text=_('If this option is set, the product will only be sold as part of bundle products. Do '
'<strong>not</strong> check this option if you want to use this product as an add-on product, '
'but only for fixed bundles!')
)
allow_cancel = models.BooleanField(
verbose_name=_('Allow product to be canceled'),
@@ -893,6 +896,8 @@ class Question(LoggedModel):
:param items: A set of ``Items`` objects that this question should be applied to
:param ask_during_checkin: Whether to ask this question during check-in instead of during check-out.
:type ask_during_checkin: bool
:param hidden: Whether to only show the question in the backend
:type hidden: bool
:param identifier: An arbitrary, internal identifier
:type identifier: str
:param dependency_question: This question will only show up if the referenced question is set to `dependency_value`.
@@ -910,6 +915,7 @@ class Question(LoggedModel):
TYPE_DATE = "D"
TYPE_TIME = "H"
TYPE_DATETIME = "W"
TYPE_COUNTRYCODE = "CC"
TYPE_CHOICES = (
(TYPE_NUMBER, _("Number")),
(TYPE_STRING, _("Text (one line)")),
@@ -921,6 +927,7 @@ class Question(LoggedModel):
(TYPE_DATE, _("Date")),
(TYPE_TIME, _("Time")),
(TYPE_DATETIME, _("Date and time")),
(TYPE_COUNTRYCODE, _("Country code (ISO 3166-1 alpha-2)")),
)
event = models.ForeignKey(
@@ -968,6 +975,11 @@ class Question(LoggedModel):
'pretixdesk 0.2 or newer.'),
default=False
)
hidden = models.BooleanField(
verbose_name=_('Hidden question'),
help_text=_('This question will only show up in the backend.'),
default=False
)
dependency_question = models.ForeignKey(
'Question', null=True, blank=True, on_delete=models.SET_NULL, related_name='dependent_questions'
)
@@ -1071,6 +1083,12 @@ class Question(LoggedModel):
return dt
except:
raise ValidationError(_('Invalid datetime input.'))
elif self.type == Question.TYPE_COUNTRYCODE and answer:
c = Country(answer.upper())
if c.name:
return answer
else:
raise ValidationError(_('Unknown country code.'))
return answer
@@ -1290,7 +1308,8 @@ class Quota(LoggedModel):
'cached_availability_state', 'cached_availability_number', 'cached_availability_time',
'cached_availability_paid_orders'
],
clear_cache=False
clear_cache=False,
using='default'
)
if _cache is not None:

View File

@@ -1,4 +1,5 @@
import copy
import hashlib
import json
import logging
import os
@@ -24,7 +25,7 @@ from django.utils.formats import date_format
from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_countries.fields import CountryField
from django_countries.fields import Country, CountryField
from i18nfield.strings import LazyI18nString
from jsonfallback.fields import FallbackJSONField
@@ -180,6 +181,10 @@ class Order(LockModel, LoggedModel):
default=False
)
sales_channel = models.CharField(max_length=190, default="web")
email_known_to_work = models.BooleanField(
default=False,
verbose_name=_('E-mail address verified')
)
class Meta:
verbose_name = _("Order")
@@ -206,6 +211,9 @@ class Order(LockModel, LoggedModel):
self.event.cache.delete('complain_testmode_orders')
self.delete()
def email_confirm_hash(self):
return hashlib.sha256(settings.SECRET_KEY.encode() + self.secret.encode()).hexdigest()[:9]
@property
def fees(self):
"""
@@ -665,7 +673,7 @@ class Order(LockModel, LoggedModel):
def send_mail(self, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent',
user: User=None, headers: dict=None, sender: str=None, invoices: list=None,
auth=None, attach_tickets=False):
auth=None, attach_tickets=False, position: 'OrderPosition'=None):
"""
Sends an email to the user that placed this order. Basically, this method does two things:
@@ -682,6 +690,9 @@ class Order(LockModel, LoggedModel):
:param headers: Dictionary with additional mail headers
:param sender: Custom email sender.
:param attach_tickets: Attach tickets of this order, if they are existing and ready to download
:param position: An order position this refers to. If given, no invoices will be attached, the tickets will
only be attached for this position and child positions, the link will only point to the
position and the attendee email will be used if available.
"""
from pretix.base.services.mail import SendMailException, mail, render_mail
@@ -693,12 +704,16 @@ class Order(LockModel, LoggedModel):
with language(self.locale):
recipient = self.email
if position and position.attendee_email:
recipient = position.attendee_email
try:
email_content = render_mail(template, context)
mail(
recipient, subject, template, context,
self.event, self.locale, self, headers, sender,
invoices=invoices, attach_tickets=attach_tickets
self.event, self.locale, self, headers=headers, sender=sender,
invoices=invoices, attach_tickets=attach_tickets,
position=position
)
except SendMailException:
raise
@@ -710,6 +725,7 @@ class Order(LockModel, LoggedModel):
data={
'subject': subject,
'message': email_content,
'position': position.positionid if position else None,
'recipient': recipient,
'invoices': [i.pk for i in invoices] if invoices else [],
'attach_tickets': attach_tickets,
@@ -729,9 +745,10 @@ class Order(LockModel, LoggedModel):
email_template = self.event.settings.mail_text_resend_link
email_context = {
'event': self.event.name,
'url': build_absolute_uri(self.event, 'presale:event.order', kwargs={
'url': build_absolute_uri(self.event, 'presale:event.order.open', kwargs={
'order': self.code,
'secret': self.secret
'secret': self.secret,
'hash': self.email_confirm_hash()
}),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
@@ -860,6 +877,8 @@ class QuestionAnswer(models.Model):
return date_format(d, "TIME_FORMAT")
except ValueError:
return self.answer
elif self.question.type == Question.TYPE_COUNTRYCODE and self.answer:
return Country(self.answer).name or self.answer
else:
return self.answer
@@ -959,6 +978,10 @@ class AbstractPosition(models.Model):
else:
return {}
@meta_info_data.setter
def meta_info_data(self, d):
self.meta_info = json.dumps(d)
def cache_answers(self, all=True):
"""
Creates two properties on the object.
@@ -976,7 +999,8 @@ class AbstractPosition(models.Model):
if hasattr(self.item, 'questions_to_ask'):
questions = list(copy.copy(q) for q in self.item.questions_to_ask)
else:
questions = list(copy.copy(q) for q in self.item.questions.filter(ask_during_checkin=False))
questions = list(copy.copy(q) for q in self.item.questions.filter(ask_during_checkin=False,
hidden=False))
else:
questions = list(copy.copy(q) for q in self.item.questions.all())
@@ -1160,7 +1184,8 @@ class OrderPayment(models.Model):
self.order.log_action('pretix.event.order.overpaid', {}, user=user, auth=auth)
order_paid.send(self.order.event, order=self.order)
def confirm(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text='', ignore_date=False, lock=True):
def confirm(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text='',
ignore_date=False, lock=True, payment_date=None):
"""
Marks the payment as complete. If possible, this also marks the order as paid if no further
payment is required
@@ -1181,8 +1206,6 @@ class OrderPayment(models.Model):
:raises Quota.QuotaExceededException: if the quota is exceeded and ``force`` is ``False``
"""
from pretix.base.services.invoices import generate_invoice, invoice_qualified
from pretix.base.services.mail import SendMailException
from pretix.multidomain.urlreverse import build_absolute_uri
with transaction.atomic():
locked_instance = OrderPayment.objects.select_for_update().get(pk=self.pk)
@@ -1191,7 +1214,7 @@ class OrderPayment(models.Model):
return
locked_instance.state = self.PAYMENT_STATE_CONFIRMED
locked_instance.payment_date = now()
locked_instance.payment_date = payment_date or now()
locked_instance.info = self.info # required for backwards compatibility
locked_instance.save(update_fields=['state', 'payment_date', 'info'])
@@ -1246,35 +1269,77 @@ class OrderPayment(models.Model):
)
if send_mail:
with language(self.order.locale):
try:
invoice_name = self.order.invoice_address.name
invoice_company = self.order.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
email_template = self.order.event.settings.mail_text_order_paid
email_context = {
'event': self.order.event.name,
'url': build_absolute_uri(self.order.event, 'presale:event.order', kwargs={
'order': self.order.code,
'secret': self.order.secret
}),
'downloads': self.order.event.settings.get('ticket_download', as_type=bool),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
'payment_info': mail_text
}
email_subject = _('Payment received for your order: %(code)s') % {'code': self.order.code}
try:
self.order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_paid', user,
invoices=[invoice] if invoice and self.order.event.settings.invoice_email_attachment else [],
attach_tickets=True
)
except SendMailException:
logger.exception('Order paid email could not be sent')
self._send_paid_mail(invoice, user, mail_text)
if self.order.event.settings.mail_send_order_paid_attendee:
for p in self.order.positions.all():
if p.addon_to_id is None and p.attendee_email and p.attendee_email != self.order.email:
self._send_paid_mail_attendee(p, user)
def _send_paid_mail_attendee(self, position, user):
from pretix.base.services.mail import SendMailException
from pretix.multidomain.urlreverse import build_absolute_uri
with language(self.order.locale):
name_scheme = PERSON_NAME_SCHEMES[self.order.event.settings.name_scheme]
email_template = self.order.event.settings.mail_text_order_paid_attendee
email_context = {
'event': self.order.event.name,
'downloads': self.order.event.settings.get('ticket_download', as_type=bool),
'url': build_absolute_uri(self.order.event, 'presale:event.order.position', kwargs={
'order': self.order.code,
'secret': position.web_secret,
'position': position.positionid
}),
'attendee_name': position.attendee_name,
}
for f, l, w in name_scheme['fields']:
email_context['attendee_name_%s' % f] = position.attendee_name_parts.get(f, '')
email_subject = _('Event registration confirmed: %(code)s') % {'code': self.order.code}
try:
self.order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_paid', user,
invoices=[], position=position,
attach_tickets=True
)
except SendMailException:
logger.exception('Order paid email could not be sent')
def _send_paid_mail(self, invoice, user, mail_text):
from pretix.base.services.mail import SendMailException
from pretix.multidomain.urlreverse import build_absolute_uri
with language(self.order.locale):
try:
invoice_name = self.order.invoice_address.name
invoice_company = self.order.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
email_template = self.order.event.settings.mail_text_order_paid
email_context = {
'event': self.order.event.name,
'url': build_absolute_uri(self.order.event, 'presale:event.order.open', kwargs={
'order': self.order.code,
'secret': self.order.secret,
'hash': self.order.email_confirm_hash()
}),
'downloads': self.order.event.settings.get('ticket_download', as_type=bool),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
'payment_info': mail_text
}
email_subject = _('Payment received for your order: %(code)s') % {'code': self.order.code}
try:
self.order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_paid', user,
invoices=[invoice] if invoice and self.order.event.settings.invoice_email_attachment else [],
attach_tickets=True
)
except SendMailException:
logger.exception('Order paid email could not be sent')
@property
def refunded_amount(self):
@@ -1663,6 +1728,7 @@ class OrderPosition(AbstractPosition):
verbose_name=_('Tax value')
)
secret = models.CharField(max_length=64, default=generate_position_secret, db_index=True)
web_secret = models.CharField(max_length=32, default=generate_secret, db_index=True)
pseudonymization_id = models.CharField(
max_length=16,
unique=True,
@@ -1786,6 +1852,60 @@ class OrderPosition(AbstractPosition):
def event(self):
return self.order.event
def send_mail(self, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent',
user: User=None, headers: dict=None, sender: str=None, invoices: list=None,
auth=None, attach_tickets=False):
"""
Sends an email to the user that placed this order. Basically, this method does two things:
* Call ``pretix.base.services.mail.mail`` with useful values for the ``event``, ``locale``, ``recipient`` and
``order`` parameters.
* Create a ``LogEntry`` with the email contents.
:param subject: Subject of the email
:param template: LazyI18nString or template filename, see ``pretix.base.services.mail.mail`` for more details
:param context: Dictionary to use for rendering the template
:param log_entry_type: Key to be used for the log entry
:param user: Administrative user who triggered this mail to be sent
:param headers: Dictionary with additional mail headers
:param sender: Custom email sender.
:param attach_tickets: Attach tickets of this order, if they are existing and ready to download
"""
from pretix.base.services.mail import SendMailException, mail, render_mail
if not self.email:
return
for k, v in self.event.meta_data.items():
context['meta_' + k] = v
with language(self.locale):
recipient = self.email
try:
email_content = render_mail(template, context)
mail(
recipient, subject, template, context,
self.event, self.locale, self, headers, sender,
invoices=invoices, attach_tickets=attach_tickets
)
except SendMailException:
raise
else:
self.log_action(
log_entry_type,
user=user,
auth=auth,
data={
'subject': subject,
'message': email_content,
'recipient': recipient,
'invoices': [i.pk for i in invoices] if invoices else [],
'attach_tickets': attach_tickets,
}
)
class CartPosition(AbstractPosition):
"""

View File

@@ -1,8 +1,10 @@
import sys
from enum import Enum
from typing import List
from django.apps import apps
from django.apps import AppConfig, apps
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
class PluginType(Enum):
@@ -39,3 +41,22 @@ def get_all_plugins(event=None) -> List[type]:
plugins,
key=lambda m: (0 if m.module.startswith('pretix.') else 1, str(m.name).lower().replace('pretix ', ''))
)
class PluginConfig(AppConfig):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not hasattr(self, 'PretixPluginMeta'):
raise ImproperlyConfigured("A pretix plugin config should have a PretixPluginMeta inner class.")
if hasattr(self.PretixPluginMeta, 'compatibility'):
import pkg_resources
try:
pkg_resources.require(self.PretixPluginMeta.compatibility)
except pkg_resources.VersionConflict as e:
print("Incompatible plugins found!")
print("Plugin {} requires you to have {}, but you installed {}.".format(
self.name, e.req, e.dist
))
sys.exit(1)

View File

@@ -71,7 +71,7 @@ class RelativeDateWrapper:
else:
base_date = getattr(event, self.data.base_date_name) or event.date_from
oldoffset = base_date.utcoffset()
oldoffset = base_date.astimezone(tz).utcoffset()
new_date = base_date.astimezone(tz) - datetime.timedelta(days=self.data.days_before)
if self.data.time:
new_date = new_date.replace(

View File

@@ -634,7 +634,7 @@ class CartManager:
Q(voucher=voucher) & Q(event=self.event) &
Q(expires__gte=self.now_dt)
).exclude(pk__in=[
op.position.voucher_id for op in self._operations if isinstance(op, self.ExtendOperation)
op.position.id for op in self._operations if isinstance(op, self.ExtendOperation)
])
cart_count = redeemed_in_carts.count()
v_avail = voucher.max_usages - voucher.redeemed - cart_count

View File

@@ -2,15 +2,20 @@ from typing import Any, Dict
from django.core.files.base import ContentFile
from django.utils.timezone import override
from django.utils.translation import ugettext
from pretix.base.i18n import language
from pretix.base.i18n import LazyLocaleException, language
from pretix.base.models import CachedFile, Event, cachedfile_name
from pretix.base.services.tasks import ProfiledTask
from pretix.base.signals import register_data_exporters
from pretix.celery_app import app
@app.task(base=ProfiledTask)
class ExportError(LazyLocaleException):
pass
@app.task(base=ProfiledTask, throws=(ExportError,))
def export(event: str, fileid: str, provider: str, form_data: Dict[str, Any]) -> None:
event = Event.objects.get(id=event)
file = CachedFile.objects.get(id=fileid)
@@ -19,7 +24,12 @@ def export(event: str, fileid: str, provider: str, form_data: Dict[str, Any]) ->
for receiver, response in responses:
ex = response(event)
if ex.identifier == provider:
file.filename, file.type, data = ex.render(form_data)
d = ex.render(form_data)
if d is None:
raise ExportError(
ugettext('Your export did not contain any data.')
)
file.filename, file.type, data = d
file.file.save(cachedfile_name(file, file.filename), ContentFile(data))
file.save()
return file.pk

View File

@@ -257,7 +257,8 @@ def invoice_pdf_task(invoice: int):
def invoice_qualified(order: Order):
if order.total == Decimal('0.00') or order.require_approval:
if order.total == Decimal('0.00') or order.require_approval or \
order.sales_channel not in order.event.settings.get('invoice_generate_sales_channels'):
return False
return True

View File

@@ -1,5 +1,6 @@
import logging
import smtplib
import warnings
from email.utils import formataddr
from typing import Any, Dict, List, Union
@@ -13,7 +14,9 @@ from i18nfield.strings import LazyI18nString
from pretix.base.email import ClassicMailRenderer
from pretix.base.i18n import language
from pretix.base.models import Event, Invoice, InvoiceAddress, Order
from pretix.base.models import (
Event, Invoice, InvoiceAddress, Order, OrderPosition,
)
from pretix.base.services.invoices import invoice_pdf_task
from pretix.base.services.tasks import TransactionAwareTask
from pretix.base.services.tickets import get_tickets_for_order
@@ -38,8 +41,8 @@ class SendMailException(Exception):
def mail(email: str, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, event: Event=None, locale: str=None,
order: Order=None, headers: dict=None, sender: str=None, invoices: list=None,
attach_tickets=False):
order: Order=None, position: OrderPosition=None, headers: dict=None, sender: str=None,
invoices: list=None, attach_tickets=False):
"""
Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation.
@@ -60,6 +63,9 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
:param order: The order this email is related to (optional). If set, this will be used to include a link to the
order below the email.
:param order: The order position this email is related to (optional). If set, this will be used to include a link
to the order position instead of the order below the email.
:param headers: A dict of custom mail headers to add to the mail
:param locale: The locale to be used while evaluating the subject and the template
@@ -100,7 +106,8 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
subject = str(subject).format_map(context)
sender = sender or (event.settings.get('mail_from') if event else settings.MAIL_FROM)
if event:
sender = formataddr((str(event.name), sender))
sender_name = event.settings.mail_from_name or str(event.name)
sender = formataddr((sender_name, sender))
else:
sender = formataddr((settings.PRETIX_INSTANCE_NAME, sender))
@@ -111,7 +118,8 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
if event:
renderer = event.get_html_mail_renderer()
if event.settings.mail_bcc:
bcc.append(event.settings.mail_bcc)
for bcc_mail in event.settings.mail_bcc.split(','):
bcc.append(bcc_mail.strip())
if event.settings.mail_from == settings.DEFAULT_FROM_EMAIL and event.settings.contact_mail and not headers.get('Reply-To'):
headers['Reply-To'] = event.settings.contact_mail
@@ -130,9 +138,26 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
body_plain += signature
body_plain += "\r\n\r\n-- \r\n"
if order:
if order.testmode:
subject = "[TESTMODE] " + subject
if order and order.testmode:
subject = "[TESTMODE] " + subject
if order and position:
body_plain += _(
"You are receiving this email because someone placed an order for {event} for you."
).format(event=event.name)
body_plain += "\r\n"
body_plain += _(
"You can view your order details at the following URL:\n{orderurl}."
).replace("\n", "\r\n").format(
event=event.name, orderurl=build_absolute_uri(
order.event, 'presale:event.order.position', kwargs={
'order': order.code,
'secret': position.web_secret,
'position': position.positionid,
}
)
)
elif order:
body_plain += _(
"You are receiving this email because you placed an order for {event}."
).format(event=event.name)
@@ -141,16 +166,24 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
"You can view your order details at the following URL:\n{orderurl}."
).replace("\n", "\r\n").format(
event=event.name, orderurl=build_absolute_uri(
order.event, 'presale:event.order', kwargs={
order.event, 'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret
'secret': order.secret,
'hash': order.email_confirm_hash()
}
)
)
body_plain += "\r\n"
try:
body_html = renderer.render(content_plain, signature, str(subject), order)
try:
body_html = renderer.render(content_plain, signature, str(subject), order, position)
except TypeError:
# Backwards compatibility
warnings.warn('E-mail renderer called without position argument because position argument is not '
'supported.',
DeprecationWarning)
body_html = renderer.render(content_plain, signature, str(subject), order)
except:
logger.exception('Could not render HTML body')
body_html = None
@@ -164,8 +197,9 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
sender=sender,
event=event.id if event else None,
headers=headers,
invoices=[i.pk for i in invoices] if invoices else [],
invoices=[i.pk for i in invoices] if invoices and not position else [],
order=order.pk if order else None,
position=position.pk if position else None,
attach_tickets=attach_tickets
)
@@ -180,8 +214,8 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
@app.task(base=TransactionAwareTask, bind=True)
def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: str, sender: str,
event: int=None, headers: dict=None, bcc: List[str]=None, invoices: List[int]=None,
order: int=None, attach_tickets=False) -> bool:
event: int=None, position: int=None, headers: dict=None, bcc: List[str]=None,
invoices: List[int]=None, order: int=None, attach_tickets=False) -> bool:
email = EmailMultiAlternatives(subject, body, sender, to=to, bcc=bcc, headers=headers)
if html is not None:
email.attach_alternative(html, "text/html")
@@ -212,10 +246,15 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
except Order.DoesNotExist:
order = None
else:
if position:
try:
position = order.positions.get(pk=position)
except OrderPosition.DoesNotExist:
attach_tickets = False
if attach_tickets:
args = []
attach_size = 0
for name, ct in get_tickets_for_order(order):
for name, ct in get_tickets_for_order(order, base_position=position):
content = ct.file.read()
args.append((name, content, ct.type))
attach_size += len(content)

View File

@@ -104,7 +104,11 @@ def send_notification_mail(notification: Notification, user: User):
mail_send_task.apply_async(kwargs={
'to': [user.email],
'subject': '[{}] {}'.format(settings.PRETIX_INSTANCE_NAME, notification.title),
'subject': '[{}] {}: {}'.format(
settings.PRETIX_INSTANCE_NAME,
notification.event.settings.mail_prefix or notification.event.slug.upper(),
notification.title
),
'body': body_plain,
'html': body_html,
'sender': settings.MAIL_FROM,

View File

@@ -43,10 +43,11 @@ from pretix.base.services.locking import LockTimeoutException, NoLockManager
from pretix.base.services.mail import SendMailException
from pretix.base.services.pricing import get_price
from pretix.base.services.tasks import ProfiledTask
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import (
allow_ticket_download, order_approved, order_canceled, order_changed,
order_denied, order_expired, order_fee_calculation, order_placed,
periodic_task,
periodic_task, validate_order,
)
from pretix.celery_app import app
from pretix.helpers.models import modelcopy
@@ -222,9 +223,10 @@ def approve_order(order, user=None, send_mail: bool=True, auth=None, force=False
'total_with_currency': LazyCurrencyNumber(order.total, order.event.currency),
'date': LazyDate(order.expires),
'event': order.event.name,
'url': build_absolute_uri(order.event, 'presale:event.order', kwargs={
'url': build_absolute_uri(order.event, 'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret
'secret': order.secret,
'hash': order.email_confirm_hash()
}),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
@@ -282,9 +284,10 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
'total_with_currency': LazyCurrencyNumber(order.total, order.event.currency),
'date': LazyDate(order.expires),
'event': order.event.name,
'url': build_absolute_uri(order.event, 'presale:event.order', kwargs={
'url': build_absolute_uri(order.event, 'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret
'secret': order.secret,
'hash': order.email_confirm_hash()
}),
'comment': comment,
'invoice_name': invoice_name,
@@ -375,9 +378,10 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
email_context = {
'event': order.event.name,
'code': order.code,
'url': build_absolute_uri(order.event, 'presale:event.order', kwargs={
'url': build_absolute_uri(order.event, 'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret
'secret': order.secret,
'hash': order.email_confirm_hash()
})
}
with language(order.locale):
@@ -531,7 +535,6 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
continue
if price.gross != cp.price and not (cp.item.free_price and cp.price > price.gross):
positions[i] = cp
cp.price = price.gross
cp.includes_tax = bool(price.rate)
cp.save()
@@ -555,7 +558,6 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
break
if quota_ok:
positions[i] = cp
cp.expires = now_dt + timedelta(
minutes=event.settings.get('reservation_time', as_type=int))
cp.save()
@@ -644,6 +646,75 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
return order, p
def _order_placed_email(event: Event, order: Order, pprov: BasePaymentProvider, email_template, log_entry: str,
invoice):
try:
invoice_name = order.invoice_address.name
invoice_company = order.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
if pprov:
payment_info = str(pprov.order_pending_mail_render(order))
else:
payment_info = None
email_context = {
'total': LazyNumber(order.total),
'currency': event.currency,
'total_with_currency': LazyCurrencyNumber(order.total, event.currency),
'date': LazyDate(order.expires),
'event': event.name,
'url': build_absolute_uri(event, 'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash()
}),
'payment_info': payment_info,
'invoice_name': invoice_name,
'invoice_company': invoice_company,
}
email_subject = _('Your order: %(code)s') % {'code': order.code}
try:
order.send_mail(
email_subject, email_template, email_context,
log_entry,
invoices=[invoice] if invoice and event.settings.invoice_email_attachment else [],
attach_tickets=True
)
except SendMailException:
logger.exception('Order received email could not be sent')
def _order_placed_email_attendee(event: Event, order: Order, position: OrderPosition, email_template, log_entry: str):
name_scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
email_context = {
'event': event.name,
'url': build_absolute_uri(event, 'presale:event.order.position', kwargs={
'order': order.code,
'secret': position.web_secret,
'position': position.positionid
}),
'attendee_name': position.attendee_name,
}
for f, l, w in name_scheme['fields']:
email_context['attendee_name_%s' % f] = position.attendee_name_parts.get(f, '')
email_subject = _('Your event registration: %(code)s') % {'code': order.code}
try:
order.send_mail(
email_subject, email_template, email_context,
log_entry,
invoices=[],
attach_tickets=True,
position=position
)
except SendMailException:
logger.exception('Order received email could not be sent to attendee')
def _perform_order(event: str, payment_provider: str, position_ids: List[str],
email: str, locale: str, address: int, meta_info: dict=None, sales_channel: str='web'):
@@ -667,6 +738,9 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
positions = CartPosition.objects.filter(id__in=position_ids, event=event)
validate_order.send(event, payment_provider=pprov, email=email, positions=positions,
locale=locale, invoice_address=addr, meta_info=meta_info)
lockfn = NoLockManager
locked = False
if positions.filter(Q(voucher__isnull=False) | Q(expires__lt=now() + timedelta(minutes=2))).exists():
@@ -705,49 +779,26 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
if order.require_approval:
email_template = event.settings.mail_text_order_placed_require_approval
log_entry = 'pretix.event.order.email.order_placed_require_approval'
email_attendees = False
elif free_order_flow:
email_template = event.settings.mail_text_order_free
log_entry = 'pretix.event.order.email.order_free'
email_attendees = event.settings.mail_send_order_free_attendee
email_attendees_template = event.settings.mail_text_order_free_attendee
else:
email_template = event.settings.mail_text_order_placed
log_entry = 'pretix.event.order.email.order_placed'
try:
invoice_name = order.invoice_address.name
invoice_company = order.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
email_attendees = event.settings.mail_send_order_placed_attendee
email_attendees_template = event.settings.mail_text_order_placed_attendee
if pprov:
payment_info = str(pprov.order_pending_mail_render(order))
else:
payment_info = None
email_context = {
'total': LazyNumber(order.total),
'currency': event.currency,
'total_with_currency': LazyCurrencyNumber(order.total, event.currency),
'date': LazyDate(order.expires),
'event': event.name,
'url': build_absolute_uri(event, 'presale:event.order', kwargs={
'order': order.code,
'secret': order.secret
}),
'payment_info': payment_info,
'invoice_name': invoice_name,
'invoice_company': invoice_company,
}
email_subject = _('Your order: %(code)s') % {'code': order.code}
try:
order.send_mail(
email_subject, email_template, email_context,
log_entry,
invoices=[invoice] if invoice and event.settings.invoice_email_attachment else [],
attach_tickets=True
)
except SendMailException:
logger.exception('Order received email could not be sent')
_order_placed_email(event, order, pprov, email_template, log_entry, invoice)
if email_attendees:
for p in order.positions.all():
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
_order_placed_email_attendee(event, order, p, email_attendees_template, log_entry)
return order.id
@@ -800,9 +851,10 @@ def send_expiry_warnings(sender, **kwargs):
email_template = eventsettings.mail_text_order_expire_warning
email_context = {
'event': o.event.name,
'url': build_absolute_uri(o.event, 'presale:event.order', kwargs={
'url': build_absolute_uri(o.event, 'presale:event.order.open', kwargs={
'order': o.code,
'secret': o.secret
'secret': o.secret,
'hash': o.email_confirm_hash()
}),
'expire_date': date_format(o.expires.astimezone(tz), 'SHORT_DATE_FORMAT'),
'invoice_name': invoice_name,
@@ -851,9 +903,10 @@ def send_download_reminders(sender, **kwargs):
email_template = e.settings.mail_text_download_reminder
email_context = {
'event': o.event.name,
'url': build_absolute_uri(o.event, 'presale:event.order', kwargs={
'url': build_absolute_uri(o.event, 'presale:event.order.open', kwargs={
'order': o.code,
'secret': o.secret
'secret': o.secret,
'hash': o.email_confirm_hash()
}),
}
email_subject = _('Your ticket is ready for download: %(code)s') % {'code': o.code}
@@ -866,6 +919,31 @@ def send_download_reminders(sender, **kwargs):
except SendMailException:
logger.exception('Reminder email could not be sent')
if e.settings.mail_send_download_reminder_attendee:
name_scheme = PERSON_NAME_SCHEMES[e.settings.name_scheme]
for p in o.positions.all():
if p.addon_to_id is None and p.attendee_email and p.attendee_email != o.email:
email_template = e.settings.mail_text_download_reminder_attendee
email_context = {
'event': e.name,
'url': build_absolute_uri(e, 'presale:event.order.position', kwargs={
'order': o.code,
'secret': p.web_secret,
'position': p.positionid
}),
'attendee_name': p.attendee_name,
}
for f, l, w in name_scheme['fields']:
email_context['attendee_name_%s' % f] = p.attendee_name_parts.get(f, '')
try:
o.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.download_reminder_sent',
attach_tickets=True, position=p
)
except SendMailException:
logger.exception('Reminder email could not be sent to attendee')
class OrderChangeManager:
error_messages = {
@@ -1350,9 +1428,10 @@ class OrderChangeManager:
email_template = order.event.settings.mail_text_order_changed
email_context = {
'event': order.event.name,
'url': build_absolute_uri(self.order.event, 'presale:event.order', kwargs={
'url': build_absolute_uri(self.order.event, 'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret
'secret': order.secret,
'hash': order.email_confirm_hash()
}),
'invoice_name': invoice_name,
'invoice_company': invoice_company,

View File

@@ -1,11 +1,11 @@
from datetime import timedelta
from django.db import models
from django.db.models import F, Max, OuterRef, Q, Subquery
from django.conf import settings
from django.db.models import Max, Q
from django.dispatch import receiver
from django.utils.timezone import now
from pretix.base.models import LogEntry, Quota
from pretix.base.models import Event, LogEntry
from pretix.celery_app import app
from ..signals import periodic_task
@@ -18,19 +18,25 @@ def build_all_quota_caches(sender, **kwargs):
@app.task
def refresh_quota_caches():
last_activity = LogEntry.objects.filter(
event=OuterRef('event_id'),
# Active events
active = LogEntry.objects.using(settings.DATABASE_REPLICA).filter(
datetime__gt=now() - timedelta(days=7)
).order_by().values('event').annotate(
m=Max('datetime')
).values(
'm'
last_activity=Max('datetime')
)
quotas = Quota.objects.annotate(
last_activity=Subquery(last_activity, output_field=models.DateTimeField())
).filter(
Q(cached_availability_time__isnull=True) |
Q(cached_availability_time__lt=F('last_activity')) |
Q(cached_availability_time__lt=now() - timedelta(hours=2), last_activity__gt=now() - timedelta(days=7))
).select_related('subevent')
for q in quotas:
q.availability()
for a in active:
try:
e = Event.objects.using(settings.DATABASE_REPLICA).get(pk=a['event'])
except Event.DoesNotExist:
continue
quotas = e.quotas.filter(
Q(cached_availability_time__isnull=True) |
Q(cached_availability_time__lt=a['last_activity']) |
Q(cached_availability_time__lt=now() - timedelta(hours=2))
).filter(
Q(subevent__isnull=True) |
Q(subevent__date_to__isnull=False, subevent__date_to__gte=now() - timedelta(days=14)) |
Q(subevent__date_from__gte=now() - timedelta(days=14))
)
for q in quotas:
q.availability()

View File

@@ -96,7 +96,7 @@ def preview(event: int, provider: str):
return prov.generate(p)
def get_tickets_for_order(order):
def get_tickets_for_order(order, base_position=None):
can_download = all([r for rr, r in allow_ticket_download.send(order.event, order=order)])
if not can_download:
return []
@@ -111,13 +111,20 @@ def get_tickets_for_order(order):
tickets = []
positions = list(order.positions_with_tickets)
if base_position:
# Only the given position and its children
positions = [
p for p in positions if p.pk == base_position.pk or p.addon_to_id == base_position.pk
]
for p in providers:
if not p.is_enabled:
continue
if p.multi_download_enabled:
if p.multi_download_enabled and not base_position:
try:
if len(list(order.positions_with_tickets)) == 0:
if len(positions) == 0:
continue
ct = CachedCombinedTicket.objects.filter(
order=order, provider=p.identifier, file__isnull=False
@@ -136,7 +143,7 @@ def get_tickets_for_order(order):
except:
logger.exception('Failed to generate ticket.')
else:
for pos in order.positions_with_tickets:
for pos in positions:
try:
ct = CachedTicket.objects.filter(
order_position=pos, provider=p.identifier, file__isnull=False
@@ -145,7 +152,7 @@ def get_tickets_for_order(order):
retval = generate_orderposition(pos.pk, p.identifier)
if not retval:
continue
ct = CachedCombinedTicket.objects.get(pk=retval)
ct = CachedTicket.objects.get(pk=retval)
tickets.append((
"{}-{}-{}-{}{}".format(
order.event.slug.upper(), order.code, pos.positionid, ct.provider, ct.extension,

View File

@@ -145,6 +145,10 @@ DEFAULTS = {
'default': 'False',
'type': str
},
'invoice_generate_sales_channels': {
'default': json.dumps(['web']),
'type': list
},
'invoice_address_from': {
'default': '',
'type': str
@@ -297,6 +301,10 @@ DEFAULTS = {
'default': settings.MAIL_FROM,
'type': str
},
'mail_from_name': {
'default': None,
'type': str
},
'mail_text_signature': {
'type': LazyI18nString,
'default': ""
@@ -323,6 +331,18 @@ The list is as follows:
{orders}
Best regards,
Your {event} team"""))
},
'mail_text_order_free_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello {attendee_name},
you have been registered for {event} successfully.
You can view the details and status of your ticket here:
{url}
Best regards,
Your {event} team"""))
},
@@ -339,6 +359,10 @@ You can change your order details and view the status of your order at
Best regards,
Your {event} team"""))
},
'mail_send_order_free_attendee': {
'type': bool,
'default': 'False'
},
'mail_text_order_placed_require_approval': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
@@ -365,6 +389,22 @@ of {total_with_currency}. Please complete your payment before {date}.
You can change your order details and view the status of your order at
{url}
Best regards,
Your {event} team"""))
},
'mail_send_order_placed_attendee': {
'type': bool,
'default': 'False'
},
'mail_text_order_placed_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello {attendee_name},
a ticket for {event} has been ordered for you.
You can view the details and status of your ticket here:
{url}
Best regards,
Your {event} team"""))
},
@@ -391,6 +431,22 @@ we successfully received your payment for {event}. Thank you!
You can change your order details and view the status of your order at
{url}
Best regards,
Your {event} team"""))
},
'mail_send_order_paid_attendee': {
'type': bool,
'default': 'False'
},
'mail_text_order_paid_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello {attendee_name},
a ticket for {event} that has been ordered for you is now paid.
You can view the details and status of your ticket here:
{url}
Best regards,
Your {event} team"""))
},
@@ -492,6 +548,22 @@ Your {event} team"""))
'type': int,
'default': None
},
'mail_send_download_reminder_attendee': {
'type': bool,
'default': 'False'
},
'mail_text_download_reminder_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello {attendee_name},
you are registered for {event}.
If you did not do so already, you can download your ticket here:
{url}
Best regards,
Your {event} team"""))
},
'mail_text_download_reminder': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,

View File

@@ -240,6 +240,19 @@ subclass of pretix.base.exporter.BaseExporter
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
validate_order = EventPluginSignal(
providing_args=["payment_provider", "positions", "email", "locale", "invoice_address",
"meta_info"]
)
"""
This signal is sent out when the user tries to confirm the order, before we actually create
the order. It allows you to inspect the cart positions. Your return value will be ignored,
but you can raise an OrderError with an appropriate exception message if you like to block
the order. We strongly discourage making changes to the order here.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
validate_cart = EventPluginSignal(
providing_args=["positions"]
)
@@ -502,3 +515,12 @@ dictionaries as values that contain keys like in the following example::
The evaluate member will be called with the order position, order and event as arguments. The event might
also be a subevent, if applicable.
"""
timeline_events = EventPluginSignal()
"""
This signal is sent out to collect events for the time line shown on event dashboards. You are passed
a ``subevent`` argument which might be none and you are expected to return a list of instances of
``pretix.base.timeline.TimelineEvent``, which is a ``namedtuple`` with the fields ``event``, ``subevent``,
``datetime``, ``description`` and ``edit_url``.
"""

View File

@@ -23,13 +23,23 @@
<table cellpadding="20"><tr><td>
<![endif]-->
<div class="content">
{% trans "You are receiving this email because you placed an order for the following event:" %}<br>
<strong>{% trans "Event:" %}</strong> {{ event.name }}<br>
<strong>{% trans "Order code:" %}</strong> {{ order.code }}<br>
<strong>{% trans "Order date:" %}</strong> {{ order.datetime|date:"SHORT_DATE_FORMAT" }}<br>
<a href="{% abseventurl event "presale:event.order" order=order.code secret=order.secret %}">
{% trans "View order details" %}
</a>
{% if position %}
{% trans "You are receiving this email because someone signed you up for the following event:" %}<br>
<strong>{% trans "Event:" %}</strong> {{ event.name }}<br>
<strong>{% trans "Order code:" %}</strong> {{ order.code }}<br>
<strong>{% trans "Order date:" %}</strong> {{ order.datetime|date:"SHORT_DATE_FORMAT" }}<br>
<a href="{% abseventurl event "presale:event.order.position" order=order.code secret=position.web_secret position=position.positionid %}">
{% trans "View registration details" %}
</a>
{% else %}
{% trans "You are receiving this email because you placed an order for the following event:" %}<br>
<strong>{% trans "Event:" %}</strong> {{ event.name }}<br>
<strong>{% trans "Order code:" %}</strong> {{ order.code }}<br>
<strong>{% trans "Order date:" %}</strong> {{ order.datetime|date:"SHORT_DATE_FORMAT" }}<br>
<a href="{% abseventurl event "presale:event.order.open" hash=order.email_confirm_hash order=order.code secret=order.secret %}">
{% trans "View order details" %}
</a>
{% endif %}
</div>
<!--[if gte mso 9]>
</td></tr></table>

View File

@@ -17,6 +17,7 @@ def money_filter(value: Decimal, arg='', hide_currency=False):
raise TypeError("Invalid data type passed to money filter: %r" % type(value))
if not arg:
raise ValueError("No currency passed.")
arg = arg.upper()
places = settings.CURRENCY_PLACES.get(arg, 2)
rounded = value.quantize(Decimal('1') / 10 ** places, ROUND_HALF_UP)

View File

@@ -47,7 +47,7 @@ ALLOWED_TAGS = [
]
ALLOWED_ATTRIBUTES = {
'a': ['href', 'title'],
'a': ['href', 'title', 'class'],
'abbr': ['title'],
'acronym': ['title'],
'table': ['width'],

200
src/pretix/base/timeline.py Normal file
View File

@@ -0,0 +1,200 @@
from collections import namedtuple
from datetime import datetime, time, timedelta
from django.db.models import Q
from django.urls import reverse
from django.utils.timezone import make_aware
from django.utils.translation import pgettext_lazy
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.signals import timeline_events
TimelineEvent = namedtuple('TimelineEvent', ('event', 'subevent', 'datetime', 'description', 'edit_url'))
def timeline_for_event(event, subevent=None):
tl = []
ev = subevent or event
if subevent:
ev_edit_url = reverse(
'control:event.subevent', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'subevent': subevent.pk
}
)
else:
ev_edit_url = reverse(
'control:event.settings', kwargs={
'event': event.slug,
'organizer': event.organizer.slug
}
)
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=ev.date_from,
description=pgettext_lazy('timeline', 'Your event starts'),
edit_url=ev_edit_url
))
if ev.date_to:
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=ev.date_to,
description=pgettext_lazy('timeline', 'Your event ends'),
edit_url=ev_edit_url
))
if ev.date_admission:
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=ev.date_admission,
description=pgettext_lazy('timeline', 'Admissions for your event start'),
edit_url=ev_edit_url
))
if ev.presale_start:
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=ev.presale_start,
description=pgettext_lazy('timeline', 'Start of ticket sales'),
edit_url=ev_edit_url
))
if ev.presale_end:
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=ev.presale_end,
description=pgettext_lazy('timeline', 'End of ticket sales'),
edit_url=ev_edit_url
))
rd = event.settings.get('last_order_modification_date', as_type=RelativeDateWrapper)
if rd:
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=rd.datetime(ev),
description=pgettext_lazy('timeline', 'Customers can no longer modify their orders'),
edit_url=ev_edit_url
))
rd = event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if rd:
d = make_aware(datetime.combine(
rd.date(ev),
time(hour=23, minute=59, second=59)
), event.timezone)
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=d,
description=pgettext_lazy('timeline', 'No more payments can be completed'),
edit_url=reverse('control:event.settings.payment', kwargs={
'event': event.slug,
'organizer': event.organizer.slug
})
))
rd = event.settings.get('ticket_download_date', as_type=RelativeDateWrapper)
if rd and event.settings.ticket_download:
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=rd.datetime(ev),
description=pgettext_lazy('timeline', 'Tickets can be downloaded'),
edit_url=reverse('control:event.settings.tickets', kwargs={
'event': event.slug,
'organizer': event.organizer.slug
})
))
rd = event.settings.get('cancel_allow_user_until', as_type=RelativeDateWrapper)
if rd and event.settings.cancel_allow_user:
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=rd.datetime(ev),
description=pgettext_lazy('timeline', 'Customers can no longer cancel free or unpaid orders'),
edit_url=reverse('control:event.settings.tickets', kwargs={
'event': event.slug,
'organizer': event.organizer.slug
})
))
rd = event.settings.get('cancel_allow_user_paid_until', as_type=RelativeDateWrapper)
if rd and event.settings.cancel_allow_user_paid:
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=rd.datetime(ev),
description=pgettext_lazy('timeline', 'Customers can no longer cancel paid orders'),
edit_url=reverse('control:event.settings.tickets', kwargs={
'event': event.slug,
'organizer': event.organizer.slug
})
))
if not event.has_subevents:
days = event.settings.get('mail_days_download_reminder', as_type=int)
if days is not None:
reminder_date = (ev.date_from - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=reminder_date,
description=pgettext_lazy('timeline', 'Download reminders are being sent out'),
edit_url=reverse('control:event.settings.mail', kwargs={
'event': event.slug,
'organizer': event.organizer.slug
})
))
for p in event.items.filter(Q(available_from__isnull=False) | Q(available_until__isnull=False)):
if p.available_from:
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=p.available_from,
description=pgettext_lazy('timeline', 'Product "{name}" becomes available').format(name=str(p)),
edit_url=reverse('control:event.item', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'item': p.pk,
})
))
if p.available_until:
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=p.available_until,
description=pgettext_lazy('timeline', 'Product "{name}" becomes unavailable').format(name=str(p)),
edit_url=reverse('control:event.item', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'item': p.pk,
})
))
pprovs = event.get_payment_providers()
# This is a special case, depending on payment providers not overriding BasePaymentProvider by too much, but it's
# preferrable to having all plugins implement this spearately.
for pprov in pprovs.values():
if not pprov.settings.get('_enabled', as_type=bool):
continue
availability_date = pprov.settings.get('_availability_date', as_type=RelativeDateWrapper)
if availability_date:
d = make_aware(datetime.combine(
availability_date.date(ev),
time(hour=23, minute=59, second=59)
), event.timezone)
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=d,
description=pgettext_lazy('timeline', 'Payment provider "{name}" can no longer be selected').format(
name=str(pprov.verbose_name)
),
edit_url=reverse('control:event.settings.payment.provider', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'provider': pprov.identifier,
})
))
for recv, resp in timeline_events.send(sender=event, subevent=subevent):
tl += resp
return sorted(tl, key=lambda e: e.datetime)

View File

@@ -4,7 +4,7 @@ from decimal import Decimal
from django import forms
from django.core.files.uploadedfile import UploadedFile
from django.db.models import Prefetch
from django.db.models import Prefetch, QuerySet
from django.utils.functional import cached_property
from pretix.base.forms.questions import (
@@ -89,19 +89,20 @@ class BaseQuestionsViewMixin:
elif k == 'attendee_email':
form.pos.attendee_email = v if v != '' else None
form.pos.save()
elif k.startswith('question_') and v is not None:
elif k.startswith('question_'):
field = form.fields[k]
if hasattr(field, 'answer'):
# We already have a cached answer object, so we don't
# have to create a new one
if v == '' or v is None or (isinstance(field, forms.FileField) and v is False):
if v == '' or v is None or (isinstance(field, forms.FileField) and v is False) \
or (isinstance(v, QuerySet) and not v.exists()):
if field.answer.file:
field.answer.file.delete()
field.answer.delete()
else:
self._save_to_answer(field, field.answer, v)
field.answer.save()
elif v != '':
elif v != '' and v is not None:
answer = QuestionAnswer(
cartposition=(form.pos if isinstance(form.pos, CartPosition) else None),
orderposition=(form.pos if isinstance(form.pos, OrderPosition) else None),

View File

@@ -200,3 +200,7 @@ class SplitDateTimeField(forms.SplitDateTimeField):
result = datetime.datetime.combine(*data_list)
return from_current_timezone(result)
return None
class FontSelect(forms.RadioSelect):
option_template_name = 'pretixcontrol/font_option.html'

View File

@@ -2,9 +2,10 @@ from django import forms
from django.conf import settings
from django.contrib.auth.hashers import check_password
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.core.validators import RegexValidator, validate_email
from django.db.models import Q
from django.forms import formset_factory
from django.urls import reverse
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.timezone import get_current_timezone_name
@@ -18,15 +19,17 @@ from i18nfield.forms import (
)
from pytz import common_timezones, timezone
from pretix.base.channels import get_all_sales_channels
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
from pretix.base.models import Event, Organizer, TaxRule
from pretix.base.models.event import EventMetaValue, SubEvent
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.control.forms import (
ExtFileField, MultipleLanguagesWidget, SingleLanguageWidget, SlugWidget,
SplitDateTimeField, SplitDateTimePickerWidget,
ExtFileField, FontSelect, MultipleLanguagesWidget, SingleLanguageWidget,
SlugWidget, SplitDateTimeField, SplitDateTimePickerWidget,
)
from pretix.control.forms.widgets import Select2
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.plugins.banktransfer.payment import BankTransfer
from pretix.presale.style import get_fonts
@@ -51,16 +54,28 @@ class EventWizardFoundationForm(forms.Form):
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
self.session = kwargs.pop('session')
super().__init__(*args, **kwargs)
qs = Organizer.objects.all()
if not self.user.has_active_staff_session(self.session.session_key):
qs = qs.filter(
id__in=self.user.teams.filter(can_create_events=True).values_list('organizer', flat=True)
)
self.fields['organizer'] = forms.ModelChoiceField(
label=_("Organizer"),
queryset=Organizer.objects.filter(
id__in=self.user.teams.filter(can_create_events=True).values_list('organizer', flat=True)
queryset=qs,
widget=Select2(
attrs={
'data-model-select2': 'generic',
'data-select2-url': reverse('control:organizers.select2') + '?can_create=1',
'data-placeholder': _('Organizer')
}
),
widget=forms.RadioSelect,
empty_label=None,
required=True
)
self.fields['organizer'].widget.choices = self.fields['organizer'].choices
if len(self.fields['organizer'].choices) == 1:
self.fields['organizer'].initial = self.fields['organizer'].queryset.first()
@@ -116,6 +131,7 @@ class EventWizardBasicsForm(I18nModelForm):
self.locales = kwargs.get('locales')
self.has_subevents = kwargs.pop('has_subevents')
kwargs.pop('user')
kwargs.pop('session')
super().__init__(*args, **kwargs)
self.initial['timezone'] = get_current_timezone_name()
self.fields['locale'].choices = [(a, b) for a, b in settings.LANGUAGES if a in self.locales]
@@ -173,7 +189,9 @@ class EventChoiceField(forms.ModelChoiceField):
class EventWizardCopyForm(forms.Form):
@staticmethod
def copy_from_queryset(user):
def copy_from_queryset(user, session):
if user.has_active_staff_session(session.session_key):
return Event.objects.all()
return Event.objects.filter(
Q(organizer_id__in=user.teams.filter(
all_events=True, can_change_event_settings=True, can_change_items=True
@@ -185,16 +203,25 @@ class EventWizardCopyForm(forms.Form):
def __init__(self, *args, **kwargs):
kwargs.pop('organizer')
kwargs.pop('locales')
self.session = kwargs.pop('session')
kwargs.pop('has_subevents')
self.user = kwargs.pop('user')
super().__init__(*args, **kwargs)
self.fields['copy_from_event'] = EventChoiceField(
label=_("Copy configuration from"),
queryset=EventWizardCopyForm.copy_from_queryset(self.user),
widget=forms.RadioSelect,
queryset=EventWizardCopyForm.copy_from_queryset(self.user, self.session),
widget=Select2(
attrs={
'data-model-select2': 'event',
'data-select2-url': reverse('control:events.typeahead') + '?can_copy=1',
'data-placeholder': _('Do not copy')
}
),
empty_label=_('Do not copy'),
required=False
)
self.fields['copy_from_event'].widget.choices = self.fields['copy_from_event'].choices
class EventMetaValueForm(forms.ModelForm):
@@ -279,7 +306,7 @@ class EventSettingsForm(SettingsForm):
)
display_net_prices = forms.BooleanField(
label=_("Show net prices instead of gross prices in the product list (not recommended!)"),
help_text=_("Independent of your choice, the cart will show gross prices as this the price that needs to be "
help_text=_("Independent of your choice, the cart will show gross prices as this is the price that needs to be "
"paid"),
required=False
)
@@ -664,6 +691,13 @@ class InvoiceSettingsForm(SettingsForm):
),
help_text=_("Invoices will never be automatically generated for free orders.")
)
invoice_generate_sales_channels = forms.MultipleChoiceField(
label=_('Generate invoices for Sales channels'),
choices=[],
widget=forms.CheckboxSelectMultiple,
help_text=_("If you have enabled invoice generation in the previous setting, you can limit it here to specific "
"sales channels.")
)
invoice_attendee_name = forms.BooleanField(
label=_("Show attendee names on invoices"),
required=False
@@ -779,6 +813,16 @@ class InvoiceSettingsForm(SettingsForm):
self.fields['invoice_numbers_prefix'].widget.attrs['placeholder'] = event.slug.upper() + '-'
locale_names = dict(settings.LANGUAGES)
self.fields['invoice_language'].choices = [('__user__', _('The user\'s language'))] + [(a, locale_names[a]) for a in event.settings.locales]
self.fields['invoice_generate_sales_channels'].choices = (
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
)
def multimail_validate(val):
s = val.split(',')
for part in s:
validate_email(part.strip())
return s
class MailSettingsForm(SettingsForm):
@@ -790,12 +834,20 @@ class MailSettingsForm(SettingsForm):
)
mail_from = forms.EmailField(
label=_("Sender address"),
help_text=_("Sender address for outgoing emails")
help_text=_("Sender address for outgoing emails"),
)
mail_bcc = forms.EmailField(
mail_from_name = forms.CharField(
label=_("Sender name"),
help_text=_("Sender name used in conjunction with the sender address for outgoing emails. "
"Defaults to your event name."),
required=False
)
mail_bcc = forms.CharField(
label=_("Bcc address"),
help_text=_("All emails will be sent to this address as a Bcc copy"),
required=False
validators=[multimail_validate],
required=False,
max_length=255
)
mail_text_signature = I18nFormField(
@@ -818,7 +870,7 @@ class MailSettingsForm(SettingsForm):
)
mail_text_order_placed = I18nFormField(
label=_("Text"),
label=_("Text sent to order contact address"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {total_with_currency}, {total}, {currency}, {date}, "
@@ -826,20 +878,62 @@ class MailSettingsForm(SettingsForm):
validators=[PlaceholderValidator(['{event}', '{total_with_currency}', '{total}', '{currency}', '{date}',
'{payment_info}', '{url}', '{invoice_name}', '{invoice_company}'])]
)
mail_send_order_placed_attendee = forms.BooleanField(
label=_("Send an email to attendees"),
help_text=_('If the order contains attendees with email addresses different from the person who orders the '
'tickets, the following email will be sent out to the attendees.'),
required=False,
)
mail_text_order_placed_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {attendee_name}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{attendee_name}'])],
)
mail_text_order_paid = I18nFormField(
label=_("Text"),
label=_("Text sent to order contact address"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}, {payment_info}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{invoice_name}', '{invoice_company}', '{payment_info}'])]
)
mail_send_order_paid_attendee = forms.BooleanField(
label=_("Send an email to attendees"),
help_text=_('If the order contains attendees with email addresses different from the person who orders the '
'tickets, the following email will be sent out to the attendees.'),
required=False,
)
mail_text_order_paid_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {attendee_name}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{attendee_name}'])],
)
mail_text_order_free = I18nFormField(
label=_("Text"),
label=_("Text sent to order contact address"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{invoice_name}', '{invoice_company}'])]
)
mail_send_order_free_attendee = forms.BooleanField(
label=_("Send an email to attendees"),
help_text=_('If the order contains attendees with email addresses different from the person who orders the '
'tickets, the following email will be sent out to the attendees.'),
required=False,
)
mail_text_order_free_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {attendee_name}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{attendee_name}'])],
)
mail_text_order_changed = I18nFormField(
label=_("Text"),
required=False,
@@ -899,12 +993,25 @@ class MailSettingsForm(SettingsForm):
'{invoice_name}', '{invoice_company}'])]
)
mail_text_download_reminder = I18nFormField(
label=_("Text"),
label=_("Text sent to order contact address"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}"),
validators=[PlaceholderValidator(['{event}', '{url}'])]
)
mail_send_download_reminder_attendee = forms.BooleanField(
label=_("Send an email to attendees"),
help_text=_('If the order contains attendees with email addresses different from the person who orders the '
'tickets, the following email will be sent out to the attendees.'),
required=False,
)
mail_text_download_reminder_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {attendee_name}, {event}, {url}"),
validators=[PlaceholderValidator(['{attendee_name}', '{event}', '{url}'])]
)
mail_days_download_reminder = forms.IntegerField(
label=_("Number of days"),
required=False,
@@ -985,13 +1092,26 @@ class MailSettingsForm(SettingsForm):
(r.identifier, r.verbose_name) for r in event.get_html_mail_renderers().values()
]
keys = list(event.meta_data.keys())
for k, v in self.fields.items():
name_scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
for k, v in list(self.fields.items()):
if k.startswith('mail_text_'):
v.help_text = str(v.help_text) + ', ' + ', '.join({
'{meta_' + p + '}' for p in keys
})
v.validators[0].limit_value += ['{meta_' + p + '}' for p in keys]
if '{attendee_name}' in v.validators[0].limit_value:
for f, l, w in name_scheme['fields']:
if f == 'full_name':
continue
v.help_text = str(v.help_text) + ', ' + '{attendee_name_%s}' % f
v.validators[0].limit_value += ['{attendee_name_' + f + '}']
if k.endswith('_attendee') and not event.settings.attendee_emails_asked:
# If we don't ask for attendee emails, we can't send them anything and we don't need to clutter
# the user interface with it
del self.fields[k]
def clean(self):
data = self.cleaned_data
if not data.get('smtp_password') and data.get('smtp_username'):
@@ -1046,6 +1166,7 @@ class DisplaySettingsForm(SettingsForm):
choices=[
('Open Sans', 'Open Sans')
],
widget=FontSelect,
help_text=_('Only respected by modern browsers.')
)
frontpage_text = I18nFormField(

View File

@@ -82,6 +82,7 @@ class QuestionForm(I18nModelForm):
'type',
'required',
'ask_during_checkin',
'hidden',
'identifier',
'items',
'dependency_question',
@@ -205,6 +206,8 @@ class ItemCreateForm(I18nModelForm):
empty_label=_('Do not copy'),
required=False
)
if self.event.tax_rules.exists():
self.fields['tax_rule'].required = True
if not self.event.has_subevents:
choices = [
@@ -363,6 +366,8 @@ class ItemUpdateForm(I18nModelForm):
'over 65. This ticket includes access to all parts of the event, except the VIP '
'area.'
)
if self.event.tax_rules.exists():
self.fields['tax_rule'].required = True
self.fields['description'].widget.attrs['rows'] = '4'
self.fields['sales_channels'] = forms.MultipleChoiceField(
label=_('Sales channels'),

View File

@@ -9,6 +9,7 @@ from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from pretix.base.forms import I18nModelForm, PlaceholderValidator
from pretix.base.forms.widgets import DatePickerWidget
from pretix.base.models import InvoiceAddress, ItemAddOn, Order, OrderPosition
from pretix.base.models.event import SubEvent
from pretix.base.services.pricing import get_price
@@ -116,6 +117,12 @@ class MarkPaidForm(ConfirmPaymentForm):
localize=True,
label=_('Payment amount'),
)
payment_date = forms.DateField(
required=True,
label=_('Payment date'),
widget=DatePickerWidget(),
initial=now
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -285,10 +292,7 @@ class OrderPositionChangeForm(forms.Form):
instance = kwargs.pop('instance')
initial = kwargs.get('initial', {})
if instance.item.tax_rule and not instance.item.tax_rule.price_includes_tax:
initial['price'] = instance.price - instance.tax_value
else:
initial['price'] = instance.price
initial['price'] = instance.price
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
@@ -340,7 +344,7 @@ class OrderContactForm(forms.ModelForm):
class Meta:
model = Order
fields = ['email']
fields = ['email', 'email_known_to_work']
class OrderLocaleForm(forms.ModelForm):

View File

@@ -12,7 +12,9 @@ from pretix.api.models import WebHook
from pretix.api.webhooks import get_all_webhook_events
from pretix.base.forms import I18nModelForm, SettingsForm
from pretix.base.models import Device, Organizer, Team
from pretix.control.forms import ExtFileField, MultipleLanguagesWidget
from pretix.control.forms import (
ExtFileField, FontSelect, MultipleLanguagesWidget,
)
from pretix.multidomain.models import KnownDomain
from pretix.presale.style import get_fonts
@@ -260,6 +262,7 @@ class OrganizerDisplaySettingsForm(SettingsForm):
choices=[
('Open Sans', 'Open Sans')
],
widget=FontSelect,
help_text=_('Only respected by modern browsers.')
)
favicon = ExtFileField(

View File

@@ -48,12 +48,14 @@ class UserEditForm(forms.ModelForm):
'email',
'require_2fa',
'is_active',
'is_staff'
'is_staff',
'last_login'
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['email'].required = True
self.fields['last_login'].disabled = True
def clean_email(self):
email = self.cleaned_data['email']

View File

@@ -146,6 +146,14 @@ class VoucherForm(I18nModelForm):
data, self.instance.event,
self.instance.quota, self.instance.item, self.instance.variation
)
if self.instance.quota:
if all(i.hide_without_voucher for i in self.instance.quota.items.all()):
raise ValidationError({
'itemvar': [
_('The quota you selected only contains hidden products. Hidden products can currently only be '
'shown by using vouchers that directly apply to the product, not via a quota.')
]
})
Voucher.clean_subevent(
data, self.instance.event
)

View File

@@ -251,6 +251,9 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.item.addons.added': _('An add-on has been added to this product.'),
'pretix.event.item.addons.removed': _('An add-on has been removed from this product.'),
'pretix.event.item.addons.changed': _('An add-on has been changed on this product.'),
'pretix.event.item.bundles.added': _('A bundled item has been added to this product.'),
'pretix.event.item.bundles.removed': _('A bundled item has been removed from this product.'),
'pretix.event.item.bundles.changed': _('A bundled item has been changed on this product.'),
'pretix.event.quota.added': _('The quota has been added.'),
'pretix.event.quota.deleted': _('The quota has been deleted.'),
'pretix.event.quota.changed': _('The quota has been changed.'),

View File

@@ -281,7 +281,7 @@ def get_event_navigation(request: HttpRequest):
merge_in(nav, sorted(
sum((list(a[1]) for a in nav_event.send(request.event, request=request)), []),
key=lambda r: r['label']
key=lambda r: (1 if r.get('parent') else 0, r['label'])
))
return nav
@@ -391,7 +391,7 @@ def get_global_navigation(request):
merge_in(nav, sorted(
sum((list(a[1]) for a in nav_global.send(request, request=request)), []),
key=lambda r: r['label']
key=lambda r: (1 if r.get('parent') else 0, r['label'])
))
return nav
@@ -464,7 +464,7 @@ def get_organizer_navigation(request):
merge_in(nav, sorted(
sum((list(a[1]) for a in nav_organizer.send(request.organizer, request=request, organizer=request.organizer)),
[]),
key=lambda r: r['label']
key=lambda r: (1 if r.get('parent') else 0, r['label'])
))
return nav
@@ -474,6 +474,8 @@ def merge_in(nav, newnav):
if 'parent' in item:
parents = [n for n in nav if n['url'] == item['parent']]
if parents:
if 'children' not in parents[0]:
parents[0]['children'] = []
parents[0]['children'].append(item)
else:
nav.append(item)

View File

@@ -2,16 +2,6 @@ from django.dispatch import Signal
from pretix.base.signals import DeprecatedSignal, EventPluginSignal
restriction_formset = EventPluginSignal(
providing_args=["item"]
)
"""
This signal is sent out to build configuration forms for all restriction formsets
(see plugin API documentation for details).
As with all plugin signals, the ``sender`` keyword argument will contain the event.
"""
html_page_start = Signal(
providing_args=[]
)
@@ -46,6 +36,12 @@ on the type of navigation. You should also return an ``active`` key with a boole
set to ``True``, when this item should be marked as active. The ``request`` object
will have an attribute ``event``.
You can optionally create sub-items to create hierarchical navigation. There are two
ways to achieve this: Either you specify a key ``children`` on your top navigation item
that contains a list of navigation items (as dictionaries), or you specify a ``parent``
key with the ``url`` value of the designated parent item.
The latter method also allows you to register navigation items as a sub-item of existing ones.
If you use this, you should read the documentation on :ref:`how to deal with URLs <urlconf>`
in pretix.
@@ -83,6 +79,12 @@ a fontawesome icon name with the key ``icon``, it will be respected depending
on the type of navigation. You should also return an ``active`` key with a boolean
set to ``True``, when this item should be marked as active.
You can optionally create sub-items to create hierarchical navigation. There are two
ways to achieve this: Either you specify a key ``children`` on your top navigation item
that contains a list of navigation items (as dictionaries), or you specify a ``parent``
key with the ``url`` value of the designated parent item.
The latter method also allows you to register navigation items as a sub-item of existing ones.
If you use this, you should read the documentation on :ref:`how to deal with URLs <urlconf>`
in pretix.
@@ -183,6 +185,12 @@ should contain at least the keys ``label`` and ``url``. You should also return
an ``active`` key with a boolean set to ``True``, when this item should be marked
as active.
You can optionally create sub-items to create hierarchical navigation. There are two
ways to achieve this: Either you specify a key ``children`` on your top navigation item
that contains a list of navigation items (as dictionaries), or you specify a ``parent``
key with the ``url`` value of the designated parent item.
The latter method also allows you to register navigation items as a sub-item of existing ones.
If your linked view should stay in the tab-like context of this page, we recommend
that you use ``pretix.control.views.organizer.OrganizerDetailViewMixin`` for your view
and your template inherits from ``pretixcontrol/organizers/base.html``.

View File

@@ -242,7 +242,10 @@
<span class="caret"></span></a>
{% endif %}
<ul class="dropdown-menu event-dropdown" role="menu" data-event-typeahead
data-source="{% url "control:nav.typeahead" %}">
data-source="{% url "control:nav.typeahead" %}"
{% if request.event %}
data-organizer="{{ request.organizer.id }}"
{% endif %}>
<li class="query-holder">
<div class="form-box">
<input type="text" class="form-control" id="event-dropdown-field"

View File

@@ -2,6 +2,10 @@
{% load i18n %}
{% load bootstrap3 %}
{% load hierarkey_form %}
{% block custom_header %}
{{ block.super }}
<link type="text/css" rel="stylesheet" href="{% url "control:pdf.css" %}">
{% endblock %}
{% block inside %}
<h1>{% trans "Display settings" %}</h1>
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">

View File

@@ -0,0 +1,36 @@
{% load i18n %}
<div class="panel panel-default items">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Your timeline" %}
</h3>
</div>
<div class="panel-body timeline">
{% regroup timeline by date as tl_list %}
{% for day in tl_list %}
<div class="row {% if day.grouper < today %}text-muted{% endif %}">
<div class="col-date">
<strong>{{ day.grouper|date:"SHORT_DATE_FORMAT" }}</strong>
</div>
<div class="col-event">
{% for e in day.list %}
<strong class="">{{ e.time|date:"TIME_FORMAT" }}</strong>
&nbsp;
<span class="{% if e.time < nearly_now %}text-muted{% endif %}">
{{ e.entry.description }}
</span>
{% if e.entry.edit_url %}
&nbsp;
<a href="{{ e.entry.edit_url }}" class="text-muted">
<span class="fa fa-edit"></span>
</a>
{% endif %}
{% if forloop.revcounter > 1 %}
<br/>
{% endif %}
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>

View File

@@ -90,6 +90,9 @@
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %}
</form>
{% endif %}
{% if not request.event.has_subevents or subevent %}
{% include "pretixcontrol/event/fragment_timeline.html" %}
{% endif %}
<div class="dashboard">
{% for w in widgets %}
<div class="widget-container widget-{{ w.display_size|default:"small" }}">

View File

@@ -9,6 +9,7 @@
<fieldset>
<legend>{% trans "General settings" %}</legend>
{% bootstrap_field form.invoice_generate layout="control" %}
{% bootstrap_field form.invoice_generate_sales_channels layout="control" %}
{% bootstrap_field form.invoice_email_attachment layout="control" %}
{% bootstrap_field form.invoice_numbers_prefix layout="control" %}
{% bootstrap_field form.invoice_numbers_consecutive layout="control" %}

View File

@@ -5,6 +5,8 @@
{% block inside %}
<h1>{% trans "Event logs" %}</h1>
<form class="form-inline helper-display-inline" action="" method="get">
<input type="hidden" name="content_type" value="{{ request.GET.content_type }}">
<input type="hidden" name="object" value="{{ request.GET.object }}">
<p>
<select name="user" class="form-control">
<option value="">{% trans "All actions" %}</option>

View File

@@ -12,6 +12,7 @@
<legend>{% trans "General settings" %}</legend>
{% bootstrap_field form.mail_prefix layout="control" %}
{% bootstrap_field form.mail_from layout="control" %}
{% bootstrap_field form.mail_from_name layout="control" %}
{% bootstrap_field form.mail_text_signature layout="control" %}
{% bootstrap_field form.mail_bcc layout="control" %}
</fieldset>
@@ -40,13 +41,13 @@
<legend>{% trans "E-mail content" %}</legend>
<div class="panel-group" id="questions_group">
{% blocktrans asvar title_placed_order %}Placed order{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_placed" title=title_placed_order items="mail_text_order_placed" %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_placed" title=title_placed_order items="mail_text_order_placed,mail_send_order_placed_attendee,mail_text_order_placed_attendee" exclude="mail_send_order_placed_attendee" %}
{% blocktrans asvar title_paid_order %}Paid order{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_paid" title=title_paid_order items="mail_text_order_paid" %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_paid" title=title_paid_order items="mail_text_order_paid,mail_send_order_paid_attendee,mail_text_order_paid_attendee" exclude="mail_send_order_paid_attendee" %}
{% blocktrans asvar title_free_order %}Free order{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_free" title=title_free_order items="mail_text_order_free" %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_free" title=title_free_order items="mail_text_order_free,mail_send_order_free_attendee,mail_text_order_free_attendee" exclude="mail_send_order_free_attendee" %}
{% blocktrans asvar title_resend_link %}Resend link{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="resend_link" title=title_resend_link items="mail_text_resend_link,mail_text_resend_all_links" %}
@@ -67,7 +68,7 @@
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="custom_mail" title=title_order_custom_mail items="mail_text_order_custom_mail" %}
{% blocktrans asvar title_download_tickets_reminder %}Reminder to download tickets{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_download_tickets_reminder items="mail_days_download_reminder,mail_text_download_reminder" exclude="mail_days_download_reminder" %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_download_tickets_reminder items="mail_days_download_reminder,mail_text_download_reminder,mail_send_download_reminder_attendee,mail_text_download_reminder_attendee" exclude="mail_days_download_reminder,mail_send_download_reminder_attendee" %}
{% blocktrans asvar title_require_approval %}Order approval process{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_require_approval items="mail_text_order_placed_require_approval,mail_text_order_approved,mail_text_order_denied" %}

View File

@@ -13,11 +13,11 @@
{% with exclude|split as exclusion %}
{% with items|split as item_list %}
{% for item in item_list %}
{% if item in exclusion %}
{% if item in exclusion and form|hasattr:item %}
{% with form|getattr:item as field %}
{% bootstrap_field field layout="horizontal" %}
{% endwith %}
{% else %}
{% elif form|hasattr:item %}
<div id="{{ item }}_panel" class="preview-panel form-group" for="{{ item }}">
{% with form|getattr:item as field %}
<label class="col-md-3 control-label">{{ field.label }}</label>

View File

@@ -38,7 +38,10 @@
{% empty %}
<tr>
<td colspan="3">
{% trans "There are no payment providers available. Please go to the plugin settings and activate one or more payment plugins." %}
{% url "control:event.settings.plugins" event=request.event.slug organizer=request.organizer.slug as plugin_settings_url %}
{% blocktrans trimmed with plugin_settings_href='href="'|add:plugin_settings_url|add:'"'|safe %}
There are no payment providers available. Please go to the <a {{ plugin_settings_href }}>plugin settings</a> and activate one or more payment plugins.
{% endblocktrans %}
</td>
</tr>
{% endfor %}

View File

@@ -40,7 +40,10 @@
</div>
{% empty %}
<div class="alert alert-warning">
{% trans "There are no ticket outputs available. Please go to the plugin settings and activate one or more ticket output plugins." %}</em>
{% url "control:event.settings.plugins" event=request.event.slug organizer=request.organizer.slug as plugin_settings_url %}
{% blocktrans trimmed with plugin_settings_href='href="'|add:plugin_settings_url|add:'"'|safe %}
There are no ticket outputs available. Please go to the <a {{ plugin_settings_href }}>plugin settings</a> and activate one or more ticket output plugins.
{% endblocktrans %}
</div>
{% endfor %}
</fieldset>

View File

@@ -0,0 +1,2 @@
{% load i18n %}{% if widget.wrap_label %}<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %} class="preload-font"
data-family="{{ widget.label }}" data-style="regular">{% endif %}{% include "django/forms/widgets/input.html" %}{% if widget.wrap_label %} <strong>{{ widget.label }}</strong><br>{% trans "The quick brown fox jumps over the lazy dog." context "typography" %}</label>{% endif %}

View File

@@ -1,7 +1,7 @@
{% load static %}
{% load i18n %}
<ul class="list-group">
{% for log in obj.all_logentries %}
{% for log in obj.top_logentries %}
<li class="list-group-item logentry">
<p class="meta">
<span class="fa fa-clock-o"></span> {{ log.datetime|date:"SHORT_DATETIME_FORMAT" }}
@@ -45,4 +45,11 @@
</p>
</li>
{% endfor %}
{% if obj.all_logentries_link and obj.top_logentries_has_more %}
<li class="list-group-item logentry">
<a href="{{ obj.all_logentries_link }}">
{% trans "View full log" %}
</a>
</li>
{% endif %}
</ul>

View File

@@ -109,6 +109,7 @@
{% bootstrap_field form.help_text layout="control" %}
{% bootstrap_field form.identifier layout="control" %}
{% bootstrap_field form.ask_during_checkin layout="control" %}
{% bootstrap_field form.hidden layout="control" %}
<div class="form-group">
<label class="col-md-3 control-label" for="id_dependency_question">

View File

@@ -60,7 +60,13 @@
<td>
<ul>
{% for item in q.items.all %}
<li><a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a></li>
{% if not item.has_variations %}
<li><a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a></li>
{% endif %}
{% endfor %}
{% for v in q.variations.all %}
<li><a href="{% url "control:event.item.variations" organizer=request.event.organizer.slug event=request.event.slug item=v.item.id %}">
{{ v.item }} {{ v }}</a></li>
{% endfor %}
</ul>
</td>

View File

@@ -121,11 +121,13 @@
<strong>{% trans "Price" %}</strong>
</div>
<div class="col-sm-5">
{{ position.price|money:request.event.currency }}<br>
{{ position.price|money:request.event.currency }}
{% if position.tax_rate %}
<small>{% blocktrans trimmed with rate=position.tax_rate name=position.tax_rule.name %}
<strong>incl.</strong> {{ rate }}% {{ name }}
{% endblocktrans %}</small>
<br>
<small>
({{ position.net_price|money:request.event.currency }}
+ {{ position.tax_rate }}%)
</small>
{% endif %}
</div>
<div class="col-sm-4 field-container">

View File

@@ -136,7 +136,10 @@
{% endif %}
<dt>{% trans "User" %}</dt>
<dd>
{{ order.email|default_if_none:"" }}&nbsp;&nbsp;
{{ order.email|default_if_none:"" }}
{% if order.email and order.email_known_to_work %}
<span class="fa fa-check-circle text-success" data-toggle="tooltip" title="{% trans "We know that this email address works because the user clicked a link we sent them." %}"></span>
{% endif %}&nbsp;&nbsp;
<a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
<span class="fa fa-edit"></span>
</a>

View File

@@ -23,6 +23,7 @@
<input type="hidden" name="status" value="p" />
{% bootstrap_form_errors form %}
{% bootstrap_field form.amount layout='horizontal' %}
{% bootstrap_field form.payment_date layout='horizontal' %}
{% if form.force %}
{% bootstrap_field form.force layout='horizontal' horizontal_label_class='sr-only' horizontal_field_class='col-md-12' %}
{% endif %}

View File

@@ -1,6 +1,10 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block custom_header %}
{{ block.super }}
<link type="text/css" rel="stylesheet" href="{% url "control:pdf.css" %}">
{% endblock %}
{% block inner %}
<h1>{% trans "Display settings" %}</h1>
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">

View File

@@ -8,7 +8,7 @@
{% compress css %}
<link type="text/css" rel="stylesheet" href="{% static "pretixcontrol/scss/pdfeditor.css" %}">
{% endcompress %}
<link type="text/css" rel="stylesheet" href="{% url "control:pdf.css" organizer=request.organizer.slug event=request.event.slug %}">
<link type="text/css" rel="stylesheet" href="{% url "control:pdf.css" %}">
{% endblock %}
{% block content %}
<h1>

View File

@@ -33,6 +33,7 @@
{% bootstrap_field form.email layout='control' %}
{% bootstrap_field form.new_pw layout='control' %}
{% bootstrap_field form.new_pw_repeat layout='control' %}
{% bootstrap_field form.last_login layout='control' %}
{% bootstrap_field form.require_2fa layout='control' %}
</fieldset>
<fieldset>

View File

@@ -11,3 +11,12 @@ def split(value, delimiter=","):
@register.filter(name="getattr")
def get_attribute(value, key):
return value[key]
@register.filter(name="hasattr")
def has_attribute(value, key):
try:
value[key]
return True
except:
return False

View File

@@ -34,6 +34,7 @@ urlpatterns = [
url(r'^users/(?P<id>\d+)/reset$', users.UserResetView.as_view(), name='users.reset'),
url(r'^users/(?P<id>\d+)/impersonate', users.UserImpersonateView.as_view(), name='users.impersonate'),
url(r'^users/(?P<id>\d+)/anonymize', users.UserAnonymizeView.as_view(), name='users.anonymize'),
url(r'^pdf/editor/webfonts.css', pdf.FontsCSSView.as_view(), name='pdf.css'),
url(r'^settings/?$', user.UserSettings.as_view(), name='user.settings'),
url(r'^settings/history/$', user.UserHistoryView.as_view(), name='user.settings.history'),
url(r'^settings/notifications/$', user.UserNotificationsEditView.as_view(), name='user.settings.notifications'),

View File

@@ -1,3 +1,4 @@
from datetime import timedelta
from decimal import Decimal
import pytz
@@ -22,6 +23,7 @@ from pretix.base.models import (
WaitingListEntry,
)
from pretix.base.models.checkin import CheckinList
from pretix.base.timeline import timeline_for_event
from pretix.control.forms.event import CommentForm
from pretix.control.signals import (
event_dashboard_widgets, user_dashboard_widgets,
@@ -279,6 +281,7 @@ def event_index(request, organizer, event):
ctx = {
'widgets': rearrange(widgets),
'logs': qs[:5],
'subevent': subevent,
'actions': a_qs[:5] if can_change_orders else [],
'comment_form': CommentForm(initial={'comment': request.event.comment})
}
@@ -302,7 +305,19 @@ def event_index(request, organizer, event):
for a in ctx['actions']:
a.display = a.display(request)
return render(request, 'pretixcontrol/event/index.html', ctx)
ctx['timeline'] = [
{
'date': t.datetime.astimezone(request.event.timezone).date(),
'entry': t,
'time': t.datetime.astimezone(request.event.timezone)
}
for t in timeline_for_event(request.event, subevent)
]
ctx['today'] = now().astimezone(request.event.timezone).date()
ctx['nearly_now'] = now().astimezone(request.event.timezone) - timedelta(seconds=20)
resp = render(request, 'pretixcontrol/event/index.html', ctx)
# resp['Content-Security-Policy'] = "style-src 'unsafe-inline'"
return resp
def annotated_event_query(request):

View File

@@ -689,13 +689,14 @@ class MailSettingsRendererPreview(MailSettingsPreview):
expires=now(), code="PREVIEW", total=119)
item = request.event.items.create(name=ugettext("Sample product"), default_price=42.23,
description=ugettext("Sample product description"))
order.positions.create(item=item, attendee_name_parts={'_legacy': ugettext("John Doe")},
price=item.default_price)
p = order.positions.create(item=item, attendee_name_parts={'_legacy': ugettext("John Doe")},
price=item.default_price)
v = renderers[request.GET.get('renderer')].render(
v,
str(request.event.settings.mail_text_signature),
ugettext('Your order: %(code)s') % {'code': order.code},
order
order,
position=p
)
r = HttpResponse(v, content_type='text/html')
r._csp_ignore = True
@@ -974,6 +975,12 @@ class EventLog(EventPermissionRequiredMixin, ListView):
elif self.request.GET.get('user'):
qs = qs.filter(user_id=self.request.GET.get('user'))
if self.request.GET.get('content_type'):
qs = qs.filter(content_type=get_object_or_404(ContentType, pk=self.request.GET.get('content_type')))
if self.request.GET.get('object'):
qs = qs.filter(object_id=self.request.GET.get('object'))
return qs
def get_context_data(self, **kwargs):

View File

@@ -3,7 +3,7 @@ import json
from django.contrib import messages
from django.core.files import File
from django.db import transaction
from django.db.models import Count, F, Q
from django.db.models import Count, F, Prefetch, Q
from django.forms.models import inlineformset_factory
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import redirect
@@ -15,6 +15,7 @@ from django.views.generic import ListView
from django.views.generic.base import TemplateView
from django.views.generic.detail import DetailView, SingleObjectMixin
from django.views.generic.edit import DeleteView
from django_countries.fields import Country
from pretix.base.forms import I18nFormSet
from pretix.base.models import (
@@ -444,6 +445,10 @@ class QuestionView(EventPermissionRequiredMixin, QuestionMixin, ChartContainingV
a['alink'] = a['answer']
a['answer'] = ugettext('Yes') if a['answer'] == 'True' else ugettext('No')
a['answer_bool'] = a['answer'] == 'True'
elif self.object.type == Question.TYPE_COUNTRYCODE:
for a in qs:
a['alink'] = a['answer']
a['answer'] = Country(a['answer']).name or a['answer']
return list(qs)
@@ -560,7 +565,14 @@ class QuotaList(PaginationMixin, ListView):
def get_queryset(self):
qs = Quota.objects.filter(
event=self.request.event
).prefetch_related("items")
).prefetch_related(
Prefetch(
"items",
queryset=Item.objects.annotate(has_variations=Count('variations'))
),
"variations",
"variations__item"
)
if self.request.GET.get("subevent", "") != "":
s = self.request.GET.get("subevent", "")
qs = qs.filter(subevent_id=s)
@@ -888,6 +900,12 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, UpdateVie
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx['plugin_forms'] = self.plugin_forms
if not ctx['item'].active and ctx['item'].bundled_with.count() > 0:
messages.info(self.request, _("You disabled this item, but it is still part of a product bundle. "
"Your participants won't be able to buy the bundle unless you remove this "
"item from it."))
return ctx

View File

@@ -97,7 +97,7 @@ class EventList(PaginationMixin, ListView):
def condition_copy(wizard):
return (
not wizard.clone_from and
EventWizardCopyForm.copy_from_queryset(wizard.request.user).exists()
EventWizardCopyForm.copy_from_queryset(wizard.request.user, wizard.request.session).exists()
)
@@ -176,7 +176,8 @@ class EventWizard(SafeSessionWizardView):
def get_form_kwargs(self, step=None):
kwargs = {
'user': self.request.user
'user': self.request.user,
'session': self.request.session,
}
if step != 'foundation':
fdata = self.get_cleaned_data_for_step('foundation')
@@ -203,6 +204,7 @@ class EventWizard(SafeSessionWizardView):
event.organizer = foundation_data['organizer']
event.plugins = settings.PRETIX_PLUGINS_DEFAULT
event.has_subevents = foundation_data['has_subevents']
event.testmode = True
form_dict['basics'].save()
has_control_rights = self.request.user.teams.filter(

View File

@@ -3,7 +3,7 @@ import logging
import mimetypes
import os
import re
from datetime import timedelta
from datetime import datetime, time, timedelta
from decimal import Decimal, DecimalException
import pytz
@@ -24,7 +24,7 @@ from django.utils import formats
from django.utils.formats import date_format
from django.utils.functional import cached_property
from django.utils.http import is_safe_url
from django.utils.timezone import now
from django.utils.timezone import make_aware, now
from django.utils.translation import ugettext_lazy as _
from django.views.generic import (
DetailView, FormView, ListView, TemplateView, View,
@@ -862,8 +862,15 @@ class OrderTransition(OrderView):
fee=None
)
payment_date = None
if self.mark_paid_form.cleaned_data['payment_date'] != now().date():
payment_date = make_aware(datetime.combine(
self.mark_paid_form.cleaned_data['payment_date'],
time(hour=0, minute=0, second=0)
), self.order.event.timezone)
try:
p.confirm(user=self.request.user, count_waitinglist=False,
p.confirm(user=self.request.user, count_waitinglist=False, payment_date=payment_date,
force=self.mark_paid_form.cleaned_data.get('force', False))
except Quota.QuotaExceededException as e:
p.state = OrderPayment.PAYMENT_STATE_FAILED
@@ -1365,6 +1372,7 @@ class OrderContactChange(OrderView):
},
user=self.request.user,
)
if self.form.cleaned_data['regenerate_secrets']:
changed = True
self.order.secret = generate_secret()
@@ -1474,9 +1482,10 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
'code': order.code,
'date': date_format(order.datetime.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
'expire_date': date_format(order.expires, 'SHORT_DATE_FORMAT'),
'url': build_absolute_uri(order.event, 'presale:event.order', kwargs={
'url': build_absolute_uri(order.event, 'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret
'secret': order.secret,
'hash': order.email_confirm_hash()
}),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
@@ -1646,6 +1655,7 @@ class ExportMixin:
class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, View):
permission = 'can_view_orders'
known_errortypes = ['ExportError']
task = export
def get_success_message(self, value):

View File

@@ -12,6 +12,7 @@ from django.utils.timezone import make_aware
from django.utils.translation import pgettext, ugettext as _
from pretix.base.models import Organizer, User
from pretix.control.forms.event import EventWizardCopyForm
from pretix.control.permissions import event_permission_required
from pretix.helpers.daterange import daterange
from pretix.helpers.i18n import i18ncomp
@@ -72,7 +73,13 @@ def event_list(request):
page = int(request.GET.get('page', '1'))
except ValueError:
page = 1
qs = request.user.get_events_with_any_permission(request).filter(
if 'can_copy' in request.GET:
qs = EventWizardCopyForm.copy_from_queryset(request.user, request.session)
else:
qs = request.user.get_events_with_any_permission(request)
qs = qs.filter(
Q(name__icontains=i18ncomp(query)) | Q(slug__icontains=query) |
Q(organizer__name__icontains=i18ncomp(query)) | Q(organizer__slug__icontains=query)
).annotate(
@@ -100,6 +107,8 @@ def event_list(request):
def nav_context_list(request):
query = request.GET.get('query', '')
organizer = request.GET.get('organizer', None)
try:
page = int(request.GET.get('page', '1'))
except ValueError:
@@ -138,6 +147,13 @@ def nav_context_list(request):
] + [
serialize_event(e) for e in qs_events.select_related('organizer')[offset:offset + (pagesize if query else 5)]
]
if show_user and organizer:
organizer = serialize_orga(Organizer.objects.get(pk=organizer))
if organizer in results:
results.remove(organizer)
results.insert(1, organizer)
doc = {
'results': results,
'pagination': {
@@ -332,7 +348,10 @@ def organizer_select2(request):
if term:
qs = qs.filter(Q(name__icontains=term) | Q(slug__icontains=term))
if not request.user.has_active_staff_session(request.session.session_key):
qs = qs.filter(pk__in=request.user.teams.values_list('organizer', flat=True))
if 'can_create' in request.GET:
qs = qs.filter(pk__in=request.user.teams.filter(can_create_events=True).values_list('organizer', flat=True))
else:
qs = qs.filter(pk__in=request.user.teams.values_list('organizer', flat=True))
total = qs.count()
pagesize = 20

View File

@@ -143,7 +143,7 @@ class VoucherDelete(EventPermissionRequiredMixin, DeleteView):
messages.error(request, _('A voucher can not be deleted if it already has been redeemed.'))
else:
self.object.log_action('pretix.voucher.deleted', user=self.request.user)
CartPosition.objects.filter(addon_to__voucher=False).delete()
CartPosition.objects.filter(addon_to__voucher=self.object).delete()
self.object.cartposition_set.all().delete()
self.object.delete()
messages.success(request, _('The selected voucher has been deleted.'))

View File

@@ -76,3 +76,21 @@ class GroupConcat(Aggregate):
function='string_agg',
template="%(function)s(%(field)s::text, '%(separator)s')",
)
class ReplicaRouter:
def db_for_read(self, model, **hints):
return 'default'
def db_for_write(self, model, **hints):
return 'default'
def allow_relation(self, obj1, obj2, **hints):
db_list = ('default', 'replica')
if obj1._state.db in db_list and obj2._state.db in db_list:
return True
return None
def allow_migrate(self, db, app_label, model_name=None, **hintrs):
return True

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-05-01 12:02+0000\n"
"POT-Creation-Date: 2019-06-05 10:21+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-05-01 12:02+0000\n"
"POT-Creation-Date: 2019-06-05 10:21+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-05-01 12:02+0000\n"
"POT-Creation-Date: 2019-06-05 10:21+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-05-01 12:02+0000\n"
"POT-Creation-Date: 2019-06-05 10:21+0000\n"
"PO-Revision-Date: 2018-04-24 14:22+0000\n"
"Last-Translator: Pernille Thorsen <perth@aarhus.dk>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"

Some files were not shown because too many files have changed in this diff Show More