Compare commits

...

186 Commits

Author SHA1 Message Date
Raphael Michel
ac14154b5d Bump version to 1.14.0 2018-04-04 15:21:47 +02:00
Raphael Michel
3352e743f7 Merge pull request #855 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2018-04-04 14:12:41 +02:00
Raphael Michel
79d8c17aaa Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (2357 of 2357 strings)

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

powered by weblate
2018-04-04 11:18:13 +00:00
Raphael Michel
ef7ce21ff3 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (2357 of 2357 strings)

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

powered by weblate
2018-04-04 11:11:15 +00:00
Raphael Michel
a8bcc6206f Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2018-04-04 13:02:33 +02:00
Raphael Michel
ddfeafc5e2 Update from Weblate. (#841)
Update from Weblate.
2018-04-04 13:00:56 +02:00
Raphael Michel
85cdd40102 Added translation on translate.pretix.eu (Spanish) 2018-04-04 11:00:10 +00:00
Raphael Michel
d412d8536c Added translation on translate.pretix.eu (Flemish) 2018-04-04 11:00:10 +00:00
Raphael Michel
bf1a314076 Added translation on translate.pretix.eu (Italian) 2018-04-04 11:00:10 +00:00
Raphael Michel
e2b2ff7f6f Added translation on translate.pretix.eu (Spanish) 2018-04-04 11:00:10 +00:00
Raphael Michel
a589c9aa69 Added translation on translate.pretix.eu (Flemish) 2018-04-04 11:00:10 +00:00
Raphael Michel
d5b05e391a Added translation on translate.pretix.eu (Italian) 2018-04-04 11:00:10 +00:00
Nicolas Pettiaux
dc7a20280f Translated on translate.pretix.eu (French)
Currently translated at 91.0% (2083 of 2288 strings)

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

powered by weblate
2018-04-04 11:00:10 +00:00
Raphael Michel
d36fc45c99 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (2288 of 2288 strings)

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

powered by weblate
2018-04-04 11:00:10 +00:00
Raphael Michel
990d92e569 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (2288 of 2288 strings)

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

powered by weblate
2018-04-04 11:00:10 +00:00
Raphael Michel
7d4ef4f9a1 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (2288 of 2288 strings)

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

powered by weblate
2018-04-04 11:00:10 +00:00
Raphael Michel
7baabcef96 Require correct permission for refunds in all cases 2018-04-04 12:52:36 +02:00
Raphael Michel
ded15ecc3f Add Jimdo to word list 2018-04-04 11:01:34 +02:00
Raphael Michel
0ad3ec444c Widget: Add a compatibility mode for Jimdo 2018-04-04 10:07:26 +02:00
Raphael Michel
7939503a11 Bulk creation for event series dates (#848)
* copy-from things

* Some frontend

* rrule UI

* .

* Fixes

* UI improvements

* First test

* Tests
2018-04-03 18:21:27 +02:00
Raphael Michel
8564f93706 Refs #782 -- Reference subevent in check in list selection 2018-04-03 17:07:40 +02:00
Raphael Michel
f3e550d003 Voucher form: do not require subevent 2018-04-03 13:43:26 +02:00
Raphael Michel
6c525b5dcd Subevent selector: Use for extending orders 2018-04-03 12:19:25 +02:00
Raphael Michel
bb10d25561 Fix #782 -- Select2 widget for item selection for vouchers 2018-04-03 12:10:34 +02:00
Raphael Michel
7ec5adb6b4 Fix #782 -- Select2 widget for check-in lists 2018-04-03 11:57:12 +02:00
Raphael Michel
ffb73d61fc Subevent selector: Allow to search by date 2018-04-03 11:23:49 +02:00
Raphael Michel
3ee6c34d08 Quick setup: Fix validation problems 2018-03-29 22:54:34 +02:00
Raphael Michel
2c26ccbc72 Fix AttributeError in orders API when questions are in use 2018-03-29 18:20:26 +02:00
Raphael Michel
cfbde151fa Fix relative date calculation around DST dates 2018-03-29 16:46:34 +02:00
Raphael Michel
e278978ad9 Enlarge field size of Item.picture 2018-03-28 14:17:52 +02:00
Raphael Michel
a284e0c2f7 Add auditable superuser mode (#824)
* Remove is_superuser everywhere

* Session handling

* List of sessions, relative timeout

* Absolute timeout

* Optionally pseudo-force audit comments

* Fix failing tests

* Add tests

* Add docs

* Rebsae migration

* Typos

* Fix tests
2018-03-28 14:16:58 +02:00
Raphael Michel
558c920181 Stripe: Business name detection 2018-03-28 13:34:51 +02:00
Tobias Kunze
7622fe9fc5 Fix broken link (#845) 2018-03-28 09:44:44 +02:00
Tobias Kunze
b32aec682c Add linkcheck ignores (#843) 2018-03-28 09:33:25 +02:00
Tobias Kunze
d54d25a432 Follow redirects in documentation urls (#842) 2018-03-28 08:44:47 +02:00
Raphael Michel
ba19bdb90a Fix quick setup in combination with subevents 2018-03-27 14:31:41 +02:00
Raphael Michel
58d10fac84 Stripe Connect error handling 2018-03-27 11:55:56 +02:00
Raphael Michel
eecc1def2a Stripe Connect: Add note to documentation 2018-03-26 23:50:23 +02:00
Raphael Michel
f75fbc3744 Stripe connect: Fix issues with test keys 2018-03-26 23:36:11 +02:00
Raphael Michel
07750c1f8c Fix #805 -- Handling of 3D secure payments 2018-03-26 23:22:30 +02:00
Raphael Michel
9ae0d9b0a1 Fix bug with Stripe 3DS 2018-03-26 23:16:06 +02:00
Raphael Michel
c9f5828eb9 Stripe: Support for restricted keys 2018-03-26 23:02:23 +02:00
pretix translation bot
35a6a1883c Update from Weblate. (#840)
* Translated on translate.pretix.eu (German)

Currently translated at 100.0% (2288 of 2288 strings)

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

powered by weblate

* Translated on translate.pretix.eu (German (informal))

Currently translated at 100.0% (2288 of 2288 strings)

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

powered by weblate

* Translated on translate.pretix.eu (German (informal))

Currently translated at 100.0% (2288 of 2288 strings)

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

powered by weblate
2018-03-26 22:50:54 +02:00
Raphael Michel
080c48327e Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2018-03-26 22:16:37 +02:00
Raphael Michel
28506538a3 Add quick-start assistant for new users (#833)
* First draft for quick-setup

* Add payment

* Fix stripe w/o connect

* cols

* Add tests
2018-03-26 20:52:24 +02:00
Raphael Michel
d578dedd0c Merge pull request #839 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2018-03-26 15:16:48 +02:00
Raphael Michel
c7aa105517 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (2260 of 2260 strings)

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

powered by weblate
2018-03-26 13:16:28 +00:00
Raphael Michel
3c140c3e4d Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (2260 of 2260 strings)

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

powered by weblate
2018-03-26 13:16:12 +00:00
Raphael Michel
8d94b67aaa Translated on translate.pretix.eu (German)
Currently translated at 100.0% (2260 of 2260 strings)

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

powered by weblate
2018-03-26 13:12:48 +00:00
Raphael Michel
887cca109f Run localegen 2018-03-26 14:50:07 +02:00
Raphael Michel
239061b688 Merge pull request #823 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2018-03-26 13:42:36 +02:00
Raphael Michel
6d067428e0 Translated on translate.pretix.eu (German)
Currently translated at 99.2% (2243 of 2260 strings)

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

powered by weblate
2018-03-26 11:42:00 +00:00
Raphael Michel
b11f13181e Translated on translate.pretix.eu (German (informal))
Currently translated at 98.9% (2237 of 2260 strings)

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

powered by weblate
2018-03-26 11:41:41 +00:00
Raphael Michel
e52b8e9a13 Translated on translate.pretix.eu (German)
Currently translated at 99.9% (2238 of 2240 strings)

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

powered by weblate
2018-03-26 13:33:17 +02:00
Felix Rindt
aa567ab078 Translated on translate.pretix.eu (German)
Currently translated at 99.8% (2237 of 2240 strings)

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

powered by weblate
2018-03-26 13:33:17 +02:00
Felix Rindt
50daf49cf0 Translated on translate.pretix.eu (German)
Currently translated at 99.6% (2233 of 2240 strings)

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

powered by weblate
2018-03-26 13:33:17 +02:00
Claude
a49fb65fac Translated on translate.pretix.eu (French)
Currently translated at 100.0% (59 of 59 strings)

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

powered by weblate
2018-03-26 13:33:17 +02:00
Claude
adfd1e614d Translated on translate.pretix.eu (French)
Currently translated at 93.2% (2089 of 2240 strings)

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

powered by weblate
2018-03-26 13:33:17 +02:00
Claude
a3eef04342 Translated on translate.pretix.eu (French)
Currently translated at 93.2% (2089 of 2240 strings)

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

powered by weblate
2018-03-26 13:33:17 +02:00
Claude
cf5e660951 Translated on translate.pretix.eu (French)
Currently translated at 100.0% (59 of 59 strings)

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

powered by weblate
2018-03-26 13:33:17 +02:00
Claude
671d6acfff Translated on translate.pretix.eu (French)
Currently translated at 100.0% (59 of 59 strings)

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

powered by weblate
2018-03-26 13:33:16 +02:00
Claude
125e759120 Translated on translate.pretix.eu (French)
Currently translated at 93.2% (2089 of 2240 strings)

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

powered by weblate
2018-03-26 13:33:16 +02:00
Mikkel Ricky
e81832e48f Translated on translate.pretix.eu (Danish)
Currently translated at 70.1% (1572 of 2240 strings)

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

powered by weblate
2018-03-26 13:33:16 +02:00
Ture Gjørup
0b17da3b87 Translated on translate.pretix.eu (Danish)
Currently translated at 70.0% (1568 of 2240 strings)

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

powered by weblate
2018-03-26 13:33:16 +02:00
Maarten van den Berg
8379902adb Translated on translate.pretix.eu (Dutch)
Currently translated at 31.7% (711 of 2240 strings)

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

powered by weblate
2018-03-26 13:33:16 +02:00
Ture Gjørup
00c4ffc154 Translated on translate.pretix.eu (Danish)
Currently translated at 68.3% (1530 of 2240 strings)

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

powered by weblate
2018-03-26 13:33:16 +02:00
anonymous
3473337e7d Translated on translate.pretix.eu (French)
Currently translated at 0.1% (4 of 2240 strings)

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

powered by weblate
2018-03-26 13:33:16 +02:00
OMar
4493178693 Translated on translate.pretix.eu (Arabic)
Currently translated at 0.1% (1 of 2240 strings)

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

powered by weblate
2018-03-26 13:33:16 +02:00
Raphael Michel
40452dcefe Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2018-03-26 13:13:54 +02:00
Raphael Michel
082afadb5b Backend UX: Use actual tax rates for invoice preview 2018-03-26 10:21:41 +02:00
Raphael Michel
9ca61f9ef5 Remove left-over print statements from debugging 2018-03-26 10:21:14 +02:00
Raphael Michel
28a628ec93 Backend UX: Hide advanced tax rule settings 2018-03-26 10:15:36 +02:00
Raphael Michel
12b5e21314 Tax rule validation: Fix use of incorrect exception 2018-03-26 10:15:12 +02:00
Raphael Michel
95aaccb35e Backend UX: Reorder warning 2018-03-26 10:07:26 +02:00
Felix Rindt
ac053b00e8 Add order expired notification (#838) 2018-03-26 10:06:05 +02:00
Raphael Michel
938c7df28a Fix #103 -- Implement Stripe Connect (#836)
* ...

* Upgrade Stripe API client

* Implement account choice

* Add disconnect and fix tests
2018-03-26 10:05:34 +02:00
Raphael Michel
6e22ea178b Fix Stripe being shown as disabled 2018-03-24 18:18:28 +01:00
Raphael Michel
253f336509 Add free_price field when copying products 2018-03-24 18:18:28 +01:00
Raphael Michel
3a7e0da80b Backend UX: Restructure payment settings 2018-03-24 18:18:28 +01:00
Felix Rindt
073860cd5b Refs #828 -- Presale: change order thankyou text (#832) 2018-03-22 18:34:59 +01:00
Raphael Michel
18be4db320 Widget: More resilient file handling 2018-03-21 09:55:59 +01:00
Raphael Michel
5cbcbe6d7e Thumbnail: Fix CMYK images 2018-03-21 09:55:38 +01:00
Raphael Michel
1ef3f83e46 Improve thumbnail quality 2018-03-20 17:14:29 +01:00
Raphael Michel
6ab0a839b1 Fix accidental push of wrong MEDIA_URL default 2018-03-20 17:13:48 +01:00
Raphael Michel
e329753939 Add a FAQ section on check-outs 2018-03-20 16:11:05 +01:00
Raphael Michel
843751b53f Revert accidental push of test settings (yes, the password has been changed) 2018-03-20 14:23:18 +01:00
Raphael Michel
1f083a52eb Increase maximum filename size of FileFields 2018-03-20 13:21:20 +01:00
Raphael Michel
879eb6ee9f Widget: fix broken iframe detection 2018-03-20 12:27:56 +01:00
Raphael Michel
2db1e6b596 Stripe: Open payment gateways in new window when shown in widget 2018-03-20 12:27:17 +01:00
Raphael Michel
94f5ba7d1a Remove print statement 2018-03-20 11:59:25 +01:00
Raphael Michel
e7458f3032 Add custom thumbnailer 2018-03-20 11:55:46 +01:00
Raphael Michel
840cee206a Compatibility with an external file storage separated in pub/ and priv/ 2018-03-20 11:55:46 +01:00
Raphael Michel
511cdbbfe2 Add correct flag for pt-br 2018-03-18 18:09:16 +01:00
Raphael Michel
35f1999b3a Allow organizers to modify answers to check-in questions 2018-03-17 22:10:43 +01:00
Raphael Michel
3f55c694b8 Do not require invoice address name if invoice address is not required 2018-03-17 22:10:16 +01:00
Raphael Michel
6df0147fe9 Remove unused method 2018-03-16 15:27:02 +01:00
Raphael Michel
5e3b4b126e Make voucher lookups case-insensitive 2018-03-16 15:27:02 +01:00
Ture Gjørup
b564fe8a0d Refs #654 -- API: Writable category endpoints (#818)
* EBILL-5: Added POST, PATCH, PUT and DELETE for categories

* EBILL-5: Fixed item category not removed on category delete
2018-03-16 14:50:22 +01:00
Raphael Michel
1dc3a7202a Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2018-03-16 12:32:28 +01:00
Raphael Michel
cfa01d3c15 Use "their" instead of "his" 2018-03-16 12:31:17 +01:00
Raphael Michel
c4cac468ff Merge pull request #809 from pretix-translations/weblate-pretix-pretix
Update from Weblate.
2018-03-16 12:25:18 +01:00
Raphael Michel
a034bf9710 Merge branch 'master' into weblate-pretix-pretix 2018-03-16 12:24:04 +01:00
Raphael Michel
0336b0a15c Do not allow duplicate slugs in a case-insensitive way 2018-03-15 13:42:33 +01:00
Felix Rindt
94a2cfe7fc Fix doc typo 2018-03-14 17:13:29 +01:00
Felix Rindt
d22d0fdab5 include internal comment in order search 2018-03-14 17:13:18 +01:00
Jan-Frederik Rieckers
6f9f47bfe3 Fixes #798 (in a very basic way) 2018-03-14 17:12:57 +01:00
Raphael Michel
b2402cdd39 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2018-03-13 09:44:09 +01:00
Ture Gjørup
289e1ee315 Translated on translate.pretix.eu (Danish)
Currently translated at 68.3% (1520 of 2224 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Matheus Nunes
c1d1cbda70 Translated on translate.pretix.eu (Portuguese (Brazil))
Currently translated at 98.3% (58 of 59 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Maarten van den Berg
17bac3714d Translated on translate.pretix.eu (Dutch)
Currently translated at 98.3% (58 of 59 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Ture Gjørup
9699fb8894 Translated on translate.pretix.eu (Danish)
Currently translated at 100.0% (59 of 59 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Matheus Nunes
16809c0136 Translated on translate.pretix.eu (Portuguese (Brazil))
Currently translated at 10.0% (224 of 2224 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Raphael Michel
e0408510b8 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (2234 of 2234 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Felix Rindt
14782c184f Translated on translate.pretix.eu (German)
Currently translated at 100.0% (2234 of 2234 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Claude
527f5c5ae6 Translated on translate.pretix.eu (French)
Currently translated at 0.1% (3 of 2224 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Maarten van den Berg
b9aa4f1482 Translated on translate.pretix.eu (Dutch)
Currently translated at 30.3% (676 of 2224 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Maarten Visscher
5ea6234cfb Translated on translate.pretix.eu (Dutch)
Currently translated at 30.3% (675 of 2224 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Maarten van den Berg
d056e1de1e Translated on translate.pretix.eu (Dutch)
Currently translated at 30.3% (675 of 2224 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Maarten Visscher
2feb0246ff Translated on translate.pretix.eu (Dutch)
Currently translated at 30.2% (673 of 2224 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Maarten van den Berg
f4be045f08 Translated on translate.pretix.eu (Dutch)
Currently translated at 30.2% (672 of 2224 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Maarten Visscher
a42ca225e3 Translated on translate.pretix.eu (Dutch)
Currently translated at 30.1% (671 of 2224 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Maarten van den Berg
5fb1bed9d2 Translated on translate.pretix.eu (Dutch)
Currently translated at 30.1% (671 of 2224 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Maarten van den Berg
c0c903d81a Translated on translate.pretix.eu (Dutch)
Currently translated at 30.1% (670 of 2224 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Maarten Visscher
60b56d61ed Translated on translate.pretix.eu (Dutch)
Currently translated at 30.0% (669 of 2224 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Maarten van den Berg
fe1f334e2e Translated on translate.pretix.eu (Dutch)
Currently translated at 30.0% (669 of 2224 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Maarten van den Berg
fe4d70a9f9 Translated on translate.pretix.eu (Dutch)
Currently translated at 30.0% (668 of 2224 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Maarten Visscher
93fded58f0 Translated on translate.pretix.eu (Dutch)
Currently translated at 29.9% (667 of 2224 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Maarten van den Berg
9a7b8ca27d Translated on translate.pretix.eu (Dutch)
Currently translated at 29.9% (665 of 2224 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Maarten Visscher
aa7d75fae4 Translated on translate.pretix.eu (Dutch)
Currently translated at 29.7% (662 of 2224 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Maarten van den Berg
be345ffcc1 Translated on translate.pretix.eu (Dutch)
Currently translated at 29.7% (661 of 2224 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Maarten Visscher
b3589312f8 Translated on translate.pretix.eu (Dutch)
Currently translated at 28.8% (642 of 2224 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Maarten van den Berg
08bd45d07e Translated on translate.pretix.eu (Dutch)
Currently translated at 28.8% (642 of 2224 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Maarten van den Berg
9af60c4192 Translated on translate.pretix.eu (Dutch)
Currently translated at 28.5% (635 of 2224 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Maarten Visscher
6c48a94ab3 Translated on translate.pretix.eu (Dutch)
Currently translated at 96.6% (57 of 59 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Mikkel Ricky
2897be7a10 Translated on translate.pretix.eu (Danish)
Currently translated at 68.2% (1518 of 2224 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Mikkel Ricky
69c0b04be6 Translated on translate.pretix.eu (Danish)
Currently translated at 100.0% (59 of 59 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Raphael Michel
837e309781 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (2234 of 2234 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
anonymous
dca369ba30 Translated on translate.pretix.eu (Portuguese (Brazil))
Currently translated at 98.3% (58 of 59 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Matheus Nunes
b49d66aa68 Translated on translate.pretix.eu (Portuguese (Brazil))
Currently translated at 47.4% (28 of 59 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Mikkel Ricky
4ce8c82244 Translated on translate.pretix.eu (Danish)
Currently translated at 100.0% (59 of 59 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Mikkel Ricky
ab9a530403 Translated on translate.pretix.eu (Danish)
Currently translated at 68.1% (1516 of 2224 strings)

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

powered by weblate
2018-03-13 08:43:43 +00:00
Maarten Visscher
f9d91178b7 Translated on translate.pretix.eu (Dutch)
Currently translated at 98.3% (58 of 59 strings)

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

powered by weblate
2018-03-13 08:43:42 +00:00
Maarten Visscher
d0b8232a36 Translated on translate.pretix.eu (Dutch)
Currently translated at 16.9% (10 of 59 strings)

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

powered by weblate
2018-03-13 08:43:42 +00:00
Maarten Visscher
8f21e2368f Translated on translate.pretix.eu (Dutch)
Currently translated at 27.3% (609 of 2224 strings)

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

powered by weblate
2018-03-13 08:43:42 +00:00
Claude
87ec1a9fc5 Translated on translate.pretix.eu (French)
Currently translated at 0.0% (0 of 59 strings)

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

powered by weblate
2018-03-13 08:43:42 +00:00
Claude
cb1dcab37d Translated on translate.pretix.eu (French)
Currently translated at 0.0% (0 of 2224 strings)

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

powered by weblate
2018-03-13 08:43:42 +00:00
Maarten Visscher
91980277e1 Translated on translate.pretix.eu (Dutch)
Currently translated at 27.1% (604 of 2224 strings)

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

powered by weblate
2018-03-13 08:43:42 +00:00
Raphael Michel
c18b259b27 Widget: Work around a scrolling quirk in Chrome 2018-03-13 09:42:39 +01:00
Raphael Michel
dffc82781b Add writable questions API methods 2018-03-13 09:09:50 +01:00
Raphael Michel
aef77965e7 Fix PATCH method for items 2018-03-13 09:09:50 +01:00
Raphael Michel
f21da0cc2b Add identifier field to questions 2018-03-13 09:09:50 +01:00
Raphael Michel
234e0ee764 Fix typo 2018-03-12 13:04:56 +01:00
Raphael Michel
4262bb801e Travis: Do not build weblate branches 2018-03-12 12:13:42 +01:00
Raphael Michel
093941f8ba Another sphinx doc fix 2018-03-11 14:44:08 +01:00
Raphael Michel
1cb1c35e2a Documentation syntax 2018-03-11 14:36:52 +01:00
Raphael Michel
432535e238 Add SPF and DKIM note to documentation 2018-03-11 14:31:48 +01:00
Raphael Michel
b40dc9d96d Fix hard-coded date format 2018-03-11 09:21:45 +01:00
Raphael Michel
cb12e1208b Fix failing middleware test 2018-03-10 15:17:54 +01:00
Raphael Michel
b8225bd206 Stop creating an empty session on first request 2018-03-10 14:19:28 +01:00
Raphael Michel
880c22eef9 Prevent cart ID creation in widget 2018-03-10 14:18:40 +01:00
Raphael Michel
b379c8380d Do not create cart ID for every shop page visitor 2018-03-10 14:07:40 +01:00
Raphael Michel
6a61a113b0 Add selectable to word list 2018-03-08 16:30:39 +01:00
Raphael Michel
4373eae1fe Fix danish flag for real 2018-03-08 15:22:10 +01:00
Raphael Michel
d12e4305bd Add helper script for Weblate 2018-03-08 15:22:10 +01:00
pretix translation bot
7c8a45fd4c Update from Weblate. (#804)
* Added translation on translate.pretix.eu (Danish)

* Added translation on translate.pretix.eu (French)

* Added translation on translate.pretix.eu (Dutch)

* Added translation on translate.pretix.eu (Portuguese (Brazil))

* Added translation on translate.pretix.eu (Arabic)

* Added translation on translate.pretix.eu (Danish)

* Added translation on translate.pretix.eu (Dutch)

* Added translation on translate.pretix.eu (French)

* Added translation on translate.pretix.eu (Portuguese (Brazil))

* Added translation on translate.pretix.eu (Arabic)

* Translated on translate.pretix.eu (German)

Currently translated at 100.0% (2234 of 2234 strings)

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

powered by weblate

* Translated on translate.pretix.eu (German (informal))

Currently translated at 100.0% (2234 of 2234 strings)

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

powered by weblate
2018-03-08 15:12:05 +01:00
Raphael Michel
c28f8f763a Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2018-03-08 14:55:27 +01:00
Raphael Michel
096de6cddf Fix flag for the Danish language 2018-03-08 14:50:22 +01:00
Raphael Michel
ad52476159 Fix typo in default settings text 2018-03-08 14:49:26 +01:00
Raphael Michel
e3d11ab681 Fix spelling mistakes 2018-03-08 14:25:00 +01:00
Raphael Michel
162f37e00f Support for inofficial languages 2018-03-08 13:19:57 +01:00
Raphael Michel
d879634810 Add documentation on the translation process 2018-03-08 12:18:33 +01:00
Raphael Michel
4634f853f1 Documentation spellcheck: Compatibility with enchant2 2018-03-08 10:49:04 +01:00
Raphael Michel
ae861f080b Add global banner message 2018-03-07 15:28:03 +01:00
Raphael Michel
d058721243 Sendmail plugin: Move to background process 2018-03-07 14:48:17 +01:00
Raphael Michel
9fdef5eb5d Show date range of event series in list of events 2018-03-07 12:09:01 +01:00
Raphael Michel
b4488bf1e7 Allow admin to create invoice if invoice setting is set to "all orders" 2018-03-07 10:36:33 +01:00
Raphael Michel
fa6d6b5438 Show "continue" instead of "checkout" also if order is free 2018-03-07 10:35:37 +01:00
Raphael Michel
02f53a55cc Contact form data was only saved to session if invoice addresses where active 2018-03-07 10:35:37 +01:00
Felix Rindt
fdf0b6263a Fix doc typo (#802) 2018-03-07 09:20:21 +01:00
Raphael Michel
59fa5112fc Prepare for using weblate 2018-03-06 18:25:46 +01:00
Raphael Michel
a8033248ae Fix issue with fees without tax rules 2018-03-06 15:33:54 +01:00
Raphael Michel
9a6f299b41 Commit POT files 2018-03-06 11:31:17 +01:00
Raphael Michel
2f1ee93e86 Install psycopg2-binary 2018-03-06 10:01:45 +01:00
Raphael Michel
34fa5d6bfc Allow customer to manually generate invoices if order is older than invoice setting 2018-03-06 09:48:36 +01:00
Raphael Michel
357f728043 pretixdroid: Online search should include name of parent position 2018-03-05 12:33:26 +01:00
Raphael Michel
9522ee93dc Bump version to 1.14.0.dev0 2018-03-03 21:39:06 +01:00
236 changed files with 125039 additions and 2878 deletions

1
.gitattributes vendored
View File

@@ -8,6 +8,7 @@ src/static/fileupload/* linguist-vendored
src/static/vuejs/* linguist-vendored
src/static/select2/* linguist-vendored
src/static/charts/* linguist-vendored
src/static/rrule/* linguist-vendored
src/static/iframeresizer/* linguist-vendored
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/fabric.* linguist-vendored
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/pdf.* linguist-vendored

View File

@@ -43,3 +43,6 @@ addons:
apt:
packages:
- enchant
branches:
except:
- /^weblate-.*/

View File

@@ -40,6 +40,9 @@ Contributing
If you want to contribute to pretix, please read the `developer documentation`_
in our documentation. If you have any further questions, please do not hesitate to ask!
.. image:: https://translate.pretix.eu/widgets/pretix/-/pretix/multi-blue.svg
:target: https://translate.pretix.eu/engage/pretix/
Code of Conduct
---------------
We have a `Code of Conduct`_ in place that applies to all project contributions,

View File

@@ -70,6 +70,10 @@ Example::
that are used to print tax amounts in the customer currency on invoices for some currencies. Set to ``off`` to
disable this feature. Defaults to ``on``.
``audit_comments``
Enables or disables nagging staff users for leaving comments on their sessions for auditability.
Defaults to ``off``.
Locale settings
---------------

View File

@@ -268,8 +268,8 @@ to re-build your custom image after you pulled ``pretix/standalone`` if you want
.. _pretix.eu: https://pretix.eu/
.. _MySQL: https://dev.mysql.com/doc/refman/5.7/en/linux-installation-apt-repo.html
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-9-4-on-debian-8
.. _redis: http://blog.programster.org/debian-8-install-redis-server/
.. _redis: https://blog.programster.org/debian-8-install-redis-server/
.. _ufw: https://en.wikipedia.org/wiki/Uncomplicated_Firewall
.. _redis website: http://redis.io/topics/security
.. _redis website: https://redis.io/topics/security
.. _redis in docker: https://hub.docker.com/r/_/redis/
.. _strong encryption settings: https://mozilla.github.io/server-side-tls/ssl-config-generator/

View File

@@ -298,6 +298,6 @@ example::
.. _pretix.eu: https://pretix.eu/
.. _MySQL: https://dev.mysql.com/doc/refman/5.7/en/linux-installation-apt-repo.html
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-9-4-on-debian-8
.. _redis: http://blog.programster.org/debian-8-install-redis-server/
.. _redis: https://blog.programster.org/debian-8-install-redis-server/
.. _ufw: https://en.wikipedia.org/wiki/Uncomplicated_Firewall
.. _strong encryption settings: https://mozilla.github.io/server-side-tls/ssl-config-generator/

View File

@@ -22,6 +22,10 @@ is_addon boolean If ``True``, it
defining add-ons for other products.
===================================== ========================== =======================================================
.. versionchanged:: 1.14
The operations POST, PATCH, PUT and DELETE have been added.
Endpoints
---------
@@ -106,3 +110,118 @@ Endpoints
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/categories/
Creates a new category
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/categories/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content: application/json
{
"name": {"en": "Tickets"},
"description": {"en": "Tickets are what you need to get in."},
"position": 1,
"is_addon": false
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 1,
"name": {"en": "Tickets"},
"description": {"en": "Tickets are what you need to get in."},
"position": 1,
"is_addon": false
}
:param organizer: The ``slug`` field of the organizer of the event to create a category for
:param event: The ``slug`` field of the event to create a category for
:statuscode 201: no error
:statuscode 400: The category could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/categories/(id)/
Update a category. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
want to change.
You can change all fields of the resource except the ``id`` field.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/categories/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"is_addon": true
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"name": {"en": "Tickets"},
"description": {"en": "Tickets are what you need to get in."},
"position": 1,
"is_addon": true
}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the category to modify
:statuscode 200: no error
:statuscode 400: The category could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/category/(id)/
Delete a category.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/categories/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the category to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.

View File

@@ -13,6 +13,7 @@ Resources and endpoints
item_variations
item_add-ons
questions
question_options
quotas
orders
invoices

View File

@@ -148,7 +148,7 @@ Endpoints
.. sourcecode:: http
HTTP/1.1 200 OK
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json

View File

@@ -6,7 +6,7 @@ Resource description
Variations of items can be use for products (items) that are available in different sizes, colors or other variations
of the same product.
The addons resource contains the following public fields:
The variations resource contains the following public fields:
.. rst-class:: rest-resource-table
@@ -158,7 +158,7 @@ Endpoints
.. sourcecode:: http
HTTP/1.1 200 OK
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json

View File

@@ -56,7 +56,8 @@ checkin_attention boolean If ``True``, th
a product is being scanned.
has_variations boolean Shows whether or not this item has variations.
variations list of objects A list with one object for each variation of this item.
Can be empty. Only writable on POST.
Can be empty. Only writable during creation,
use separate endpoint to modify this later.
├ id integer Internal ID of the variation
├ default_price money (string) The price set directly for this variation or ``null``
├ price money (string) The price used for this variation. This is either the
@@ -67,7 +68,8 @@ variations list of objects A list with one
Markdown syntax or can be ``null``.
└ position integer An integer, used for sorting
addons list of objects Definition of add-ons that can be chosen for this item.
Only writable on POST.
Only writable during creation,
use separate endpoint to modify this later.
├ addon_category integer Internal ID of the item category the add-on can be
chosen from.
├ min_count integer The minimal number of add-ons that need to be chosen.
@@ -256,7 +258,7 @@ Endpoints
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/items/
Creates a new item
@@ -315,7 +317,7 @@ Endpoints
.. sourcecode:: http
HTTP/1.1 200 OK
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
@@ -369,7 +371,7 @@ Endpoints
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/items/(id)/
Update an item. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you

View File

@@ -129,7 +129,9 @@ downloads list of objects List of ticket
answers list of objects Answers to user-defined questions
├ question integer Internal ID of the answered question
├ answer string Text representation of the answer
└ options list of integers Internal IDs of selected option(s)s (only for choice types)
├ question_identifier string The question's ``identifier`` field
├ options list of integers Internal IDs of selected option(s)s (only for choice types)
└ option_identifiers list of strings The ``identifier`` fields of the selected option(s)s
===================================== ========================== =======================================================
.. versionchanged:: 1.7
@@ -140,6 +142,10 @@ answers list of objects Answers to user
The attribute ``checkins.list`` has been added.
.. versionchanged:: 1.14
The attributes ``answers.question_identifier`` and ``answers.option_identifiers`` have been added.
Order endpoints
---------------
@@ -222,7 +228,9 @@ Order endpoints
"answers": [
{
"question": 12,
"question_identifier": "WY3TP9SL",
"answer": "Foo",
"option_idenfiters": [],
"options": []
}
],
@@ -330,7 +338,9 @@ Order endpoints
"answers": [
{
"question": 12,
"question_identifier": "WY3TP9SL",
"answer": "Foo",
"option_idenfiters": [],
"options": []
}
],
@@ -649,7 +659,9 @@ Order position endpoints
"answers": [
{
"question": 12,
"question_identifier": "WY3TP9SL",
"answer": "Foo",
"option_idenfiters": [],
"options": []
}
],
@@ -729,7 +741,9 @@ Order position endpoints
"answers": [
{
"question": 12,
"question_identifier": "WY3TP9SL",
"answer": "Foo",
"option_idenfiters": [],
"options": []
}
],

View File

@@ -0,0 +1,233 @@
Question options
================
Resource description
--------------------
Questions of type "choice" or "multiple choice" can have different options attached.
The options resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the option
position integer An integer, used for sorting
identifier string An arbitrary string that can be used for matching with
other sources.
answer multi-lingual string The displayed value of this option
===================================== ========================== =======================================================
.. versionchanged:: 1.12
This resource has been added.
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/questions/(question)/options/
Returns a list of all options for a given question.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/questions/11/options/ 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": 2,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"identifier": "LVETRWVU",
"position": 1,
"answer": {"en": "S"}
},
{
"id": 2,
"identifier": "DFEMJWMJ",
"position": 2,
"answer": {"en": "M"}
},
{
"id": 3,
"identifier": "W9AH7RDE",
"position": 3,
"answer": {"en": "L"}
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:query boolean active: If set to ``true`` or ``false``, only questions with this value for the field ``active`` will be
returned.
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param question: The ``id`` field of the question to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/question does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/questions/(question)/options/(id)/
Returns information on one option, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/questions/1/options/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"identifier": "LVETRWVU",
"position": 1,
"answer": {"en": "S"}
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param question: The ``id`` field of the question to fetch
:param id: The ``id`` field of the option to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/questions/(question)/options/
Creates a new option
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/questions/1/options/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content: application/json
{
"identifier": "LVETRWVU",
"position": 1,
"answer": {"en": "S"}
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 1,
"identifier": "LVETRWVU",
"position": 1,
"answer": {"en": "S"}
}
:param organizer: The ``slug`` field of the organizer of the event/question to create a option for
:param event: The ``slug`` field of the event to create a option for
:param question: The ``id`` field of the question to create a option for
:statuscode 201: no error
:statuscode 400: The option could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/questions/(question)/options/(id)/
Update an option. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
want to change.
You can change all fields of the resource except the ``id`` field.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/questions/1/options/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"position": 3
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"identifier": "LVETRWVU",
"position": 1,
"answer": {"en": "S"}
}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the question to modify
:param id: The ``id`` field of the option to modify
:statuscode 200: no error
:statuscode 400: The option could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/questions/(id)/options/(id)/
Delete an option.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/questions/1/options/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the question to modify
:param id: The ``id`` field of the option to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.

View File

@@ -31,12 +31,18 @@ type string The expected ty
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.
identifier string An arbitrary string that can be used for matching with
other sources.
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.
options list of objects In case of question type ``C`` or ``M``, this lists the
available objects.
available objects. Only writable during creation,
use separate endpoint to modify this later.
├ id integer Internal ID of the option
├ position integer An integer, used for sorting
├ identifier string An arbitrary string that can be used for matching with
other sources.
└ answer multi-lingual string The displayed value of this option
===================================== ========================== =======================================================
@@ -45,6 +51,11 @@ options list of objects In case of ques
The values ``D``, ``H``, and ``W`` for the field ``type`` are now allowed and the ``ask_during_checkin`` field has
been added.
.. versionchanged:: 1.14
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.
Endpoints
---------
@@ -80,18 +91,25 @@ Endpoints
"required": false,
"items": [1, 2],
"position": 1,
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"options": [
{
"id": 1,
"identifier": "LVETRWVU",
"position": 0,
"answer": {"en": "S"}
},
{
"id": 2,
"identifier": "DFEMJWMJ",
"position": 1,
"answer": {"en": "M"}
},
{
"id": 3,
"identifier": "W9AH7RDE",
"position": 2,
"answer": {"en": "L"}
}
]
@@ -134,19 +152,26 @@ Endpoints
"type": "C",
"required": false,
"items": [1, 2],
"ask_during_checkin": false,
"position": 1,
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"options": [
{
"id": 1,
"identifier": "LVETRWVU",
"position": 1,
"answer": {"en": "S"}
},
{
"id": 2,
"identifier": "DFEMJWMJ",
"position": 2,
"answer": {"en": "M"}
},
{
"id": 3,
"identifier": "W9AH7RDE",
"position": 3,
"answer": {"en": "L"}
}
]
@@ -158,3 +183,179 @@ Endpoints
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/questions/
Creates a new question
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/questions/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content: application/json
{
"question": {"en": "T-Shirt size"},
"type": "C",
"required": false,
"items": [1, 2],
"position": 1,
"ask_during_checkin": false,
"options": [
{
"answer": {"en": "S"}
},
{
"answer": {"en": "M"}
},
{
"answer": {"en": "L"}
}
]
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 1,
"question": {"en": "T-Shirt size"},
"type": "C",
"required": false,
"items": [1, 2],
"position": 1,
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"options": [
{
"id": 1,
"identifier": "LVETRWVU",
"position": 1,
"answer": {"en": "S"}
},
{
"id": 2,
"identifier": "DFEMJWMJ",
"position": 2,
"answer": {"en": "M"}
},
{
"id": 3,
"identifier": "W9AH7RDE",
"position": 3,
"answer": {"en": "L"}
}
]
}
:param organizer: The ``slug`` field of the organizer of the event to create an item for
:param event: The ``slug`` field of the event to create an item for
:statuscode 201: no error
:statuscode 400: The item could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/questions/(id)/
Update a question. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
want to change.
You can change all fields of the resource except the ``options`` field. If
you need to update/delete options please use the nested dedicated endpoints.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/items/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"position": 2
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"question": {"en": "T-Shirt size"},
"type": "C",
"required": false,
"items": [1, 2],
"position": 2,
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"options": [
{
"id": 1,
"identifier": "LVETRWVU",
"position": 1,
"answer": {"en": "S"}
},
{
"id": 2,
"identifier": "DFEMJWMJ",
"position": 2,
"answer": {"en": "M"}
},
{
"id": 3,
"identifier": "W9AH7RDE",
"position": 3,
"answer": {"en": "L"}
}
]
}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the question to modify
:statuscode 200: no error
:statuscode 400: The item could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/questions/(id)/
Delete a question.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/items/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the item to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.

View File

@@ -135,7 +135,7 @@ Endpoints
.. sourcecode:: http
HTTP/1.1 200 OK
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json

View File

@@ -31,6 +31,13 @@ import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pretix.testutils.settings")
django.setup()
try:
import enchant
HAS_PYENCHANT = True
except:
HAS_PYENCHANT = False
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
@@ -45,8 +52,9 @@ extensions = [
'sphinx.ext.coverage',
'sphinxcontrib.httpdomain',
'sphinxcontrib.images',
'sphinxcontrib.spelling',
]
if HAS_PYENCHANT:
extensions.append('sphinxcontrib.spelling')
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@@ -292,21 +300,25 @@ images_config = {
'default_image_width': '250px'
}
linkcheck_ignore = [
r'http://localhost.*', r'.*yourdomain.*', r'https://en.wikipedia.org', 'https://pretix.eu/',
]
# -- Options for Spelling output ------------------------------------------
if HAS_PYENCHANT:
# String specifying the language, as understood by PyEnchant and enchant.
# Defaults to en_US for US English.
spelling_lang = 'en_US'
# String specifying the language, as understood by PyEnchant and enchant.
# Defaults to en_US for US English.
spelling_lang = 'en_US'
# String specifying a file containing a list of words known to be spelled
# correctly but that do not appear in the language dictionary selected by
# spelling_lang. The file should contain one word per line.
spelling_word_list_filename='spelling_wordlist.txt'
# String specifying a file containing a list of words known to be spelled
# correctly but that do not appear in the language dictionary selected by
# spelling_lang. The file should contain one word per line.
spelling_word_list_filename='spelling_wordlist.txt'
# Boolean controlling whether suggestions for misspelled words are printed.
# Defaults to False.
spelling_show_suggestions=True
# Boolean controlling whether suggestions for misspelled words are printed.
# Defaults to False.
spelling_show_suggestions=True
# List of filter classes to be added to the tokenizer that produces words to be checked.
from checkin_filter import CheckinFilter
spelling_filters=[CheckinFilter]
# List of filter classes to be added to the tokenizer that produces words to be checked.
from checkin_filter import CheckinFilter
spelling_filters=[CheckinFilter]

View File

@@ -13,7 +13,7 @@ Output registration
-------------------
The invoice renderer API does not make a lot of usage from signals, however, it
does use a signal to get a list of all available ticket outputs. Your plugin
does use a signal to get a list of all available invoice renderers. Your plugin
should listen for this signal and return the subclass of ``pretix.base.invoice.BaseInvoiceRenderer``
that we'll provide in this plugin::

View File

@@ -142,5 +142,5 @@ your Django app label.
.. _Django app: https://docs.djangoproject.com/en/1.7/ref/applications/
.. _signal dispatcher: https://docs.djangoproject.com/en/1.7/topics/signals/
.. _namespace packages: http://legacy.python.org/dev/peps/pep-0420/
.. _entry point: https://pythonhosted.org/setuptools/setuptools.html#dynamic-discovery-of-services-and-plugins
.. _entry point: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#locating-plugins
.. _cookiecutter: https://cookiecutter.readthedocs.io/en/latest/

View File

@@ -77,6 +77,6 @@ Attribution
-----------
This Code of Conduct is adapted from the `Contributor Covenant`_, version 1.4,
available at http://contributor-covenant.org/version/1/4/
available at https://www.contributor-covenant.org/version/1/4/
.. _Contributor Covenant: http://contributor-covenant.org
.. _Contributor Covenant: https://www.contributor-covenant.org

View File

@@ -24,7 +24,7 @@ Coding style and quality
``Fix #123 -- Problems with order creation`` or ``Refs #123 -- Fix this part of that bug``.
.. _PEP 8: http://legacy.python.org/dev/peps/pep-0008/
.. _PEP 8: https://legacy.python.org/dev/peps/pep-0008/
.. _flake8: https://pypi.python.org/pypi/flake8
.. _Django Coding Style: https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/
.. _translation: https://docs.djangoproject.com/en/1.11/topics/i18n/translation/

View File

@@ -16,4 +16,5 @@ Contents:
settings
background
email
permissions
logging

View File

@@ -31,6 +31,9 @@ Organizers and events
.. autoclass:: pretix.base.models.Team
:members:
.. autoclass:: pretix.base.models.TeamAPIToken
:members:
.. autoclass:: pretix.base.models.RequiredAction
:members:

View File

@@ -0,0 +1,194 @@
Permissions
===========
pretix uses a fine-grained permission system to control who is allowed to control what parts of the system.
The central concept here is the concept of *Teams*. You can read more on `configuring teams and permissions <user-teams>`_
and the :class:`pretix.base.models.Team` model in the respective parts of the documentation. The basic digest is:
An organizer account can have any number of teams, and any number of users can be part of a team. A team can be
assigned a set of permissions and connected to some or all of the events of the organizer.
A second way to access pretix is via the REST API, which allows authentication via tokens that are bound to a team,
but not to a user. You can read more at :class:`pretix.base.models.TeamAPIToken`. This page will show you how to
work with permissions in plugins and within the pretix code base.
Requiring permissions for a view
--------------------------------
pretix provides a number of useful mixins and decorators that allow you to specify that a user needs a certain
permission level to access a view::
from pretix.control.permissions import (
OrganizerPermissionRequiredMixin, organizer_permission_required
)
class MyOrgaView(OrganizerPermissionRequiredMixin, View):
permission = 'can_change_organizer_settings'
# Only users with the permission ``can_change_organizer_settings`` on
# this organizer can access this
class MyOtherOrgaView(OrganizerPermissionRequiredMixin, View):
permission = None
# Only users with *any* permission on this organizer can access this
@organizer_permission_required('can_change_organizer_settings')
def my_orga_view(request, organizer, **kwargs):
# Only users with the permission ``can_change_organizer_settings`` on
# this organizer can access this
@organizer_permission_required()
def my_other_orga_view(request, organizer, **kwargs):
# Only users with *any* permission on this organizer can access this
Of course, the same is available on event level::
from pretix.control.permissions import (
EventPermissionRequiredMixin, event_permission_required
)
class MyEventView(EventPermissionRequiredMixin, View):
permission = 'can_change_event_settings'
# Only users with the permission ``can_change_event_settings`` on
# this event can access this
class MyOtherEventView(EventPermissionRequiredMixin, View):
permission = None
# Only users with *any* permission on this event can access this
@event_permission_required('can_change_event_settings')
def my_event_view(request, organizer, **kwargs):
# Only users with the permission ``can_change_event_settings`` on
# this event can access this
@event_permission_required()
def my_other_event_view(request, organizer, **kwargs):
# Only users with *any* permission on this event can access this
You can also require that this view is only accessible by system administrators with an active "admin session"
(see below for what this means)::
from pretix.control.permissions import (
AdministratorPermissionRequiredMixin, administrator_permission_required
)
class MyGlobalView(AdministratorPermissionRequiredMixin, View):
# ...
@administrator_permission_required
def my_global_view(request, organizer, **kwargs):
# ...
In rare cases it might also be useful to expose a feature only to people who have a staff account but do not
necessarily have an active admin session::
from pretix.control.permissions import (
StaffMemberRequiredMixin, staff_member_required
)
class MyGlobalView(StaffMemberRequiredMixin, View):
# ...
@staff_member_required
def my_global_view(request, organizer, **kwargs):
# ...
Requiring permissions in the REST API
-------------------------------------
When creating your own ``viewset`` using Django REST framework, you just need to set the ``permission`` attribute
and pretix will check it automatically for you::
class MyModelViewSet(viewsets.ReadOnlyModelViewSet):
permission = 'can_view_orders'
Checking permission in code
---------------------------
If you need to work with permissions manually, there are a couple of useful helper methods on the :class:`pretix.base.models.Event`,
:class:`pretix.base.models.User` and :class:`pretix.base.models.TeamAPIToken` classes. Here's a quick overview.
Return all users that are in any team that is connected to this event::
>>> event.get_users_with_any_permission()
<QuerySet: …>
Return all users that are in a team with a specific permission for this event::
>>> event.get_users_with_permission('can_change_event_settings')
<QuerySet: …>
Determine if a user has a certain permission for a specific event::
>>> user.has_event_permission(organizer, event, 'can_change_event_settings', request=request)
True
Determine if a user has any permission for a specific event::
>>> user.has_event_permission(organizer, event, request=request)
True
In the two previous commands, the ``request`` argument is optional, but required to support staff sessions (see below).
The same method exists for organizer-level permissions::
>>> user.has_organizer_permission(organizer, 'can_change_event_settings', request=request)
True
Sometimes, it might be more useful to get the set of permissions at once::
>>> user.get_event_permission_set(organizer, event)
{'can_change_event_settings', 'can_view_orders', 'can_change_orders'}
>>> user.get_organizer_permission_set(organizer, event)
{'can_change_organizer_settings', 'can_create_events'}
Within a view on the ``/control`` subpath, the results of these two methods are already available in the
``request.eventpermset`` and ``request.orgapermset`` properties. This makes it convenient to query them in templates::
{% if "can_change_orders" in request.eventpermset %}
{% endif %}
You can also do the reverse to get any events a user has access to::
>>> user.get_events_with_permission('can_change_event_settings', request=request)
<QuerySet: …>
>>> user.get_events_with_any_permission(request=request)
<QuerySet: …>
Most of these methods work identically on :class:`pretix.base.models.TeamAPIToken`.
Staff sessions
--------------
.. versionchanged:: 1.14
In 1.14, the ``User.is_superuser`` attribute has been deprecated and statically set to return ``False``. Staff
sessions have been newly introduced.
System administrators of a pretix instance are identified by the ``is_staff`` attribute on the user model. By default,
the regular permission rules apply for users with ``is_staff = True``. The only difference is that such users can
temporarily turn on "staff mode" via a button in the user interface that grants them **all permissions** as long as
staff mode is active. You can check if a user is in staff mode using their session key:
>>> user.has_active_staff_session(request.session.session_key)
False
Staff mode has a hard time limit and during staff mode, a middleware will log all requests made by that user. Later,
the user is able to also save a message to comment on what they did in their administrative session. This feature is
intended to help compliance with data protection rules as imposed e.g. by GDPR.

View File

@@ -8,5 +8,6 @@ Developer documentation
setup
contribution/index
implementation/index
translation/index
api/index
structure

View File

@@ -145,6 +145,10 @@ and update the ``*.po`` files accordingly::
make localegen
However, most of the time you don't need to care about this. Just create your pull request
with functionality and English strings only, and we'll push the new translation strings
to our translation platform after the merge.
To actually see pretix in your language, you have to compile the ``*.po`` files to their
optimized binary ``*.mo`` counterparts::

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View File

@@ -0,0 +1,88 @@
Translating pretix
==================
pretix has been designed for multi-language capabilities from its start. Organizers can enter their event information
in multiple languages at the same time. However, the software interface of pretix also needs to be translated for
this to be useful.
Since we (the developers of pretix) only speak a very limited number of languages, we need help from the community
to achieve this goal. To make translating pretix easy not only for software developers, we set up a translation
platform at `translate.pretix.eu`_.
Official and inofficial languages
---------------------------------
In the pretix project, there are three types of languages:
Official languages
are translated and maintained by the core team behind pretix or as part of long-term partnerships. We are
committed to keeping these translations up-to-date with new features or changes in pretix and try to offer
support in this language.
Inofficial languages
are contributed and maintained by the Community. We ship them with pretix so you can use them, but we can not
guarantee that new or changed features in pretix will be translated in time.
Incubating languages
are currently in the process of being translated. They can not yet be selected in pretix by end users on
production installations and are only available in development mode for testing.
Please contact translate@pretix.eu if you think an incubated language should be promoted to an inofficial one or if
you are interested in a partnership to make your language official.
The current translation status of various languages is:
.. image:: https://translate.pretix.eu/widgets/pretix/-/multi-blue.svg
:target: https://translate.pretix.eu/engage/pretix/?utm_source=widget
Using our translation platform
------------------------------
If you visit `translate.pretix.eu`_ for the first time, it admittedly looks pretty bare.
.. image:: img/weblate1.png
:class: screenshot
It gets better if you create an account, which you will need to contribute translations. Click on "Register" in the
top-right corner to get started:
.. image:: img/weblate2.png
:class: screenshot
You can either create an account or choose to log in with your GitHub account, whichever you like more.
After creating and activating your account, we recommend that you change your profile and select which languages you
can translate to and which languages you understand. You can find your profile settings by clicking on your name in
the top-right corner.
.. image:: img/weblate3.png
:class: screenshot
Going back to the dashboard by clicking on the logo in the top-left corner, you can select between different lists
of translation projects. You can either filter by projects that already have a translation in your language, or you
go to the `pretix project page`_ where you can select specific components.
.. note::
If you want to translate pretix to a new language that is not yet listed here, you are very welcome to do so!
While you technically can add the language to the portal yourself, we ask you to drop us a short mail to
translate@pretix.eu so we can add it to all components at once and also make it selectable in pretix itself.
.. image:: img/weblate4.png
:class: screenshot
Once you selected a component of a language, you can start going through strings to translate. You can start of by
clicking the "Strings needing action" line in this view:
.. image:: img/weblate5.png
:class: screenshot
In the translate view, you can input your translation for a given source string. If you're unsure about your
translation, you can also just "Suggest" it or mark it as "Needs editing". If you have no idea, just "Skip". If you
scroll down, there is also a "Comments" section to discuss any questions with fellow translators or us developers.
.. image:: img/weblate6.png
:class: screenshot
.. _translate.pretix.eu: https://translate.pretix.eu
.. _pretix project page: https://translate.pretix.eu/projects/pretix/

0
doc/doc_warnings Normal file
View File

View File

@@ -4,4 +4,5 @@ sphinx-rtd-theme
sphinxcontrib-httpdomain
sphinxcontrib-images
sphinxcontrib-spelling
pyenchant
# See https://github.com/rfk/pyenchant/pull/130
git+https://github.com/raphaelm/pyenchant.git@patch-1#egg=pyenchant

View File

@@ -1,6 +1,7 @@
addon
addons
api
auditability
auth
autobuild
backend
@@ -33,8 +34,10 @@ gettext
gunicorn
hardcoded
hostname
inofficial
invalidations
iterable
Jimdo
libsass
linters
memcached
@@ -77,6 +80,7 @@ renderer
renderers
reportlab
screenshot
selectable
serializers
serializers
sexualized

View File

@@ -126,4 +126,29 @@ With the checkbox "Use custom SMTP server" you can turn using your SMTP server o
button "Save and test custom SMTP connection", you can test if the connection and authentication to your SMTP server
succeeds, even before turning that checkbox on.
Spam issues
-----------
If you use an email address of your own domain as a sender address and do not use a custom SMTP server, it is very
likely that at least some of your emails will go to the spam folders of their recipients. We **strongly recommend**
to use your organization's SMTP server in this case, making your email really come from your organization. If you don't
want that or cannot do that, you should add the pretix application server to your SPF record.
If you are using our hosted service at pretix.eu, you can add the following to your SPF record::
include:_spf.pretix.eu
A complete record could look like this::
v=spf1 a mx include:_spf.pretix.eu ~all
Make sure to read up on the `SPF specification`_. If you want to authenticate your emails with DKIM, set up a DNS TXT
record for the subdomain ``pretix._domainkey`` with the following contents::
v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDXrDk6lwOWX00e2MbiiJac6huI+gnzLf9N4G1FnBv3PXq8fz3i2q1szH72OF5mAlKm3zXO4cl/uxx+lfidS1ERbX6Bn9BRstBTQUKWC4JFj8Yk9+fwT7LWehDURazLdTzfsIjJFudLLvxtOKSaOCtMhbPX05DIhziaqVCBqgz/NQIDAQAB
Then, please contact support@pretix.eu and we will enable DKIM for your domain on our mail servers.
.. _Sender Policy Framework: https://en.wikipedia.org/wiki/Sender_Policy_Framework
.. _SPF specification: http://www.openspf.org/SPF_Record_Syntax

View File

@@ -36,6 +36,12 @@ The second snippet should be embedded at the position where the widget should sh
You can of course embed multiple widgets of multiple events on your page. In this case, please add the first
snippet only *once* and the second snippets once *for each event*.
.. note::
Some website builders like Jimdo have trouble with our custom HTML tag. In that case, you can use
``<div class="pretix-widget-compat" …></div>`` instead of ``<pretix-widget …></pretix-widget>`` starting with
pretix 1.14.
Example
-------

View File

@@ -51,3 +51,25 @@ If you created a product and it doesn't show up, please follow the following ste
quota that is assigned to the series date that you access the shop for.
6. If the sale period has not started yet or is already over, check the "Show items outside presale period" setting of
your event.
How can I revert a check-in?
----------------------------
Neither our apps nor our web interface can currently undo the check-in of a tickets. We know that this is
inconvenient for some of you, but we have a good reason for it:
Our Desktop and Android apps both support an asynchronous mode in which they can scan tickets while staying
independent of their internet connection. When scanning with multiple devices, it can of course happen that two
devices scan the same ticket without knowing of the other scan. As soon as one of the devices regains connectivity, it
will upload its activity and the server marks the ticket as checked in -- regardless of the order in which the two
scans were made and uploaded (which could be two different orders).
If we'd provide a "check out" feature, it would not only be used to fix an accidental scan, but scan at entry and
exit to count the current number of people inside etc. In this case, the order of operations matters very much for them
to make sense and provide useful results. This makes implementing an asynchronous mode much more complicated.
In this trade off, we chose offline-capabilities over the check out feature. We plan on solving this problem in the
future, but we're not there yet.
If you're just *testing* the check-in capabilities and want to clean out everything for the real process, you can just
delete and re-create the check-in list.

View File

@@ -1,3 +1,5 @@
.. _user-teams:
Teams
=====

View File

@@ -1,7 +1,7 @@
General settings
================
At "Settings" → "Pages", you can configure every aspect related to the payments you want to accept. The upper part
At "Settings" → "Payment", you can configure every aspect related to the payments you want to accept. The upper part
of the page shows a number of general settings that affect all payment methods:
.. thumbnail:: ../../screens/event/settings_payment.png

View File

@@ -3,6 +3,10 @@
Stripe
======
.. note:: If you use the Hosted version of pretix at pretix.eu, you do not need to copy API keys and create webhooks
any more. Instead, you can just click "Connect with Stripe" in pretix and everything will connect
automatically.
To integrate Stripe with pretix, you first need to have an active Stripe merchant account. If you do not already have a
Stripe account, you can create one on `stripe.com`_. Then, click on "API" in the left navigation of the Stripe
Dashboard. As you can see in the following screenshot, you will be presented with two sets of API keys, one for test

37
src/.update-locales Executable file
View File

@@ -0,0 +1,37 @@
#!/bin/sh
COMPONENTS="pretix/pretix pretix/pretix-js"
DIR=pretix/locale
# Renerates .po files used for translating the plugin
set -e
set -x
# Lock Weblate
for c in $COMPONENTS; do
wlc lock $c;
done
# Push changes from Weblate to GitHub
for c in $COMPONENTS; do
wlc commit $c;
done
# Pull changes from GitHub
git pull --rebase
# Update po files itself
make localegen
# Commit changes
git add $DIR/*/*/*.po
git add $DIR/*.pot
git commit -s -m "Update po files
[CI skip]"
# Push changes
git push
# Unlock Weblate
for c in $COMPONENTS; do
wlc unlock $c;
done

View File

@@ -1,12 +1,13 @@
all: localecompile staticfiles
production: localecompile staticfiles compress
LNGS:=`find pretix/locale/ -mindepth 1 -maxdepth 1 -type d -printf "-l %f "`
localecompile:
./manage.py compilemessages
localegen:
./manage.py makemessages --all --ignore "pretix/helpers/*"
./manage.py makemessages --all -d djangojs --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "build/*"
./manage.py makemessages --keep-pot --ignore "pretix/helpers/*" $(LNGS)
./manage.py makemessages --keep-pot -d djangojs --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "build/*" $(LNGS)
staticfiles: jsi18n
./manage.py collectstatic --noinput

View File

@@ -1 +1 @@
__version__ = "1.13.0"
__version__ = "1.14.0"

View File

@@ -1,4 +1,3 @@
from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import SAFE_METHODS, BasePermission
from pretix.base.models import Event
@@ -57,18 +56,3 @@ class EventPermission(BasePermission):
if required_permission and required_permission not in request.orgapermset:
return False
return True
def permission_required(required_permission):
def decorator(function):
def wrapper(self, request, *args, **kw):
if 'event' in request.resolver_match.kwargs and 'organizer' in request.resolver_match.kwargs:
if required_permission and required_permission not in request.eventpermset:
raise PermissionDenied('You do not have permission to perform this operation.')
elif 'organizer' in request.resolver_match.kwargs:
if required_permission and required_permission not in request.orgapermset:
raise PermissionDenied('You do not have permission to perform this operation.')
return function(self, request, *args, **kw)
return wrapper
return decorator

View File

@@ -87,6 +87,9 @@ class ItemSerializer(I18nAwareModelSerializer):
def validate(self, data):
data = super().validate(data)
if self.instance and ('addons' in data or 'variations' in data):
raise ValidationError(_('Updating add-ons or variations via PATCH/PUT is not supported. Please use the '
'dedicated nested endpoint.'))
Item.clean_per_order(data.get('min_per_order'), data.get('max_per_order'))
Item.clean_available(data.get('available_from'), data.get('available_until'))
@@ -101,17 +104,8 @@ class ItemSerializer(I18nAwareModelSerializer):
Item.clean_tax_rule(value, self.context['event'])
return value
def validate_variations(self, value):
if self.instance is not None:
raise ValidationError(_('Updating variations via PATCH/PUT is not supported. Please use the dedicated'
' nested endpoint.'))
return value
def validate_addons(self, value):
if self.instance is not None:
raise ValidationError(_('Updating add-ons via PATCH/PUT is not supported. Please use the dedicated'
' nested endpoint.'))
else:
if not self.instance:
for addon_data in value:
ItemAddOn.clean_categories(self.context['event'], None, self.instance, addon_data['addon_category'])
ItemAddOn.clean_min_count(addon_data['min_count'])
@@ -138,20 +132,72 @@ class ItemCategorySerializer(I18nAwareModelSerializer):
fields = ('id', 'name', 'description', 'position', 'is_addon')
class InlineQuestionOptionSerializer(I18nAwareModelSerializer):
class QuestionOptionSerializer(I18nAwareModelSerializer):
identifier = serializers.CharField(allow_null=True)
class Meta:
model = QuestionOption
fields = ('id', 'answer')
fields = ('id', 'identifier', 'answer', 'position')
def validate_identifier(self, value):
QuestionOption.clean_identifier(self.context['event'], value, self.instance)
return value
class InlineQuestionOptionSerializer(I18nAwareModelSerializer):
identifier = serializers.CharField(allow_null=True)
class Meta:
model = QuestionOption
fields = ('id', 'identifier', 'answer', 'position')
class QuestionSerializer(I18nAwareModelSerializer):
options = InlineQuestionOptionSerializer(many=True)
options = InlineQuestionOptionSerializer(many=True, required=False)
identifier = serializers.CharField(allow_null=True)
class Meta:
model = Question
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
'ask_during_checkin')
'ask_during_checkin', 'identifier')
def validate_identifier(self, value):
Question._clean_identifier(self.context['event'], value, self.instance)
return value
def validate(self, data):
data = super().validate(data)
if self.instance and 'options' in data:
raise ValidationError(_('Updating options via PATCH/PUT is not supported. Please use the dedicated'
' nested endpoint.'))
event = self.context['event']
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
full_data.update(data)
Question.clean_items(event, full_data.get('items'))
return data
def validate_options(self, value):
if not self.instance:
known = []
for opt_data in value:
if opt_data.get('identifier'):
QuestionOption.clean_identifier(self.context['event'], opt_data.get('identifier'), self.instance,
known)
known.append(opt_data.get('identifier'))
return value
@transaction.atomic
def create(self, validated_data):
options_data = validated_data.pop('options') if 'options' in validated_data else []
items = validated_data.pop('items')
question = Question.objects.create(**validated_data)
question.items.set(items)
for opt_data in options_data:
QuestionOption.objects.create(question=question, **opt_data)
return question
class QuotaSerializer(I18nAwareModelSerializer):

View File

@@ -29,10 +29,23 @@ class InvoiceAdddressSerializer(I18nAwareModelSerializer):
'vat_id_validated', 'internal_reference')
class AnswerQuestionIdentifierField(serializers.Field):
def to_representation(self, instance: QuestionAnswer):
return instance.question.identifier
class AnswerQuestionOptionsIdentifierField(serializers.Field):
def to_representation(self, instance: QuestionAnswer):
return [o.identifier for o in instance.options.all()]
class AnswerSerializer(I18nAwareModelSerializer):
question_identifier = AnswerQuestionIdentifierField(source='*', read_only=True)
option_identifiers = AnswerQuestionOptionsIdentifierField(source='*', read_only=True)
class Meta:
model = QuestionAnswer
fields = ('question', 'answer', 'options')
fields = ('question', 'answer', 'question_identifier', 'options', 'option_identifiers')
class CheckinSerializer(I18nAwareModelSerializer):

View File

@@ -29,6 +29,9 @@ event_router.register(r'checkinlists', checkin.CheckinListViewSet)
checkinlist_router = routers.DefaultRouter()
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet)
question_router = routers.DefaultRouter()
question_router.register(r'options', item.QuestionOptionViewSet)
item_router = routers.DefaultRouter()
item_router.register(r'variations', item.ItemVariationViewSet)
item_router.register(r'addons', item.ItemAddOnViewSet)
@@ -44,6 +47,8 @@ urlpatterns = [
url(r'^organizers/(?P<organizer>[^/]+)/', include(orga_router.urls)),
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/', include(event_router.urls)),
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/items/(?P<item>[^/]+)/', include(item_router.urls)),
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/questions/(?P<question>[^/]+)/',
include(question_router.urls)),
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/checkinlists/(?P<list>[^/]+)/',
include(checkinlist_router.urls)),
]

View File

@@ -10,10 +10,12 @@ from rest_framework.response import Response
from pretix.api.serializers.item import (
ItemAddOnSerializer, ItemCategorySerializer, ItemSerializer,
ItemVariationSerializer, QuestionSerializer, QuotaSerializer,
ItemVariationSerializer, QuestionOptionSerializer, QuestionSerializer,
QuotaSerializer,
)
from pretix.base.models import (
Item, ItemAddOn, ItemCategory, ItemVariation, Question, Quota,
Item, ItemAddOn, ItemCategory, ItemVariation, Question, QuestionOption,
Quota,
)
from pretix.base.models.organizer import TeamAPIToken
from pretix.helpers.dicts import merge_dicts
@@ -201,7 +203,7 @@ class ItemCategoryFilter(FilterSet):
fields = ['is_addon']
class ItemCategoryViewSet(viewsets.ReadOnlyModelViewSet):
class ItemCategoryViewSet(viewsets.ModelViewSet):
serializer_class = ItemCategorySerializer
queryset = ItemCategory.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
@@ -209,12 +211,47 @@ class ItemCategoryViewSet(viewsets.ReadOnlyModelViewSet):
ordering_fields = ('id', 'position')
ordering = ('position', 'id')
permission = 'can_change_items'
write_permission = 'can_change_items'
def get_queryset(self):
return self.request.event.categories.all()
def perform_create(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
'pretix.event.category.added',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
class QuestionViewSet(viewsets.ReadOnlyModelViewSet):
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
return ctx
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
'pretix.event.category.changed',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
def perform_destroy(self, instance):
for item in instance.items.all():
item.category = None
item.save()
instance.log_action(
'pretix.event.category.deleted',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
)
super().perform_destroy(instance)
class QuestionViewSet(viewsets.ModelViewSet):
serializer_class = QuestionSerializer
queryset = Question.objects.none()
filter_backends = (OrderingFilter,)
@@ -225,6 +262,85 @@ class QuestionViewSet(viewsets.ReadOnlyModelViewSet):
def get_queryset(self):
return self.request.event.questions.prefetch_related('options').all()
def perform_create(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
'pretix.event.question.added',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
return ctx
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
'pretix.event.question.changed',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
def perform_destroy(self, instance):
instance.log_action(
'pretix.event.question.deleted',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
)
super().perform_destroy(instance)
class QuestionOptionViewSet(viewsets.ModelViewSet):
serializer_class = QuestionOptionSerializer
queryset = QuestionOption.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter,)
ordering_fields = ('id', 'position')
ordering = ('position',)
permission = 'can_change_items'
write_permission = 'can_change_items'
def get_queryset(self):
q = get_object_or_404(Question, pk=self.kwargs['question'], event=self.request.event)
return q.options.all()
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['question'] = get_object_or_404(Question, pk=self.kwargs['question'], event=self.request.event)
return ctx
def perform_create(self, serializer):
q = get_object_or_404(Question, pk=self.kwargs['question'], event=self.request.event)
serializer.save(question=q)
q.log_action(
'pretix.event.question.option.added',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
)
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.question.log_action(
'pretix.event.question.option.changed',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
)
def perform_destroy(self, instance):
instance.question.log_action(
'pretix.event.question.option.deleted',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data={'id': instance.pk}
)
super().perform_destroy(instance)
class QuotaFilter(FilterSet):
class Meta:

View File

@@ -50,7 +50,7 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
def get_queryset(self):
return self.request.event.orders.prefetch_related(
'positions', 'positions__checkins', 'positions__item', 'positions__answers', 'positions__answers__options',
'fees'
'positions__answers__question', 'fees'
).select_related(
'invoice_address'
)
@@ -234,7 +234,7 @@ class OrderPositionViewSet(viewsets.ReadOnlyModelViewSet):
def get_queryset(self):
return OrderPosition.objects.filter(order__event=self.request.event).prefetch_related(
'checkins', 'answers', 'answers__options'
'checkins', 'answers', 'answers__options', 'answers__question'
).select_related(
'item', 'order', 'order__event', 'order__event__organizer'
)

View File

@@ -12,7 +12,7 @@ class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
def get_queryset(self):
if self.request.user.is_authenticated():
if self.request.user.is_superuser:
if self.request.user.has_active_staff_session(self.request.session.session_key):
return Organizer.objects.all()
else:
return Organizer.objects.filter(pk__in=self.request.user.teams.values_list('organizer', flat=True))

View File

@@ -12,7 +12,7 @@ class PretixBaseConfig(AppConfig):
from . import exporters # NOQA
from . import invoice # NOQA
from . import notifications # NOQA
from .services import export, mail, tickets, cart, orders, invoices, cleanup, update_check, quotas, notifications # NOQA
from .services import auth, export, mail, tickets, cart, orders, invoices, cleanup, update_check, quotas, notifications # NOQA
try:
from .celery_app import app as celery_app # NOQA

View File

@@ -69,4 +69,5 @@ class SettingsForm(i18nfield.forms.I18nFormMixin, HierarkeyForm):
)
else:
fname = '%s/%s.%s.%s' % (self.obj.slug, name, nonce, name.split('.')[-1])
return fname
# TODO: make sure pub is always correct
return 'pub/' + fname

View File

@@ -8,6 +8,7 @@ from django.utils.translation import ugettext_lazy as _
from pytz import common_timezones
from pretix.base.models import User
from pretix.control.forms import SingleLanguageWidget
class UserSettingsForm(forms.ModelForm):
@@ -47,6 +48,9 @@ class UserSettingsForm(forms.ModelForm):
'timezone',
'email'
]
widgets = {
'locale': SingleLanguageWidget
}
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')

View File

@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.11 on 2018-03-12 11:19
from __future__ import unicode_literals
from django.db import migrations, models
from django.utils.crypto import get_random_string
def set_identifiers(apps, schema_editor):
Question = apps.get_model('pretixbase', 'Question')
QuestionOption = apps.get_model('pretixbase', 'QuestionOption')
for q in Question.objects.select_related('event'):
if not q.identifier:
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
while True:
code = get_random_string(length=8, allowed_chars=charset)
if not Question.objects.filter(event=q.event, identifier=code).exists():
q.identifier = code
q.save()
break
for q in QuestionOption.objects.select_related('question', 'question__event'):
if not q.identifier:
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
while True:
code = get_random_string(length=8, allowed_chars=charset)
if not QuestionOption.objects.filter(question__event=q.question.event, identifier=code).exists():
q.identifier = code
q.save()
break
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0084_questionoption_position'),
]
operations = [
migrations.AddField(
model_name='question',
name='identifier',
field=models.CharField(default='', max_length=190),
preserve_default=False,
),
migrations.AddField(
model_name='questionoption',
name='identifier',
field=models.CharField(default='', max_length=190),
preserve_default=False,
),
migrations.AlterField(
model_name='user',
name='locale',
field=models.CharField(choices=[('en', 'English'), ('de', 'German'), ('de-informal', 'German (informal)'), ('nl', 'Dutch'), ('da', 'Danish'), ('pt-br', 'Portuguese (Brazil)')], default='en', max_length=50, verbose_name='Language'),
),
migrations.RunPython(set_identifiers, migrations.RunPython.noop)
]

View File

@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.11 on 2018-03-20 12:19
from __future__ import unicode_literals
from django.db import migrations, models
import pretix.base.models.invoices
import pretix.base.models.orders
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0085_auto_20180312_1119'),
]
operations = [
migrations.AlterField(
model_name='cachedcombinedticket',
name='file',
field=models.FileField(blank=True, max_length=255, null=True, upload_to=pretix.base.models.orders.cachedcombinedticket_name),
),
migrations.AlterField(
model_name='cachedticket',
name='file',
field=models.FileField(blank=True, max_length=255, null=True, upload_to=pretix.base.models.orders.cachedticket_name),
),
migrations.AlterField(
model_name='invoice',
name='file',
field=models.FileField(blank=True, max_length=255, null=True, upload_to=pretix.base.models.invoices.invoice_filename),
),
migrations.AlterField(
model_name='question',
name='identifier',
field=models.CharField(help_text='You can enter any value here to make it easier to match the data with other sources. If you do not input one, we will generate one automatically.', max_length=190, verbose_name='Internal identifier'),
),
migrations.AlterField(
model_name='questionanswer',
name='file',
field=models.FileField(blank=True, max_length=255, null=True, upload_to=pretix.base.models.orders.answerfile_name),
),
]

View File

@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.10 on 2018-03-17 19:52
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
def set_is_staff(apps, schema_editor):
User = apps.get_model('pretixbase', 'User')
User.objects.filter(is_superuser=True).update(is_staff=True)
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0086_auto_20180320_1219'),
]
operations = [
migrations.RunPython(
set_is_staff,
migrations.RunPython.noop,
),
migrations.RemoveField(
model_name='user',
name='is_superuser',
),
migrations.CreateModel(
name='StaffSession',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date_start', models.DateTimeField(auto_now_add=True)),
('date_end', models.DateTimeField(blank=True, null=True)),
('session_key', models.CharField(max_length=255)),
('comment', models.TextField()),
],
),
migrations.CreateModel(
name='StaffSessionAuditLog',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('datetime', models.DateTimeField(auto_now_add=True)),
('url', models.CharField(max_length=255)),
('session', models.ForeignKey(on_delete=models.deletion.CASCADE, related_name='logs', to='pretixbase.StaffSession')),
],
),
migrations.AddField(
model_name='staffsession',
name='user',
field=models.ForeignKey(default=None, on_delete=models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
preserve_default=False,
),
migrations.AddField(
model_name='staffsessionauditlog',
name='impersonating',
field=models.ForeignKey(blank=True, null=True, on_delete=models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='staffsessionauditlog',
name='method',
field=models.CharField(default='GET', max_length=255),
preserve_default=False,
),
]

View File

@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.11 on 2018-03-28 12:17
from __future__ import unicode_literals
from django.db import migrations, models
import pretix.base.models.items
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0087_auto_20180317_1952'),
]
operations = [
migrations.AlterModelOptions(
name='staffsession',
options={'ordering': ('date_start',)},
),
migrations.AlterModelOptions(
name='staffsessionauditlog',
options={'ordering': ('datetime',)},
),
migrations.AlterField(
model_name='item',
name='picture',
field=models.ImageField(blank=True, max_length=255, null=True, upload_to=pretix.base.models.items.itempicture_upload_to, verbose_name='Product picture'),
),
]

View File

@@ -19,7 +19,9 @@ from .orders import (
cachedcombinedticket_name, cachedticket_name, generate_position_secret,
generate_secret,
)
from .organizer import Organizer, Organizer_SettingsStore, Team, TeamInvite
from .organizer import (
Organizer, Organizer_SettingsStore, Team, TeamAPIToken, TeamInvite,
)
from .tax import TaxRule
from .vouchers import Voucher
from .waitinglist import WaitingListEntry

View File

@@ -1,4 +1,4 @@
from typing import Union
from datetime import timedelta
from django.conf import settings
from django.contrib.auth.models import (
@@ -9,6 +9,7 @@ from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models import Q
from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from django_otp.models import Device
@@ -36,7 +37,6 @@ class UserManager(BaseUserManager):
raise Exception("You must provide a password")
user = self.model(email=email)
user.is_staff = True
user.is_superuser = True
user.set_password(password)
user.save()
return user
@@ -46,6 +46,11 @@ def generate_notifications_token():
return get_random_string(length=32)
class SuperuserPermissionSet:
def __contains__(self, item):
return True
class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
"""
This is the user model used by pretix for authentication.
@@ -114,6 +119,10 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
def __str__(self):
return self.email
@property
def is_superuser(self):
return False
def get_short_name(self) -> str:
"""
Returns the first of the following user properties that is found to exist:
@@ -194,40 +203,36 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
))
return self._teamcache['e{}'.format(event.pk)]
class SuperuserPermissionSet:
def __contains__(self, item):
return True
def get_event_permission_set(self, organizer, event) -> Union[set, SuperuserPermissionSet]:
def get_event_permission_set(self, organizer, event) -> set:
"""
Gets a set of permissions (as strings) that a user holds for a particular event
:param organizer: The organizer of the event
:param event: The event to check
:return: set in case of a normal user and a SuperuserPermissionSet in case of a superuser (fake object where
a in b always returns true).
:return: set
"""
if self.is_superuser:
return self.SuperuserPermissionSet()
teams = self._get_teams_for_event(organizer, event)
return set.union(*[t.permission_set() for t in teams])
sets = [t.permission_set() for t in teams]
if sets:
return set.union(*sets)
else:
return set()
def get_organizer_permission_set(self, organizer) -> Union[set, SuperuserPermissionSet]:
def get_organizer_permission_set(self, organizer) -> set:
"""
Gets a set of permissions (as strings) that a user holds for a particular organizer
:param organizer: The organizer of the event
:return: set in case of a normal user and a SuperuserPermissionSet in case of a superuser (fake object where
a in b always returns true).
:return: set
"""
if self.is_superuser:
return self.SuperuserPermissionSet()
teams = self._get_teams_for_organizer(organizer)
return set.union(*[t.permission_set() for t in teams])
sets = [t.permission_set() for t in teams]
if sets:
return set.union(*sets)
else:
return set()
def has_event_permission(self, organizer, event, perm_name=None) -> bool:
def has_event_permission(self, organizer, event, perm_name=None, request=None) -> bool:
"""
Checks if this user is part of any team that grants access of type ``perm_name``
to the event ``event``.
@@ -235,9 +240,10 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
:param organizer: The organizer of the event
:param event: The event to check
:param perm_name: The permission, e.g. ``can_change_teams``
:param request: The current request (optional). Required to detect staff sessions properly.
:return: bool
"""
if self.is_superuser:
if request and self.has_active_staff_session(request.session.session_key):
return True
teams = self._get_teams_for_event(organizer, event)
if teams:
@@ -246,16 +252,17 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
return True
return False
def has_organizer_permission(self, organizer, perm_name=None):
def has_organizer_permission(self, organizer, perm_name=None, request=None):
"""
Checks if this user is part of any team that grants access of type ``perm_name``
to the organizer ``organizer``.
:param organizer: The organizer to check
:param perm_name: The permission, e.g. ``can_change_teams``
:param request: The current request (optional). Required to detect staff sessions properly.
:return: bool
"""
if self.is_superuser:
if request and self.has_active_staff_session(request.session.session_key):
return True
teams = self._get_teams_for_organizer(organizer)
if teams:
@@ -263,15 +270,16 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
return True
return False
def get_events_with_any_permission(self):
def get_events_with_any_permission(self, request=None):
"""
Returns a queryset of events the user has any permissions to.
:param request: The current request (optional). Required to detect staff sessions properly.
:return: Iterable of Events
"""
from .event import Event
if self.is_superuser:
if request and self.has_active_staff_session(request.session.session_key):
return Event.objects.all()
return Event.objects.filter(
@@ -279,15 +287,16 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
| Q(id__in=self.teams.values_list('limit_events__id', flat=True))
)
def get_events_with_permission(self, permission):
def get_events_with_permission(self, permission, request=None):
"""
Returns a queryset of events the user has a specific permissions to.
:param request: The current request (optional). Required to detect staff sessions properly.
:return: Iterable of Events
"""
from .event import Event
if self.is_superuser:
if request and self.has_active_staff_session(request.session.session_key):
return Event.objects.all()
kwargs = {permission: True}
@@ -297,6 +306,56 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
| Q(id__in=self.teams.filter(**kwargs).values_list('limit_events__id', flat=True))
)
def has_active_staff_session(self, session_key=None):
"""
Returns whether or not a user has an active staff session (formerly known as superuser session)
with the given session key.
"""
return self.get_active_staff_session(session_key) is not None
def get_active_staff_session(self, session_key=None):
if not self.is_staff:
return None
if not hasattr(self, '_staff_session_cache'):
self._staff_session_cache = {}
if session_key not in self._staff_session_cache:
qs = StaffSession.objects.filter(
user=self, date_end__isnull=True
)
if session_key:
qs = qs.filter(session_key=session_key)
sess = qs.first()
if sess:
if sess.date_start < now() - timedelta(seconds=settings.PRETIX_SESSION_TIMEOUT_ABSOLUTE):
sess.date_end = now()
sess.save()
sess = None
self._staff_session_cache[session_key] = sess
return self._staff_session_cache[session_key]
class StaffSession(models.Model):
user = models.ForeignKey('User')
date_start = models.DateTimeField(auto_now_add=True)
date_end = models.DateTimeField(null=True, blank=True)
session_key = models.CharField(max_length=255)
comment = models.TextField()
class Meta:
ordering = ('date_start',)
class StaffSessionAuditLog(models.Model):
session = models.ForeignKey('StaffSession', related_name='logs')
datetime = models.DateTimeField(auto_now_add=True)
url = models.CharField(max_length=255)
method = models.CharField(max_length=255)
impersonating = models.ForeignKey('User', null=True, blank=True)
class Meta:
ordering = ('datetime',)
class U2FDevice(Device):
json_data = models.TextField()

View File

@@ -437,7 +437,8 @@ class Event(EventMixin, LoggedModel):
if s.value.startswith('file://'):
fi = default_storage.open(s.value[7:], 'rb')
nonce = get_random_string(length=8)
fname = '%s/%s/%s.%s.%s' % (
# TODO: make sure pub is always correct
fname = 'pub/%s/%s/%s.%s.%s' % (
self.organizer.slug, self.slug, s.key, nonce, s.value.split('.')[-1]
)
newname = default_storage.save(fname, fi)
@@ -553,9 +554,7 @@ class Event(EventMixin, LoggedModel):
Q(all_events=True) | Q(limit_events__pk=self.pk)
)
return User.objects.annotate(twp=Exists(team_with_perm)).filter(
Q(is_superuser=True) | Q(twp=True)
)
return User.objects.annotate(twp=Exists(team_with_perm)).filter(twp=True)
def allow_delete(self):
return not self.orders.exists() and not self.invoices.exists()

View File

@@ -84,7 +84,7 @@ class Invoice(models.Model):
foreign_currency_rate = models.DecimalField(decimal_places=4, max_digits=10, null=True, blank=True)
foreign_currency_rate_date = models.DateField(null=True, blank=True)
file = models.FileField(null=True, blank=True, upload_to=invoice_filename)
file = models.FileField(null=True, blank=True, upload_to=invoice_filename, max_length=255)
internal_reference = models.TextField(blank=True)
@staticmethod

View File

@@ -11,6 +11,7 @@ from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import F, Func, Q, Sum
from django.utils import formats
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 _
@@ -85,7 +86,7 @@ class ItemCategory(LoggedModel):
def itempicture_upload_to(instance, filename: str) -> str:
return '%s/%s/item-%s-%s.%s' % (
return 'pub/%s/%s/item-%s-%s.%s' % (
instance.event.organizer.slug, instance.event.slug, instance.id,
str(uuid.uuid4()), filename.split('.')[-1]
)
@@ -247,7 +248,7 @@ class Item(LoggedModel):
)
picture = models.ImageField(
verbose_name=_("Product picture"),
null=True, blank=True,
null=True, blank=True, max_length=255,
upload_to=itempicture_upload_to
)
available_from = models.DateTimeField(
@@ -630,6 +631,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 identifier: An arbitrary, internal identifier
:type identifier: str
"""
TYPE_NUMBER = "N"
TYPE_STRING = "S"
@@ -661,6 +664,12 @@ class Question(LoggedModel):
question = I18nTextField(
verbose_name=_("Question")
)
identifier = models.CharField(
max_length=190,
verbose_name=_("Internal identifier"),
help_text=_('You can enter any value here to make it easier to match the data with other sources. If you do '
'not input one, we will generate one automatically.')
)
help_text = I18nTextField(
verbose_name=_("Help text"),
help_text=_("If the question needs to be explained or clarified, do it here!"),
@@ -706,7 +715,25 @@ class Question(LoggedModel):
if self.event:
self.event.cache.clear()
def clean_identifier(self, code):
Question._clean_identifier(self.event, code, self)
@staticmethod
def _clean_identifier(event, code, instance=None):
qs = Question.objects.filter(event=event, identifier=code)
if instance:
qs = qs.exclude(pk=instance.pk)
if qs.exists():
raise ValidationError(_('This identifier is already used for a different question.'))
def save(self, *args, **kwargs):
if not self.identifier:
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
while True:
code = get_random_string(length=8, allowed_chars=charset)
if not Question.objects.filter(event=self.event, identifier=code).exists():
self.identifier = code
break
super().save(*args, **kwargs)
if self.event:
self.event.cache.clear()
@@ -776,15 +803,40 @@ class Question(LoggedModel):
return answer
@staticmethod
def clean_items(event, items):
for item in items:
if event != item.event:
raise ValidationError(_('One or more items do not belong to this event.'))
class QuestionOption(models.Model):
question = models.ForeignKey('Question', related_name='options')
identifier = models.CharField(max_length=190)
answer = I18nCharField(verbose_name=_('Answer'))
position = models.IntegerField(default=0)
def __str__(self):
return str(self.answer)
def save(self, *args, **kwargs):
if not self.identifier:
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
while True:
code = get_random_string(length=8, allowed_chars=charset)
if not QuestionOption.objects.filter(question__event=self.question.event, identifier=code).exists():
self.identifier = code
break
super().save(*args, **kwargs)
@staticmethod
def clean_identifier(event, code, instance=None, known=[]):
qs = QuestionOption.objects.filter(question__event=event, identifier=code)
if instance:
qs = qs.exclude(pk=instance.pk)
if qs.exists() or code in known:
raise ValidationError(_('The identifier "{}" is already used for a different option.').format(code))
class Meta:
verbose_name = _("Question option")
verbose_name_plural = _("Question options")

View File

@@ -471,7 +471,8 @@ class QuestionAnswer(models.Model):
)
answer = models.TextField()
file = models.FileField(
null=True, blank=True, upload_to=answerfile_name
null=True, blank=True, upload_to=answerfile_name,
max_length=255
)
@property
@@ -969,7 +970,7 @@ class CachedTicket(models.Model):
provider = models.CharField(max_length=255)
type = models.CharField(max_length=255)
extension = models.CharField(max_length=255)
file = models.FileField(null=True, blank=True, upload_to=cachedticket_name)
file = models.FileField(null=True, blank=True, upload_to=cachedticket_name, max_length=255)
created = models.DateTimeField(auto_now_add=True)
@@ -978,7 +979,7 @@ class CachedCombinedTicket(models.Model):
provider = models.CharField(max_length=255)
type = models.CharField(max_length=255)
extension = models.CharField(max_length=255)
file = models.FileField(null=True, blank=True, upload_to=cachedcombinedticket_name)
file = models.FileField(null=True, blank=True, upload_to=cachedcombinedticket_name, max_length=255)
created = models.DateTimeField(auto_now_add=True)

View File

@@ -264,7 +264,7 @@ class TeamAPIToken(models.Model):
"""
return self.team.permission_set() if self.team.organizer == organizer else set()
def has_event_permission(self, organizer, event, perm_name=None) -> bool:
def has_event_permission(self, organizer, event, perm_name=None, request=None) -> bool:
"""
Checks if this token is part of a team that grants access of type ``perm_name``
to the event ``event``.
@@ -272,6 +272,7 @@ class TeamAPIToken(models.Model):
:param organizer: The organizer of the event
:param event: The event to check
:param perm_name: The permission, e.g. ``can_change_teams``
:param request: This parameter is ignored and only defined for compatibility reasons.
:return: bool
"""
has_event_access = (self.team.all_events and organizer == self.team.organizer) or (
@@ -279,13 +280,14 @@ class TeamAPIToken(models.Model):
)
return has_event_access and (not perm_name or self.team.has_permission(perm_name))
def has_organizer_permission(self, organizer, perm_name=None):
def has_organizer_permission(self, organizer, perm_name=None, request=None):
"""
Checks if this token is part of a team that grants access of type ``perm_name``
to the organizer ``organizer``.
:param organizer: The organizer to check
:param perm_name: The permission, e.g. ``can_change_teams``
:param request: This parameter is ignored and only defined for compatibility reasons.
:return: bool
"""
return organizer == self.team.organizer and (not perm_name or self.team.has_permission(perm_name))

View File

@@ -1,6 +1,7 @@
import json
from decimal import Decimal
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.formats import localize
from django.utils.translation import ugettext_lazy as _
@@ -113,7 +114,7 @@ class TaxRule(LoggedModel):
def clean(self):
if self.eu_reverse_charge and not self.home_country:
raise ValueError(_('You need to set your home country to use the reverse charge feature.'))
raise ValidationError(_('You need to set your home country to use the reverse charge feature.'))
def __str__(self):
if self.price_includes_tax:
@@ -124,6 +125,10 @@ class TaxRule(LoggedModel):
s += ' ({})'.format(_('reverse charge enabled'))
return str(s)
@property
def has_custom_rules(self):
return self.custom_rules and self.custom_rules != '[]'
def tax(self, base_price, base_price_is='auto'):
if self.rate == Decimal('0.00'):
return TaxedPrice(

View File

@@ -25,7 +25,7 @@ def _generate_random_code(prefix=None):
def generate_code(prefix=None):
while True:
code = _generate_random_code(prefix=prefix)
if not Voucher.objects.filter(code=code).exists():
if not Voucher.objects.filter(code__iexact=code).exists():
return code
@@ -278,11 +278,9 @@ class Voucher(LoggedModel):
if old_instance.quota:
quotas.add(old_instance.quota)
elif old_instance.variation:
quotas |= set(old_instance.variation.quotas.filter(
subevent=old_instance.subevent))
quotas |= set(old_instance.variation.quotas.filter(subevent=old_instance.subevent))
elif old_instance.item:
quotas |= set(old_instance.item.quotas.filter(
subevent=old_instance.subevent))
quotas |= set(old_instance.item.quotas.filter(subevent=old_instance.subevent))
return quotas
@staticmethod
@@ -313,7 +311,7 @@ class Voucher(LoggedModel):
@staticmethod
def clean_voucher_code(data, event, pk):
if 'code' in data and Voucher.objects.filter(Q(code=data['code']) & Q(event=event) & ~Q(pk=pk)).exists():
if 'code' in data and Voucher.objects.filter(Q(code__iexact=data['code']) & Q(event=event) & ~Q(pk=pk)).exists():
raise ValidationError(_('A voucher with this code already exists.'))
def save(self, *args, **kwargs):

View File

@@ -204,6 +204,12 @@ def register_default_notification_types(sender, **kwargs):
_('Order canceled'),
_('Order {order.code} has been canceled.')
),
ParametrizedOrderNotificationType(
sender,
'pretix.event.order.expired',
_('Order expired'),
_('Order {order.code} has been marked as expired.'),
),
ParametrizedOrderNotificationType(
sender,
'pretix.event.order.modified',

View File

@@ -169,6 +169,22 @@ class BasePaymentProvider:
label=_('Enable payment method'),
required=False,
)),
('_availability_date',
RelativeDateField(
label=_('Available until'),
help_text=_('Users will not be able to choose this payment provider after the given date.'),
required=False,
)),
('_invoice_text',
I18nFormField(
label=_('Text on invoices'),
help_text=_('Will be printed just below the payment figures and above the closing text on invoices. '
'This will only be used if the invoice is generated before the order is paid. If the '
'invoice is generated later, it will show a text stating that it has already been paid.'),
required=False,
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}}
)),
('_fee_abs',
forms.DecimalField(
label=_('Additional fee'),
@@ -187,12 +203,6 @@ class BasePaymentProvider:
localize=True,
required=False,
)),
('_availability_date',
RelativeDateField(
label=_('Available until'),
help_text=_('Users will not be able to choose this payment provider after the given date.'),
required=False,
)),
('_fee_reverse_calc',
forms.BooleanField(
label=_('Calculate the fee from the total value including the fee.'),
@@ -202,16 +212,6 @@ class BasePaymentProvider:
'above!').format(docs_url='https://docs.pretix.eu/en/latest/user/payments/fees.html'),
required=False
)),
('_invoice_text',
I18nFormField(
label=_('Text on invoices'),
help_text=_('Will be printed just below the payment figures and above the closing text on invoices. '
'This will only be used if the invoice is generated before the order is paid. If the '
'invoice is generated later, it will show a text stating that it has already been paid.'),
required=False,
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}}
)),
])
def settings_content_render(self, request: HttpRequest) -> str:
@@ -220,7 +220,7 @@ class BasePaymentProvider:
page, this method is called. It may return HTML containing additional information
that is displayed below the form fields configured in ``settings_form_fields``.
"""
pass
return ""
def render_invoice_text(self, order: Order) -> str:
"""

View File

@@ -71,6 +71,7 @@ class RelativeDateWrapper:
else:
base_date = getattr(event, self.data.base_date_name) or event.date_from
oldoffset = base_date.utcoffset()
new_date = base_date.astimezone(tz) - datetime.timedelta(days=self.data.days_before)
if self.data.time:
new_date = new_date.replace(
@@ -78,6 +79,9 @@ class RelativeDateWrapper:
minute=self.data.time.minute,
second=self.data.time.second
)
new_date = new_date.astimezone(tz)
newoffset = new_date.utcoffset()
new_date += oldoffset - newoffset
return new_date
def to_string(self) -> str:
@@ -149,6 +153,11 @@ class RelativeDateTimeField(forms.MultiValueField):
('absolute', _('Fixed date:')),
('relative', _('Relative date:')),
]
if kwargs.get('limit_choices'):
limit = kwargs.pop('limit_choices')
choices = [(k, v) for k, v in BASE_CHOICES if k in limit]
else:
choices = BASE_CHOICES
if not kwargs.get('required', True):
status_choices.insert(0, ('unset', _('Not set')))
fields = (
@@ -163,7 +172,7 @@ class RelativeDateTimeField(forms.MultiValueField):
required=False
),
forms.ChoiceField(
choices=BASE_CHOICES,
choices=choices,
required=False
),
forms.TimeField(
@@ -171,7 +180,7 @@ class RelativeDateTimeField(forms.MultiValueField):
),
)
if 'widget' not in kwargs:
kwargs['widget'] = RelativeDateTimeWidget(status_choices=status_choices, base_choices=BASE_CHOICES)
kwargs['widget'] = RelativeDateTimeWidget(status_choices=status_choices, base_choices=choices)
kwargs.pop('max_length', 0)
kwargs.pop('empty_value', 0)
super().__init__(

View File

@@ -0,0 +1,19 @@
from datetime import timedelta
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.auth import StaffSession
from ..signals import periodic_task
@receiver(signal=periodic_task)
def close_inactive_staff_sessions(sender, **kwargs):
StaffSession.objects.annotate(last_used=Max('logs__datetime')).filter(
Q(last_used__lte=now() - timedelta(seconds=settings.PRETIX_SESSION_TIMEOUT_RELATIVE)) & Q(date_end__isnull=True)
).update(
date_end=now()
)

View File

@@ -295,7 +295,7 @@ class CartManager:
if i.get('voucher'):
try:
voucher = self.event.vouchers.get(code=i.get('voucher').strip())
voucher = self.event.vouchers.get(code__iexact=i.get('voucher').strip())
except Voucher.DoesNotExist:
raise CartError(error_messages['voucher_invalid'])
else:

View File

@@ -265,11 +265,20 @@ def build_preview_invoice_pdf(event):
invoice.save()
invoice.lines.all().delete()
InvoiceLine.objects.create(
invoice=invoice, description=_("Sample product A"),
gross_value=119, tax_value=19,
tax_rate=19
)
if event.tax_rules.exists():
for i, tr in enumerate(event.tax_rules.all()):
tax = tr.tax(Decimal('100.00'))
InvoiceLine.objects.create(
invoice=invoice, description=_("Sample product {}").format(i + 1),
gross_value=tax.gross, tax_value=tax.tax,
tax_rate=tax.rate
)
else:
InvoiceLine.objects.create(
invoice=invoice, description=_("Sample product A"),
gross_value=100, tax_value=0, tax_rate=0
)
return event.invoice_renderer.generate(invoice)

View File

@@ -504,7 +504,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
for fee in fees:
fee.order = order
fee._calculate_tax()
if not fee.tax_rule.pk:
if fee.tax_rule and not fee.tax_rule.pk:
fee.tax_rule = None # TODO: deprecate
fee.save()

View File

@@ -321,7 +321,7 @@ Your {event} team"""))
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
we did not yet receive a payment for your order for {event}.
Please keep in mind that if we only guarantee your order if we receive
Please keep in mind that we only guarantee your order if we receive
your payment before {expire_date}.
You can view the payment information and the status of your order at
@@ -486,7 +486,15 @@ Your {event} team"""))
'update_check_id': {
'default': None,
'type': str
}
},
'banner_message': {
'default': '',
'type': LazyI18nString
},
'banner_message_detail': {
'default': '',
'type': LazyI18nString
},
}
settings_hierarkey = Hierarkey(attribute_name='settings')

View File

@@ -144,6 +144,7 @@ class BaseQuestionsViewMixin:
class OrderQuestionsViewMixin(BaseQuestionsViewMixin):
invoice_form_class = BaseInvoiceAddressForm
only_user_visible = True
@cached_property
def _positions_for_questions(self):
@@ -151,6 +152,9 @@ class OrderQuestionsViewMixin(BaseQuestionsViewMixin):
@cached_property
def positions(self):
qqs = Question.objects.all()
if self.only_user_visible:
qqs = qqs.filter(ask_during_checkin=False)
return list(self.order.positions.select_related(
'item', 'variation'
).prefetch_related(
@@ -158,7 +162,7 @@ class OrderQuestionsViewMixin(BaseQuestionsViewMixin):
QuestionAnswer.objects.prefetch_related('options'),
to_attr='answerlist'),
Prefetch('item__questions',
Question.objects.filter(ask_during_checkin=False).prefetch_related(
qqs.prefetch_related(
Prefetch('options', QuestionOption.objects.prefetch_related(Prefetch(
# This prefetch statement is utter bullshit, but it actually prevents Django from doing
# a lot of queries since ModelChoiceIterator stops trying to be clever once we have

View File

@@ -3,11 +3,15 @@ from importlib import import_module
from django.conf import settings
from django.core.urlresolvers import Resolver404, get_script_prefix, resolve
from django.db.models import Q
from django.utils.translation import get_language
from pretix.base.models.auth import StaffSession
from pretix.base.settings import GlobalSettingsObject
from ..helpers.i18n import get_javascript_format, get_moment_locale
from ..helpers.i18n import (
get_javascript_format, get_javascript_output_format, get_moment_locale,
)
from .signals import html_head, nav_event, nav_global, nav_topbar
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
@@ -80,6 +84,7 @@ def contextprocessor(request):
ctx['js_datetime_format'] = get_javascript_format('DATETIME_INPUT_FORMATS')
ctx['js_date_format'] = get_javascript_format('DATE_INPUT_FORMATS')
ctx['js_long_date_format'] = get_javascript_output_format('DATE_FORMAT')
ctx['js_time_format'] = get_javascript_format('TIME_INPUT_FORMATS')
ctx['js_locale'] = get_moment_locale()
ctx['select2locale'] = get_language()[:2]
@@ -91,11 +96,21 @@ def contextprocessor(request):
ctx['warning_update_available'] = False
ctx['warning_update_check_active'] = False
if request.user.is_superuser:
gs = GlobalSettingsObject()
gs = GlobalSettingsObject()
ctx['global_settings'] = gs.settings
if request.user.is_staff:
if gs.settings.update_check_result_warning:
ctx['warning_update_available'] = True
if not gs.settings.update_check_ack and 'runserver' not in sys.argv:
ctx['warning_update_check_active'] = True
if request.user.is_authenticated:
ctx['staff_session'] = request.user.has_active_staff_session(request.session.session_key)
ctx['staff_need_to_explain'] = (
StaffSession.objects.filter(user=request.user, date_end__isnull=False).filter(
Q(comment__isnull=True) | Q(comment="")
)
if request.user.is_staff and settings.PRETIX_ADMIN_AUDIT_COMMENTS else StaffSession.objects.none()
)
return ctx

View File

@@ -1,6 +1,7 @@
import os
from django import forms
from django.conf import settings
from django.utils.html import conditional_escape
from django.utils.translation import ugettext_lazy as _
@@ -102,3 +103,68 @@ class SlugWidget(forms.TextInput):
ctx = super().get_context(name, value, attrs)
ctx['pre'] = self.prefix
return ctx
class MultipleLanguagesWidget(forms.CheckboxSelectMultiple):
option_template_name = 'pretixcontrol/multi_languages_widget.html'
def sort(self):
self.choices = sorted(self.choices, key=lambda l: (
(
0 if l[0] in settings.LANGUAGES_OFFICIAL
else (
1 if l[0] not in settings.LANGUAGES_INCUBATING
else 2
)
), str(l[1])
))
def options(self, name, value, attrs=None):
self.sort()
return super().options(name, value, attrs)
def optgroups(self, name, value, attrs=None):
self.sort()
return super().optgroups(name, value, attrs)
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
opt = super().create_option(name, value, label, selected, index, subindex, attrs)
opt['official'] = value in settings.LANGUAGES_OFFICIAL
opt['incubating'] = value in settings.LANGUAGES_INCUBATING
return opt
class SingleLanguageWidget(forms.Select):
def modify(self):
if hasattr(self, '_modified'):
return self.choices
self.choices = sorted(self.choices, key=lambda l: (
(
0 if l[0] in settings.LANGUAGES_OFFICIAL
else (
1 if l[0] not in settings.LANGUAGES_INCUBATING
else 2
)
), str(l[1])
))
new_choices = []
for k, v in self.choices:
new_choices.append((
k,
v if k in settings.LANGUAGES_OFFICIAL
else (
'{} (inofficial translation)'.format(v) if k not in settings.LANGUAGES_INCUBATING
else '{} (translation in progress)'.format(v)
)
))
self._modified = True
self.choices = new_choices
def options(self, name, value, attrs=None):
self.modify()
return super().options(name, value, attrs)
def optgroups(self, name, value, attrs=None):
self.modify()
return super().optgroups(name, value, attrs)

View File

@@ -9,7 +9,9 @@ from django.utils.timezone import get_current_timezone_name
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_countries import Countries
from django_countries.fields import LazyTypedChoiceField
from i18nfield.forms import I18nFormField, I18nTextarea
from i18nfield.forms import (
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextarea, I18nTextInput,
)
from pytz import common_timezones, timezone
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
@@ -17,9 +19,11 @@ 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.control.forms import (
ExtFileField, SlugWidget, SplitDateTimePickerWidget,
ExtFileField, MultipleLanguagesWidget, SingleLanguageWidget, SlugWidget,
SplitDateTimePickerWidget,
)
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.plugins.banktransfer.payment import BankTransfer
from pretix.presale.style import get_fonts
@@ -27,7 +31,7 @@ class EventWizardFoundationForm(forms.Form):
locales = forms.MultipleChoiceField(
choices=settings.LANGUAGES,
label=_("Use languages"),
widget=forms.CheckboxSelectMultiple,
widget=MultipleLanguagesWidget,
help_text=_('Choose all languages that your event should be available in.')
)
has_subevents = forms.BooleanField(
@@ -144,7 +148,7 @@ class EventWizardBasicsForm(I18nModelForm):
def clean_slug(self):
slug = self.cleaned_data['slug']
if Event.objects.filter(slug=slug, organizer=self.organizer).exists():
if Event.objects.filter(slug__iexact=slug, organizer=self.organizer).exists():
raise forms.ValidationError(
self.error_messages['duplicate_slug'],
code='duplicate_slug'
@@ -279,11 +283,12 @@ class EventSettingsForm(SettingsForm):
)
locales = forms.MultipleChoiceField(
choices=settings.LANGUAGES,
widget=forms.CheckboxSelectMultiple,
widget=MultipleLanguagesWidget,
label=_("Available languages"),
)
locale = forms.ChoiceField(
choices=settings.LANGUAGES,
widget=SingleLanguageWidget,
label=_("Default language"),
)
show_quota_left = forms.BooleanField(
@@ -360,6 +365,8 @@ class EventSettingsForm(SettingsForm):
)
imprint_url = forms.URLField(
label=_("Imprint URL"),
help_text=_("This should point e.g. to a part of your website that has your contact details and legal "
"information."),
required=False,
)
confirm_text = I18nFormField(
@@ -373,7 +380,7 @@ class EventSettingsForm(SettingsForm):
contact_mail = forms.EmailField(
label=_("Contact address"),
required=False,
help_text=_("Public email address for contacting the organizer")
help_text=_("We'll show this publicly to allow attendees to contact you.")
)
cancel_allow_user = forms.BooleanField(
label=_("Allow users to cancel unpaid orders"),
@@ -409,7 +416,10 @@ class EventSettingsForm(SettingsForm):
class PaymentSettingsForm(SettingsForm):
payment_term_days = forms.IntegerField(
label=_('Payment term in days'),
help_text=_("The number of days after placing an order the user has to pay to preserve his reservation."),
help_text=_("The number of days after placing an order the user has to pay to preserve their reservation. If "
"you use slow payment methods like bank transfer, we recommend 14 days. If you only use real-time "
"payment methods, we recommend still setting two or three days to allow people to retry failed "
"payments."),
)
payment_term_last = RelativeDateField(
label=_('Last date of payments'),
@@ -637,6 +647,8 @@ class InvoiceSettingsForm(SettingsForm):
(r.identifier, r.verbose_name) for r in event.get_invoice_renderers().values()
]
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]
class MailSettingsForm(SettingsForm):
@@ -971,6 +983,11 @@ class WidgetCodeForm(forms.Form):
"bought via the widget, this voucher will be used. This can for example be used to provide "
"widgets that give discounts or unlock secret products.")
)
compatibility_mode = forms.BooleanField(
label=_("Compatibility mode"),
help_text=_("Our regular widget doesn't work in all website builders. If you run into trouble, try using "
"this compatibility mode.")
)
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event')
@@ -1032,3 +1049,118 @@ class EventDeleteForm(forms.Form):
code='slug_wrong',
)
return slug
class QuickSetupForm(I18nForm):
show_quota_left = forms.BooleanField(
label=_("Show number of tickets left"),
help_text=_("Publicly show how many tickets of a certain type are still available."),
required=False
)
waiting_list_enabled = forms.BooleanField(
label=_("Waiting list"),
help_text=_("Once a ticket is sold out, people can add themselves to a waiting list. As soon as a ticket "
"becomes available again, it will be reserved for the first person on the waiting list and this "
"person will receive an email notification with a voucher that can be used to buy a ticket."),
required=False
)
ticket_download = forms.BooleanField(
label=_("Ticket downloads"),
help_text=_("Your customers will be able to download their tickets in PDF format."),
required=False
)
attendee_names_required = forms.BooleanField(
label=_("Require all attendees to fill in their names"),
help_text=_("By default, we will ask for names but not require them. You can turn this off completely in the "
"settings."),
required=False
)
imprint_url = forms.URLField(
label=_("Imprint URL"),
help_text=_("This should point e.g. to a part of your website that has your contact details and legal "
"information."),
required=False,
)
contact_mail = forms.EmailField(
label=_("Contact address"),
required=False,
help_text=_("We'll show this publicly to allow attendees to contact you.")
)
total_quota = forms.IntegerField(
label=_("Total capacity"),
min_value=0,
widget=forms.NumberInput(
attrs={
'placeholder': ''
}
),
required=False
)
payment_stripe__enabled = forms.BooleanField(
label=_("Payment via Stripe"),
help_text=_("Stripe is an online payments processor supporting credit cards and lots of other payment options. "
"To accept payments via Stripe, you will need to set up an account with them, which takes less "
"than five minutes using their simple interface."),
required=False
)
payment_banktransfer__enabled = forms.BooleanField(
label=_("Payment by bank transfer"),
help_text=_("Your customers will be instructed to wire the money to your account. You can then import your "
"bank statements to process the payments within pretix, or mark them as paid manually."),
required=False
)
payment_banktransfer_bank_details = BankTransfer.form_field(required=False)
def __init__(self, *args, **kwargs):
self.obj = kwargs.pop('event', None)
self.locales = self.obj.settings.get('locales') if self.obj else kwargs.pop('locales', None)
kwargs['locales'] = self.locales
super().__init__(*args, **kwargs)
if not self.obj.settings.payment_stripe_connect_client_id:
del self.fields['payment_stripe__enabled']
self.fields['payment_banktransfer_bank_details'].required = False
class QuickSetupProductForm(I18nForm):
name = I18nFormField(
max_length=255,
label=_("Product name"),
widget=I18nTextInput
)
default_price = forms.DecimalField(
label=_("Price (optional)"),
max_digits=7, decimal_places=2, required=False,
localize=True,
widget=forms.TextInput(
attrs={
'placeholder': _('Free')
}
),
)
quota = forms.IntegerField(
label=_("Quantity available"),
min_value=0,
widget=forms.NumberInput(
attrs={
'placeholder': ''
}
),
initial=100,
required=False
)
class BaseQuickSetupProductFormSet(I18nFormSetMixin, forms.BaseFormSet):
def __init__(self, *args, **kwargs):
event = kwargs.pop('event', None)
if event:
kwargs['locales'] = event.settings.get('locales')
super().__init__(*args, **kwargs)
QuickSetupProductFormSet = formset_factory(
QuickSetupProductForm,
formset=BaseQuickSetupProductFormSet,
can_order=False, can_delete=True, extra=0
)

View File

@@ -137,6 +137,7 @@ class OrderFilterForm(FilterForm):
| Q(invoice_address__name__icontains=u)
| Q(invoice_address__company__icontains=u)
| Q(pk__in=matching_invoices)
| Q(comment__icontains=u)
| Q(has_pos=True)
)
@@ -233,7 +234,7 @@ class OrderSearchFilterForm(OrderFilterForm):
def __init__(self, *args, **kwargs):
request = kwargs.pop('request')
super().__init__(*args, **kwargs)
if request.user.is_superuser:
if request.user.has_active_staff_session(request.session.session_key):
self.fields['organizer'].queryset = Organizer.objects.all()
else:
self.fields['organizer'].queryset = Organizer.objects.filter(
@@ -392,7 +393,7 @@ class EventFilterForm(FilterForm):
def __init__(self, *args, **kwargs):
request = kwargs.pop('request')
super().__init__(*args, **kwargs)
if request.user.is_superuser:
if request.user.has_active_staff_session(request.session.session_key):
self.fields['organizer'].queryset = Organizer.objects.all()
else:
self.fields['organizer'].queryset = Organizer.objects.filter(
@@ -582,9 +583,9 @@ class UserFilterForm(FilterForm):
qs = qs.filter(is_active=False)
if fdata.get('superuser') == 'yes':
qs = qs.filter(is_superuser=True)
qs = qs.filter(is_staff=True)
elif fdata.get('superuser') == 'no':
qs = qs.filter(is_superuser=False)
qs = qs.filter(is_staff=False)
if fdata.get('query'):
qs = qs.filter(

View File

@@ -2,7 +2,7 @@ from collections import OrderedDict
from django import forms
from django.utils.translation import ugettext_lazy as _
from i18nfield.forms import I18nFormField, I18nTextInput
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
from pretix.base.forms import SettingsForm
from pretix.base.settings import GlobalSettingsObject
@@ -26,7 +26,17 @@ class GlobalSettingsForm(SettingsForm):
required=False,
label=_("Additional footer link"),
help_text=_("Will be included as the link in the additional footer text.")
))
)),
('banner_message', I18nFormField(
widget=I18nTextarea,
required=False,
label=_("Global message banner"),
)),
('banner_message_detail', I18nFormField(
widget=I18nTextarea,
required=False,
label=_("Global message banner detail text"),
)),
])
responses = register_global_settings.send(self)
for r, response in sorted(responses, key=lambda r: str(r[0])):
@@ -34,6 +44,9 @@ class GlobalSettingsForm(SettingsForm):
# We need to be this explicit, since OrderedDict.update does not retain ordering
self.fields[key] = value
self.fields['banner_message'].widget.attrs['rows'] = '2'
self.fields['banner_message_detail'].widget.attrs['rows'] = '3'
class UpdateSettingsForm(SettingsForm):
update_check_perform = forms.BooleanField(

View File

@@ -41,6 +41,7 @@ class QuestionForm(I18nModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['items'].queryset = self.instance.event.items.all()
self.fields['identifier'].required = False
class Meta:
model = Question
@@ -51,6 +52,7 @@ class QuestionForm(I18nModelForm):
'type',
'required',
'ask_during_checkin',
'identifier',
'items'
]
widgets = {
@@ -221,6 +223,7 @@ class ItemCreateForm(I18nModelForm):
self.instance.min_per_order = self.cleaned_data['copy_from'].min_per_order
self.instance.max_per_order = self.cleaned_data['copy_from'].max_per_order
self.instance.checkin_attention = self.cleaned_data['copy_from'].checkin_attention
self.instance.free_price = self.cleaned_data['copy_from'].free_price
self.instance.position = (self.event.items.aggregate(p=Max('position'))['p'] or 0) + 1
instance = super().save(*args, **kwargs)

View File

@@ -2,6 +2,7 @@ from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
@@ -11,6 +12,7 @@ from pretix.base.models import (
)
from pretix.base.models.event import SubEvent
from pretix.base.services.pricing import get_price
from pretix.control.forms.widgets import Select2
from pretix.helpers.money import change_decimal_field
@@ -164,6 +166,18 @@ class OrderPositionAddForm(forms.Form):
if order.event.has_subevents:
self.fields['subevent'].queryset = order.event.subevents.all()
self.fields['subevent'].widget = Select2(
attrs={
'data-model-select2': 'event',
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
'event': order.event.slug,
'organizer': order.event.organizer.slug,
}),
'data-placeholder': pgettext_lazy('subevent', 'Date')
}
)
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
self.fields['subevent'].required = True
else:
del self.fields['subevent']
change_decimal_field(self.fields['price'], order.event.currency)

View File

@@ -7,7 +7,7 @@ from i18nfield.forms import I18nFormField, I18nTextarea
from pretix.base.forms import I18nModelForm, SettingsForm
from pretix.base.models import Organizer, Team
from pretix.control.forms import ExtFileField
from pretix.control.forms import ExtFileField, MultipleLanguagesWidget
from pretix.multidomain.models import KnownDomain
from pretix.presale.style import get_fonts
@@ -23,7 +23,7 @@ class OrganizerForm(I18nModelForm):
def clean_slug(self):
slug = self.cleaned_data['slug']
if Organizer.objects.filter(slug=slug).exists():
if Organizer.objects.filter(slug__iexact=slug).exists():
raise forms.ValidationError(
self.error_messages['duplicate_slug'],
code='duplicate_slug',
@@ -158,7 +158,7 @@ class OrganizerDisplaySettingsForm(SettingsForm):
locales = forms.MultipleChoiceField(
choices=settings.LANGUAGES,
label=_("Use languages"),
widget=forms.CheckboxSelectMultiple,
widget=MultipleLanguagesWidget,
help_text=_('Choose all languages that your organizer homepage should be available in.')
)
primary_font = forms.ChoiceField(

View File

@@ -1,10 +1,16 @@
from datetime import timedelta
from django import forms
from django.forms import formset_factory
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from i18nfield.forms import I18nInlineFormSet
from pretix.base.forms import I18nModelForm
from pretix.base.models.event import SubEvent, SubEventMetaValue
from pretix.base.models.items import SubEventItem
from pretix.base.reldate import RelativeDateTimeField
from pretix.base.templatetags.money import money_filter
from pretix.control.forms import SplitDateTimePickerWidget
from pretix.helpers.money import change_decimal_field
@@ -46,6 +52,44 @@ class SubEventForm(I18nModelForm):
}
class SubEventBulkForm(SubEventForm):
time_from = forms.TimeField(
label=_('Event start time'),
widget=forms.TimeInput(attrs={'class': 'timepickerfield'})
)
time_to = forms.TimeField(
label=_('Event end time'),
widget=forms.TimeInput(attrs={'class': 'timepickerfield'}),
required=False
)
time_admission = forms.TimeField(
label=_('Admission time'),
widget=forms.TimeInput(attrs={'class': 'timepickerfield'}),
required=False
)
rel_presale_start = RelativeDateTimeField(
label=_('Start of presale'),
help_text=_('Optional. No products will be sold before this date.'),
required=False,
limit_choices=('date_from', 'date_to'),
)
rel_presale_end = RelativeDateTimeField(
label=_('End of presale'),
help_text=_('Optional. No products will be sold after this date. If you do not set this value, the presale '
'will end after the end date of your event.'),
required=False,
limit_choices=('date_from', 'date_to'),
)
def __init__(self, *args, **kwargs):
self.event = kwargs['event']
super().__init__(*args, **kwargs)
self.fields['location'].widget.attrs['rows'] = '3'
del self.fields['date_from']
del self.fields['date_to']
del self.fields['date_admission']
class SubEventItemOrVariationFormMixin:
def __init__(self, *args, **kwargs):
self.item = kwargs.pop('item')
@@ -97,6 +141,7 @@ class QuotaFormSet(I18nInlineFormSet):
kwargs['locales'] = self.locales
kwargs['event'] = self.event
kwargs['items'] = self.items
kwargs['items'] = self.items
return super()._construct_form(i, **kwargs)
@property
@@ -155,3 +200,175 @@ class CheckinListFormSet(I18nInlineFormSet):
)
self.add_fields(form, None)
return form
class RRuleForm(forms.Form):
# TODO: calendar.setfirstweekday
exclude = forms.BooleanField(
label=_('Exclude these dates instead of adding them.'),
required=False
)
freq = forms.ChoiceField(
choices=[
('yearly', _('year(s)')),
('monthly', _('month(s)')),
('weekly', _('week(s)')),
('daily', _('day(s)')),
]
)
interval = forms.IntegerField(
label=_('Interval'),
initial=1
)
dtstart = forms.DateField(
label=_('Start date'),
widget=forms.DateInput(
attrs={
'class': 'datepickerfield',
'required': 'required'
}
),
initial=lambda: now().date()
)
end = forms.ChoiceField(
choices=[
('count', ''),
('until', ''),
],
initial='count',
widget=forms.RadioSelect
)
count = forms.IntegerField(
label=_('Number of repititions'),
initial=10
)
until = forms.DateField(
widget=forms.DateInput(
attrs={
'class': 'datepickerfield',
'required': 'required'
}
),
label=_('Last date'),
required=True,
initial=lambda: now() + timedelta(days=365)
)
yearly_bysetpos = forms.ChoiceField(
choices=[
('1', pgettext_lazy('rrule', 'first')),
('2', pgettext_lazy('rrule', 'second')),
('3', pgettext_lazy('rrule', 'third')),
('-1', pgettext_lazy('rrule', 'last')),
],
required=False
)
yearly_same = forms.ChoiceField(
choices=[
('on', ''),
('off', ''),
],
initial='on',
widget=forms.RadioSelect
)
yearly_byweekday = forms.ChoiceField(
choices=[
('MO', _('Monday')),
('TU', _('Tuesday')),
('WE', _('Wednesday')),
('TH', _('Thursday')),
('FR', _('Friday')),
('SA', _('Saturday')),
('SU', _('Sunday')),
('MO,TU,WE,TH,FR,SA,SU', _('Day')),
('MO,TU,WE,TH,FR', _('Weekday')),
('SA,SU', _('Weekend day')),
],
required=False
)
yearly_bymonth = forms.ChoiceField(
choices=[
('1', _('January')),
('2', _('February')),
('3', _('March')),
('4', _('April')),
('5', _('May')),
('6', _('June')),
('7', _('July')),
('8', _('August')),
('9', _('September')),
('10', _('October')),
('11', _('November')),
('12', _('December')),
],
required=False
)
monthly_same = forms.ChoiceField(
choices=[
('on', ''),
('off', ''),
],
initial='on',
widget=forms.RadioSelect
)
monthly_bysetpos = forms.ChoiceField(
choices=[
('1', pgettext_lazy('rrule', 'first')),
('2', pgettext_lazy('rrule', 'second')),
('3', pgettext_lazy('rrule', 'third')),
('-1', pgettext_lazy('rrule', 'last')),
],
required=False
)
monthly_byweekday = forms.ChoiceField(
choices=[
('MO', _('Monday')),
('TU', _('Tuesday')),
('WE', _('Wednesday')),
('TH', _('Thursday')),
('FR', _('Friday')),
('SA', _('Saturday')),
('SU', _('Sunday')),
('MO,TU,WE,TH,FR,SA,SU', _('Day')),
('MO,TU,WE,TH,FR', _('Weekday')),
('SA,SU', _('Weekend day')),
],
required=False
)
weekly_byweekday = forms.MultipleChoiceField(
choices=[
('MO', _('Monday')),
('TU', _('Tuesday')),
('WE', _('Wednesday')),
('TH', _('Thursday')),
('FR', _('Friday')),
('SA', _('Saturday')),
('SU', _('Sunday')),
],
required=False,
widget=forms.CheckboxSelectMultiple
)
def parse_weekdays(self, value):
m = {
'MO': 0,
'TU': 1,
'WE': 2,
'TH': 3,
'FR': 4,
'SA': 5,
'SU': 6
}
if ',' in value:
return [m.get(a) for a in value.split(',')]
else:
return m.get(value)
RRuleFormSet = formset_factory(
RRuleForm,
can_order=False, can_delete=True, extra=1
)

View File

@@ -8,6 +8,13 @@ from django.utils.translation import ugettext_lazy as _
from pytz import common_timezones
from pretix.base.models import User
from pretix.base.models.auth import StaffSession
class StaffSessionForm(forms.ModelForm):
class Meta:
model = StaffSession
fields = ['comment']
class UserEditForm(forms.ModelForm):
@@ -41,7 +48,7 @@ class UserEditForm(forms.ModelForm):
'email',
'require_2fa',
'is_active',
'is_superuser'
'is_staff'
]
def __init__(self, *args, **kwargs):

View File

@@ -1,17 +1,25 @@
import copy
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db.models.functions import Lower
from django.urls import reverse
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from pretix.base.forms import I18nModelForm
from pretix.base.models import Item, ItemVariation, Quota, Voucher
from pretix.base.models import Item, Voucher
from pretix.control.forms import SplitDateTimePickerWidget
from pretix.control.forms.widgets import Select2, Select2ItemVarQuota
from pretix.control.signals import voucher_form_validation
class FakeChoiceField(forms.ChoiceField):
def valid_value(self, value):
return True
class VoucherForm(I18nModelForm):
itemvar = forms.ChoiceField(
itemvar = FakeChoiceField(
label=_("Product"),
help_text=_(
"This product is added to the user's cart if the voucher is redeemed."
@@ -53,47 +61,64 @@ class VoucherForm(I18nModelForm):
if instance.event.has_subevents:
self.fields['subevent'].queryset = instance.event.subevents.all()
self.fields['subevent'].widget = Select2(
attrs={
'data-model-select2': 'event',
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
'event': instance.event.slug,
'organizer': instance.event.organizer.slug,
}),
'data-placeholder': pgettext_lazy('subevent', 'Date')
}
)
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
self.fields['subevent'].required = False
elif 'subevent':
del self.fields['subevent']
choices = []
for i in self.instance.event.items.prefetch_related('variations').all():
variations = list(i.variations.all())
if variations:
choices.append((str(i.pk), _('{product} Any variation').format(product=i.name)))
for v in variations:
choices.append(('%d-%d' % (i.pk, v.pk), '%s %s' % (i.name, v.value)))
else:
choices.append((str(i.pk), i.name))
for q in self.instance.event.quotas.all():
choices.append(('q-%d' % q.pk, _('Any product in quota "{quota}"').format(quota=q)))
self.fields['itemvar'].choices = choices
self.fields['itemvar'].widget = Select2ItemVarQuota(
attrs={
'data-model-select2': 'generic',
'data-select2-url': reverse('control:event.vouchers.itemselect2', kwargs={
'event': instance.event.slug,
'organizer': instance.event.organizer.slug,
}),
'data-placeholder': ''
}
)
self.fields['itemvar'].widget.choices = self.fields['itemvar'].choices
self.fields['itemvar'].required = True
def clean(self):
data = super().clean()
if not self._errors:
itemid = quotaid = None
iv = self.data.get('itemvar', '')
if iv.startswith('q-'):
quotaid = iv[2:]
elif '-' in iv:
itemid, varid = iv.split('-')
else:
itemid, varid = iv, None
if itemid:
self.instance.item = Item.objects.get(pk=itemid, event=self.instance.event)
if varid:
self.instance.variation = ItemVariation.objects.get(pk=varid, item=self.instance.item)
try:
itemid = quotaid = None
iv = self.data.get('itemvar', '')
if iv.startswith('q-'):
quotaid = iv[2:]
elif '-' in iv:
itemid, varid = iv.split('-')
else:
self.instance.variation = None
self.instance.quota = None
itemid, varid = iv, None
else:
self.instance.quota = Quota.objects.get(pk=quotaid, event=self.instance.event)
self.instance.item = None
self.instance.variation = None
if itemid:
self.instance.item = self.instance.event.items.get(pk=itemid)
if varid:
self.instance.variation = self.instance.item.variations.get(pk=varid)
else:
self.instance.variation = None
self.instance.quota = None
else:
self.instance.quota = self.instance.event.quotas.get(pk=quotaid)
self.instance.item = None
self.instance.variation = None
except ObjectDoesNotExist:
raise ValidationError(_("Invalid product selected."))
if 'codes' in data:
data['codes'] = [a.strip() for a in data.get('codes', '').strip().split("\n") if a]
@@ -164,7 +189,10 @@ class VoucherBulkForm(VoucherForm):
def clean(self):
data = super().clean()
if Voucher.objects.filter(code__in=data['codes'], event=self.instance.event).exists():
vouchers = self.instance.event.vouchers.annotate(
code_lower=Lower('code')
).filter(code_lower__in=[c.lower() for c in data['codes']])
if vouchers.exists():
raise ValidationError(_('A voucher with one of these codes already exists.'))
return data

View File

@@ -36,3 +36,23 @@ class Select2(Select2Mixin, forms.Select):
class Select2Multiple(Select2Mixin, forms.SelectMultiple):
pass
class Select2ItemVarQuotaMixin(Select2Mixin):
def options(self, name, value, attrs=None):
if value and value[0]:
yield self.create_option(
None,
value[0],
value[0],
True,
0,
subindex=None,
attrs=attrs
)
return
class Select2ItemVarQuota(Select2ItemVarQuotaMixin, forms.Select):
pass

View File

@@ -4,12 +4,14 @@ from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME, logout
from django.core.urlresolvers import get_script_prefix, resolve, reverse
from django.http import Http404
from django.shortcuts import redirect, resolve_url
from django.shortcuts import get_object_or_404, redirect, resolve_url
from django.utils.deprecation import MiddlewareMixin
from django.utils.encoding import force_str
from django.utils.translation import ugettext as _
from hijack.templatetags.hijack_tags import is_hijacked
from pretix.base.models import Event, Organizer
from pretix.base.models.auth import SuperuserPermissionSet, User
from pretix.helpers.security import (
SessionInvalid, SessionReauthRequired, assert_session_valid,
)
@@ -81,16 +83,52 @@ class PermissionMiddleware(MiddlewareMixin):
slug=url.kwargs['event'],
organizer__slug=url.kwargs['organizer'],
).select_related('organizer').first()
if not request.event or not request.user.has_event_permission(request.event.organizer, request.event):
if not request.event or not request.user.has_event_permission(request.event.organizer, request.event,
request=request):
raise Http404(_("The selected event was not found or you "
"have no permission to administrate it."))
request.organizer = request.event.organizer
request.eventpermset = request.user.get_event_permission_set(request.organizer, request.event)
if request.user.has_active_staff_session(request.session.session_key):
request.eventpermset = SuperuserPermissionSet()
else:
request.eventpermset = request.user.get_event_permission_set(request.organizer, request.event)
elif 'organizer' in url.kwargs:
request.organizer = Organizer.objects.filter(
slug=url.kwargs['organizer'],
).first()
if not request.organizer or not request.user.has_organizer_permission(request.organizer):
if not request.organizer or not request.user.has_organizer_permission(request.organizer, request=request):
raise Http404(_("The selected organizer was not found or you "
"have no permission to administrate it."))
request.orgapermset = request.user.get_organizer_permission_set(request.organizer)
if request.user.has_active_staff_session(request.session.session_key):
request.orgapermset = SuperuserPermissionSet()
else:
request.orgapermset = request.user.get_organizer_permission_set(request.organizer)
class AuditLogMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.path.startswith(get_script_prefix() + 'control') and request.user.is_authenticated:
if is_hijacked(request):
hijack_history = request.session.get('hijack_history', False)
hijacker = get_object_or_404(User, pk=hijack_history[0])
ss = hijacker.get_active_staff_session(request.session.get('hijacker_session'))
if ss:
ss.logs.create(
url=request.path,
method=request.method,
impersonating=request.user
)
else:
ss = request.user.get_active_staff_session(request.session.session_key)
if ss:
ss.logs.create(
url=request.path,
method=request.method
)
response = self.get_response(request)
return response

View File

@@ -1,4 +1,7 @@
from django.core.exceptions import PermissionDenied
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.http import urlquote
from django.utils.translation import ugettext as _
@@ -18,8 +21,7 @@ def event_permission_required(permission):
raise PermissionDenied()
allowed = (
request.user.is_superuser
or request.user.has_event_permission(request.organizer, request.event, permission)
request.user.has_event_permission(request.organizer, request.event, permission, request=request)
)
if allowed:
return function(request, *args, **kw)
@@ -57,10 +59,7 @@ def organizer_permission_required(permission):
# just a double check, should not ever happen
raise PermissionDenied()
allowed = (
request.user.is_superuser
or request.user.has_organizer_permission(request.organizer, permission)
)
allowed = request.user.has_organizer_permission(request.organizer, permission, request=request)
if allowed:
return function(request, *args, **kw)
@@ -85,14 +84,33 @@ class OrganizerPermissionRequiredMixin:
def administrator_permission_required():
"""
This view decorator rejects all requests with a 403 response which are not from
users with the is_superuser flag.
users with a current staff member session.
"""
def decorator(function):
def wrapper(request, *args, **kw):
if not request.user.is_authenticated: # NOQA
# just a double check, should not ever happen
raise PermissionDenied()
if not request.user.is_superuser:
if not request.user.has_active_staff_session(request.session.session_key):
if request.user.is_staff:
return redirect(reverse('control:user.sudo') + '?next=' + urlquote(request.path))
raise PermissionDenied(_('You do not have permission to view this content.'))
return function(request, *args, **kw)
return wrapper
return decorator
def staff_member_required():
"""
This view decorator rejects all requests with a 403 response which are not staff
members (but do not need to have an active session).
"""
def decorator(function):
def wrapper(request, *args, **kw):
if not request.user.is_authenticated: # NOQA
# just a double check, should not ever happen
raise PermissionDenied()
if not request.user.is_staff:
raise PermissionDenied(_('You do not have permission to view this content.'))
return function(request, *args, **kw)
return wrapper
@@ -108,3 +126,14 @@ class AdministratorPermissionRequiredMixin:
def as_view(cls, **initkwargs):
view = super(AdministratorPermissionRequiredMixin, cls).as_view(**initkwargs)
return administrator_permission_required()(view)
class StaffMemberRequiredMixin:
"""
This mixin is equivalent to the staff_memer_required view decorator but
is in a form suitable for class-based views.
"""
@classmethod
def as_view(cls, **initkwargs):
view = super(StaffMemberRequiredMixin, cls).as_view(**initkwargs)
return staff_member_required()(view)

View File

@@ -30,6 +30,7 @@
<script type="text/javascript" src="{% static "charts/raphael-min.js" %}"></script>
<script type="text/javascript" src="{% static "charts/morris.js" %}"></script>
<script type="text/javascript" src="{% static "clipboard/clipboard.js" %}"></script>
<script type="text/javascript" src="{% static "rrule/rrule.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/jquery.qrcode.min.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/clipboard.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/menu.js" %}"></script>
@@ -40,6 +41,7 @@
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/question.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/mail.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/typeahead.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/quicksetup.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/asynctask.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/asyncdownload.js" %}"></script>
<script type="text/javascript" src="{% static "colorpicker/bootstrap-colorpicker.js" %}"></script>
@@ -51,7 +53,7 @@
<link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}">
{% block custom_header %}{% endblock %}
</head>
<body data-datetimeformat="{{ js_datetime_format }}" data-timeformat="{{ js_time_format }}" data-dateformat="{{ js_date_format }}" data-datetimelocale="{{ js_locale }}" data-payment-weekdays-disabled="{{ js_payment_weekdays_disabled }}" data-select2-locale="{{ select2locale }}">
<body data-datetimeformat="{{ js_datetime_format }}" data-timeformat="{{ js_time_format }}" data-dateformat="{{ js_date_format }}" data-datetimelocale="{{ js_locale }}" data-payment-weekdays-disabled="{{ js_payment_weekdays_disabled }}" data-select2-locale="{{ select2locale }}" data-longdateformat="{{ js_long_date_format }}">
<div id="wrapper">
<nav class="navbar navbar-inverse navbar-static-top" role="navigation">
<div class="navbar-header">
@@ -150,6 +152,22 @@
</li>
{% endfor %}
{% if request.user.is_staff and not staff_session %}
<li>
<form action="{% url 'control:user.sudo' %}?next={{ request.path|urlencode }}" method="post">
{% csrf_token %}
<button type="submit" class="btn btn-link" id="button-sudo">
<i class="fa fa-id-card"></i> {% trans "Admin mode" %}
</button>
</form>
</li>
{% elif request.user.is_staff and staff_session %}
<li>
<a href="{% url 'control:user.sudo.stop' %}" class="danger">
<i class="fa fa-id-card"></i> {% trans "End admin session" %}
</a>
</li>
{% endif %}
{% if warning_update_available %}
<li>
<a href="{% url 'control:global.update' %}" class="danger">
@@ -190,7 +208,7 @@
{% trans "Dashboard" %}
</a>
</li>
{% if request.user.is_superuser %}
{% if staff_session %}
<li>
<a href="{% url 'control:global.settings' %}"
{% if "global.settings" in url_name %}class="active"{% endif %}>
@@ -218,14 +236,21 @@
{% trans "Order search" %}
</a>
</li>
{% if request.user.is_superuser %}
{% if staff_session %}
<li>
<a href="{% url 'control:users' %}"
{% if "users" in url_name %}class="active"{% endif %}>
{% if "users" in url_name %}class="active"{% endif %}>
<i class="fa fa-user fa-fw"></i>
{% trans "Users" %}
</a>
</li>
<li>
<a href="{% url 'control:user.sudo.list' %}"
{% if "sudo" in url_name %}class="active"{% endif %}>
<i class="fa fa-id-card fa-fw"></i>
{% trans "Admin sessions" %}
</a>
</li>
{% endif %}
{% for nav in nav_global %}
<li>
@@ -259,6 +284,21 @@
</div>
</div>
</nav>
{% if staff_need_to_explain %}
<div class="impersonate-warning">
<span class="fa fa-id-card"></span>
{% blocktrans trimmed %}
Please leave a short comment on what you did in the following admin sessions:
{% endblocktrans %}
<ul>
{% for s in staff_need_to_explain %}
<li>
<a href="{% url "control:user.sudo.edit" id=s.pk %}">#{{ s.pk }}</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if request|is_hijacked %}
<div class="impersonate-warning">
<span class="fa fa-user-secret"></span>
@@ -272,6 +312,17 @@
</form>
</div>
{% endif %}
{% if global_settings.banner_message %}
<div class="impersonate-warning">
<span class="fa fa-bell"></span>
{{ global_settings.banner_message }}
{% if global_settings.banner_message %}
<a href="{% url 'control:global.message' %}">
{% trans "Read more" %}
</a>
{% endif %}
</div>
{% endif %}
<div id="page-wrapper">
<div class="container-fluid">
{% if messages %}

View File

@@ -109,7 +109,7 @@
</div>
<div class="col-lg-2 col-sm-6 col-xs-12">
{% if log.user %}
{% if log.user.is_superuser %}
{% if log.user.is_staff %}
<span class="fa fa-id-card fa-danger fa-fw"
data-toggle="tooltip"
title="{% trans "This change was performed by a pretix administrator." %}">

View File

@@ -34,7 +34,7 @@
</div>
<div class="col-lg-2 col-sm-6 col-xs-12">
{% if log.user %}
{% if log.user.is_superuser %}
{% if log.user.is_staff %}
<span class="fa fa-id-card fa-danger fa-fw"
data-toggle="tooltip"
title="{% trans "This change was performed by a pretix administrator." %}">

View File

@@ -5,49 +5,53 @@
<form action="" method="post" class="form-horizontal form-plugins">
{% csrf_token %}
<fieldset>
<legend>{% trans "Payment settings" %}</legend>
{% bootstrap_field sform.payment_term_days layout="control" %}
{% bootstrap_field sform.payment_term_last layout="control" %}
{% bootstrap_field sform.payment_term_weekdays layout="control" %}
{% bootstrap_field sform.payment_term_expire_automatically layout="control" %}
{% bootstrap_field sform.payment_term_accept_late layout="control" %}
{% bootstrap_field sform.tax_rate_default layout="control" %}
<legend>{% trans "Payment providers" %}</legend>
<table class="table table-payment-providers">
<tbody>
{% for provider in providers %}
<tr>
<td>
<strong>{{ provider.verbose_name }}</strong>
</td>
<td>
{% if provider.is_enabled %}
<span class="text-success">
<span class="fa fa-check"></span>
{% trans "Enabled" %}
</span>
{% else %}
<span class="text-danger">
<span class="fa fa-times"></span>
{% trans "Disabled" %}
</span>
{% endif %}
</td>
<td class="text-right">
<a href="{% url 'control:event.settings.payment.provider' event=request.event.slug organizer=request.organizer.slug provider=provider.identifier %}"
class="btn btn-default">
<span class="fa fa-cog"></span>
{% trans "Settings" %}
</a>
</td>
</tr>
{% 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." %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</fieldset>
<fieldset>
<legend>{% trans "Payment providers" %}</legend>
<div class="alert alert-warning">
<span class="fa fa-w fa-legal fa-4x pull-left"></span>
<strong>{% trans "Warning:" %}</strong>
{% blocktrans trimmed %}
Please note that EU Directive 2015/2366 bans surcharging payment fees for most common payment
methods within the European Union. Depending on the payment method, this might affect
selling to consumers only or to business customers as well. Depending on your country, this
legislation might already be in effect or become relevant from January 2018 at the latest. This
is not legal advice. If in doubt, consult a lawyer or refrain from charging payment fees.
{% endblocktrans %}
</div>
{% for provider in providers %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
<a class="collapsed" data-toggle="collapse" href="#{{ provider.identifier }}">
{{ provider.verbose_name }}
<i class="fa fa-angle-down collapse-indicator"></i>
</a>
</h3>
</div>
<div id="{{ provider.identifier }}" class="panel-collapse collapse">
<div class="panel-body">
{% bootstrap_form provider.form layout='control' %}
{% with c=provider.settings_content %}
{% if c %}{{ c|safe }}{% endif %}
{% endwith %}
</div>
</div>
</div>
{% empty %}
<em>{% trans "There are no payment providers available. Please go to the plugin settings and activate one or more payment plugins." %}</em>
{% endfor %}
<legend>{% trans "General payment settings" %}</legend>
{% bootstrap_field form.payment_term_days layout="control" %}
{% bootstrap_field form.payment_term_last layout="control" %}
{% bootstrap_field form.payment_term_weekdays layout="control" %}
{% bootstrap_field form.payment_term_expire_automatically layout="control" %}
{% bootstrap_field form.payment_term_accept_late layout="control" %}
{% bootstrap_field form.tax_rate_default layout="control" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">

View File

@@ -0,0 +1,40 @@
{% extends "pretixcontrol/event/settings_base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inside %}
<form action="" method="post" class="form-horizontal form-plugins">
{% csrf_token %}
<fieldset>
<legend>
<a href="{% url 'control:event.settings.payment' event=request.event.slug organizer=request.organizer.slug %}"
class="btn btn-default btn-sm btn-link">
<span class="fa fa-caret-left"></span>
{% trans "Back" %}
</a>
{% trans "Payment provider:" %} {{ provider.verbose_name }}
</legend>
{% bootstrap_form form layout='control' %}
{% if settings_content %}{{ settings_content|safe }}{% endif %}
<p>&nbsp;</p>
<div class="alert alert-warning">
<span class="fa fa-w fa-legal fa-2x pull-left"></span>
<strong>{% trans "Warning:" %}</strong>
{% blocktrans trimmed %}
Please note that EU Directive 2015/2366 bans surcharging payment fees for most common payment
methods within the European Union. If in doubt, consult a lawyer or refrain from charging payment
fees.
{% endblocktrans %}
<br>
{% blocktrans trimmed %}
In simple terms, this means you need to pay any fees imposed by the payment providers and cannot
pass it on to your customers.
{% endblocktrans %}
</div>
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -23,7 +23,7 @@
<div class="col-sm-4">
{% if plugin.app.compatibility_errors %}
<button class="btn disabled btn-block btn-default" disabled="disabled">{% trans "Incompatible" %}</button>
{% elif plugin.restricted and not request.user.is_superuser %}
{% elif plugin.restricted and not request.user.is_staff %}
<button class="btn disabled btn-block btn-default" disabled="disabled">{% trans "Not available" %}</button>
{% elif plugin.module in plugins_active %}
<button class="btn btn-default btn-block" name="plugin:{{ plugin.module }}" value="disable">{% trans "Disable" %}</button>
@@ -44,7 +44,7 @@
{% endblocktrans %}</p>
{% endif %}
<p>{{ plugin.description }}</p>
{% if plugin.restricted and not request.user.is_superuser %}
{% if plugin.restricted and not request.user.is_staff %}
<div class="alert alert-warning">
{% trans "This plugin needs to be enabled by a system administrator for your event." %}
</div>

View File

@@ -0,0 +1,198 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load formset_tags %}
{% block title %}{{ request.event.name }}{% endblock %}
{% block content %}
<div class="quick-setup-step">
<div class="quick-icon">
<span class="fa fa-fw fa-check-circle text-success"></span>
</div>
<div class="quick-content">
<h2>{% trans "Congratulations!" %}</h2>
<p>
<strong>{% trans "You just created an event!" %}</strong>
</p>
<p>
{% blocktrans trimmed %}
You can scroll down and create your first ticket products quickly, or you can use the navigation
on the left to modify the settings of your event in much more detail.
{% endblocktrans %}
</p>
<div class="clearfix"></div>
</div>
</div>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<fieldset class="quick-setup-step">
<div class="quick-icon">
<span class="fa fa-fw fa-ticket text-muted"></span>
</div>
<div class="quick-content">
<legend>{% trans "Create ticket types" %}</legend>
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
{{ formset.management_form }}
{% bootstrap_formset_errors formset %}
<div class="row hidden-sm hidden-xs" data-formset-form>
<div class="col-md-6">
<strong>{% trans "Ticket name" %}</strong>
</div>
<div class="col-md-3">
<strong>{% trans "Price (optional)" %}</strong>
</div>
<div class="col-md-2">
<strong>{% trans "Capacity (optional)" %}</strong>
</div>
</div>
<div data-formset-body id="ticket-type-formset">
{% for iform in formset %}
<div class="row question-option-row" data-formset-form>
<div class="sr-only">
{{ iform.id }}
{% bootstrap_field iform.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-md-6">
{% bootstrap_form_errors iform %}
{% bootstrap_field iform.name layout='inline' form_group_class="" %}
</div>
<div class="col-md-3">
{% bootstrap_field iform.default_price addon_after=request.event.currency layout='inline' form_group_class="" %}
</div>
<div class="col-md-2">
{% bootstrap_field iform.quota layout='inline' form_group_class="" %}
</div>
<div class="col-md-1 text-right">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="row question-option-row" data-formset-form>
<div class="sr-only">
{{ formset.empty_form.id }}
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-md-6">
{% bootstrap_field formset.empty_form.name layout='inline' form_group_class="" %}
</div>
<div class="col-md-3">
{% bootstrap_field formset.empty_form.default_price addon_after=request.event.currency layout='inline' form_group_class="" %}
</div>
<div class="col-md-2">
{% bootstrap_field formset.empty_form.quota layout='inline' form_group_class="" %}
</div>
<div class="col-md-1 text-right">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endescapescript %}
</script>
<div class="row question-option-row helper-width-100">
<div class="col-md-6">
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a new ticket type" %}</button>
</div>
<div class="col-md-3">
</div>
<div class="col-md-3 form-inline form-quicksetup-total-capacity">
<strong>{% trans "Total capacity:" %}</strong>
<span id="total-capacity"></span>
{% bootstrap_field form.total_quota layout="inline" field_class="sr-only" %}
<a href="#" data-toggle="tooltip" title="{% trans 'You can set a limit on the total number of tickets sold for your event, regardless of the ticket type.' %}" id="total-capacity-edit">
<span class="fa fa-edit"></span>
</a>
</div>
</div>
</div>
<p>&nbsp;</p>
<p class="bigger">
{% blocktrans trimmed %}
If you want to use more advanced features like non-admission products, product variations, custom
quotas, add-on products or want to modify your ticket types in more detail, you can later do so
in the "Products" section in the navigation. Don't worry, you can change everything you input here.
{% endblocktrans %}
</p>
<p>&nbsp;</p>
</div>
</fieldset>
<fieldset class="quick-setup-step">
<div class="quick-icon">
<span class="fa fa-fw fa-wrench text-muted"></span>
</div>
<div class="quick-content">
<legend>{% trans "Features" %}</legend>
<p class="bigger">
{% blocktrans trimmed %}
We recommend that you take some time to go through the "Settings" part of your event, but if
you're in a hurry and want to get started quickly, here's a short version:
{% endblocktrans %}
</p>
{% bootstrap_field form.ticket_download layout="control" label_class="sr-only" field_class="col-md-12" %}
{% bootstrap_field form.waiting_list_enabled layout="control" label_class="sr-only" field_class="col-md-12" %}
{% bootstrap_field form.show_quota_left layout="control" label_class="sr-only" field_class="col-md-12" %}
{% bootstrap_field form.attendee_names_required layout="control" label_class="sr-only" field_class="col-md-12" %}
<p>&nbsp;</p>
</div>
</fieldset>
<fieldset class="quick-setup-step" id="quick-setup-step-payment">
<div class="quick-icon">
<span class="fa fa-fw fa-money text-muted"></span>
</div>
<div class="quick-content">
<legend>{% trans "Payment" %}</legend>
<p class="bigger">
{% blocktrans trimmed %}
pretix supports a
<a href="https://pretix.eu/about/en/features/payment" target="_blank">wide range of payment
providers</a> allowing you to choose the payment methods that fit your workflow best.
Here are just two of them as examples, you can add more in the "Settings" part of your event.
{% endblocktrans %}
</p>
{% bootstrap_field form.payment_banktransfer__enabled layout="control" label_class="sr-only" field_class="col-md-12" %}
<div data-display-dependency="#id_payment_banktransfer__enabled">
{% bootstrap_field form.payment_banktransfer_bank_details layout="control" %}
</div>
{% if form.payment_stripe__enabled %}
{% bootstrap_field form.payment_stripe__enabled layout="control" label_class="sr-only" field_class="col-md-12" %}
<div data-display-dependency="#id_payment_stripe__enabled">
<div class="alert alert-info">
{% blocktrans trimmed %}
After you saved this page, we will redirect you to Stripe to create or connect an account
there. Once you completed this, you will be taken back to pretix.
{% endblocktrans %}
</div>
</div>
{% endif %}
<p>&nbsp;</p>
</div>
</fieldset>
<fieldset class="quick-setup-step">
<div class="quick-icon">
<span class="fa fa-fw fa-envelope text-muted"></span>
</div>
<div class="quick-content">
<legend>{% trans "Getting in touch with you" %}</legend>
<p class="bigger">
{% blocktrans trimmed %}
In case something goes wrong or is unclear, we strongly suggest that you provide ways for your
attendees to contact you:
{% endblocktrans %}
</p>
{% bootstrap_field form.contact_mail layout="control" %}
{% bootstrap_field form.imprint_url layout="control" %}
</div>
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -20,85 +20,101 @@
{% bootstrap_form_errors form %}
{% bootstrap_field form.name layout="control" %}
{% bootstrap_field form.rate addon_after="%" layout="control" %}
<legend>{% trans "Advanced settings" %}</legend>
<div class="alert alert-warning">
<span class="fa fa-fw fa-legal fa-4x pull-left"></span>
{% blocktrans trimmed with docs="https://docs.pretix.eu/en/latest/user/events/taxes.html" %}
These settings are intended for advanced users. See the <a href="{{ docs }}">documentation</a>
for more information. Note that we are not responsible for the correct handling
of taxes in your ticket shop. If in doubt, please contact a lawyer or tax consultant.
{% endblocktrans %}
<div class="clearfix"></div>
</div>
{% bootstrap_field form.price_includes_tax layout="control" %}
{% bootstrap_field form.eu_reverse_charge layout="control" %}
{% bootstrap_field form.home_country layout="control" %}
<legend>{% trans "Custom taxation rules" %}</legend>
<div class="alert alert-warning">
<span class="fa fa-fw fa-exclamation-circle fa-4x pull-left"></span>
{% blocktrans trimmed %}
These settings are intended for professional users with very specific taxation situations.
If you create any rule here, the reverse charge settings above will be ignored. The rules will be
checked in order and once the first rule matches the order, it will be used and all further rules will
be ignored. If no rule matches, tax will be charged.
{% endblocktrans %}
<div class="clearfix"></div>
</div>
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
{{ formset.management_form }}
{% bootstrap_formset_errors formset %}
<div data-formset-body>
{% for form in formset %}
{% bootstrap_form_errors form %}
<div class="row" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-sm-4">
{% bootstrap_field form.country layout='inline' form_group_class="" %}
</div>
<div class="col-sm-3">
{% bootstrap_field form.address_type layout='inline' form_group_class="" %}
</div>
<div class="col-sm-3">
{% bootstrap_field form.action layout='inline' form_group_class="" %}
</div>
<div class="col-sm-2 text-right">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endfor %}
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" href="#advanced">
<strong>{% trans "Advanced settings" %}</strong>
<i class="fa fa-angle-down collapse-indicator"></i>
</a>
</h4>
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="row" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-sm-4">
{% bootstrap_field formset.empty_form.country layout='inline' form_group_class="" %}
</div>
<div class="col-sm-3">
{% bootstrap_field formset.empty_form.address_type layout='inline' form_group_class="" %}
</div>
<div class="col-sm-3">
{% bootstrap_field formset.empty_form.action layout='inline' form_group_class="" %}
</div>
<div class="col-sm-2 text-right">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
<div id="advanced" class="panel-collapse collapsed {% if rule.eu_reverse_charge or rule.has_custom_rules or form.errors %}in{% endif %}">
<div class="panel-body">
<legend>{% trans "Advanced settings" %}</legend>
<div class="alert alert-warning">
<span class="fa fa-fw fa-legal fa-4x pull-left"></span>
{% blocktrans trimmed with docs="https://docs.pretix.eu/en/latest/user/events/taxes.html" %}
These settings are intended for advanced users. See the
<a href="{{ docs }}">documentation</a>
for more information. Note that we are not responsible for the correct handling
of taxes in your ticket shop. If in doubt, please contact a lawyer or tax consultant.
{% endblocktrans %}
<div class="clearfix"></div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a new rule" %}</button>
</p>
{% bootstrap_field form.price_includes_tax layout="control" %}
{% bootstrap_field form.eu_reverse_charge layout="control" %}
{% bootstrap_field form.home_country layout="control" %}
<legend>{% trans "Custom taxation rules" %}</legend>
<div class="alert alert-warning">
<span class="fa fa-fw fa-exclamation-circle fa-4x pull-left"></span>
{% blocktrans trimmed %}
These settings are intended for professional users with very specific taxation situations.
If you create any rule here, the reverse charge settings above will be ignored. The rules will be
checked in order and once the first rule matches the order, it will be used and all further rules will
be ignored. If no rule matches, tax will be charged.
{% endblocktrans %}
<div class="clearfix"></div>
</div>
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
{{ formset.management_form }}
{% bootstrap_formset_errors formset %}
<div data-formset-body>
{% for form in formset %}
{% bootstrap_form_errors form %}
<div class="row" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-sm-4">
{% bootstrap_field form.country layout='inline' form_group_class="" %}
</div>
<div class="col-sm-3">
{% bootstrap_field form.address_type layout='inline' form_group_class="" %}
</div>
<div class="col-sm-3">
{% bootstrap_field form.action layout='inline' form_group_class="" %}
</div>
<div class="col-sm-2 text-right">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="row" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-sm-4">
{% bootstrap_field formset.empty_form.country layout='inline' form_group_class="" %}
</div>
<div class="col-sm-3">
{% bootstrap_field formset.empty_form.address_type layout='inline' form_group_class="" %}
</div>
<div class="col-sm-3">
{% bootstrap_field formset.empty_form.action layout='inline' form_group_class="" %}
</div>
<div class="col-sm-2 text-right">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a new rule" %}</button>
</p>
</div>
</div>
</div>
</div>

View File

@@ -31,6 +31,20 @@
{% else %}
{% abseventurl request.event "presale:event.index" as indexurl %}
{% endif %}
{% if form.cleaned_data.compatibility_mode %}
<pre>&lt;div class="pretix-widget-compat" event="{% abseventurl request.event "presale:event.index" %}"{% if form.cleaned_data.subevent %} subevent="{{ form.cleaned_data.subevent.pk }}"{% endif %}{% if form.cleaned_data.voucher %} voucher="{{ form.cleaned_data.voucher }}"{% endif %}&gt;&lt;/div&gt;
&lt;noscript&gt;
&lt;div class="pretix-widget"&gt;
&lt;div class="pretix-widget-info-message"&gt;
{% blocktrans trimmed with a_attr='target="_blank" rel="noopener" href="'|add:indexurl|add:'"'|safe %}
JavaScript is disabled in your browser. To access our ticket shop without JavaScript,
please &lt;a {{ a_attr }}&gt;click here&lt;/a&gt;.
{% endblocktrans %}
&lt;/div&gt;
&lt;/div&gt;
&lt;/noscript&gt;
</pre>
{% else %}
<pre>&lt;pretix-widget event="{% abseventurl request.event "presale:event.index" %}"{% if form.cleaned_data.subevent %} subevent="{{ form.cleaned_data.subevent.pk }}"{% endif %}{% if form.cleaned_data.voucher %} voucher="{{ form.cleaned_data.voucher }}"{% endif %}&gt;&lt;/pretix-widget&gt;
&lt;noscript&gt;
&lt;div class="pretix-widget"&gt;
@@ -43,6 +57,7 @@
&lt;/div&gt;
&lt;/noscript&gt;
</pre>
{% endif %}
<p>
<a href="https://docs.pretix.eu/en/latest/user/events/widget.html" target="_blank" rel="noopener">
<span class="fa fa-question-circle"></span>

View File

@@ -29,7 +29,7 @@
<div class="alert alert-danger">
{% trans "Every event needs to be created as part of an organizer account. Currently, you do not have access to any organizer accounts." %}
</div>
{% if request.user.is_superuser %}
{% if staff_session %}
<a href="{% url "control:organizers.add" %}" class="btn btn-default">
{% trans "Create a new organizer" %}
</a>

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