Compare commits

...

247 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
Raphael Michel
c68b6116a2 Bump version to 1.13.0 2018-03-03 21:38:07 +01:00
Raphael Michel
f0db879c9c Update docs and German translation 2018-03-03 21:16:17 +01:00
Felix Rindt
07d8a3d765 Fix #774 -- Make question options sortable (#786)
* add position field

* add question option sorting logic
* add meta class to question option for sorting
* regenerate migration
* add template content and view mechanics

* Rename migration after rebase & update dependency
2018-03-03 20:36:30 +01:00
Raphael Michel
e35e264d81 Improve voucher redemption filter (#792) 2018-03-03 11:58:59 +01:00
Raphael Michel
d537e6a869 Order confirmation: Add e-mail to contact information box 2018-03-03 11:56:33 +01:00
Leonardo
d4dd1861a9 Fix #740 -- Date picker: Fix line height for decade span (#761)
* Fix line height for decade span

* Move to own file
2018-03-03 11:31:23 +01:00
Mohit Jindal
3019a31fbb Fix #735 -- Display of event series on public organizer page (#753) 2018-03-03 11:24:07 +01:00
Raphael Michel
303b9912ff Add „button“ operation mode of the widget (#778) 2018-03-03 11:20:41 +01:00
Raphael Michel
0259b2e5b9 Update paypal documentation 2018-03-02 22:55:37 +01:00
Raphael Michel
5c7e8029f4 Fix incorrect test case 2018-03-02 22:05:56 +01:00
Raphael Michel
08e3fd3141 Fix spelling 2018-03-02 21:54:36 +01:00
Raphael Michel
30123fd6ff Add currency property to subevent 2018-03-02 21:54:08 +01:00
Raphael Michel
3955299983 Catch VAT WebServiceError 2018-03-01 09:21:21 +01:00
Raphael Michel
b5d0df3ca7 Fix determination of VAT ID validation 2018-03-01 09:19:04 +01:00
Raphael Michel
22c65da9d1 Fix invalid use of money_filter 2018-03-01 09:17:59 +01:00
Raphael Michel
578c1ecfaf Add support for custom taxation rules 2018-02-28 23:03:25 +01:00
Raphael Michel
d8d00a7e26 Add total argument to fee calculation signals 2018-02-28 21:03:38 +01:00
Raphael Michel
37f0f7a138 Add service fees as a first-level fee type 2018-02-27 22:39:07 +01:00
Raphael Michel
f61e9367ec Update German translation 2018-02-26 10:51:44 +01:00
Raphael Michel
3c3e59e932 Refs #99 -- Improve support for currencies with less than 2 decimal places (#783)
* Refs #99 -- Fix stripe support for zero-decimal currencies

* Add new money formatting method

* Force decimal places in many places

* Locale-aware currency rendering

* Fix currencies in more places

* More currency fixes
2018-02-26 10:46:07 +01:00
Raphael Michel
29e22a0c6c Fix check-in of unpaid orders in web check-in list 2018-02-26 10:42:58 +01:00
Raphael Michel
0d1f424425 Improve performance of voucher bulk creation 2018-02-26 10:42:58 +01:00
Tim Freund
1c01e23867 Name presale index + unit test for URL names (#784)
* Name the default URL

If metrics collection is enabled, the index page of the site will fail
to load: without a name, the metrics middleware throws a TypeError.

* Test for names on all URLs

This test passes if all URLs have names. Without names, URLs will cause
the optional metrics middleware to throw a TypeError.
2018-02-26 10:17:42 +01:00
Felix Rindt
f763a8694b Fix #779: add form field for unpaid option of checkin lists in subevent detail view (#781)
* add form field for unpaid option of checkin lists in subevent detail view

* change order of include_pending field

* also change the order in new check in lists
2018-02-26 10:17:28 +01:00
Raphael Michel
675b853b29 Remove organizer property from ICalendar files as we used it not as it is intended to be used. 2018-02-23 10:51:32 +01:00
Raphael Michel
2434bf14d5 Add checkin_attetion field to Order model 2018-02-22 13:25:26 +01:00
Felix Rindt
70fbbfe2a0 Refs #757: show voucher input for subevents only if subevent is selected (#777)
* show voucher input for subevents only if subevent is selected

* move logic to python
2018-02-22 09:44:53 +01:00
Raphael Michel
e096898a05 Update German translation 2018-02-21 16:17:06 +01:00
Raphael Michel
3fbccf3f64 Allow check-in lists to include unpaid orders 2018-02-21 16:17:06 +01:00
Raphael Michel
36585395f1 Voucher list: add more filters 2018-02-21 16:17:06 +01:00
Felix Rindt
e4b0a1613f Refs #754 -- check item tax_rule is not none (#776) 2018-02-21 12:51:50 +01:00
Raphael Michel
1192e474c5 Prevent duplicate All/None links 2018-02-20 10:20:24 +01:00
Raphael Michel
e48ea99e48 Fix datetime in check-in list on MySQL 2018-02-20 10:19:55 +01:00
Raphael Michel
072f2a0ee9 Pin sessions to the user agent in use 2018-02-19 13:02:55 +01:00
Tim Freund
aecb536a34 Use config.getboolean to get metrics enabled value (#770)
Given the following configuration:

[metrics]
enabled=False

Using config.get results in a METRICS_ENABLED value that always
evaluates to True. This PR switches to config.getboolean so that metrics
can be disabled without deleting the configuration values.
2018-02-18 17:40:13 +01:00
Tim Freund
a68686cb06 Docs: Fix link to the Celery configuration documentation (#771) 2018-02-18 17:39:51 +01:00
Tim Freund
ba8cf3e01e Replace PREFIX_CONFIG_FILE with PRETIX_CONFIG_FILE (#769)
The code looks for PRETIX_CONFIG_FILE in src/pretix/settings.py.
This change updates the documentation to match.
2018-02-18 17:39:34 +01:00
Raphael Michel
b0c5189c4b Fix timezone for footer of printed exports 2018-02-14 11:50:24 +01:00
Raphael Michel
d44eb67dec Allow http: forms during testing 2018-02-14 11:50:10 +01:00
Raphael Michel
58d36b08e2 Pin Sphinx version 2018-02-14 11:49:50 +01:00
Raphael Michel
98906731e3 Move plugin list to website 2018-02-14 11:49:44 +01:00
Raphael Michel
035a4b0928 Add next parameter to logout view 2018-02-14 11:49:16 +01:00
Raphael Michel
85fbe666ea Order modification page: Make cancel button more useful 2018-02-12 12:38:30 +01:00
Tobias Kunze
741d0bc686 Put event slugs in export filenames (#768) 2018-02-12 12:30:13 +01:00
Raphael Michel
ded539ce7a Ignore event end date for subevents 2018-02-07 13:51:22 +01:00
Raphael Michel
c53fd25d1c Use a consistant CSS compression method 2018-02-05 13:48:47 +01:00
Raphael Michel
da32621c55 Add "is_implicit" attribute to payment providers 2018-02-04 23:14:18 +01:00
Raphael Michel
4ccf33af03 Add support for orders without email addresses 2018-02-04 22:42:41 +01:00
Raphael Michel
a5af7a70f3 Add support for iframeResizer 2018-02-04 22:42:04 +01:00
Raphael Michel
16ab0d29d6 Add request argument to contact_form_fields signal 2018-02-04 22:15:58 +01:00
Raphael Michel
05ad9022c0 Always use full width when used in an iframe 2018-02-04 22:02:54 +01:00
Raphael Michel
fef211b220 Change typeahead.css and morris.css to scss files 2018-02-04 21:06:44 +01:00
Raphael Michel
6aee1ee41f Stip HTML from text in PDFs except for <br>, make <br> not break things 2018-02-04 19:45:00 +01:00
Raphael Michel
bab7f9b1f3 Notification view: use select2 event selection 2018-02-04 19:09:22 +01:00
Raphael Michel
340e7afd06 Fix bug that lead to notifications being sent for all events 2018-02-04 18:53:56 +01:00
Raphael Michel
cb83c9cff2 Add a short system check before publishing packages 2018-02-04 18:33:50 +01:00
Raphael Michel
911a8fed06 Fix waiting list test 2018-02-04 18:28:29 +01:00
Raphael Michel
eb8b43fe36 Add missing __init__.py file 2018-02-04 18:27:45 +01:00
Raphael Michel
2a15dc57d8 Waiting list: Do not send out for disabled events 2018-02-04 14:24:53 +01:00
Raphael Michel
67678e35bb Disable shop and waiting list after end of event 2018-02-04 14:14:49 +01:00
Raphael Michel
2f00db8081 Bump version to 1.13.0.dev0 2018-02-03 17:00:40 +01:00
319 changed files with 131207 additions and 4881 deletions

2
.gitattributes vendored
View File

@@ -8,6 +8,8 @@ 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

@@ -19,6 +19,10 @@ pypi:
- pip install -U pip wheel setuptools
- XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements.txt -r src/requirements/dev.txt -r src/requirements/py34.txt
- cd src
- python setup.py sdist
- pip install dist/pretix-*.tar.gz
- python -m pretix migrate
- python -m pretix check
- python setup.py sdist upload
- python setup.py bdist_wheel upload
tags:

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

@@ -12,7 +12,7 @@ at the following locations. It will try to read the file from the specified path
the following order. The file that is found *last* will override the settings from
the files found before.
1. ``PREFIX_CONFIG_FILE`` environment variable
1. ``PRETIX_CONFIG_FILE`` environment variable
2. ``/etc/pretix/pretix.cfg``
3. ``~/.pretix.cfg``
4. ``pretix.cfg`` in the current working directory
@@ -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
---------------
@@ -288,4 +292,4 @@ various places like order codes, secrets in the ticket QR codes, etc. Example::
voucher_code=16
.. _Python documentation: https://docs.python.org/3/library/configparser.html?highlight=configparser#supported-ini-file-structure
.. _Celery documentation: http://docs.celeryproject.org/en/latest/configuration.html
.. _Celery documentation: http://docs.celeryproject.org/en/latest/userguide/configuration.html

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

@@ -21,11 +21,12 @@ Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the check-in list
name string The internal name of the check-in list
all_products boolean If ``True``, the check-in lists contains tickets of all products in this event. The ``limit_products`` field is ignored in this case.
all_products boolean If ``true``, the check-in lists contains tickets of all products in this event. The ``limit_products`` field is ignored in this case.
limit_products list of integers List of item IDs to include in this list.
subevent integer ID of the date inside an event series this list belongs to (or ``null``).
position_count integer Number of tickets that match this list (read-only).
checkin_count integer Number of check-ins performed on this list (read-only).
include_pending boolean If ``true``, the check-in list also contains tickets from orders in pending state.
===================================== ========================== =======================================================
.. versionchanged:: 1.10
@@ -36,6 +37,10 @@ checkin_count integer Number of check
The ``positions`` endpoints have been added.
.. versionchanged:: 1.13
The ``include_pending`` field has been added.
Endpoints
---------
@@ -71,6 +76,7 @@ Endpoints
"position_count": 456,
"all_products": true,
"limit_products": [],
"include_pending": false,
"subevent": null
}
]
@@ -111,6 +117,7 @@ Endpoints
"position_count": 456,
"all_products": true,
"limit_products": [],
"include_pending": false,
"subevent": null
}
@@ -156,6 +163,7 @@ Endpoints
"position_count": 0,
"all_products": false,
"limit_products": [1, 2],
"include_pending": false,
"subevent": null
}
@@ -204,6 +212,7 @@ Endpoints
"position_count": 42,
"all_products": false,
"limit_products": [1, 2],
"include_pending": false,
"subevent": null
}

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

@@ -34,6 +34,9 @@ payment_fee_tax_value money (string) Tax value inclu
payment_fee_tax_rule integer The ID of the used tax rule (or ``null``)
total money (string) Total value of this order
comment string Internal comment on this order
checkin_attention boolean If ``True``, the check-in app should show a warning
that this ticket requires special attention if a ticket
of this order is scanned.
invoice_address object Invoice address information (can be ``null``)
├ last_modified datetime Last modification date of the address
├ company string Customer company name
@@ -88,6 +91,10 @@ downloads list of objects List of ticket
First write operations (``…/mark_paid/``, ``…/mark_pending/``, ``…/mark_canceled/``, ``…/mark_expired/``) have been added.
The attribute ``invoice_address.internal_reference`` has been added.
.. versionchanged:: 1.13
The field ``checkin_attention`` has been added.
.. _order-position-resource:
Order position resource
@@ -122,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
@@ -133,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
---------------
@@ -175,6 +188,7 @@ Order endpoints
"fees": [],
"total": "23.00",
"comment": "",
"checkin_attention": false,
"invoice_address": {
"last_modified": "2017-12-01T10:00:00Z",
"is_business": True,
@@ -214,7 +228,9 @@ Order endpoints
"answers": [
{
"question": 12,
"question_identifier": "WY3TP9SL",
"answer": "Foo",
"option_idenfiters": [],
"options": []
}
],
@@ -282,6 +298,7 @@ Order endpoints
"fees": [],
"total": "23.00",
"comment": "",
"checkin_attention": false,
"invoice_address": {
"last_modified": "2017-12-01T10:00:00Z",
"company": "Sample company",
@@ -321,7 +338,9 @@ Order endpoints
"answers": [
{
"question": 12,
"question_identifier": "WY3TP9SL",
"answer": "Foo",
"option_idenfiters": [],
"options": []
}
],
@@ -640,7 +659,9 @@ Order position endpoints
"answers": [
{
"question": 12,
"question_identifier": "WY3TP9SL",
"answer": "Foo",
"option_idenfiters": [],
"options": []
}
],
@@ -720,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

@@ -4,7 +4,8 @@ Tax rules
Resource description
--------------------
Tax rules specify how tax should be calculated for specific products.
Tax rules specify how tax should be calculated for specific products. Custom taxation rule sets are currently to
available via the API.
.. rst-class:: rest-resource-table

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

@@ -102,6 +102,8 @@ The provider class
.. automethod:: order_control_refund_perform
.. automethod:: is_implicit
Additional views
----------------

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,56 +4,7 @@
List of plugins
===============
The following plugins are shipped with pretix and are supported in the same
ways that pretix itself is:
A detailed list of plugins that are available for pretix can be found on the
`project website`_.
* Bank transfer
* PayPal
* Stripe
* Check-in lists
* pretixdroid
* Report exporter
* Send out emails
* Statistics
* PDF ticket output
The following plugins are not shipped with pretix but are maintained by the
same team. We update them regularly to make them compatible with the latest
pretix releases:
* `SEPA direct debit`_
* `Wirecard payment`_
* `Pages`_
* `Passbook/Wallet ticket output`_
* `Cartshare`_
* `Fontpack Free fonts`_
* `Mailing list subscription`_
The following closed-source plugins are available to customers of the hosted pretix.eu platform.
Please get in touch with the pretix team if you want to have them for your self-hosted
pretix installation:
* Campaign tracking
* Integration with Google Analytics and Facebook Pixel
* Integration with Slack
* Integration with MailChimp
The following plugins are from independent third-party authors, so we can make
no statements about their functionality, security, stability or compatibility:
* `esPass ticket output`_
* `IcePay integration`_
* `Average price chart`_
* `Pay in cash upon arrival`_
.. _SEPA direct debit: https://github.com/pretix/pretix-sepadebit
.. _Passbook/Wallet ticket output: https://github.com/pretix/pretix-passbook
.. _Cartshare: https://github.com/pretix/pretix-cartshare
.. _Pages: https://github.com/pretix/pretix-pages
.. _esPass ticket output: https://github.com/esPass/pretix-espass
.. _IcePay integration: https://github.com/chotee/pretix-icepay
.. _Fontpack Free fonts: https://github.com/pretix/pretix-fontpack-free
.. _Wirecard payment: https://github.com/pretix/pretix-wirecard
.. _Mailing list subscription: https://github.com/pretix/pretix-newsletter-ml
.. _Average price chart: https://github.com/rixx/pretix-avgchart
.. _Pay in cash upon arrival: https://github.com/pc-coholic/pretix-cashpayment
.. _project website: https://pretix.eu/about/en/plugins

View File

@@ -15,6 +15,10 @@ uses to communicate with the pretix server.
negotiated live, so clients which do not need this feature can ignore the change. For this reason, the API version
has not been increased and is still set to 3.
.. versionchanged:: 1.13
Support for checking in unpaid tickets has been added.
.. http:post:: /pretixdroid/api/(organizer)/(event)/redeem/
@@ -49,6 +53,9 @@ uses to communicate with the pretix server.
check-in. This is meant to be used to prevent duplicate check-ins when you are just retrying after a connection
failure.
You **may** set the additional parameter ``ignore_unpaid`` to indicate that the check-in should be performed even
if the order is in pending state.
If questions are supported and required, you will receive a dictionary ``questions`` containing details on the
particular questions to ask. To answer them, just re-send your redemption request with additional parameters of
the form ``answer_<question>=<answer>``, e.g. ``answer_12=24``.
@@ -73,6 +80,7 @@ uses to communicate with the pretix server.
"attendee_name": "Peter Higgs",
"attention": false,
"redeemed": true,
"checkin_allowed": true,
"paid": true
}
}
@@ -97,6 +105,7 @@ uses to communicate with the pretix server.
"attendee_name": "Peter Higgs",
"attention": false,
"redeemed": true,
"checkin_allowed": true,
"paid": true
},
"questions": [
@@ -142,6 +151,7 @@ uses to communicate with the pretix server.
"attendee_name": "Peter Higgs",
"attention": false,
"redeemed": true,
"checkin_allowed": true,
"paid": true
}
}
@@ -201,6 +211,7 @@ uses to communicate with the pretix server.
"attendee_name": "Peter Higgs",
"redeemed": false,
"attention": false,
"checkin_allowed": true,
"paid": true
},
...
@@ -244,6 +255,7 @@ uses to communicate with the pretix server.
"attendee_name": "Peter Higgs",
"redeemed": false,
"attention": false,
"checkin_allowed": true,
"paid": true
},
...

View File

@@ -1,7 +1,8 @@
-r ../src/requirements.txt
sphinx
sphinx==1.6.*
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

@@ -100,6 +100,16 @@ taxes" at the end of the page.
errors of usually up to one cent from the intended price. This is unavoidable due to the
flexible nature in which prices are being calculated.
Custom tax rules
----------------
If you have very special requirements for the conditions in which VAT will or will not be charged, you can use the
"Custom tax rules" section instead of the options listed above. Here, you can create a set of rules consisting of
conditions (i.e. a country or a type of customer) and actions (i.e. do or do not charge VAT).
The rules will then be checked from top to bottom and the first matching rule will be used to decide if VAT will be
charged to the user.
Taxation of payment fees
------------------------

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
-------
@@ -101,4 +107,43 @@ voucher's settings.
</div>
</noscript>
pretix Button
-------------
Instead of a product list, you can also display just a single button. When pressed, the button will add a number of
products associated with the button to the cart and will immediately proceed to checkout if the operation succeeded.
You can try out this behavior here:
.. raw:: html
<pretix-button event="https://pretix.eu/demo/democon/" items="item_6424=1">Buy ticket!</pretix-button>
<noscript>
<div class="pretix-widget">
<div class="pretix-widget-info-message">
JavaScript is disabled in your browser. To access our ticket shop without javascript, please <a target="_blank" href="https://pretix.eu/demo/democon/">click here</a>.
</div>
</div>
</noscript>
<br><br>
You can embed the pretix Button just like the pretix Widget. Just like above, first embed the CSS and JavaScript
resources. Then, instead of the ``pretix-widget`` tag, use the ``pretix-button`` tag::
<pretix-button event="https://pretix.eu/demo/democon/" items="item_6424=1">
Buy ticket!
</pretix-button>
As you can see, the ``pretix-button`` element takes an additional ``items`` attribute that specifies the items that
should be added to the cart. The syntax of this attribute is ``item_ITEMID=1,item_ITEMID=2,variation_ITEMID_VARID=4``
where ``ITEMID`` are the internal IDs of items to be added and ``VARID`` are the internal IDs of variations of those
items, if the items have variations.
Just as the widget, the button supports the optional attributes ``voucher`` and ``skip-ssl-check``.
You can style the button using the ``pretix-button`` CSS class.
.. versionchanged:: 1.13
The pretix Button has been added in version 1.13.
.. _Let's Encrypt: https://letsencrypt.org/

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

@@ -12,6 +12,12 @@ If you look into pretix' settings, you are required to fill in two keys:
Unfortunately, it is not straightforward how to get those keys from PayPal's website. In order to do so, you
need to go to `developer.paypal.com`_ to link the account to your pretix event.
.. warning::
Unfortunately, PayPal tries to confuse you by having multiple APIs with different keys. You really need to
go to https://developer.paypal.com for the API we use, not to your normal account settings!
Click on "Log In" in the top-right corner and log in with your PayPal account.
.. image:: img/paypal2.png
@@ -46,8 +52,8 @@ webhooks. To create one, scroll a bit down and click "Add Webhook".
.. image:: img/paypal7.png
:class: screenshot
Then, enter the webhook URL that you find on the pretix settings page. It should look similar to the one in the
screenshot but contain your event name. Tick the box "All events" and save.
Then, enter the webhook URL that you find on the pretix settings page. If you use pretix Hosted, this is always ``https://pretix.eu/_paypal/webhook/``.
Tick the box "All events" and save.
.. image:: img/paypal8.png
:class: screenshot

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.12.0"
__version__ = "1.14.0"

View File

@@ -1,12 +1,10 @@
import time
from django.conf import settings
from django.contrib.auth import logout
from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import SAFE_METHODS, BasePermission
from pretix.base.models import Event
from pretix.base.models.organizer import Organizer, TeamAPIToken
from pretix.helpers.security import (
SessionInvalid, SessionReauthRequired, assert_session_valid,
)
class EventPermission(BasePermission):
@@ -24,16 +22,13 @@ class EventPermission(BasePermission):
required_permission = None
if request.user.is_authenticated:
# If this logic is updated, make sure to also update the logic in pretix/control/middleware.py
if not settings.PRETIX_LONG_SESSIONS or not request.session.get('pretix_auth_long_session', False):
last_used = request.session.get('pretix_auth_last_used', time.time())
if time.time() - request.session.get('pretix_auth_login_time', time.time()) > settings.PRETIX_SESSION_TIMEOUT_ABSOLUTE:
logout(request)
request.session['pretix_auth_login_time'] = 0
return False
if time.time() - last_used > settings.PRETIX_SESSION_TIMEOUT_RELATIVE:
return False
request.session['pretix_auth_last_used'] = int(time.time())
try:
# If this logic is updated, make sure to also update the logic in pretix/control/middleware.py
assert_session_valid(request)
except SessionInvalid:
return False
except SessionReauthRequired:
return False
perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken)
else request.user)
@@ -61,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

@@ -12,7 +12,8 @@ class CheckinListSerializer(I18nAwareModelSerializer):
class Meta:
model = CheckinList
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count')
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
'include_pending')
def validate(self, data):
data = super().validate(data)

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):
@@ -135,7 +148,7 @@ class OrderSerializer(I18nAwareModelSerializer):
model = Order
fields = ('code', 'status', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
'payment_fee', 'payment_fee_tax_rate', 'payment_fee_tax_value')
'payment_fee', 'payment_fee_tax_rate', 'payment_fee_tax_value', 'checkin_attention')
class InlineInvoiceLineSerializer(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

@@ -126,7 +126,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
qs = OrderPosition.objects.filter(
order__event=self.request.event,
order__status=Order.STATUS_PAID,
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.checkinlist.include_pending else [Order.STATUS_PAID],
subevent=self.checkinlist.subevent
).annotate(
last_checked_in=Subquery(cqs)

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

@@ -1,5 +1,12 @@
from decimal import ROUND_HALF_UP, Decimal
from django.conf import settings
def round_decimal(dec):
def round_decimal(dec, currency=None):
if currency:
places = settings.CURRENCY_PLACES.get(currency, 2)
return Decimal(dec).quantize(
Decimal('1') / 10 ** places, ROUND_HALF_UP
)
return Decimal(dec).quantize(Decimal('0.01'), ROUND_HALF_UP)

View File

@@ -55,7 +55,7 @@ class AnswerFilesExporter(BaseExporter):
i.file.close()
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
return 'answers.zip', 'application/zip', zipf.read()
return '{}_answers.zip'.format(self.event.slug), 'application/zip', zipf.read()
@receiver(register_data_exporters, dispatch_uid="exporter_answers")

View File

@@ -21,7 +21,7 @@ class MailExporter(BaseExporter):
pos = OrderPosition.objects.filter(
order__event=self.event, order__status__in=form_data['status']
).values('attendee_email')
data = "\r\n".join(set(a['email'] for a in addrs)
data = "\r\n".join(set(a['email'] for a in addrs if a['email'])
| set(a['attendee_email'] for a in pos if a['attendee_email']))
return '{}_pretixemails.txt'.format(self.event.slug), 'text/plain', data.encode("utf-8")

View File

@@ -140,7 +140,7 @@ class OrderListExporter(BaseExporter):
row.append(', '.join([i.number for i in order.invoices.all()]))
writer.writerow(row)
return 'orders.csv', 'text/csv', output.getvalue().encode("utf-8")
return '{}_orders.csv'.format(self.event.slug), 'text/csv', output.getvalue().encode("utf-8")
class QuotaListExporter(BaseExporter):

View File

@@ -11,16 +11,16 @@ from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from .validators import PlaceholderValidator # NOQA
logger = logging.getLogger('pretix.plugins.ticketoutputpdf')
logger = logging.getLogger(__name__)
class BaseI18nModelForm(i18nfield.forms.BaseI18nModelForm):
# compatibility shim for django-i18nfield library
def __init__(self, *args, **kwargs):
event = kwargs.pop('event', None)
if event:
kwargs['locales'] = event.settings.get('locales')
self.event = kwargs.pop('event', None)
if self.event:
kwargs['locales'] = self.event.settings.get('locales')
super().__init__(*args, **kwargs)
@@ -32,9 +32,9 @@ class I18nFormSet(i18nfield.forms.I18nModelFormSet):
# compatibility shim for django-i18nfield library
def __init__(self, *args, **kwargs):
event = kwargs.pop('event', None)
if event:
kwargs['locales'] = event.settings.get('locales')
self.event = kwargs.pop('event', None)
if self.event:
kwargs['locales'] = self.event.settings.get('locales')
super().__init__(*args, **kwargs)
@@ -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

@@ -232,5 +232,13 @@ class BaseInvoiceAddressForm(forms.ModelForm):
'your country is currently not available. We will therefore '
'need to charge VAT on your invoice. You can get the tax amount '
'back via the VAT reimbursement process.'))
except vat_moss.errors.WebServiceError:
logger.exception('VAT ID checking failed for country {}'.format(data.get('country')))
self.instance.vat_id_validated = False
if self.request and self.vat_warning:
messages.warning(self.request, _('Your VAT ID could not be checked, as the VAT checking service of '
'your country returned an incorrect result. We will therefore '
'need to charge VAT on your invoice. Please contact support to '
'resolve this manually.'))
else:
self.instance.vat_id_validated = False

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

@@ -12,6 +12,8 @@ from i18nfield.forms import I18nFormField # noqa
from i18nfield.strings import LazyI18nString # noqa
from i18nfield.utils import I18nJSONEncoder # noqa
from pretix.base.templatetags.money import money_filter
class LazyDate:
def __init__(self, value):
@@ -24,6 +26,18 @@ class LazyDate:
return date_format(self.value, "SHORT_DATE_FORMAT")
class LazyCurrencyNumber:
def __init__(self, value, currency):
self.value = value
self.currency = currency
def __format__(self, format_spec):
return self.__str__()
def __str__(self):
return money_filter(self.value, self.currency)
class LazyNumber:
def __init__(self, value, decimal_pos=2):
self.value = value

View File

@@ -24,6 +24,7 @@ from reportlab.platypus import (
from pretix.base.decimal import round_decimal
from pretix.base.models import Event, Invoice
from pretix.base.signals import register_invoice_renderers
from pretix.base.templatetags.money import money_filter
class BaseInvoiceRenderer:
@@ -376,14 +377,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
Paragraph(line.description, self.stylesheet['Normal']),
"1",
localize(line.tax_rate) + " %",
localize(line.net_value) + " " + self.invoice.event.currency,
localize(line.gross_value) + " " + self.invoice.event.currency,
money_filter(line.net_value, self.invoice.event.currency),
money_filter(line.gross_value, self.invoice.event.currency),
))
else:
tdata.append((
Paragraph(line.description, self.stylesheet['Normal']),
"1",
localize(line.gross_value) + " " + self.invoice.event.currency,
money_filter(line.gross_value, self.invoice.event.currency),
))
taxvalue_map[line.tax_rate, line.tax_name] += line.tax_value
grossvalue_map[line.tax_rate, line.tax_name] += line.gross_value
@@ -391,12 +392,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if has_taxes:
tdata.append([
pgettext('invoice', 'Invoice total'), '', '', '', localize(total) + " " + self.invoice.event.currency
pgettext('invoice', 'Invoice total'), '', '', '', money_filter(total, self.invoice.event.currency)
])
colwidths = [a * doc.width for a in (.50, .05, .15, .15, .15)]
else:
tdata.append([
pgettext('invoice', 'Invoice total'), '', localize(total) + " " + self.invoice.event.currency
pgettext('invoice', 'Invoice total'), '', money_filter(total, self.invoice.event.currency)
])
colwidths = [a * doc.width for a in (.65, .05, .30)]
@@ -436,9 +437,9 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
tax = taxvalue_map[idx]
tdata.append([
localize(rate) + " % " + name,
localize(gross - tax) + " " + self.invoice.event.currency,
localize(gross) + " " + self.invoice.event.currency,
localize(tax) + " " + self.invoice.event.currency,
money_filter(gross - tax, self.invoice.event.currency),
money_filter(gross, self.invoice.event.currency),
money_filter(tax, self.invoice.event.currency),
''
])

View File

@@ -187,7 +187,7 @@ class SecurityMiddleware(MiddlewareMixin):
# form-actions redirect to. In the context of e.g. payment providers or
# single-sign-on this can be nearly anything so we cannot really restrict
# this. However, we'll restrict it to HTTPS.
'form-action': ["{dynamic}", "https:"],
'form-action': ["{dynamic}", "https:"] + (['http:'] if settings.SITE_URL.startswith('http://') else []),
'report-uri': ["/csp_report/"],
}
if 'Content-Security-Policy' in resp:

View File

@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.10 on 2018-02-20 10:31
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0080_question_ask_during_checkin'),
]
operations = [
migrations.AddField(
model_name='checkinlist',
name='include_pending',
field=models.BooleanField(default=False, verbose_name='Include pending orders'),
),
migrations.AlterField(
model_name='event',
name='presale_end',
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold after this date. If you do not set this value, the presale will end after the end date of your event.', null=True, verbose_name='End of presale'),
),
migrations.AlterField(
model_name='logentry',
name='event',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='pretixbase.Event'),
),
migrations.AlterField(
model_name='question',
name='ask_during_checkin',
field=models.BooleanField(default=False, help_text='This will only work if you handle your check-in with pretixdroid 1.8 or newer or pretixdesk 0.2 or newer.', verbose_name='Ask during check-in instead of in the ticket buying process'),
),
migrations.AlterField(
model_name='subevent',
name='presale_end',
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold after this date. If you do not set this value, the presale will end after the end date of your event.', null=True, verbose_name='End of presale'),
),
migrations.AlterField(
model_name='user',
name='require_2fa',
field=models.BooleanField(default=False, verbose_name='Two-factor authentification is required to log in'),
),
]

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.10 on 2018-02-22 09:38
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0081_auto_20180220_1031'),
]
operations = [
migrations.AddField(
model_name='order',
name='checkin_attention',
field=models.BooleanField(default=False, help_text='If you set this, the check-in app will show a visible warning that tickets of this order require special attention. This will not show any details or custom message, so you need to brief your check-in staff how to handle these cases.', verbose_name='Requires special attention'),
),
migrations.AlterField(
model_name='checkinlist',
name='include_pending',
field=models.BooleanField(default=False, help_text='With this option, people will be able to check in even if the order have not been paid. This only works with pretixdesk 0.3.0 or newer or pretixdroid 1.9 or newer.', verbose_name='Include pending orders'),
),
]

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.10 on 2018-02-28 21:02
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0082_auto_20180222_0938'),
]
operations = [
migrations.AddField(
model_name='taxrule',
name='custom_rules',
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name='orderfee',
name='fee_type',
field=models.CharField(choices=[('payment', 'Payment fee'), ('shipping', 'Shipping fee'), ('service', 'Service fee'), ('other', 'Other fees')], max_length=100),
),
]

View File

@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.9 on 2018-03-03 16:41
from __future__ import unicode_literals
from django.db import migrations, models
def set_position(apps, schema_editor):
Question = apps.get_model('pretixbase', 'Question')
for q in Question.objects.all():
for i, option in enumerate(q.options.all()):
option.position = i
option.save()
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0083_auto_20180228_2102'),
]
operations = [
migrations.AlterModelOptions(
name='questionoption',
options={'ordering': ('position', 'id'), 'verbose_name': 'Question option', 'verbose_name_plural': 'Question options'},
),
migrations.AddField(
model_name='questionoption',
name='position',
field=models.IntegerField(default=0),
),
migrations.AlterField(
model_name='question',
name='position',
field=models.PositiveIntegerField(default=0, verbose_name='Position'),
),
migrations.RunPython(
set_position,
reverse_code=migrations.RunPython.noop,
),
]

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

@@ -36,7 +36,7 @@ def cached_file_delete(sender, instance, **kwargs):
class LoggingMixin:
def log_action(self, action, data=None, user=None, api_token=None):
def log_action(self, action, data=None, user=None, api_token=None, save=True):
"""
Create a LogEntry object that is related to this object.
See the LogEntry documentation for details.
@@ -60,10 +60,12 @@ class LoggingMixin:
logentry = LogEntry(content_object=self, user=user, action_type=action, event=event, api_token=api_token)
if data:
logentry.data = json.dumps(data, cls=CustomJSONEncoder)
logentry.save()
if save:
logentry.save()
if action in get_all_notification_types():
notify.apply_async(args=(logentry.pk,))
if action in get_all_notification_types():
notify.apply_async(args=(logentry.pk,))
return logentry
class LoggedModel(models.Model, LoggingMixin):

View File

@@ -14,6 +14,11 @@ class CheckinList(LoggedModel):
limit_products = models.ManyToManyField('Item', verbose_name=_("Limit to products"), blank=True)
subevent = models.ForeignKey('SubEvent', null=True, blank=True,
verbose_name=pgettext_lazy('subevent', 'Date'))
include_pending = models.BooleanField(verbose_name=pgettext_lazy('checkin', 'Include pending orders'),
default=False,
help_text=_('With this option, people will be able to check in even if the '
'order have not been paid. This only works with pretixdesk '
'0.3.0 or newer or pretixdroid 1.9 or newer.'))
@staticmethod
def annotate_with_numbers(qs, event):
@@ -29,7 +34,7 @@ class CheckinList(LoggedModel):
# position and to the list in question. Then, we check that it also belongs to the
# correct subevent (just to be sure) and aggregate over lists (so, over everything,
# since we filtered by lists).
cqs = Checkin.objects.filter(
cqs_paid = Checkin.objects.filter(
position__order__event=event,
position__order__status=Order.STATUS_PAID,
list=OuterRef('pk')
@@ -41,12 +46,24 @@ class CheckinList(LoggedModel):
).order_by().values('list').annotate(
c=Count('*')
).values('c')
cqs_paid_and_pending = Checkin.objects.filter(
position__order__event=event,
position__order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING],
list=OuterRef('pk')
).filter(
# This assumes that in an event with subevents, *all* positions have subevents
# and *all* checkin lists have a subevent assigned
Q(position__subevent=OuterRef('subevent'))
| (Q(position__subevent__isnull=True))
).order_by().values('list').annotate(
c=Count('*')
).values('c')
# Now for the hard part: getting all order positions that contribute to this list. This
# requires us to use TWO subqueries. The first one, pqs_all, will only be used for check-in
# lists that contain all the products of the event. This is the simpler one, it basically
# looks like the check-in counter above.
pqs_all = OrderPosition.objects.filter(
pqs_all_paid = OrderPosition.objects.filter(
order__event=event,
order__status=Order.STATUS_PAID,
).filter(
@@ -57,13 +74,24 @@ class CheckinList(LoggedModel):
).order_by().values('order__event').annotate(
c=Count('*')
).values('c')
pqs_all_paid_and_pending = OrderPosition.objects.filter(
order__event=event,
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING]
).filter(
# This assumes that in an event with subevents, *all* positions have subevents
# and *all* checkin lists have a subevent assigned
Q(subevent=OuterRef('subevent'))
| (Q(subevent__isnull=True))
).order_by().values('order__event').annotate(
c=Count('*')
).values('c')
# Now we need a subquery for the case of checkin lists that are limited to certain
# products. We cannot use OuterRef("limit_products") since that would do a cross-product
# with the products table and we'd get duplicate rows in the output with different annotations
# on them, which isn't useful at all. Therefore, we need to add a second layer of subqueries
# to retrieve all of those items and then check if the item_id is IN this subquery result.
pqs_limited = OrderPosition.objects.filter(
pqs_limited_paid = OrderPosition.objects.filter(
order__event=event,
order__status=Order.STATUS_PAID,
item_id__in=Subquery(Item.objects.filter(checkinlist__pk=OuterRef(OuterRef('pk'))).values('pk'))
@@ -75,17 +103,44 @@ class CheckinList(LoggedModel):
).order_by().values('order__event').annotate(
c=Count('*')
).values('c')
pqs_limited_paid_and_pending = OrderPosition.objects.filter(
order__event=event,
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING],
item_id__in=Subquery(Item.objects.filter(checkinlist__pk=OuterRef(OuterRef('pk'))).values('pk'))
).filter(
# This assumes that in an event with subevents, *all* positions have subevents
# and *all* checkin lists have a subevent assigned
Q(subevent=OuterRef('subevent'))
| (Q(subevent__isnull=True))
).order_by().values('order__event').annotate(
c=Count('*')
).values('c')
# Finally, we put all of this together. We force empty subquery aggregates to 0 by using Coalesce()
# and decide which subquery to use for this row. In the end, we compute an integer percentage in case
# we want to display a progress bar.
return qs.annotate(
checkin_count=Coalesce(Subquery(cqs, output_field=models.IntegerField()), 0),
position_count=Coalesce(Case(
When(all_products=True, then=Subquery(pqs_all, output_field=models.IntegerField())),
default=Subquery(pqs_limited, output_field=models.IntegerField()),
output_field=models.IntegerField()
), 0)
checkin_count=Coalesce(
Case(
When(include_pending=True, then=Subquery(cqs_paid_and_pending, output_field=models.IntegerField())),
default=Subquery(cqs_paid, output_field=models.IntegerField()),
output_field=models.IntegerField()
),
0
),
position_count=Coalesce(
Case(
When(all_products=True, include_pending=False,
then=Subquery(pqs_all_paid, output_field=models.IntegerField())),
When(all_products=True, include_pending=True,
then=Subquery(pqs_all_paid_and_pending, output_field=models.IntegerField())),
When(all_products=False, include_pending=False,
then=Subquery(pqs_limited_paid, output_field=models.IntegerField())),
default=Subquery(pqs_limited_paid_and_pending, output_field=models.IntegerField()),
output_field=models.IntegerField()
),
0
)
).annotate(
percent=Case(
When(position_count__gt=0, then=F('checkin_count') * 100 / F('position_count')),

View File

@@ -43,7 +43,7 @@ class EventMixin:
Returns a shorter formatted string containing the start date of the event with respect
to the current locale and to the ``show_times`` setting.
"""
tz = tz or pytz.timezone(self.settings.timezone)
tz = tz or self.timezone
return _date(
self.date_from.astimezone(tz),
"SHORT_DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT"
@@ -55,7 +55,7 @@ class EventMixin:
to the current locale and to the ``show_times`` setting. Returns an empty string
if ``show_date_to`` is ``False``.
"""
tz = tz or pytz.timezone(self.settings.timezone)
tz = tz or self.timezone
if not self.settings.show_date_to or not self.date_to:
return ""
return _date(
@@ -68,7 +68,7 @@ class EventMixin:
Returns a formatted string containing the start date of the event with respect
to the current locale and to the ``show_times`` setting.
"""
tz = tz or pytz.timezone(self.settings.timezone)
tz = tz or self.timezone
return _date(
self.date_from.astimezone(tz),
"DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT"
@@ -79,7 +79,7 @@ class EventMixin:
Returns a formatted string containing the start time of the event, ignoring
the ``show_times`` setting.
"""
tz = tz or pytz.timezone(self.settings.timezone)
tz = tz or self.timezone
return _date(
self.date_from.astimezone(tz), "TIME_FORMAT"
)
@@ -90,7 +90,7 @@ class EventMixin:
to the current locale and to the ``show_times`` setting. Returns an empty string
if ``show_date_to`` is ``False``.
"""
tz = tz or pytz.timezone(self.settings.timezone)
tz = tz or self.timezone
if not self.settings.show_date_to or not self.date_to:
return ""
return _date(
@@ -100,23 +100,30 @@ class EventMixin:
def get_date_range_display(self, tz=None) -> str:
"""
Returns a formatted string containing the start date and the event date
Returns a formatted string containing the start date and the end date
of the event with respect to the current locale and to the ``show_times`` and
``show_date_to`` settings.
"""
tz = tz or pytz.timezone(self.settings.timezone)
tz = tz or self.timezone
if not self.settings.show_date_to or not self.date_to:
return _date(self.date_from.astimezone(tz), "DATE_FORMAT")
return daterange(self.date_from.astimezone(tz), self.date_to.astimezone(tz))
@property
def timezone(self):
return pytz.timezone(self.settings.timezone)
@property
def presale_has_ended(self):
"""
Is true, when ``presale_end`` is set and in the past.
"""
if self.presale_end and now() > self.presale_end:
return True
return False
if self.presale_end:
return now() > self.presale_end
elif self.date_to:
return now() > self.date_to
else:
return now().astimezone(self.timezone).date() > self.date_from.astimezone(self.timezone).date()
@property
def presale_is_running(self):
@@ -126,9 +133,7 @@ class EventMixin:
"""
if self.presale_start and now() < self.presale_start:
return False
if self.presale_end and now() > self.presale_end:
return False
return True
return not self.presale_has_ended
@property
def event_microdata(self):
@@ -229,7 +234,8 @@ class Event(EventMixin, LoggedModel):
presale_end = models.DateTimeField(
null=True, blank=True,
verbose_name=_("End of presale"),
help_text=_("Optional. No products will be sold after this date."),
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."),
)
presale_start = models.DateTimeField(
null=True, blank=True,
@@ -262,6 +268,13 @@ class Event(EventMixin, LoggedModel):
def __str__(self):
return str(self.name)
@property
def presale_has_ended(self):
if self.has_subevents:
return self.presale_end and now() > self.presale_end
else:
return super().presale_has_ended
def save(self, *args, **kwargs):
obj = super().save(*args, **kwargs)
self.cache.clear()
@@ -323,10 +336,6 @@ class Event(EventMixin, LoggedModel):
else:
return get_connection(fail_silently=False)
@property
def timezone(self):
return pytz.timezone(self.settings.timezone)
@property
def payment_term_last(self):
"""
@@ -428,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)
@@ -544,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()
@@ -590,7 +598,8 @@ class SubEvent(EventMixin, LoggedModel):
presale_end = models.DateTimeField(
null=True, blank=True,
verbose_name=_("End of presale"),
help_text=_("Optional. No products will be sold after this date."),
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."),
)
presale_start = models.DateTimeField(
null=True, blank=True,
@@ -646,6 +655,10 @@ class SubEvent(EventMixin, LoggedModel):
data.update({v.property.name: v.value for v in self.meta_values.select_related('property').all()})
return data
@property
def currency(self):
return self.event.currency
def allow_delete(self):
return self.event.subevents.count() > 1

View File

@@ -83,7 +83,8 @@ class Invoice(models.Model):
foreign_currency_display = models.CharField(max_length=50, null=True, blank=True)
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!"),
@@ -682,8 +691,9 @@ class Question(LoggedModel):
blank=True,
help_text=_('This question will be asked to buyers of the selected products')
)
position = models.IntegerField(
default=0
position = models.PositiveIntegerField(
default=0,
verbose_name=_("Position")
)
ask_during_checkin = models.BooleanField(
verbose_name=_('Ask during check-in instead of in the ticket buying process'),
@@ -705,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()
@@ -775,14 +803,45 @@ 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")
ordering = ('position', 'id')
class Quota(LoggedModel):
"""
@@ -799,7 +858,7 @@ class Quota(LoggedModel):
Please read the documentation section on quotas carefully before doing
anything with quotas. This might confuse you otherwise.
http://docs.pretix.eu/en/latest/development/concepts.html#restriction-by-number
https://docs.pretix.eu/en/latest/development/concepts.html#quotas
The AVAILABILITY_* constants represent various states of a quota allowing
its items/variations to be up for sale.

View File

@@ -162,6 +162,13 @@ class Order(LoggedModel):
help_text=_("The text entered in this field will not be visible to the user and is available for your "
"convenience.")
)
checkin_attention = models.BooleanField(
verbose_name=_('Requires special attention'),
default=False,
help_text=_('If you set this, the check-in app will show a visible warning that tickets of this order require '
'special attention. This will not show any details or custom message, so you need to brief your '
'check-in staff how to handle these cases.')
)
expiry_reminder_sent = models.BooleanField(
default=False
)
@@ -395,6 +402,9 @@ class Order(LoggedModel):
"""
from pretix.base.services.mail import SendMailException, mail, render_mail
if not self.email:
return
with language(self.locale):
recipient = self.email
try:
@@ -461,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
@@ -658,10 +669,12 @@ class OrderFee(models.Model):
"""
FEE_TYPE_PAYMENT = "payment"
FEE_TYPE_SHIPPING = "shipping"
FEE_TYPE_SERVICE = "service"
FEE_TYPE_OTHER = "other"
FEE_TYPES = (
(FEE_TYPE_PAYMENT, _("Payment fee")),
(FEE_TYPE_SHIPPING, _("Shipping fee")),
(FEE_TYPE_SERVICE, _("Service fee")),
(FEE_TYPE_OTHER, _("Other fees")),
)
@@ -957,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)
@@ -966,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,5 +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 _
@@ -8,6 +10,7 @@ from i18nfield.fields import I18nCharField
from pretix.base.decimal import round_decimal
from pretix.base.models.base import LoggedModel
from pretix.base.templatetags.money import money_filter
class TaxedPrice:
@@ -23,6 +26,13 @@ class TaxedPrice:
def __repr__(self):
return '{} + {}% = {}'.format(localize(self.net), localize(self.rate), localize(self.gross))
def print(self, currency):
return '{} + {}% = {}'.format(
money_filter(self.net, currency),
localize(self.rate),
money_filter(self.gross, currency)
)
TAXED_ZERO = TaxedPrice(
gross=Decimal('0.00'),
@@ -80,6 +90,7 @@ class TaxRule(LoggedModel):
help_text=_('Your country of residence. This is the country the EU reverse charge rule will not apply in, '
'if configured above.'),
)
custom_rules = models.TextField(blank=True, null=True)
def allow_delete(self):
from pretix.base.models.orders import OrderFee, OrderPosition
@@ -103,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:
@@ -114,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(
@@ -129,10 +144,12 @@ class TaxRule(LoggedModel):
if base_price_is == 'gross':
gross = base_price
net = gross - round_decimal(base_price * (1 - 100 / (100 + self.rate)))
net = round_decimal(gross - (base_price * (1 - 100 / (100 + self.rate))),
self.event.currency if self.event else None)
elif base_price_is == 'net':
net = base_price
gross = round_decimal(net * (1 + self.rate / 100))
gross = round_decimal((net * (1 + self.rate / 100)),
self.event.currency if self.event else None)
else:
raise ValueError('Unknown base price type: {}'.format(base_price_is))
@@ -141,7 +158,27 @@ class TaxRule(LoggedModel):
rate=self.rate, name=self.name
)
def get_matching_rule(self, invoice_address):
rules = json.loads(self.custom_rules)
for r in rules:
if r['country'] == 'EU' and str(invoice_address.country) not in EU_COUNTRIES:
continue
if r['country'] not in ('ZZ', 'EU') and r['country'] != str(invoice_address.country):
continue
if r['address_type'] == 'individual' and invoice_address.is_business:
continue
if r['address_type'] in ('business', 'business_vat_id') and not invoice_address.is_business:
continue
if r['address_type'] == 'business_vat_id' and (not invoice_address.vat_id or not invoice_address.vat_id_validated):
continue
return r
return {'action': 'vat'}
def is_reverse_charge(self, invoice_address):
if self.custom_rules:
rule = self.get_matching_rule(invoice_address)
return rule['action'] == 'reverse'
if not self.eu_reverse_charge:
return False
@@ -160,6 +197,10 @@ class TaxRule(LoggedModel):
return False
def tax_applicable(self, invoice_address):
if self.custom_rules:
rule = self.get_matching_rule(invoice_address)
return rule.get('action', 'vat') == 'vat'
if not self.eu_reverse_charge:
# No reverse charge rules? Always apply VAT!
return True

View File

@@ -1,4 +1,4 @@
from decimal import Decimal
from decimal import ROUND_HALF_UP, Decimal
from django.conf import settings
from django.core.exceptions import ValidationError
@@ -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
@@ -42,7 +42,7 @@ class Voucher(LoggedModel):
:param max_usages: The number of times this voucher can be redeemed
:type max_usages: int
:param redeemed: The number of times this voucher already has been redeemed
:type redeemed: bool
:type redeemed: int
:param valid_until: The expiration date of this voucher (optional)
:type valid_until: datetime
:param block_quota: If set to true, this voucher will reserve quota for its holder
@@ -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):
@@ -368,9 +366,15 @@ class Voucher(LoggedModel):
"""
if self.value is not None:
if self.price_mode == 'set':
return self.value
p = self.value
elif self.price_mode == 'subtract':
return max(original_price - self.value, Decimal('0.00'))
p = max(original_price - self.value, Decimal('0.00'))
elif self.price_mode == 'percent':
return round_decimal(original_price * (Decimal('100.00') - self.value) / Decimal('100.00'))
p = round_decimal(original_price * (Decimal('100.00') - self.value) / Decimal('100.00'))
else:
p = original_price
places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
if places < 2:
return p.quantize(Decimal('1') / 10 ** places, ROUND_HALF_UP)
return p
return original_price

View File

@@ -2,11 +2,12 @@ import logging
from collections import OrderedDict, namedtuple
from django.dispatch import receiver
from django.utils.formats import date_format, localize
from django.utils.formats import date_format
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import Event, LogEntry
from pretix.base.signals import register_notification_types
from pretix.base.templatetags.money import money_filter
from pretix.helpers.urls import build_absolute_uri
logger = logging.getLogger(__name__)
@@ -174,7 +175,7 @@ class ParametrizedOrderNotificationType(NotificationType):
url=order_url
)
n.add_attribute(_('Order code'), order.code)
n.add_attribute(_('Order total'), '{} {}'.format(localize(order.total), logentry.event.currency))
n.add_attribute(_('Order total'), money_filter(order.total, logentry.event.currency))
n.add_attribute(_('Order date'), date_format(order.datetime, 'SHORT_DATETIME_FORMAT'))
n.add_attribute(_('Order status'), order.get_status_display())
n.add_attribute(_('Order positions'), str(order.positions.count()))
@@ -203,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

@@ -1,10 +1,11 @@
import logging
from collections import OrderedDict
from decimal import Decimal
from decimal import ROUND_HALF_UP, Decimal
from typing import Any, Dict, Union
import pytz
from django import forms
from django.conf import settings
from django.contrib import messages
from django.dispatch import receiver
from django.forms import Form
@@ -15,11 +16,11 @@ from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from i18nfield.forms import I18nFormField, I18nTextarea
from i18nfield.strings import LazyI18nString
from pretix.base.decimal import round_decimal
from pretix.base.models import CartPosition, Event, Order, Quota
from pretix.base.reldate import RelativeDateField, RelativeDateWrapper
from pretix.base.settings import SettingsSandbox
from pretix.base.signals import register_payment_providers
from pretix.helpers.money import DecimalTextInput
from pretix.presale.views import get_cart_total
from pretix.presale.views.cart import get_or_create_cart_id
@@ -50,6 +51,16 @@ class BasePaymentProvider:
def __str__(self):
return self.identifier
@property
def is_implicit(self) -> bool:
"""
Returns whether or whether not this payment provider is an "implicit" payment provider that will
*always* and unconditionally be used if is_allowed() returns True and does not require any input.
This is intended to be used by the FreePaymentProvider, which skips the payment choice page.
By default, this returns ``False``. Please do not set this if you don't know exactly what you are doing.
"""
return False
@property
def is_meta(self) -> bool:
"""
@@ -81,10 +92,15 @@ class BasePaymentProvider:
fee_abs = self.settings.get('_fee_abs', as_type=Decimal, default=0)
fee_percent = self.settings.get('_fee_percent', as_type=Decimal, default=0)
fee_reverse_calc = self.settings.get('_fee_reverse_calc', as_type=bool, default=True)
places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
if fee_reverse_calc:
return round_decimal((price + fee_abs) * (1 / (1 - fee_percent / 100)) - price)
return ((price + fee_abs) * (1 / (1 - fee_percent / 100)) - price).quantize(
Decimal('1') / 10 ** places, ROUND_HALF_UP
)
else:
return round_decimal(price * fee_percent / 100) + fee_abs
return (price * fee_percent / 100 + fee_abs).quantize(
Decimal('1') / 10 ** places, ROUND_HALF_UP
)
@property
def verbose_name(self) -> str:
@@ -146,41 +162,19 @@ class BasePaymentProvider:
.. WARNING:: It is highly discouraged to alter the ``_enabled`` field of the default
implementation.
"""
places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
return OrderedDict([
('_enabled',
forms.BooleanField(
label=_('Enable payment method'),
required=False,
)),
('_fee_abs',
forms.DecimalField(
label=_('Additional fee'),
help_text=_('Absolute value'),
required=False
)),
('_fee_percent',
forms.DecimalField(
label=_('Additional fee'),
help_text=_('Percentage of the order total. Note that this percentage will currently only '
'be calculated on the summed price of sold tickets, not on other fees like e.g. shipping '
'fees, if there are any.'),
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.'),
help_text=_('We recommend to enable this if you want your users to pay the payment fees of your '
'payment provider. <a href="{docs_url}" target="_blank" rel="noopener">Click here '
'for detailed information on what this does.</a> Don\'t forget to set the correct fees '
'above!').format(docs_url='https://docs.pretix.eu/en/latest/user/payments/fees.html'),
required=False
)),
('_invoice_text',
I18nFormField(
label=_('Text on invoices'),
@@ -191,6 +185,33 @@ class BasePaymentProvider:
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}}
)),
('_fee_abs',
forms.DecimalField(
label=_('Additional fee'),
help_text=_('Absolute value'),
localize=True,
required=False,
decimal_places=places,
widget=DecimalTextInput(places=places)
)),
('_fee_percent',
forms.DecimalField(
label=_('Additional fee'),
help_text=_('Percentage of the order total. Note that this percentage will currently only '
'be calculated on the summed price of sold tickets, not on other fees like e.g. shipping '
'fees, if there are any.'),
localize=True,
required=False,
)),
('_fee_reverse_calc',
forms.BooleanField(
label=_('Calculate the fee from the total value including the fee.'),
help_text=_('We recommend to enable this if you want your users to pay the payment fees of your '
'payment provider. <a href="{docs_url}" target="_blank" rel="noopener">Click here '
'for detailed information on what this does.</a> Don\'t forget to set the correct fees '
'above!').format(docs_url='https://docs.pretix.eu/en/latest/user/payments/fees.html'),
required=False
)),
])
def settings_content_render(self, request: HttpRequest) -> str:
@@ -199,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:
"""
@@ -552,6 +573,10 @@ class PaymentException(Exception):
class FreeOrderProvider(BasePaymentProvider):
@property
def is_implicit(self) -> bool:
return True
@property
def is_enabled(self) -> bool:
return True

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

@@ -112,7 +112,7 @@ class CartManager:
def _check_presale_dates(self):
if self.event.presale_start and self.now_dt < self.event.presale_start:
raise CartError(error_messages['not_started'])
if self.event.presale_end and self.now_dt > self.event.presale_end:
if self.event.presale_has_ended:
raise CartError(error_messages['ended'])
def _extend_expiry_of_valid_existing_positions(self):
@@ -188,7 +188,7 @@ class CartManager:
if op.subevent and op.subevent.presale_start and self.now_dt < op.subevent.presale_start:
raise CartError(error_messages['not_started'])
if op.subevent and op.subevent.presale_end and self.now_dt > op.subevent.presale_end:
if op.subevent and op.subevent.presale_has_ended:
raise CartError(error_messages['ended'])
if isinstance(op, self.AddOperation):
@@ -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:
@@ -667,7 +667,8 @@ def get_fees(event, request, total, invoice_address, provider):
tax_rule=payment_fee_tax_rule
))
for recv, resp in fee_calculation_for_cart.send(sender=event, request=request, invoice_address=invoice_address):
for recv, resp in fee_calculation_for_cart.send(sender=event, request=request, invoice_address=invoice_address,
total=total):
fees += resp
return fees

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

@@ -39,6 +39,7 @@ def notify(logentry_id: int):
notify_global = {
(ns.user, ns.method): ns.enabled
for ns in NotificationSetting.objects.filter(
event__isnull=True,
action_type=logentry.action_type,
user__pk__in=users.values_list('pk', flat=True)
)

View File

@@ -17,7 +17,7 @@ from django.utils.timezone import make_aware, now
from django.utils.translation import ugettext as _
from pretix.base.i18n import (
LazyDate, LazyLocaleException, LazyNumber, language,
LazyCurrencyNumber, LazyDate, LazyLocaleException, LazyNumber, language,
)
from pretix.base.models import (
CartPosition, Event, Item, ItemVariation, Order, OrderPosition, Quota,
@@ -321,7 +321,7 @@ class OrderError(LazyLocaleException):
def _check_date(event: Event, now_dt: datetime):
if event.presale_start and now_dt < event.presale_start:
raise OrderError(error_messages['not_started'])
if event.presale_end and now_dt > event.presale_end:
if event.presale_has_ended:
raise OrderError(error_messages['ended'])
@@ -361,7 +361,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
cp.delete()
break
if cp.subevent and cp.subevent.presale_end and now_dt > cp.subevent.presale_end:
if cp.subevent and cp.subevent.presale_has_ended:
err = err or error_messages['some_subevent_ended']
cp.delete()
break
@@ -439,8 +439,8 @@ def _get_fees(positions: List[CartPosition], payment_provider: BasePaymentProvid
fees.append(OrderFee(fee_type=OrderFee.FEE_TYPE_PAYMENT, value=payment_fee,
internal_type=payment_provider.identifier))
for recv, resp in order_fee_calculation.send(sender=event, invoice_address=address,
meta_info=meta_info, posiitons=positions):
for recv, resp in order_fee_calculation.send(sender=event, invoice_address=address, total=total,
meta_info=meta_info, positions=positions):
fees += resp
return fees
@@ -504,6 +504,8 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
for fee in fees:
fee.order = order
fee._calculate_tax()
if fee.tax_rule and not fee.tax_rule.pk:
fee.tax_rule = None # TODO: deprecate
fee.save()
OrderPosition.transform_cart_positions(positions, order)
@@ -521,6 +523,9 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
if not pprov:
raise OrderError(error_messages['internal'])
if email == settings.PRETIX_EMAIL_NONE_VALUE:
email = None
addr = None
if address is not None:
try:
@@ -542,44 +547,49 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
invoice = order.invoices.last() # Might be generated by plugin already
if event.settings.get('invoice_generate') == 'True' and invoice_qualified(order):
if not invoice:
invoice = generate_invoice(order, trigger_pdf=not event.settings.invoice_email_attachment)
invoice = generate_invoice(
order,
trigger_pdf=not event.settings.invoice_email_attachment or not order.email
)
# send_mail will trigger PDF generation later
if order.payment_provider == 'free':
email_template = event.settings.mail_text_order_free
log_entry = 'pretix.event.order.email.order_free'
else:
email_template = event.settings.mail_text_order_placed
log_entry = 'pretix.event.order.email.order_placed'
if order.email:
if order.payment_provider == 'free':
email_template = event.settings.mail_text_order_free
log_entry = 'pretix.event.order.email.order_free'
else:
email_template = event.settings.mail_text_order_placed
log_entry = 'pretix.event.order.email.order_placed'
try:
invoice_name = order.invoice_address.name
invoice_company = order.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
email_context = {
'total': LazyNumber(order.total),
'currency': event.currency,
'date': LazyDate(order.expires),
'event': event.name,
'url': build_absolute_uri(event, 'presale:event.order', kwargs={
'order': order.code,
'secret': order.secret
}),
'payment_info': str(pprov.order_pending_mail_render(order)),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
}
email_subject = _('Your order: %(code)s') % {'code': order.code}
try:
order.send_mail(
email_subject, email_template, email_context,
log_entry,
invoices=[invoice] if invoice and event.settings.invoice_email_attachment else []
)
except SendMailException:
logger.exception('Order received email could not be sent')
try:
invoice_name = order.invoice_address.name
invoice_company = order.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
email_context = {
'total': LazyNumber(order.total),
'currency': event.currency,
'total_with_currency': LazyCurrencyNumber(order.total, event.currency),
'date': LazyDate(order.expires),
'event': event.name,
'url': build_absolute_uri(event, 'presale:event.order', kwargs={
'order': order.code,
'secret': order.secret
}),
'payment_info': str(pprov.order_pending_mail_render(order)),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
}
email_subject = _('Your order: %(code)s') % {'code': order.code}
try:
order.send_mail(
email_subject, email_template, email_context,
log_entry,
invoices=[invoice] if invoice and event.settings.invoice_email_attachment else []
)
except SendMailException:
logger.exception('Order received email could not be sent')
return order.id
@@ -805,7 +815,7 @@ class OrderChangeManager:
if price is None:
price = get_price(item, variation, subevent=subevent, invoice_address=self._invoice_address)
else:
if item.tax_rule.tax_applicable(self._invoice_address):
if item.tax_rule and item.tax_rule.tax_applicable(self._invoice_address):
price = item.tax(price, base_price_is='gross')
else:
price = TaxedPrice(gross=price, net=price, tax=Decimal('0.00'), rate=Decimal('0.00'), name='')

View File

@@ -1,5 +1,6 @@
from decimal import Decimal
from pretix.base.decimal import round_decimal
from pretix.base.models import (
AbstractPosition, InvoiceAddress, Item, ItemAddOn, ItemVariation, Voucher,
)
@@ -59,4 +60,8 @@ def get_price(item: Item, variation: ItemVariation = None,
price.gross = price.net
price.name = ''
price.gross = round_decimal(price.gross, item.event.currency)
price.net = round_decimal(price.net, item.event.currency)
price.tax = price.gross - price.net
return price

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