Compare commits

...

1058 Commits

Author SHA1 Message Date
Raphael Michel
b704d21e88 Bump to 3.2.0 2019-10-10 18:16:25 +02:00
Raphael Michel
4bfe0e3784 Order change manager: Allow to add multiple products 2019-10-10 12:59:16 +02:00
Raphael Michel
d4d046ca60 Order change manager: Allow to disable invoice issuing 2019-10-10 12:19:06 +02:00
Raphael Michel
fb3fc05522 Remove references to our legacy apps 2019-10-10 09:27:14 +02:00
Raphael Michel
fdcd750487 Disable autocomplete in context switcher 2019-10-09 17:16:47 +02:00
Raphael Michel
92754136a6 Refs #1432 -- Proper grouping of autocomplete properties 2019-10-09 12:40:05 +02:00
Raphael Michel
3b4d39ec27 Fix #1432 -- Correct autocomplete attributes of name part fields 2019-10-09 12:40:05 +02:00
Sohalt
88bb3b483c Only disable up arrow on first page and down arrow on last page (#1437) 2019-10-09 09:15:35 +02:00
Raphael Michel
247370839b Fix placeholder form validation 2019-10-09 08:58:05 +02:00
Raphael Michel
b0510f47b3 Move new option down a little 2019-10-08 17:43:28 +02:00
Raphael Michel
4fa086bbc5 Update from Weblate (#1435)
Update from Weblate
2019-10-08 16:09:46 +02:00
Raphael Michel
cb03e7c843 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3218 of 3218 strings)

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

powered by weblate
2019-10-08 14:09:12 +00:00
Raphael Michel
d012d0804a Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3218 of 3218 strings)

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

powered by weblate
2019-10-08 14:09:11 +00:00
Raphael Michel
3731d5f431 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-10-08 15:57:05 +02:00
Raphael Michel
9f04d53564 Add safety mechanism for order code generation 2019-10-08 15:53:37 +02:00
Martin Gross
748a389acb Auto-check-in for specific sales channels (#1409)
* Autocheckin data model/cosmetics

* Expose automatically checked-in OrderPositions

* Expose automatically checked-in OrderPositions in CSV/PDF Exports

* Fix some tests, try to fix MultiStringField/CheckboxSelectMultiple

* Actually fix MultiStringField/CheckboxSelectMultiple.
(Not pretty, but it works)

* Fix more tests

* Squash migration

* Also fix CSV/nameparts-test

* Changes for Autocheckin code-review

* Perform Auto-Checkins through new core plugin

* Update config-doc to reflect also checkinlists

* Explicitly output AutoCheckin Yes/No for CSV-Export (+ fix test)

* Move autocheckin from plugin to service

* API-doc

* Fix API-doc spelling

* Checkinlist-API and autocheckin order tests

* Performance improvement when reading checkinlists for autocheckin

Co-Authored-By: Raphael Michel <michel@rami.io>

* Autocheckin test for order created through API

* Resolve migration conflict
2019-10-08 15:50:22 +02:00
Sohalt
05a1df244b Fix #1388 -- Prevent some words from occurring in order codes (#1422)
* prevent some words from occurring in order codes

* Use regex to match against blacklist

* Prevent some words from occurring in voucher codes

* Rename blacklist to banlist
2019-10-08 14:28:51 +02:00
Raphael Michel
9f7d5156cc Refs #1430 -- Fix untranslated string 2019-10-08 12:29:10 +02:00
Martin Gross
143fe6c1a6 Fix #1430 - Fix fieldname-filter for BaseInvoiceNameForm 2019-10-07 17:48:18 +02:00
Raphael Michel
1e3ccc4449 Merge pull request #1431 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-10-07 12:16:32 +02:00
Raphael Michel
d09000b716 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3210 of 3210 strings)

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

powered by weblate
2019-10-07 10:14:52 +00:00
Raphael Michel
fc4572767e Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3210 of 3210 strings)

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

powered by weblate
2019-10-07 10:14:51 +00:00
Raphael Michel
eeedd9a9c2 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-10-07 11:49:10 +02:00
Raphael Michel
1d0c148170 Fix #467 -- Pluggable email placeholders (#1429)
* Fix #467 -- Pluggable email placeholders

* Previews

* Polishing

* Fix tests

* Add missing doc file
2019-10-07 11:48:25 +02:00
Raphael Michel
cb37e7435d Use a different-colored favicon in development mode 2019-10-07 09:03:46 +02:00
Raphael Michel
749ddbf21c Allow topbar navigation subitems not to have an URL 2019-10-06 17:02:53 +02:00
Raphael Michel
9f6634025f Update from Weblate (#1425)
Update from Weblate
2019-10-06 11:56:32 +02:00
Chris Spy
ad039ae08e Translated on translate.pretix.eu (Greek)
Currently translated at 96.1% (99 of 103 strings)

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

powered by weblate
2019-10-06 09:53:51 +00:00
Chris Spy
eeaaca574d Translated on translate.pretix.eu (Greek)
Currently translated at 95.5% (3076 of 3220 strings)

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

powered by weblate
2019-10-06 09:53:51 +00:00
Fabian Rodriguez
bef15ec442 Translated on translate.pretix.eu (French)
Currently translated at 100.0% (103 of 103 strings)

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

powered by weblate
2019-10-06 09:53:51 +00:00
Raphael Michel
447a8b0a8c Add missing mark_safe call 2019-10-06 11:53:37 +02:00
Sohalt
05b4d954d9 Make voucher code in notification clickable (#1423)
* Make voucher code in notification clickable

* Move html out of translated string
2019-10-06 11:52:34 +02:00
Felix Rindt
515d8c4899 remove .csv from default filename in List Exporter (#1428) 2019-10-06 11:51:24 +02:00
Raphael Michel
82497cfb89 Lazy-load widgets on global dashboard 2019-10-06 11:46:42 +02:00
Raphael Michel
4ade9d39cd Add "back" parameter to logout view 2019-10-06 11:35:29 +02:00
Raphael Michel
4360d5652b ALlow to pre-select organizer for event creation 2019-10-06 11:35:29 +02:00
Raphael Michel
9fca3188b2 Device and team creation: List events ordered and with date 2019-10-04 17:28:48 +02:00
Raphael Michel
0ed48fac7f Add pagination to event list on organizer detail page 2019-10-04 17:28:48 +02:00
Raphael Michel
27a32173e6 Move more code into change_payment_provider 2019-10-04 17:28:48 +02:00
Martin Gross
81b6188777 New ApplePay domain association file for Stripe 2019-10-02 21:31:40 +02:00
Raphael Michel
9e85d3c94c PDF editor: Catch ValueError during float conversion 2019-09-30 14:37:09 +02:00
Raphael Michel
4e58ba7594 money_filter: Idempotency on empty strings
PRETIXEU-1EH
2019-09-30 14:37:09 +02:00
Raphael Michel
248493dbf2 Stripe plugin: Fix AttributeError 2019-09-29 18:32:19 +02:00
Raphael Michel
f1bd240096 Update from Weblate (#1417)
Update from Weblate
2019-09-29 14:05:15 +02:00
Serge Bazanski
0b673dc68c Translated on translate.pretix.eu (Polish)
Currently translated at 100.0% (103 of 103 strings)

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

powered by weblate
2019-09-27 07:11:22 +00:00
Serge Bazanski
b2a7fe13da Translated on translate.pretix.eu (Polish)
Currently translated at 14.7% (474 of 3220 strings)

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

powered by weblate
2019-09-27 07:11:22 +00:00
Martin Gross
d28f735fca Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3220 of 3220 strings)

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

powered by weblate
2019-09-27 07:11:22 +00:00
Jonathan Oberländer
7a3b450bc6 Added translation on translate.pretix.eu (Romanian) 2019-09-27 07:11:22 +00:00
Raphael Michel
f1ec129c0a Fix ZeroDivisionError 2019-09-27 09:11:03 +02:00
Raphael Michel
ce6e46dfd2 Fix performance of check-in list API list 2019-09-26 15:18:53 +02:00
Martin Gross
f296f262e6 Properly indent handling for non-addons 2019-09-23 11:12:50 +02:00
Martin Gross
7f8d290ae1 Add-Ons inhert question-answers from parent item if necessary 2019-09-23 10:47:05 +02:00
Raphael Michel
ca0c0f4ae3 Revert to multipart/related 2019-09-20 11:22:11 +02:00
Sohalt
ac1e69f2d8 Install pretix into system-site-packages in Docker image (#1410) 2019-09-20 09:43:00 +02:00
Raphael Michel
6338cc69ae docs: fix BaseDataShredder class documentation (#1412)
The BaseDataShredder documentation contained two references to BaseInvoiceRenderer, probably due to a minor copy/paste mistake
2019-09-20 09:41:32 +02:00
Felix Rindt
39eaf3ad6a Code style improvements (#1411)
* docstring corrections

* move omit_hyphen formfield
2019-09-20 09:34:24 +02:00
Raphael Michel
76e75bef65 Fix broken organizer settings test 2019-09-19 18:33:30 +02:00
Raphael Michel
a39822aedc Use transaction aware task for regenerate_css 2019-09-19 18:17:43 +02:00
Raphael Michel
73d5a2cec0 Revert "Make all EventTasks transaction-aware"
This reverts commit 3f7807d242.
2019-09-19 18:15:47 +02:00
Raphael Michel
9f668e5fd6 Fix crash in OrderEmailHistory for unknown orders 2019-09-19 18:04:31 +02:00
Raphael Michel
1b92a891d7 Fix issues with context providers in error pages 2019-09-19 18:03:35 +02:00
Raphael Michel
827925e3c9 Fix bug in 3f7807d24 2019-09-19 18:03:28 +02:00
Johannes Lauinger
c0be574974 docs: fix BaseDataShredder class documentation
The BaseDataShredder documentation contained two references to BaseInvoiceRenderer, probably due to a minor copy/paste mistake
2019-09-19 17:57:07 +02:00
Raphael Michel
738413e8fd Allow to copy categories and quotas 2019-09-19 16:59:25 +02:00
Raphael Michel
2c6125adeb Allow to copy vouchers 2019-09-19 16:33:59 +02:00
Raphael Michel
3f7807d242 Make all EventTasks transaction-aware 2019-09-19 16:23:40 +02:00
Raphael Michel
59b7f0a03d Fix a new bug in Stripe webhooks PRETIXEU-64 2019-09-19 10:36:33 +02:00
Raphael Michel
b7dea16db3 Promote danish to an inofficial language 2019-09-19 10:13:34 +02:00
Raphael Michel
3a81706aeb Fix KeyError in bank import 2019-09-17 16:48:41 +02:00
Raphael Michel
ad3369b059 Fix issue on old-style webhook URLs (#1385)
* Fix issue on old-style webhook URLs

* Fix test issue
2019-09-17 13:59:57 +02:00
Martin Gross
ab0709558d Fix #1389: PaymentIntents are 'processing' and not 'pending' 2019-09-17 13:03:22 +02:00
Raphael Michel
e2e64ac01d Event selector: Prevent race conditions 2019-09-17 10:08:58 +02:00
Martin Gross
cf14dcf889 Add Subevent-Filter for Voucher-Tags (#1407)
* Add Subevent-Filter for Voucher-Tags

* Filter Subevent Voucher-Tags with proper Filter

* Apply filter before annotating totals and usage
2019-09-16 14:08:23 +02:00
Raphael Michel
a884a25d2b Update from Weblate (#1403)
Update from Weblate
2019-09-16 13:42:41 +02:00
Gianmarco Palumbo
8493778028 Translated on translate.pretix.eu (Italian)
Currently translated at 44.7% (46 of 103 strings)

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

powered by weblate
2019-09-13 18:00:10 +00:00
Martin Gross
36a276c360 Translated on translate.pretix.eu (French)
Currently translated at 72.5% (2335 of 3220 strings)

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

powered by weblate
2019-09-13 18:00:09 +00:00
Maarten van den Berg
795c423d73 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3220 of 3220 strings)

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

powered by weblate
2019-09-12 22:00:12 +00:00
Maarten van den Berg
3ce9ec79f0 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3220 of 3220 strings)

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

powered by weblate
2019-09-12 22:00:10 +00:00
Translators EN IT Team
debc5e255b Translated on translate.pretix.eu (Italian)
Currently translated at 4.7% (151 of 3220 strings)

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

powered by weblate
2019-09-12 00:00:09 +00:00
Raphael Michel
145aa0d7fd Merge pull request #1397 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-09-11 08:58:42 +02:00
Raphael Michel
3997bdd098 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3220 of 3220 strings)

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

powered by weblate
2019-09-11 06:58:06 +00:00
Raphael Michel
a51a905512 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3220 of 3220 strings)

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

powered by weblate
2019-09-11 06:58:06 +00:00
Maarten van den Berg
d3ac9e8880 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3220 of 3220 strings)

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

powered by weblate
2019-09-11 06:55:48 +00:00
Maarten van den Berg
07402c9ea0 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3220 of 3220 strings)

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

powered by weblate
2019-09-11 06:55:47 +00:00
Raphael Michel
5f81bf55ca Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-09-11 08:55:36 +02:00
Raphael Michel
0120a5a930 Clarify cancellation description 2019-09-11 08:55:03 +02:00
Raphael Michel
ba555f956e Fix #1395 -- Incorrect data in tax list exporter if an order contains multiple tax rates 2019-09-10 14:41:49 +02:00
Raphael Michel
5e3876ddde Tax exporters: Use lazy gettext calls 2019-09-10 14:40:05 +02:00
Raphael Michel
f5719687aa Use lazy translation for exporter name 2019-09-10 13:13:15 +02:00
Raphael Michel
18449efcc7 Fix #1394 -- Forcefully update defusedcsv 2019-09-10 12:10:41 +02:00
Raphael Michel
3bb20d943a Fix incorrect condition in template 2019-09-10 11:58:02 +02:00
Raphael Michel
586e544fce Add "resend link" option to attendees 2019-09-10 11:44:59 +02:00
Raphael Michel
3a4fc69db1 Add link to ticket page to order page in backend 2019-09-10 11:34:52 +02:00
Raphael Michel
8b5d49d82f Dekodi exporter: Fix AttributeError 2019-09-10 11:34:19 +02:00
Raphael Michel
7665faa39f Bump version to 3.2.0.dev0 2019-09-10 10:54:25 +02:00
Raphael Michel
027d28e646 Bump verstion to 3.1.0 2019-09-10 10:50:18 +02:00
Raphael Michel
babcf66a2e Update from Weblate (#1393)
Update from Weblate
2019-09-10 10:18:46 +02:00
Raphael Michel
20f9e88d61 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3220 of 3220 strings)

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

powered by weblate
2019-09-10 08:16:51 +00:00
Raphael Michel
9594607a8c Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3220 of 3220 strings)

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

powered by weblate
2019-09-10 08:16:50 +00:00
Raphael Michel
ad3bcaf43a Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-09-10 10:00:56 +02:00
Raphael Michel
2c4ee3b3c7 Replace U2F with WebAuthn (#1392)
* Replace U2F with WebAuthn

* Imports

* Fix backwards compatibility

* Add explanatory comment

* Fix tests
2019-09-10 09:58:31 +02:00
Raphael Michel
21451db412 Fix Greek VAT IDs 2019-09-10 09:46:00 +02:00
Raphael Michel
12b1f7d90e Fix #1374 -- Correct encoding of images 2019-09-10 09:39:16 +02:00
Raphael Michel
c3901c567e Correctly respect both attention flag in check-in list exports 2019-09-09 11:07:22 +02:00
Raphael Michel
0a3eddcd5c Merge pull request #1384 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-09-09 09:48:36 +02:00
Raphael Michel
dfa99cd325 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3219 of 3219 strings)

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

powered by weblate
2019-09-09 07:48:08 +00:00
Raphael Michel
62a040255e Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3219 of 3219 strings)

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

powered by weblate
2019-09-09 07:48:07 +00:00
Maarten van den Berg
0e2b02c778 Translated on translate.pretix.eu (Dutch)
Currently translated at 99.9% (3216 of 3219 strings)

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

powered by weblate
2019-09-09 05:00:09 +00:00
Ture Gjørup
0f0ed90be9 Translated on translate.pretix.eu (Danish)
Currently translated at 61.2% (63 of 103 strings)

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

powered by weblate
2019-09-08 19:26:46 +00:00
Raphael Michel
b1e19d776c Fix exporter tests 2019-09-08 21:26:31 +02:00
Raphael Michel
0bcc784aaf Include attention flag and comment in order and check-in list exports 2019-09-08 20:52:15 +02:00
Raphael Michel
262fb82237 Add-ons should not have a seat 2019-09-06 11:54:18 +02:00
Raphael Michel
adbe959314 Merge pull request #1383 from pajowu/pajowu/no_test_data
Remove obsolete reference to test data
2019-09-05 23:04:15 +02:00
pajowu
02d0a68d57 Remove obsolete reference to test data 2019-09-05 19:29:04 +02:00
Raphael Michel
d6985123b4 Regenerate event CSS on plugins change 2019-09-04 15:39:23 +02:00
Raphael Michel
f7a356c340 Fixed bug in card extensions 2019-09-03 12:51:38 +02:00
Raphael Michel
c6265b4517 Fix allow_ignore_quota with bundled items 2019-09-03 12:25:22 +02:00
Raphael Michel
1ee352e114 Fix error in locale file 2019-09-03 11:59:05 +02:00
Raphael Michel
6c830a7d36 Do not enforce voucher constraints for bundled items 2019-09-03 11:51:29 +02:00
Raphael Michel
129c360fff Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-09-03 11:36:55 +02:00
Raphael Michel
b5c7ad92b6 Merge pull request #1379 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-09-03 11:36:19 +02:00
Raphael Michel
33efd8c157 order_overview: Allow to restrict to admission products 2019-09-03 11:34:00 +02:00
Bostjan Marusic
f318c8e017 Translated on translate.pretix.eu (Slovenian)
Currently translated at 21.9% (702 of 3208 strings)

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

powered by weblate
2019-09-03 09:22:31 +00:00
Bostjan Marusic
319334706d Translated on translate.pretix.eu (Slovenian)
Currently translated at 18.8% (602 of 3208 strings)

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

powered by weblate
2019-09-03 09:22:31 +00:00
Raphael Michel
1a25138bef PDF editor: Allow to easily change the page size 2019-09-03 11:22:21 +02:00
Raphael Michel
daa5383b89 Fix a layout animation issue in the sidebar 2019-09-02 18:07:10 +02:00
Raphael Michel
31333280d2 Fix download link for addons on ticket pages 2019-08-31 11:59:17 +02:00
Raphael Michel
b78f8d70e8 Add serializable to spelling whitelist 2019-08-30 17:46:15 +02:00
Raphael Michel
a847538a2e Merge pull request #1367 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-08-30 17:40:38 +02:00
Raphael Michel
e3ef9eba9e Widget: Fix invalid coloring of days in mobile calendar 2019-08-30 17:37:37 +02:00
Bostjan Marusic
9e4a4402fb Translated on translate.pretix.eu (Slovenian)
Currently translated at 15.6% (502 of 3208 strings)

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

powered by weblate
2019-08-30 15:25:28 +00:00
Bostjan Marusic
96ed9f5cf5 Translated on translate.pretix.eu (Slovenian)
Currently translated at 100.0% (103 of 103 strings)

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

powered by weblate
2019-08-30 15:25:28 +00:00
oocf
7216cebce5 Translated on translate.pretix.eu (Spanish)
Currently translated at 94.3% (3025 of 3208 strings)

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

powered by weblate
2019-08-30 15:25:28 +00:00
Bostjan Marusic
a3892fd4de Translated on translate.pretix.eu (Slovenian)
Currently translated at 10.7% (343 of 3208 strings)

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

powered by weblate
2019-08-30 15:25:28 +00:00
saad91
7718462528 Translated on translate.pretix.eu (Arabic)
Currently translated at 0.2% (8 of 3208 strings)

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

powered by weblate
2019-08-30 15:25:28 +00:00
Maarten van den Berg
60b20829f3 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3208 of 3208 strings)

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

powered by weblate
2019-08-30 15:25:28 +00:00
Maarten van den Berg
e11d03f418 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3208 of 3208 strings)

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

powered by weblate
2019-08-30 15:25:28 +00:00
Tobias Sundgren
69e4db58fd Translated on translate.pretix.eu (Swedish)
Currently translated at 100.0% (103 of 103 strings)

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

powered by weblate
2019-08-30 15:25:28 +00:00
Tobias Sundgren
9f27a84f52 Translated on translate.pretix.eu (Swedish)
Currently translated at 1.0% (33 of 3208 strings)

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

powered by weblate
2019-08-30 15:25:28 +00:00
Maarten van den Berg
e5c204dc95 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3208 of 3208 strings)

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

powered by weblate
2019-08-30 15:25:28 +00:00
Raphael Michel
7fc7dd0163 Allow to print question answers on invoices 2019-08-30 17:24:57 +02:00
Raphael Michel
aa99dbc830 Add payment provider specific details to the API 2019-08-30 17:04:22 +02:00
Raphael Michel
e3a4ec93fc Fix user log that always shows empty 2019-08-30 13:01:35 +02:00
Raphael Michel
67da6a18a8 Add missing signal to documentation 2019-08-30 12:58:20 +02:00
Raphael Michel
adc4128f9f Fix exceptions introduced in last commit 2019-08-30 11:56:04 +02:00
Raphael Michel
eed217262f Add signal global_email_filter 2019-08-30 11:02:59 +02:00
Raphael Michel
4bae824a03 Add user argument to email_filter 2019-08-30 11:02:59 +02:00
Martin Gross
be1a1f7995 Fix widget-URL for subevents 2019-08-30 10:41:08 +02:00
Martin Gross
f4b81aa032 Fix import sorting 2019-08-28 17:17:42 +02:00
Martin Gross
2559439c4e Fix eMail-previews for per-attendant emails 2019-08-28 16:48:59 +02:00
Martin Gross
fce9117dfd Fix explanation on per-attendant emails 2019-08-28 16:47:54 +02:00
Martin Gross
4c8dc8f31c Show attendee-name of item - even if item is an add-on 2019-08-28 09:04:41 +02:00
Martin Gross
b4f69fb13f Move question hint-icons into their own columns 2019-08-27 15:43:02 +02:00
Martin Gross
2aed894bd4 Hide hidden questions from users in presale 2019-08-27 15:31:22 +02:00
Martin Gross
2ff5416afb Show hints for hidden/check-in questions in question overview 2019-08-27 15:19:28 +02:00
Raphael Michel
59f7098a70 Clean up duplicate seats when generating seats 2019-08-26 16:57:48 +02:00
Raphael Michel
83dd865b78 Fix crash when de-selecting all languages 2019-08-26 16:44:20 +02:00
Raphael Michel
ebf411b7a0 Fail gracefully if seats exist multiple times 2019-08-26 16:33:35 +02:00
Raphael Michel
733a4ce8f4 Fix seatingframe for subevents 2019-08-26 16:27:43 +02:00
Raphael Michel
ad94263374 Fix i18n of OrderError 2019-08-17 14:07:56 +02:00
Raphael Michel
102772ec55 Disable Sentry in shell sessions 2019-08-17 14:05:06 +02:00
Raphael Michel
9a826b694f Fix a type error PRETIXEU-1BF 2019-08-17 12:39:38 +02:00
Raphael Michel
bcf8e9cd04 Add short option to get_date_*_display 2019-08-16 16:44:01 +02:00
Raphael Michel
200ce93bb4 Prevent autoresponders 2019-08-14 15:09:19 +02:00
Raphael Michel
0582a4d9e5 Lazy widgets: Fix missing ID 2019-08-14 09:57:55 +02:00
Raphael Michel
1cbab04108 Docs: Add API guide on custom checkout 2019-08-14 09:22:58 +02:00
Raphael Michel
98c18b162f Order creation API: Add support for reverse charge 2019-08-14 09:22:58 +02:00
Raphael Michel
d972cd4c49 Add new bundled plugin "returnurl" 2019-08-14 09:22:58 +02:00
Raphael Michel
985f354293 Order API: Add order URL 2019-08-14 09:22:58 +02:00
Raphael Michel
9c23216bd1 Order creation API: Do not allow empty orders 2019-08-14 09:22:58 +02:00
Raphael Michel
f8bf44c262 Order payment flow: Allow to be used as an iframe session start 2019-08-14 09:22:58 +02:00
Raphael Michel
6badfdf576 Order API: Expose explicit URL of payment steps 2019-08-14 09:22:58 +02:00
Raphael Michel
c2eba21359 Order creation API: Allow to create orders without payment provider 2019-08-14 09:22:58 +02:00
Raphael Michel
5cda04a994 Order creation API: Allow to pass vouchers 2019-08-14 09:22:58 +02:00
Raphael Michel
bc9d8f5bd8 Order creation API: Do not consume expired carts 2019-08-14 09:22:58 +02:00
Raphael Michel
9d6ff20191 Order creation API: Allow to auto-calculate prices 2019-08-14 09:22:58 +02:00
Raphael Michel
82684e6df3 Order creation API: Allow to send emails 2019-08-14 09:22:58 +02:00
Martin Gross
d681ae4dce Only output GA if event is using seating 2019-08-13 13:26:44 +02:00
Martin Gross
213e724e18 Include Seating in default PDF ticket template 2019-08-13 13:26:21 +02:00
Raphael Michel
6e0b80706c Badge exporter: Exclude add-on positions by default 2019-08-13 10:11:24 +02:00
Raphael Michel
5363f4206e Lazy-load dashboard widgets 2019-08-12 12:19:02 +02:00
Raphael Michel
9bdb715874 Re-order list of devices 2019-08-12 11:48:50 +02:00
Raphael Michel
90a9709838 Redirect to profile after creating a new organizer 2019-08-12 11:45:54 +02:00
Raphael Michel
669b438c91 Allow to disables fee in stats module 2019-08-09 15:02:59 +02:00
Raphael Michel
a1353b3773 Add a note on Google Analytics in the widget documentation 2019-08-09 15:02:59 +02:00
Martin Gross
f5c611982a Do not localize date, time, datetime in csv/excel exports 2019-08-09 13:40:32 +02:00
Raphael Michel
e9ab56486a Fix broken test case 2019-08-09 12:53:56 +02:00
Raphael Michel
74bc495eb7 PDF exporter: Deal with duplicate question answers 2019-08-09 12:36:01 +02:00
Raphael Michel
b0b0f7474d Allow state selection without JavaScript 2019-08-09 12:13:09 +02:00
Raphael Michel
663ff60d0a Update from Weblate (#1363)
Update from Weblate
2019-08-09 11:51:59 +02:00
Raphael Michel
7bafb0bc76 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3208 of 3208 strings)

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

powered by weblate
2019-08-09 08:26:33 +00:00
Raphael Michel
1ab225f40a Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3208 of 3208 strings)

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

powered by weblate
2019-08-09 08:26:33 +00:00
Raphael Michel
3b09456755 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-08-09 09:58:52 +02:00
Raphael Michel
d919605d79 Invoice addresses: Ask for a state in some countries (#1362)
* Invoice addresses: Ask for a state in some countries

* API, tests, noscript

* Fix shredder tests

* Add test for addresses with long state names
2019-08-09 09:55:46 +02:00
Raphael Michel
547f71aac6 Widget builder: explicit encoding for file reading 2019-08-08 19:43:44 +02:00
Raphael Michel
191729c07a Remove "3DS mode" setting as it no longer has an effect 2019-08-08 19:25:31 +02:00
Raphael Michel
8f0a5d859d Bump version to 3.1.0.dev0 2019-08-08 10:48:59 +02:00
Raphael Michel
9286ca14f9 Bump version to 3.0.0 2019-08-08 10:48:27 +02:00
Raphael Michel
c5f9a78bdb Update from Weblate (#1361)
Update from Weblate
2019-08-08 10:48:13 +02:00
Raphael Michel
08eb5bfb8f Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3211 of 3211 strings)

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

powered by weblate
2019-08-08 08:47:28 +00:00
Raphael Michel
804e33b773 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3211 of 3211 strings)

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

powered by weblate
2019-08-08 08:47:28 +00:00
Raphael Michel
c264d8bd5b Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-08-08 09:55:08 +02:00
Raphael Michel
cd7be48cf2 Merge pull request #1359 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-08-07 15:47:42 +02:00
Raphael Michel
2290b00161 MIMEEncode inline images as CID (#1358)
MIMEEncode inline images as CID
2019-08-07 15:47:22 +02:00
Raphael Michel
9fb2d3a43b Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 97.5% (3128 of 3209 strings)

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

powered by weblate
2019-08-07 13:39:12 +00:00
Raphael Michel
d0f3c24b2a Fix button without voucher 2019-08-07 15:39:03 +02:00
Martin Gross
94e2c2fa3c Deduplicate CID images 2019-08-07 13:24:13 +02:00
Martin Gross
a0e3bbcc82 Fix payment cancelation of Stripe sources 2019-08-07 10:11:07 +02:00
Raphael Michel
9a9de523e0 Allow separate numbering schemes for invoices and cancellations 2019-08-06 14:18:31 +02:00
Raphael Michel
6dd1c927ef Add fail_on_no_quotas parameter to Item.check_quotas 2019-08-06 14:08:34 +02:00
Raphael Michel
51446574e2 Do not allow misleading NULL value in mail_days_order_expire_warning 2019-08-06 11:09:17 +02:00
Raphael Michel
cfbfb74996 Move method once again 2019-08-06 11:03:52 +02:00
Raphael Michel
527a250435 Deal with bundled products with no quotas
Fix PRETIXEU-1A8
2019-08-06 10:31:57 +02:00
Raphael Michel
87fb5f06ff Move method to correct class
Fix PRETIXEU-1A4
2019-08-06 10:22:04 +02:00
Raphael Michel
661cba876f French gets a capital F 2019-08-06 10:02:24 +02:00
Raphael Michel
be37e3635b Merge pull request #1355 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-08-06 10:02:16 +02:00
Raphael Michel
8bc4793f4e Translated on translate.pretix.eu (French)
Currently translated at 73.0% (2342 of 3209 strings)

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

powered by weblate
2019-08-06 08:00:32 +00:00
Maarten van den Berg
1604d0bf7a Translated on translate.pretix.eu (Dutch)
Currently translated at 98.6% (3165 of 3209 strings)

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

powered by weblate
2019-08-05 10:16:39 +00:00
Maarten van den Berg
f042932d1d Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (103 of 103 strings)

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

powered by weblate
2019-08-05 10:16:39 +00:00
Maarten van den Berg
bfc6422e6e Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (103 of 103 strings)

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

powered by weblate
2019-08-05 10:16:39 +00:00
Maarten van den Berg
942feb09fc Translated on translate.pretix.eu (Dutch)
Currently translated at 97.6% (3131 of 3209 strings)

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

powered by weblate
2019-08-05 10:16:39 +00:00
Martin Gross
b372ce84a5 Translated on translate.pretix.eu (French)
Currently translated at 64.1% (66 of 103 strings)

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

powered by weblate
2019-08-05 10:16:39 +00:00
Martin Gross
7c0c7202da Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 98.1% (101 of 103 strings)

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

powered by weblate
2019-08-05 10:16:39 +00:00
Martin Gross
b8bf5ce2d3 Translated on translate.pretix.eu (Dutch)
Currently translated at 98.1% (101 of 103 strings)

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

powered by weblate
2019-08-05 10:16:39 +00:00
Martin Gross
d25a9d077d Translated on translate.pretix.eu (French)
Currently translated at 73.0% (2341 of 3209 strings)

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

powered by weblate
2019-08-05 10:16:39 +00:00
Martin Gross
4a4dad3d5c Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 97.5% (3128 of 3209 strings)

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

powered by weblate
2019-08-05 10:16:39 +00:00
Christophe Piret
7b6b83eaf4 Translated on translate.pretix.eu (French)
Currently translated at 73.0% (2341 of 3209 strings)

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

powered by weblate
2019-08-05 10:16:39 +00:00
Martin Gross
bf54222cac Handle PayPal-declines if organizer-account is blocked by risk 2019-08-05 12:16:29 +02:00
Martin Gross
d37939bc2a Change BS-parser to lxml insted of html5lib, do prevent another dependency 2019-08-04 17:25:02 +02:00
Martin Gross
4b3f6ba94b Fix urlparse import 2019-08-04 14:55:54 +02:00
Martin Gross
18c8933c64 MIMEEncode inline images as CID 2019-08-04 14:46:47 +02:00
Martin Gross
6a6a84e8c8 Fix eMail-renderer documentation 2019-08-01 20:41:04 +02:00
Raphael Michel
32edf4b833 Fix attributeerror 2019-07-30 18:18:28 +02:00
Raphael Michel
35ae7e4968 Tabs: Do not hide HTMl5 validation 2019-07-30 14:38:16 +02:00
Raphael Michel
b5fb48a55f Widget: Remove confusion with resuming sessions 2019-07-30 14:16:33 +02:00
Raphael Michel
814364fbda Add waitinglist to word whitelist 2019-07-29 16:52:35 +02:00
Raphael Michel
5bff5053be Update from Weblate (#1354)
Update from Weblate
2019-07-29 16:34:48 +02:00
Raphael Michel
32f4813d33 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3209 of 3209 strings)

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

powered by weblate
2019-07-29 14:34:29 +00:00
Raphael Michel
871a677e5e Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3209 of 3209 strings)

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

powered by weblate
2019-07-29 14:33:59 +00:00
Raphael Michel
95a777516e Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-07-29 16:28:32 +02:00
Raphael Michel
ad8f109e77 Add Item.allow_waitinglist 2019-07-29 16:27:27 +02:00
Raphael Michel
c60d1c8a5d Subevent editor: Redirect back to same page/filter 2019-07-29 16:05:13 +02:00
Raphael Michel
49288ff4e5 Fix incorrect link 2019-07-29 15:39:59 +02:00
Raphael Michel
e90356546f Backend: Modify order information: Do not send email by default 2019-07-29 15:35:14 +02:00
Raphael Michel
a664d51dbc Persist and show full "path" of seats 2019-07-29 15:23:09 +02:00
Raphael Michel
79ee851fae Fix broken order process 2019-07-29 14:59:36 +02:00
Raphael Michel
00905836dc Merge pull request #1353 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-07-29 10:36:58 +02:00
Raphael Michel
e5f57c8ff4 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (103 of 103 strings)

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

powered by weblate
2019-07-29 08:36:22 +00:00
Raphael Michel
c4cbfc726c Translated on translate.pretix.eu (German)
Currently translated at 100.0% (103 of 103 strings)

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

powered by weblate
2019-07-29 08:36:22 +00:00
Raphael Michel
869694a026 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3200 of 3200 strings)

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

powered by weblate
2019-07-29 08:36:21 +00:00
Raphael Michel
843f28d94e Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3200 of 3200 strings)

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

powered by weblate
2019-07-29 08:36:20 +00:00
Raphael Michel
ce35551e97 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-07-29 10:18:15 +02:00
Raphael Michel
3dae8bcdec Widget: Do not label button "Buy" if all items are free 2019-07-29 10:17:33 +02:00
Raphael Michel
3763edbc57 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-07-29 09:35:42 +02:00
Raphael Michel
c1d89284a4 Use tabs for all long settings and CRUD forms (#1352)
* First tabs

* Convert more pages

* Convert question page

* Item form

* Add item_formsets signal

* Revert "Add new signal nav_item"

This reverts commit 1ce613ff89.

* Formset is a word!
2019-07-29 09:35:00 +02:00
Raphael Michel
609f0b632c Do not block "add to cart" button when seating is used 2019-07-28 16:06:14 +02:00
Raphael Michel
10aeadf835 Do not show +/- icons for cart rows with seats 2019-07-28 16:06:00 +02:00
Raphael Michel
26726043c2 Update seating plan schema 2019-07-27 16:48:47 +02:00
Martin Gross
34d1fcf077 Add PayPal-Partner-Attribution-Id to PayPal API-Calls 2019-07-26 10:59:57 +02:00
Raphael Michel
e83e8cdcc0 Allow to hide a product unless a specific quota is sold out (#1351)
* Allow to hide a product unless a specific quota is sold out

* Fix required property

* Add API property and copy between events
2019-07-25 16:14:24 +02:00
Raphael Michel
2dd75ea252 Hide fees on changing payment method when no fees are taken 2019-07-25 11:47:23 +02:00
Raphael Michel
4857cfad6e Fix another waiting list bug with subevents 2019-07-25 10:49:18 +02:00
Raphael Michel
55f8e1c123 Fix waiting list assignment for subevents 2019-07-25 09:39:05 +02:00
Raphael Michel
6df1960f79 Use robust plugin calling in runperiodic 2019-07-25 09:20:34 +02:00
Raphael Michel
3091139aab Update from Weblate (#1350)
Update from Weblate
2019-07-24 15:57:33 +02:00
Raphael Michel
020c7faaef Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3191 of 3191 strings)

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

powered by weblate
2019-07-24 13:56:48 +00:00
Raphael Michel
e9b26cc51e Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3191 of 3191 strings)

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

powered by weblate
2019-07-24 13:56:47 +00:00
Raphael Michel
7948cefee1 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (102 of 102 strings)

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

powered by weblate
2019-07-24 13:45:19 +00:00
Raphael Michel
62195f14be Translated on translate.pretix.eu (German)
Currently translated at 100.0% (102 of 102 strings)

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

powered by weblate
2019-07-24 13:45:19 +00:00
Martin Gross
5a216b7be9 Fix Stripe refunds for PaymentIntents 2019-07-24 15:37:39 +02:00
Raphael Michel
20d79152a6 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-07-24 15:16:28 +02:00
Raphael Michel
d97a0b1941 Consistent display of price ranges 2019-07-24 15:13:10 +02:00
Raphael Michel
fe6e65ccb0 erge remote-tracking branch 'origin/pretixscan' 2019-07-23 19:08:35 +02:00
Raphael Michel
9886f22b83 Add pretix' Stripe partner ID 2019-07-23 09:50:23 +02:00
Sohalt
591ed969b8 Autofocus login form (#1346) 2019-07-22 14:31:18 +02:00
Raphael Michel
3ab475ba6d Fix order page 2019-07-18 19:45:05 +02:00
Raphael Michel
307b1a2748 Fix that allow_cancel is 0 for UI-created events 2019-07-18 17:38:12 +02:00
Raphael Michel
cb3f3f5084 Advertise pretixSCAN 2019-07-18 17:26:49 +02:00
Raphael Michel
85edbe4837 Improved device validation 2019-07-18 17:26:34 +02:00
Raphael Michel
6d12b3780c Allow to hide all sold out items 2019-07-18 15:01:33 +02:00
Raphael Michel
a99616b1e0 API: Check-in response code for canceled 2019-07-18 15:01:33 +02:00
Martin Gross
a5ba7440fe Fix #1345 - Only enable payment button once Stripe Elements are ready 2019-07-16 15:41:37 +02:00
Raphael Michel
a02ea45dba Allow quotas to "close" when once full (#1344)
* Model

* Some UI

* API and logging

* Permission check

* Add tests

* Move option around
2019-07-16 14:02:27 +02:00
Raphael Michel
c1e2fb36ba Auto-expand variation description when variation is selected 2019-07-16 11:53:43 +02:00
Raphael Michel
b67c684969 Revert "Allow to show description of add-on product variations by default"
This reverts commit 8d674965d1.
2019-07-16 11:46:11 +02:00
Raphael Michel
dc42dbb837 Allow to use a selection for name titles 2019-07-16 10:23:43 +02:00
Raphael Michel
44ffc0685e Show date_to in PDF variable "event_date_range" regardless of event settings
Z#2349533
2019-07-16 09:31:40 +02:00
Raphael Michel
a79a156a28 Show preview of answered images 2019-07-16 09:31:36 +02:00
Raphael Michel
fb1f6c65af Display invoices as inline PDF
They are not user-controllable enough to cause any harm here
2019-07-16 09:16:33 +02:00
Raphael Michel
8d674965d1 Allow to show description of add-on product variations by default 2019-07-15 11:26:42 +02:00
Raphael Michel
020122b44f Fix missing words 2019-07-15 11:01:33 +02:00
Raphael Michel
f55fff6495 Update from Weblate (#1342)
Update from Weblate
2019-07-15 11:01:31 +02:00
Raphael Michel
08316129d3 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3165 of 3165 strings)

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

powered by weblate
2019-07-15 09:00:55 +00:00
Raphael Michel
a39563aa3e Translated on translate.pretix.eu (German)
Currently translated at 99.9% (3164 of 3165 strings)

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

powered by weblate
2019-07-15 09:00:54 +00:00
Raphael Michel
a3707a962b Fix problems with CartMixin on empty order
Fix PRETIXEU-18A
2019-07-15 10:46:38 +02:00
Raphael Michel
4bb8c3991e Fix badge-creation task
PRETIXEU-150
2019-07-15 10:46:27 +02:00
Raphael Michel
0d5c2f6329 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-07-15 10:31:30 +02:00
Raphael Michel
17c0cfb395 Add signal: order_split 2019-07-15 10:30:44 +02:00
Raphael Michel
e55f0cdf11 Retire make_testdata.py 2019-07-14 17:55:51 +02:00
Christian González
a2fbc376a5 typo in comment (#1339) 2019-07-14 16:59:08 +02:00
Raphael Michel
be310a4e47 Docs: Add agenda plugin to structure guide 2019-07-12 13:28:46 +02:00
Raphael Michel
35037c79cc Add signal validate_cart_addons 2019-07-12 13:06:29 +02:00
Raphael Michel
f8bb139651 AddOnsForm: Already validate min_count/max_count 2019-07-12 12:32:43 +02:00
Raphael Michel
77046136f2 asynctask.js: Hack to allow form validation 2019-07-12 12:23:34 +02:00
Raphael Michel
53a0d62d93 Allow dependent questions to depend on multiple values (#1336) 2019-07-11 13:32:45 +02:00
Raphael Michel
d994fc674a Do not CASCADE-delete vouchers when deleting items or quotas 2019-07-11 12:35:52 +02:00
Raphael Michel
f066ed01ff Show event meta data in backend list of events 2019-07-11 11:16:36 +02:00
Raphael Michel
fb66434fc9 Update from Weblate (#1335)
Update from Weblate
2019-07-11 10:37:46 +02:00
Vitor Piedras
3f9269f6e5 Translated on translate.pretix.eu (Portuguese (Brazil))
Currently translated at 15.9% (501 of 3152 strings)

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

powered by weblate
2019-07-11 06:56:27 +00:00
Raphael Michel
2a30a1a039 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3152 of 3152 strings)

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

powered by weblate
2019-07-11 06:56:27 +00:00
Raphael Michel
846f20692d Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3152 of 3152 strings)

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

powered by weblate
2019-07-11 06:56:27 +00:00
Raphael Michel
2eb5adb6c1 Stripe: Improve exception handling
PRETIXEU-17Y
2019-07-11 08:56:01 +02:00
Raphael Michel
491753008d Introduce Item.show_quota_left 2019-07-10 16:08:21 +02:00
Raphael Michel
6d6cd3b7cf [SECURITY] Fix XSS in global admin mode 2019-07-10 14:52:58 +02:00
Raphael Michel
eaf6da7272 Protect against main javascript being loaded before translations 2019-07-10 14:31:49 +02:00
Raphael Michel
22ce7a388d Do not send notifications to disabled users 2019-07-10 09:00:41 +02:00
Raphael Michel
e687eee9f1 Widget: Allow voucher with itemless button 2019-07-09 18:55:10 +02:00
Raphael Michel
e7baca952b Fix voucher queryset (again) 2019-07-09 18:02:08 +02:00
Raphael Michel
eef713816e Sort keys in JSON payment metadata 2019-07-09 16:13:37 +02:00
Raphael Michel
5a03033255 Add utility to get IP address 2019-07-09 16:13:37 +02:00
Raphael Michel
59daeba477 Do not redirect to order.pay.complete for pending orders 2019-07-09 16:13:37 +02:00
Raphael Michel
c1a4b8d343 Payment provider API: Add payment argument to render_invoice_text and order_pending_mail_render 2019-07-09 16:13:37 +02:00
Raphael Michel
0ac98f5127 Use inspect instead of TypeError for backwards-compatible APIs 2019-07-09 16:13:37 +02:00
Raphael Michel
55d423af18 Widget: Allow to filter by attributes 2019-07-08 23:27:46 +02:00
Raphael Michel
285694955c Fix AttributeError 2019-07-08 18:25:31 +02:00
Raphael Michel
2352f3b811 Fix voucher validation in CartManager 2019-07-08 17:50:22 +02:00
Raphael Michel
08bfe13dc3 Re-add validation for hidden vouchers 2019-07-08 14:25:35 +02:00
Raphael Michel
ec522ed7e5 Tax list exporter as Excel 2019-07-08 14:25:22 +02:00
Raphael Michel
197ec84f05 Order overview: Allow to filter by date 2019-07-08 14:25:22 +02:00
Martin Gross
42af8b1602 Remove excessive chars in U2F_GET_API_VERSION_RESPONSE 2019-07-08 13:39:12 +02:00
Raphael Michel
f6a4c5271e Remove obsolete validation 2019-07-08 11:05:08 +02:00
Martin Gross
fb53beee2d Option to notify users when questions have been changed in backend 2019-07-08 10:23:32 +02:00
Raphael Michel
ca1c387a41 Allow quota-level vouchers for hidden products (#1123)
* Changes in checks

* Backwards-compatible implementation

* Add test

* Fix voucher bulk form
2019-07-07 13:36:04 +02:00
Raphael Michel
5180b5e48b Fix #1329 -- Fix image lightbox for products with variations 2019-07-05 16:58:39 +02:00
Raphael Michel
a5e94bf63f Protect against fee signal returning None 2019-07-05 14:33:43 +02:00
Raphael Michel
09ef7aac6e Subevent: Allow to pass empty mapping 2019-07-04 18:25:48 +02:00
Raphael Michel
d90510a1bd Fix incorrect headline 2019-07-04 17:59:25 +02:00
Raphael Michel
48790e7743 Fix incorrect header in documentation samples 2019-07-04 17:59:17 +02:00
Raphael Michel
cbeaf399df Update Stripe API 2019-07-04 11:08:05 +02:00
Raphael Michel
779a3698a8 Catch general HTTP errors during VAT validation 2019-07-04 10:39:41 +02:00
Raphael Michel
a5e2caf438 Consistently include other fees in percentual payment fee 2019-07-04 09:31:21 +02:00
Martin Gross
ce79769293 Fix overlooked Stripe-Tests, still using _token instead of _payment_method_id 2019-07-03 22:04:05 +02:00
Martin Gross
9fbb8fa781 Do not _handle_payment_intent() in Stripe's pending order view 2019-07-03 19:19:40 +02:00
Raphael Michel
83c551c1ba API: Correctly set default position IDs for orders 2019-07-03 16:46:03 +02:00
Raphael Michel
328cd9bdc5 Use shell_plus in shell_scoped 2019-07-03 14:32:07 +02:00
Raphael Michel
4ce7655958 Docs: Remove experimental note from order creation endpoint 2019-07-03 13:39:43 +02:00
Raphael Michel
bccc73f1dc Optimized command-line exports 2019-07-03 13:35:26 +02:00
Raphael Michel
5eeba88283 Stripe: Robust webhook recognition 2019-07-03 10:57:36 +02:00
Raphael Michel
4c2fe9fc20 Update from Weblate (#1326)
Update from Weblate
2019-07-02 12:41:13 +02:00
Maarten van den Berg
f2ba409b03 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3152 of 3152 strings)

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

powered by weblate
2019-07-02 10:37:14 +00:00
Maarten van den Berg
296c2b6e28 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (100 of 100 strings)

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

powered by weblate
2019-07-02 10:37:14 +00:00
Maarten van den Berg
ab27bcca42 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 99.9% (3151 of 3152 strings)

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

powered by weblate
2019-07-02 10:37:14 +00:00
Maarten van den Berg
b0a365a099 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3152 of 3152 strings)

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

powered by weblate
2019-07-02 10:37:14 +00:00
Maarten van den Berg
97fc095d20 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (100 of 100 strings)

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

powered by weblate
2019-07-02 10:37:14 +00:00
Maarten van den Berg
cfb1cd8fdb Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3152 of 3152 strings)

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

powered by weblate
2019-07-02 10:37:14 +00:00
Martin Gross
446cf68377 Stripe SCA (#1275)
* Stripe SCA
- Upgrade to latest Stripe API
- Deprecate Stripe Checkout for CC
- Migrate CC payments to Payment Intents

* Move SCA to its own view

* Handle CardErrors for PaymentIntents

* Abilty to handle charge webhooks with PaymentIntents

* Better handling of Stripe References

* Fix Stripe Tests

* Move SCA page into orderlayout; perform iFrame SCA

* Handle disputes and pi-webhooks better, fill more into ReferencedStripeObject

* Optionally pass prefetched PaymentIntent to handle-func

* Fix style

* Send message to window.parent not window.top (widget compatibility)

* More accurate loading message

* Show a cog on sca_return.html. On a good internet connection, you barely see it, but on a bad one…

* Robust error handling

* If it's a method and used like a method, let's actually call it like a method!

* Remove logging statement

* Fix JavaScript interference with other frame events

* Use 4:3 aspect ratio, but at least 600px

* Adjust to django_scopes
2019-07-02 12:37:07 +02:00
Raphael Michel
b727207e79 API: Fix query for check-in list status 2019-07-01 17:18:22 +02:00
Raphael Michel
fcc4170a4a Add shell_scoped command. Thanks @rixx! 2019-06-27 11:39:12 +02:00
Raphael Michel
c7f345e98e Allow to filter order list by variations 2019-06-26 14:27:02 +02:00
Raphael Michel
d30fbf4e6a Event front page: Show calendar by default when a month is selected 2019-06-25 13:02:38 +02:00
Raphael Michel
5326aa7486 Merge pull request #1325 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-06-25 11:33:19 +02:00
Raphael Michel
53147c0f0c Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3152 of 3152 strings)

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

powered by weblate
2019-06-25 09:32:44 +00:00
Raphael Michel
fe31318413 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3152 of 3152 strings)

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

powered by weblate
2019-06-25 09:32:43 +00:00
Raphael Michel
bb4821eeb5 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-06-25 11:08:47 +02:00
Raphael Michel
003d958cc5 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-06-25 11:04:17 +02:00
Raphael Michel
93089d87e3 Add support for reserved seating (#1228)
* Initial work on seating

* Add seat guids

* Add product_list_top

* CartAdd: Ignore item when a seat is passed

* Cart display

* product_list_top → render_seating_plan

* Render seating plan in voucher redemption

* Fix failing tests

* Add tests for extending cart positions with seats

* Add subevent_forms to docs

* Update schema, migrations

* Dealing with expired orders

* steps to order change

* Change order positions

* Allow to add seats

* tests for ocm

* Fix things after rebase

* Seating plans API

* Add more tests for cart behaviour

* Widget support

* Adjust widget tests

* Re-enable CSP

* Update schema

* Api: position.seat

* Add guid to word list

* API: (sub)event.seating_plan

* Vali fixes

* Fix api

* Fix reference in test

* Fix test for real
2019-06-25 11:00:03 +02:00
Raphael Michel
f79d17cb6a Navigation: Only show orders/vouchers with a search query 2019-06-24 11:41:36 +02:00
Raphael Michel
0e8db3181c OrderChangeManager: Allow to add positions to empty orders 2019-06-21 14:33:10 +02:00
Raphael Michel
23031642bd Fix crash when re-using logged emails
Fix PRETIXEU-16Q
2019-06-21 12:01:51 +02:00
Raphael Michel
93cca34eab PayPal: Add scopes decorator to oauth_return 2019-06-20 19:29:23 +02:00
Raphael Michel
e29c8a1708 Stripe: disable scopes for oauth return 2019-06-20 13:57:15 +02:00
Raphael Michel
acfec59abc Fix ineffective permission check in typeahead 2019-06-19 09:32:30 +02:00
Raphael Michel
7adf203863 Make order search search in used voucher codes 2019-06-19 09:17:46 +02:00
Raphael Michel
3c2de09216 Integrate orders and vouchers into navigation typeahead 2019-06-19 09:16:33 +02:00
Raphael Michel
26a96f107f Add signal quota_availability 2019-06-18 16:52:01 +02:00
Raphael Michel
819dd7eee6 Correctly show infinite quotas in backend 2019-06-18 16:29:36 +02:00
Raphael Michel
ccc662228c Force evaluation of template responses in frontend 2019-06-17 22:59:45 +02:00
Raphael Michel
99a2fde373 Voucher form: Move product above price mode 2019-06-17 22:56:51 +02:00
Raphael Michel
dda48d92c6 Update from Weblate (#1317)
Update from Weblate
2019-06-17 17:08:44 +02:00
Martin Gross
0a1429ed60 Add setting for enforcing 2FA (#1259)
* Add setting for enforcing 2FA

* Changes after code-review

* Add Test-Cases for Obligatory 2FA
2019-06-17 17:08:27 +02:00
Mattias Axell
8487a5446d Translated on translate.pretix.eu (Swedish)
Currently translated at 100.0% (99 of 99 strings)

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

powered by weblate
2019-06-17 12:57:56 +00:00
Vlad
64833c0bab Translated on translate.pretix.eu (Russian)
Currently translated at 63.6% (63 of 99 strings)

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

powered by weblate
2019-06-17 12:57:56 +00:00
Vlad
3f40525af5 Translated on translate.pretix.eu (Russian)
Currently translated at 0.6% (19 of 3125 strings)

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

powered by weblate
2019-06-17 12:57:56 +00:00
Maarten van den Berg
7b6b3b1348 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (99 of 99 strings)

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

powered by weblate
2019-06-17 12:57:56 +00:00
Maarten van den Berg
0b9f4cd739 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (99 of 99 strings)

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

powered by weblate
2019-06-17 12:57:56 +00:00
Maarten van den Berg
885eefbcb0 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3125 of 3125 strings)

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

powered by weblate
2019-06-17 12:57:56 +00:00
Maarten van den Berg
573757e2bf Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3125 of 3125 strings)

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

powered by weblate
2019-06-17 12:57:56 +00:00
Raphael Michel
c5a2bd35b7 Devices list: Correctly use revoked parameter 2019-06-17 14:41:23 +02:00
Raphael Michel
4b65b94bd5 Disable scopes for all unique ID generation 2019-06-17 14:05:05 +02:00
Raphael Michel
d716f7e014 Fix scoping issue in mail_send_task 2019-06-17 11:07:05 +02:00
Raphael Michel
d85ddb5bda Integrate django-scopes (#1319)
* Install django-scopes

* Fix tests.api

* Update tasks and cronjobs

* Fix remaining tests

* Remove unused import

* Fix tests after rebase

* Disable scopes for get_Events_with_any_permission

* Disable scopes for a management command
2019-06-17 10:46:55 +02:00
Martin Gross
b1db5dbb3e Decrement voucher usage counter when deleting testmode orders (#1321)
* Decrement voucher usage counter when deleting testmode orders

* Only decrement voucher usage counter for uncancelled orders and on uncancelled positions

* Have the tests actually test something
2019-06-14 12:41:07 +02:00
Raphael Michel
fed389b990 Remove plugins task from travis 2019-06-14 12:21:34 +02:00
Raphael Michel
1ce613ff89 Add new signal nav_item 2019-06-14 12:20:27 +02:00
Raphael Michel
44bef85b66 Require recent django-localflavor 2019-06-12 12:49:22 +02:00
Raphael Michel
49c4acefd0 Fix critical error in previous commit 2019-06-10 17:18:08 +02:00
Raphael Michel
61e111742d Avoid unneccesary logs in some highly-used API endpoints 2019-06-09 23:54:48 +02:00
Raphael Michel
b2274039b3 Sendmail: Fix using old log entries 2019-06-06 11:40:21 +02:00
Raphael Michel
4913190730 Update from Weblate (#1312)
Update from Weblate
2019-06-06 11:25:58 +02:00
Raphael Michel
276a087fdb Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3125 of 3125 strings)

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

powered by weblate
2019-06-06 09:24:50 +00:00
Raphael Michel
3639f2cea1 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3125 of 3125 strings)

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

powered by weblate
2019-06-06 09:24:49 +00:00
Raphael Michel
692e9c38f1 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-06-06 11:11:46 +02:00
Raphael Michel
dd4075b2cc Clarify UX around subevent selection 2019-06-06 11:10:51 +02:00
Raphael Michel
b549cb451a Fix invalid signature 2019-06-05 16:44:49 +02:00
Raphael Michel
576132b2d0 Bump to 2.9.0.dev0 2019-06-05 16:28:49 +02:00
Raphael Michel
e0c432d014 [SECURITY] Do not allow to enumerate organizers 2019-06-05 16:27:21 +02:00
Raphael Michel
b66a35df7a Bump to 2.8.0 2019-06-05 16:13:37 +02:00
Raphael Michel
2e1347cf9a Update from Weblate (#1311)
Update from Weblate
2019-06-05 13:39:11 +02:00
Raphael Michel
8d1c9e44fc Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3123 of 3123 strings)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Add a position detail page to the frontend

* Mail templates

* Send mails

* Send reminder email

* Add position support to sendmail plugin

* Add and fix some tests

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

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

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

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

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

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

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

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

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

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

Currently translated at 0.1% (1 of 3067 strings)

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

powered by weblate

* Translated on translate.pretix.eu (Slovenian)

Currently translated at 0.3% (9 of 3067 strings)

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

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

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

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

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

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

* Display timeline

* …

* More events

* Plugin support

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

powered by weblate
2019-05-01 12:12:39 +00:00
Raphael Michel
9bc6941c14 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-05-01 14:02:11 +02:00
Raphael Michel
987da83894 Refs #1102 -- Accept order URLs in order lookup 2019-05-01 14:01:26 +02:00
Raphael Michel
d029d92a92 Fix #1102 -- "View in backend" (doesn't work with custom domains) 2019-05-01 14:01:26 +02:00
Raphael Michel
f1b07777bc Timezone indicators in the backend 2019-05-01 14:01:26 +02:00
Raphael Michel
db187a2537 Fix #1126 -- Use short datetime format on order details page 2019-05-01 14:01:26 +02:00
Raphael Michel
e9a340d9ca Refs #1128 -- Popover on disabled "add to cart" button 2019-05-01 14:01:26 +02:00
Raphael Michel
6841a30d8f Fix #1153 -- Show preview of uploaded pictures in the backend 2019-05-01 14:01:26 +02:00
Raphael Michel
30b8c0f4b9 Fix ClearableBasenameFileInput with current Django 2019-05-01 14:01:26 +02:00
Raphael Michel
3e8f32e7e3 Fix #1178 -- Invalidate ticket cache after order locale change 2019-05-01 14:01:26 +02:00
Raphael Michel
2b145e254b Fix #1211 -- Locale selection on organizer profile 2019-05-01 14:01:26 +02:00
Raphael Michel
e5c2470fde "Go to shop" for organizers 2019-05-01 14:01:26 +02:00
Raphael Michel
2da93eba26 Fix #1230 -- Stripe: Recognize canceled sources in webhook 2019-05-01 14:01:26 +02:00
Raphael Michel
788f73d842 Fix #1255 -- Approvals of free orders after last date of payments 2019-05-01 14:01:26 +02:00
Raphael Michel
d86b3a2173 Update from Weblate (#1265)
Update from Weblate
2019-04-30 09:51:49 +02:00
Tobias Sundgren
7be6046ed5 Translated on translate.pretix.eu (Swedish)
Currently translated at 100.0% (97 of 97 strings)

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

powered by weblate
2019-04-30 07:51:24 +00:00
Maarten van den Berg
6b90689067 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (97 of 97 strings)

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

powered by weblate
2019-04-30 07:51:24 +00:00
Maarten van den Berg
815816b9d6 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (97 of 97 strings)

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

powered by weblate
2019-04-30 07:51:24 +00:00
Tobias Sundgren
3199687fe4 Translated on translate.pretix.eu (Swedish)
Currently translated at 0.8% (24 of 3060 strings)

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

powered by weblate
2019-04-30 07:51:24 +00:00
Maarten van den Berg
6d8b8c6346 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3060 of 3060 strings)

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

powered by weblate
2019-04-30 07:51:24 +00:00
Maarten van den Berg
8a850773f4 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3060 of 3060 strings)

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

powered by weblate
2019-04-30 07:51:24 +00:00
Raphael Michel
2a10f875e4 Added translation on translate.pretix.eu (Swedish) 2019-04-30 07:51:24 +00:00
Raphael Michel
d8d2a21bda Added translation on translate.pretix.eu (Swedish) 2019-04-30 07:51:24 +00:00
oocf
18eb468d8e Translated on translate.pretix.eu (Spanish)
Currently translated at 99.0% (3028 of 3060 strings)

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

powered by weblate
2019-04-30 07:51:24 +00:00
ThanosTeste
2842b0e720 Translated on translate.pretix.eu (Greek)
Currently translated at 81.4% (79 of 97 strings)

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

powered by weblate
2019-04-30 07:51:24 +00:00
Chris Spy
42936a931b Translated on translate.pretix.eu (Greek)
Currently translated at 81.4% (79 of 97 strings)

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

powered by weblate
2019-04-30 07:51:24 +00:00
Raphael Michel
a6c72abe75 Change semantics of changing orders (#1260)
* Change semantics of changing orders

This basically does two things to the "Change products" view of orders and the
OrderChangeManager program API:

1) It decouples changing items or subevents from changing prices.
   OrderChangeManager.change_item() and .change_subevent() no longer
   touch the price of a position. Instead .change_price() needs to be
   called explicitly. However, a client-side JavaScript component now
   *proposes* a new price based on the changed item or subevent.

2) The user interface now exposes the possibility of doing multiple
   things at the same time, i.e. changing the item, subevent and price
   in the same operation. OrderChangeManager already allowed this
   before.

(1) is basically a consequence of (2), while (2) is a prerequesite for
e.g. the `seating` branch, where changing the subevent will always
require changing the seat.

* Add tests for price calculation API
2019-04-30 09:51:19 +02:00
Raphael Michel
df3e6f4b9a dekodi: Fix version and mandatory fields 2019-04-30 09:50:47 +02:00
Raphael Michel
8ef99ba828 Dekodi: Merchant PayPal IDs 2019-04-30 09:50:17 +02:00
Raphael Michel
e8e5f5c7bf Dekodi: Get rid of null values 2019-04-29 15:46:48 +02:00
Martin Gross
f0128429e4 Format amount in GiroCode/EPC-QR with dot instead of locale 2019-04-29 13:54:53 +02:00
Raphael Michel
cc8e5a7f83 Widget: original price for variations 2019-04-29 09:30:03 +02:00
Raphael Michel
d4d3928146 Expose is_public in subevent editor 2019-04-29 09:30:03 +02:00
Raphael Michel
cc4602c308 API Auth: Respect staff sessions 2019-04-26 16:24:13 +02:00
Raphael Michel
2bc0dd6076 Dekodi export: date filter 2019-04-26 15:22:10 +02:00
Raphael Michel
f286c5af28 Dekodi: Never encode money as strings 2019-04-25 21:07:10 +02:00
Raphael Michel
ec27ed198b Add Dekodi exporter 2019-04-25 20:36:24 +02:00
Raphael Michel
2ee0f684c5 PDF variable: price including add-ons 2019-04-25 19:34:51 +02:00
Raphael Michel
951386b32c Add subevent column to order list export 2019-04-25 15:08:22 +02:00
Raphael Michel
f498e8fafa Fix faulty test cases 2019-04-25 14:00:55 +02:00
Raphael Michel
b79947fba4 Widget: Original price for variations 2019-04-25 11:54:21 +02:00
Raphael Michel
ef600ceddb Fix invalid handling of variations with quota-level vouchers 2019-04-25 11:54:03 +02:00
Raphael Michel
13bf975dd5 Fix KeyError during form validation 2019-04-25 10:36:29 +02:00
Raphael Michel
8e56c8dcf7 Fix documentation typos 2019-04-23 17:39:09 +02:00
Raphael Michel
a42b31560c Check-in API: Fall back from attendee_name 2019-04-23 17:25:35 +02:00
Raphael Michel
e15e7a5877 Check-in API: Return 400 instead of 404 on checking in unpaid orders 2019-04-23 17:18:16 +02:00
Raphael Michel
e7384f7e85 Check-in API: require_attention and ignore_status 2019-04-23 17:06:24 +02:00
Raphael Michel
840b30c3c2 Linkify email addresses 2019-04-23 17:06:24 +02:00
Martin Gross
1adabec989 Fix test: Price override shows old price as <del> 2019-04-23 15:37:51 +02:00
Martin Gross
171bea59df Show strikethrough price when voucher is granting discount 2019-04-23 14:26:21 +02:00
Martin Gross
3c4b086992 Show strikethrough original_price when redeeming voucher 2019-04-23 11:55:31 +02:00
Raphael Michel
6a4e6e227c Fix isort issue 2019-04-23 11:19:19 +02:00
Raphael Michel
9c3abc5338 More precise log message for skipped attachments 2019-04-23 11:18:03 +02:00
Raphael Michel
91b2d7989a Update from Weblate (#1261)
Update from Weblate
2019-04-23 10:55:30 +02:00
Raphael Michel
c5a80e6daf Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (97 of 97 strings)

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

powered by weblate
2019-04-23 08:55:15 +00:00
Raphael Michel
37ce9fa9af Translated on translate.pretix.eu (German)
Currently translated at 100.0% (97 of 97 strings)

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

powered by weblate
2019-04-23 08:55:14 +00:00
Raphael Michel
64fe3d772c Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3060 of 3060 strings)

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

powered by weblate
2019-04-23 08:55:14 +00:00
Raphael Michel
5c82781fcc Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3060 of 3060 strings)

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

powered by weblate
2019-04-23 08:54:56 +00:00
Raphael Michel
0d70e3c8e3 Specify minor version of urllib3 2019-04-23 10:50:59 +02:00
Raphael Michel
85a7f0c0cc Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-04-23 10:50:48 +02:00
Raphael Michel
6d0e1097e6 Update from Weblate (#1257)
Update from Weblate
2019-04-23 10:50:10 +02:00
David100mark
c557087252 Translated on translate.pretix.eu (French)
Currently translated at 77.3% (2366 of 3059 strings)

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

powered by weblate
2019-04-23 08:49:35 +00:00
David100mark
62796cdc5f Translated on translate.pretix.eu (French)
Currently translated at 77.2% (2362 of 3059 strings)

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

powered by weblate
2019-04-23 08:49:35 +00:00
David100mark
bbe5f9bd98 Translated on translate.pretix.eu (French)
Currently translated at 76.1% (2329 of 3059 strings)

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

powered by weblate
2019-04-23 08:49:35 +00:00
Maarten van den Berg
003ccd83bf Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3059 of 3059 strings)

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

powered by weblate
2019-04-23 08:49:35 +00:00
Maarten van den Berg
f8f6dc4a51 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3059 of 3059 strings)

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

powered by weblate
2019-04-23 08:49:35 +00:00
Raphael Michel
cddf716784 Translated on translate.pretix.eu (German (informal))
Currently translated at 99.9% (3057 of 3059 strings)

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

powered by weblate
2019-04-23 08:49:35 +00:00
Raphael Michel
ee495f2777 Add property SubEvent.is_public 2019-04-23 10:46:09 +02:00
Raphael Michel
5bdc9011c1 Widget: Specific wording for mobing back to subevents 2019-04-23 10:37:25 +02:00
Raphael Michel
c6ea30ec1e Widget: Handle resize events 2019-04-23 10:35:07 +02:00
Raphael Michel
f9341b4d47 Downgrade urllib3 2019-04-23 10:15:01 +02:00
Raphael Michel
2205e57650 Fail consistently on invalid payment providers 2019-04-23 09:47:55 +02:00
Raphael Michel
ad8fdd6935 Ignore quota errors during order creation 2019-04-23 09:47:44 +02:00
Raphael Michel
02e936ee7a Fix #522 -- Do not allow any orders after the last date of payments 2019-04-23 09:46:34 +02:00
Raphael Michel
45a6923220 Refs #522 -- Do not allow to create orders after the last date of payments 2019-04-23 09:41:01 +02:00
Raphael Michel
e4417305a2 Fix updatestyles not being sent to background queue 2019-04-18 17:44:14 +02:00
Raphael Michel
bc5d0bea00 updatestyles: Prioritize future events over past ones 2019-04-18 17:27:34 +02:00
Raphael Michel
dbce9b0395 Allow error pages to be embedded in frames (to ease widget troubleshooting) 2019-04-18 17:19:42 +02:00
Martin Gross
2eb88840bd Original price for variations (#1258)
* Original price for variations

* Documentation

* API-GET

* Fix existing tests to accomodate new attribute

* Test for variation's original_price on API
2019-04-18 16:13:49 +02:00
Martin Gross
4838835b1b Remove debug-toolbar template override 2019-04-18 12:21:42 +02:00
Raphael Michel
ab452bd9e3 Fix typo 2019-04-18 09:50:07 +02:00
Raphael Michel
ae298bddb8 Make FakeRedis play nice with metrics 2019-04-18 09:17:55 +02:00
Raphael Michel
9ad4607d26 Move ticket cache invalidation to background task 2019-04-18 09:17:01 +02:00
Raphael Michel
b3684377cd Fix crash in item validation
Fixes Sentry PRETIXEU-10B
2019-04-17 15:40:25 +02:00
Raphael Michel
441badfdbd Bank transfer: Move ack field 2019-04-17 15:38:26 +02:00
Raphael Michel
0d242a0304 Fix internal error during validation
Sentry PRETIXEU-10A
2019-04-17 15:21:42 +02:00
Raphael Michel
2fac8592d4 Add modern invoice renderer 2019-04-17 15:08:58 +02:00
Raphael Michel
58b1a2f115 Fix timezone handling in widget 2019-04-17 14:42:00 +02:00
Raphael Michel
420d44e909 Fix #1170 -- E-mail address in check-in list 2019-04-17 12:12:07 +02:00
Raphael Michel
e0063fce52 Allow superusers to inspect payments and refunds 2019-04-17 10:15:14 +02:00
Raphael Michel
21ef6c7950 Update framework classifier 2019-04-17 10:07:02 +02:00
Sohalt
651f429ffb Fix #1247 -- Allow team invites to be resent (#1250)
* Fix #1247 -- Allow team invites to be resent

* Test resending invalid invites

* Fix tooltip

* Fix test

* Handle invalid types for pk parameter

* Style button
2019-04-16 16:39:31 +02:00
Raphael Michel
66dd7c448b Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-04-16 13:35:36 +02:00
Raphael Michel
e9b4205145 Fix translation of widget headlines 2019-04-16 13:35:07 +02:00
Raphael Michel
6dedea1025 Items API: Note that tax_rate is read-only 2019-04-16 13:35:07 +02:00
Raphael Michel
348ed4e909 Merge pull request #1244 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-04-16 13:34:26 +02:00
Maarten van den Berg
091b3358e4 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (96 of 96 strings)

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

powered by weblate
2019-04-16 05:00:07 +00:00
Maarten van den Berg
186e2a6b9a Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (96 of 96 strings)

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

powered by weblate
2019-04-16 05:00:06 +00:00
Maarten van den Berg
198b90972c Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3057 of 3057 strings)

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

powered by weblate
2019-04-16 05:00:06 +00:00
Maarten van den Berg
4989b6235c Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3057 of 3057 strings)

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

powered by weblate
2019-04-16 05:00:05 +00:00
mussol
4cfebab11c Translated on translate.pretix.eu (Catalan)
Currently translated at 35.1% (1073 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
Martin Gross
fe944ec643 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3057 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
Martin Gross
9d92c7b10f Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3057 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
Maarten van den Berg
3b810a3a76 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3057 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
Maarten van den Berg
7860417177 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 99.4% (3038 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
Maarten van den Berg
1438edb3c8 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3057 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
mussol
a17720062b Translated on translate.pretix.eu (Catalan)
Currently translated at 31.8% (972 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
mussol
37ab45b352 Translated on translate.pretix.eu (Catalan)
Currently translated at 31.6% (966 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
mussol
6be212df8c Translated on translate.pretix.eu (Catalan)
Currently translated at 31.1% (952 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
mussol
b4e85780f4 Translated on translate.pretix.eu (Catalan)
Currently translated at 30.0% (917 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
mussol
a9732c4788 Translated on translate.pretix.eu (Catalan)
Currently translated at 30.0% (917 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
mussol
273316be25 Translated on translate.pretix.eu (Catalan)
Currently translated at 29.4% (900 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
Maarten van den Berg
0f3b269931 Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 98.9% (3023 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
Maarten van den Berg
4462054d0e Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3057 of 3057 strings)

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

powered by weblate
2019-04-15 13:47:00 +00:00
Raphael Michel
ec53022cc8 Do not call task synchronously inside task (celery doesn't allow it any more) 2019-04-15 15:46:37 +02:00
Raphael Michel
0b65b18459 Send emails in an TransactionAwareTask 2019-04-15 15:22:58 +02:00
Raphael Michel
2fac03f47b Add a test case for free orders 2019-04-15 15:14:35 +02:00
Raphael Michel
750d5eda48 Do not mark free orders as paid that require approval 2019-04-15 15:12:26 +02:00
Raphael Michel
f2cd9a2002 Fix logic bug in attachment size check 2019-04-15 12:58:36 +02:00
Raphael Michel
874b38db17 Mark order as paid immediately 2019-04-15 12:58:20 +02:00
Raphael Michel
0f58e1c396 CSV import: Do not skip rows without a reference 2019-04-08 17:55:28 +02:00
Raphael Michel
36e0afc09e Further improvements to the print stylesheet 2019-04-08 17:42:06 +02:00
Raphael Michel
7164124a70 Display category description in add-on step 2019-04-08 15:23:40 +02:00
Raphael Michel
887d8832c0 Improve print CSS of order details 2019-04-07 18:12:12 +02:00
Raphael Michel
beb144f9a0 Fix API log cleanup 2019-04-07 15:31:35 +02:00
Raphael Michel
6d1dea7922 Upgrade to Django 2.2 and modern DRF and py.test (#1246)
* Upgrade django and stuff

* Update to Django 2.2 and recent versions of similar packages

* Provide explicit orderings to all models used in paginated queries

* Resolve naive datetime warnings in test suite

* Deal with deprecation warnings

* Fix sqlparse version
2019-04-07 14:09:49 +01:00
Raphael Michel
cb531a7a6a Cut test time by 65% by caching templates and not compiling sass 2019-04-07 13:53:59 +02:00
Raphael Michel
d5820d74d3 Fix #1025 -- Python 3.7 support (#1245)
* Fix #1025 -- Python 3.7 support

* Upgrade redis-py

* Travis: xenial

* Fix version specifier
2019-04-06 22:58:36 +01:00
Raphael Michel
b686978074 Add order lifecycle signals 2019-04-06 15:05:39 +02:00
Raphael Michel
c372bffc57 Fix tests on PostgreSQL 2019-04-05 16:17:57 +02:00
Raphael Michel
282c6108bf Remove duplicate test 2019-04-05 15:32:25 +02:00
Raphael Michel
f2437c7ff7 Correcly read bytesfield 2019-04-05 15:04:47 +02:00
Raphael Michel
dd0b6e6647 Adjust test to internal type change 2019-04-05 14:59:05 +02:00
Raphael Michel
f3128591d8 More flexible response content handling 2019-04-05 14:54:36 +02:00
Raphael Michel
d395db8142 Box office payments: Always display device and receipt ID 2019-04-05 14:40:58 +02:00
Raphael Michel
0c82e92882 REST API: Add support for idempotency keys 2019-04-05 14:21:51 +02:00
Raphael Michel
db0c13a3c2 REST API: Order creation: Allow to set payment_date 2019-04-05 08:55:57 +02:00
Raphael Michel
19a2f4163a Add a few permission tests 2019-04-04 18:17:56 +02:00
Raphael Michel
76526465c0 Fix a test failure in test_items 2019-04-04 18:14:27 +02:00
Raphael Michel
d0d0f9aa4c Fix logic flaw in cart position deletion 2019-04-04 17:18:12 +02:00
Martin Gross
482f6b1eb8 Fix Item/Question tests to also include obligatory items[] as imposed by b931d27486 2019-04-04 16:12:20 +02:00
Raphael Michel
327418299a Cart view: Make questions a little bit less bold 2019-04-04 14:22:36 +02:00
Raphael Michel
5dfd1e6337 Prefill attendee name/email of first ticket with contact email and invoice recipient 2019-04-04 14:13:08 +02:00
Raphael Michel
bc01124584 Fix stepping back to the invoice address 2019-04-04 14:12:51 +02:00
Raphael Michel
c0df418265 Make sure package pinning is copied to setup.py 2019-04-04 13:45:07 +02:00
Martin Gross
af06f6fc38 Pin pytest-xdist to 1.27.*, as 1.28.0++ requires pytest>=4.4.0 2019-04-04 10:24:59 +02:00
Raphael Michel
4c0e8f69ea Cancellation: Do not display refund notices if not required 2019-04-04 09:57:57 +02:00
Raphael Michel
243e4ac4c8 Allow not to ask for invoice addresses on free orders 2019-04-04 09:57:57 +02:00
Raphael Michel
b931d27486 Solve cart deletion issues once and for all 2019-04-04 09:57:57 +02:00
Raphael Michel
2810e2a760 CartManager: Do not try to extend positions while they are being removed 2019-04-04 09:57:57 +02:00
Martin Gross
04465393b2 Set explicit description for Stripe Charges 2019-04-03 19:30:56 +02:00
Raphael Michel
4c9032f2a8 Bump version to 2.6.0 2019-04-03 16:02:39 +02:00
Raphael Michel
cae2bb944a Merge pull request #1243 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-04-03 15:02:23 +01:00
Raphael Michel
724e745b8d Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3057 of 3057 strings)

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

powered by weblate
2019-04-03 14:02:01 +00:00
Raphael Michel
f4cead1c20 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3057 of 3057 strings)

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

powered by weblate
2019-04-03 13:26:15 +00:00
Raphael Michel
7cab1924bb Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-04-03 15:19:57 +02:00
Raphael Michel
641148fecc Merge pull request #1239 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-04-03 14:19:23 +01:00
mussol
9b3860e5fd Translated on translate.pretix.eu (Catalan)
Currently translated at 27.5% (839 of 3047 strings)

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

powered by weblate
2019-04-03 11:13:19 +00:00
mussol
cb9d4c10df Translated on translate.pretix.eu (Catalan)
Currently translated at 22.0% (669 of 3047 strings)

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

powered by weblate
2019-04-03 11:13:19 +00:00
oocf
84105b9585 Translated on translate.pretix.eu (Spanish)
Currently translated at 100.0% (96 of 96 strings)

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

powered by weblate
2019-04-03 11:13:19 +00:00
oocf
3f38caeb24 Translated on translate.pretix.eu (Spanish)
Currently translated at 99.3% (3026 of 3047 strings)

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

powered by weblate
2019-04-03 11:13:19 +00:00
mussol
eae552e474 Translated on translate.pretix.eu (Catalan)
Currently translated at 21.1% (643 of 3047 strings)

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

powered by weblate
2019-04-03 11:13:19 +00:00
mussol
f27c10c2ac Translated on translate.pretix.eu (Catalan)
Currently translated at 8.8% (267 of 3047 strings)

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

powered by weblate
2019-04-03 11:13:19 +00:00
Raphael Michel
abd237b969 Checkout redirection: Respect cart_namespace 2019-04-03 13:12:49 +02:00
Raphael Michel
99c61c9060 Orders API: Add a missing sorting method to the documentation 2019-04-03 11:18:13 +02:00
Raphael Michel
246f307e21 Pin version of pillow (incompatibility with reportlab) 2019-04-02 11:31:01 +02:00
Raphael Michel
1f672e7df2 Fix incorrect test 2019-04-02 11:30:47 +02:00
Raphael Michel
b261a2041a Actually set the revoked flag 2019-04-02 09:44:31 +02:00
Raphael Michel
2d37c6d94d Make device token revokation more explicit 2019-04-02 09:36:07 +02:00
Raphael Michel
e75ae80fb5 REST API: Allow to filter orders by datetime 2019-03-29 17:15:15 +01:00
Raphael Michel
73ec5bac79 Allow to set a custom error message when presale is ended 2019-03-29 16:38:47 +01:00
Raphael Michel
46166159b0 Allow to force order creation through the API 2019-03-28 18:11:06 +01:00
Raphael Michel
598693fab2 Add Chinese as a selectable language 2019-03-28 17:06:28 +01:00
Raphael Michel
2420d884fc Merge pull request #1232 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-03-28 16:06:26 +00:00
Raphael Michel
f95005a8d4 Added translation on translate.pretix.eu (Catalan) 2019-03-28 16:04:15 +00:00
Raphael Michel
e773096df3 Added translation on translate.pretix.eu (Catalan) 2019-03-28 16:03:59 +00:00
yichengsd
c42905421d Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 100.0% (96 of 96 strings)

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

powered by weblate
2019-03-28 15:58:20 +00:00
Alvaro Enrique Ruano
46c2e28def Translated on translate.pretix.eu (Spanish)
Currently translated at 99.0% (3018 of 3047 strings)

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

powered by weblate
2019-03-28 15:58:20 +00:00
yichengsd
07bc3df6d3 Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 99.8% (3041 of 3047 strings)

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

powered by weblate
2019-03-28 15:58:20 +00:00
Maarten van den Berg
2992c4c48a Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (96 of 96 strings)

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

powered by weblate
2019-03-28 15:58:20 +00:00
Maarten van den Berg
c53718381e Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3047 of 3047 strings)

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

powered by weblate
2019-03-28 15:58:20 +00:00
Maarten van den Berg
98e5f0b95d Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 71.9% (69 of 96 strings)

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

powered by weblate
2019-03-28 15:58:20 +00:00
Maarten van den Berg
7f11f06f3f Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (96 of 96 strings)

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

powered by weblate
2019-03-28 15:58:20 +00:00
Raphael Michel
949057a9cc Allow to persist filter attributes in session 2019-03-28 16:58:05 +01:00
Raphael Michel
edd643cc32 Event index: Filter subevent list as well 2019-03-28 16:54:21 +01:00
Raphael Michel
5f15ebc46f Fix TypeError in offset calculation
sentry issue PRETIXEU-ZB
2019-03-27 18:12:49 +01:00
Raphael Michel
3415fd947a Hotfix: Redirect with a / 2019-03-27 17:46:14 +01:00
Raphael Michel
a70a42c273 Hotfix: Do not use absolute URLs 2019-03-27 17:02:22 +01:00
Raphael Michel
697cdfd5c9 Allow to redirect to checkout directly after adding a product to the cart 2019-03-27 16:45:15 +01:00
Raphael Michel
d8a7de8b23 Allow to filter subevents by attributes in query parameters 2019-03-27 16:15:16 +01:00
Raphael Michel
9f7f0e74ff Fix arrow position in month button 2019-03-27 16:15:16 +01:00
Martin Gross
7ef289da45 Minor JSON spelling mistakes 2019-03-27 15:41:56 +01:00
Raphael Michel
e82bc732a3 Docs: Fix spelling issues 2019-03-27 12:08:22 +01:00
Raphael Michel
4636ccac3b Add signals html_page_header, sass_preamble, sass_postamble 2019-03-27 09:14:51 +01:00
Raphael Michel
e3518bfb4b Fix date-dependent test 2019-03-26 10:20:26 +01:00
Raphael Michel
b2471169af Bank transfer: Improve error message 2019-03-26 09:46:40 +01:00
Raphael Michel
487418678c Banktransfer: Workaround for OrderPayment.MultipleObjectsReturned
Fix sentry issue PRETIXEU-Z7
2019-03-26 09:44:26 +01:00
Raphael Michel
d4795868d6 Correcly cancel payments when starting a new one 2019-03-26 09:41:03 +01:00
Raphael Michel
45af18a23d Work around SubEvent.DoesNotExist in refresh_quota_caches
Fix PRETIXEU-Z8
2019-03-26 09:06:34 +01:00
Raphael Michel
a6de586b80 Make ItemBundle.designated_price non-nullable 2019-03-23 23:42:58 +01:00
Raphael Michel
e6859fa82b Docs: Allow "subnet" in word list 2019-03-23 15:25:39 +01:00
Raphael Michel
2d5e14e517 Fix error in tests 2019-03-23 15:06:29 +01:00
Raphael Michel
7219575b84 Fix #1066 -- Change installation tutorials to PostgreSQL
This is the recommended database server so this documentation should use that
2019-03-23 15:04:12 +01:00
Raphael Michel
991e4127f6 Refs #654 -- Allow to update invoice addresses 2019-03-23 13:51:25 +01:00
Raphael Michel
420649e10a Refs #654 -- REST API: Allow to resend order link 2019-03-23 13:33:57 +01:00
Raphael Michel
0d02e2fe8c Refs #654 -- REST API: Allow to cycle order secrets 2019-03-23 13:25:23 +01:00
Raphael Michel
afdba9f268 Refs #654 -- REST API: Allow invoice creation 2019-03-23 13:25:21 +01:00
Raphael Michel
394f7e04c3 Docs: Add a guide on building product structures 2019-03-23 13:06:13 +01:00
Martin Gross
c3a5cef051 Merge pull request #1227 from felixrindt/patch-6
Fix doc typo
2019-03-23 11:25:53 +01:00
Felix Rindt
47b7bcbfca Fix doc typo 2019-03-23 11:15:35 +01:00
Raphael Michel
2cd1345035 Adjust item API tests 2019-03-23 00:43:02 +01:00
Raphael Michel
c24ce551ba Refs #654 -- REST API: Allow PATCH for some order fields 2019-03-23 00:08:45 +01:00
Raphael Michel
0bb6e460e8 Fix #1195 -- REST API: Fix wrong data type of variation price 2019-03-23 00:08:45 +01:00
Raphael Michel
26257f0829 Refs #1195 -- Fix missing null annotations in the API 2019-03-23 00:08:45 +01:00
Raphael Michel
6badb342bf Merge pull request #1226 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-03-22 15:02:47 +00:00
Raphael Michel
865a70d5d5 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (96 of 96 strings)

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

powered by weblate
2019-03-22 15:00:25 +00:00
Raphael Michel
df1c0d4f3a Translated on translate.pretix.eu (German)
Currently translated at 100.0% (96 of 96 strings)

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

powered by weblate
2019-03-22 15:00:25 +00:00
Raphael Michel
adb982c451 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3047 of 3047 strings)

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

powered by weblate
2019-03-22 15:00:24 +00:00
Raphael Michel
94ba26d841 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3047 of 3047 strings)

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

powered by weblate
2019-03-22 15:00:05 +00:00
Raphael Michel
45d5487eb5 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-03-22 15:50:33 +01:00
Raphael Michel
38f5f75a1b Add deprecation note to the PayPal documentation 2019-03-22 15:49:44 +01:00
Raphael Michel
90f881c48e Fix #1001 -- Add product bundles (#1041)
* Data model + Editor

* Cart and order management

* Rebase migrations

* Fix typos, add tests on cart handling

* Add tests for checkout and quotas

* Add API endpoints

* Validation of settings

* Front page tax display

* Voucher handling

* Widget foo

* Show correct net pricing

* Front page tests

* reverse charge foo

* Allow to require bundling

* Fix test failure on postgres
2019-03-22 14:48:48 +00:00
Raphael Michel
c4b18a4c81 Force widget data to be a dictionary 2019-03-22 12:16:20 +01:00
Raphael Michel
8e2ef604f7 Widget API: Fix parameters 2019-03-22 11:37:12 +01:00
Raphael Michel
970e4f6d52 Merge pull request #1225 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-03-22 09:43:56 +00:00
Raphael Michel
59c9731bae Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (94 of 94 strings)

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

powered by weblate
2019-03-22 09:43:24 +00:00
Raphael Michel
8afd09a647 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3027 of 3027 strings)

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

powered by weblate
2019-03-22 09:43:24 +00:00
Raphael Michel
fa375950a7 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3027 of 3027 strings)

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

powered by weblate
2019-03-22 09:43:23 +00:00
Raphael Michel
ccdfa716c0 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (94 of 94 strings)

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

powered by weblate
2019-03-22 09:42:51 +00:00
Raphael Michel
08ffa17e01 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-03-22 10:22:25 +01:00
Raphael Michel
7f8c91ec9d Merge pull request #1220 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-03-22 09:20:24 +00:00
yichengsd
dde3a53e09 Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 100.0% (3019 of 3019 strings)

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

powered by weblate
2019-03-22 09:18:09 +00:00
徐志能
73bc3259e8 Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 100.0% (3019 of 3019 strings)

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

powered by weblate
2019-03-22 09:18:09 +00:00
徐志能
34767a2029 Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 99.9% (3018 of 3019 strings)

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

powered by weblate
2019-03-22 09:18:09 +00:00
Vitor Reis
85bd1a0d44 Translated on translate.pretix.eu (Portuguese (Brazil))
Currently translated at 98.6% (70 of 71 strings)

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

powered by weblate
2019-03-22 09:18:09 +00:00
Lorhan Sohaky
1ad4b6019e Translated on translate.pretix.eu (Portuguese (Brazil))
Currently translated at 98.6% (70 of 71 strings)

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

powered by weblate
2019-03-22 09:18:09 +00:00
yichengsd
a778675857 Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 100.0% (71 of 71 strings)

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

powered by weblate
2019-03-22 09:18:09 +00:00
Lorhan Sohaky
e5a980aef4 Translated on translate.pretix.eu (Portuguese (Brazil))
Currently translated at 16.1% (487 of 3019 strings)

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

powered by weblate
2019-03-22 09:18:09 +00:00
Vitor Reis
e8338a2941 Translated on translate.pretix.eu (Portuguese (Brazil))
Currently translated at 16.1% (487 of 3019 strings)

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

powered by weblate
2019-03-22 09:18:09 +00:00
徐志能
289bbf84a9 Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 96.1% (2901 of 3019 strings)

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

powered by weblate
2019-03-22 09:18:09 +00:00
Raphael Michel
f13dbb85cb Fix #1224 -- Cache widget responses for a short time 2019-03-22 09:18:04 +00:00
Raphael Michel
49e706a580 Fix #878 -- Add multi-event widget 2019-03-22 09:18:04 +00:00
Martin Gross
ca7d55082b Refs #654 -- Add writable API for subevents (#1217)
- [x] Write operations for subevents
- [x] Tests
- [x] Documentation
2019-03-21 20:40:59 +00:00
Raphael Michel
516fab52da Do not send payment reminders to orders pending approval 2019-03-20 23:09:13 +01:00
Raphael Michel
ddf6af278c Widget: Return a useful error message on disabled events 2019-03-18 20:42:47 +01:00
Raphael Michel
07b4b8c473 Allow to add a custom text above the invoice address 2019-03-18 17:01:23 +01:00
Raphael Michel
a0af0cfb06 Merge pull request #1218 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-03-18 17:00:00 +01:00
Raphael Michel
3eb86a371a Merge pull request #1216 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-03-18 16:54:38 +01:00
Raphael Michel
ae7175c00b Translated on translate.pretix.eu (Greek)
Currently translated at 0.6% (17 of 3019 strings)

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

powered by weblate
2019-03-18 15:54:26 +00:00
徐志能
4286176e73 Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 80.6% (2433 of 3019 strings)

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

powered by weblate
2019-03-18 15:54:26 +00:00
徐志能
0f53ab67df Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 80.3% (2424 of 3019 strings)

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

powered by weblate
2019-03-18 15:54:26 +00:00
徐志能
0d6db082ca Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 77.5% (2339 of 3019 strings)

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

powered by weblate
2019-03-18 15:54:26 +00:00
oocf
2a9b8baa98 Translated on translate.pretix.eu (Spanish)
Currently translated at 100.0% (3019 of 3019 strings)

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

powered by weblate
2019-03-18 15:54:26 +00:00
徐志能
6bdaab5319 Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 63.3% (1911 of 3019 strings)

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

powered by weblate
2019-03-18 15:54:26 +00:00
oocf
489d5e3d01 Translated on translate.pretix.eu (Spanish)
Currently translated at 100.0% (3019 of 3019 strings)

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

powered by weblate
2019-03-18 15:54:26 +00:00
Alvaro Enrique Ruano
86e2cf2786 Translated on translate.pretix.eu (Spanish)
Currently translated at 100.0% (3019 of 3019 strings)

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

powered by weblate
2019-03-18 15:54:26 +00:00
徐志能
f075dbc78e Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 59.1% (1784 of 3019 strings)

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

powered by weblate
2019-03-18 15:54:26 +00:00
Serge Bazanski
10500ee6a9 Translated on translate.pretix.eu (Polish)
Currently translated at 100.0% (71 of 71 strings)

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

powered by weblate
2019-03-18 15:54:26 +00:00
Serge Bazanski
6c766d872d Translated on translate.pretix.eu (Polish)
Currently translated at 15.7% (475 of 3019 strings)

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

powered by weblate
2019-03-18 15:54:26 +00:00
Serge Bazanski
c8b1206e61 Translated on translate.pretix.eu (Polish)
Currently translated at 90.1% (64 of 71 strings)

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

powered by weblate
2019-03-18 15:54:26 +00:00
Serge Bazanski
424d1489bf Translated on translate.pretix.eu (Polish)
Currently translated at 15.7% (475 of 3019 strings)

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

powered by weblate
2019-03-18 15:54:26 +00:00
Serge Bazanski
b3e567a188 Translated on translate.pretix.eu (Polish)
Currently translated at 50.7% (36 of 71 strings)

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

powered by weblate
2019-03-18 15:54:26 +00:00
Serge Bazanski
66e42f66e5 Translated on translate.pretix.eu (Polish)
Currently translated at 15.6% (472 of 3019 strings)

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

powered by weblate
2019-03-18 15:54:26 +00:00
Serge Bazanski
786fbc6e29 Translated on translate.pretix.eu (Polish)
Currently translated at 15.5% (11 of 71 strings)

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

powered by weblate
2019-03-18 15:54:26 +00:00
Serge Bazanski
f2ff5d7510 Translated on translate.pretix.eu (Polish)
Currently translated at 15.5% (469 of 3019 strings)

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

powered by weblate
2019-03-18 15:54:26 +00:00
徐志能
0bcc4de2de Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 53.5% (1615 of 3019 strings)

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

powered by weblate
2019-03-18 15:54:26 +00:00
Raphael Michel
586e7cc997 Add sales channel to order export 2019-03-18 16:54:08 +01:00
Raphael Michel
bf33cc1499 Do not uppercase labels in Greek invoices 2019-03-18 16:53:48 +01:00
Raphael Michel
faff7b4166 Translated on translate.pretix.eu (Greek)
Currently translated at 0.6% (17 of 3019 strings)

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

powered by weblate
2019-03-18 15:52:14 +00:00
徐志能
44263a17e6 Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 80.6% (2433 of 3019 strings)

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

powered by weblate
2019-03-18 15:52:14 +00:00
徐志能
87b4d1aaed Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 80.3% (2424 of 3019 strings)

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

powered by weblate
2019-03-18 10:34:58 +00:00
徐志能
0d12c1589a Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 77.5% (2339 of 3019 strings)

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

powered by weblate
2019-03-18 10:34:58 +00:00
oocf
96ddd8ce4e Translated on translate.pretix.eu (Spanish)
Currently translated at 100.0% (3019 of 3019 strings)

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

powered by weblate
2019-03-18 10:34:58 +00:00
徐志能
dc2da88220 Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 63.3% (1911 of 3019 strings)

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

powered by weblate
2019-03-18 10:34:58 +00:00
oocf
b8796b0632 Translated on translate.pretix.eu (Spanish)
Currently translated at 100.0% (3019 of 3019 strings)

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

powered by weblate
2019-03-18 10:34:58 +00:00
Alvaro Enrique Ruano
961b1d4efa Translated on translate.pretix.eu (Spanish)
Currently translated at 100.0% (3019 of 3019 strings)

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

powered by weblate
2019-03-18 10:34:58 +00:00
徐志能
ac674565cf Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 59.1% (1784 of 3019 strings)

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

powered by weblate
2019-03-18 10:34:58 +00:00
Serge Bazanski
301e9d1d48 Translated on translate.pretix.eu (Polish)
Currently translated at 100.0% (71 of 71 strings)

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

powered by weblate
2019-03-18 10:34:58 +00:00
Serge Bazanski
81e08021db Translated on translate.pretix.eu (Polish)
Currently translated at 15.7% (475 of 3019 strings)

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

powered by weblate
2019-03-18 10:34:58 +00:00
Serge Bazanski
da1beac49c Translated on translate.pretix.eu (Polish)
Currently translated at 90.1% (64 of 71 strings)

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

powered by weblate
2019-03-18 10:34:58 +00:00
Serge Bazanski
a81fc5bfe0 Translated on translate.pretix.eu (Polish)
Currently translated at 15.7% (475 of 3019 strings)

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

powered by weblate
2019-03-18 10:34:58 +00:00
Serge Bazanski
eb3ec0f99a Translated on translate.pretix.eu (Polish)
Currently translated at 50.7% (36 of 71 strings)

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

powered by weblate
2019-03-18 10:34:58 +00:00
Serge Bazanski
7e98846315 Translated on translate.pretix.eu (Polish)
Currently translated at 15.6% (472 of 3019 strings)

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

powered by weblate
2019-03-18 10:34:58 +00:00
Serge Bazanski
22f9178617 Translated on translate.pretix.eu (Polish)
Currently translated at 15.5% (11 of 71 strings)

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

powered by weblate
2019-03-18 10:34:58 +00:00
Serge Bazanski
ef73abc3f6 Translated on translate.pretix.eu (Polish)
Currently translated at 15.5% (469 of 3019 strings)

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

powered by weblate
2019-03-18 10:34:58 +00:00
徐志能
ac6e6a526e Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 53.5% (1615 of 3019 strings)

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

powered by weblate
2019-03-18 10:34:58 +00:00
Raphael Michel
8185c6a0d6 Delete all cart positions when disabling test mode 2019-03-18 11:34:48 +01:00
Raphael Michel
a7b294fc61 Add "searchable" to spell-check wordlist 2019-03-18 10:00:45 +01:00
Raphael Michel
b81f07b237 Bring documentation up to date 2019-03-17 21:33:19 +01:00
Raphael Michel
67bdb0ec1f Quick setup: Enable products on all sales channels 2019-03-17 20:31:10 +01:00
Raphael Michel
7b6ff01740 That wasn't an efficient bugfix… 2019-03-15 12:19:39 +01:00
Raphael Michel
83f5182db2 Fix a bug in polish translation 2019-03-15 12:03:23 +01:00
Raphael Michel
ee2050b8f9 Do not ever ask people to select a payment method for 0.00 2019-03-15 11:40:30 +01:00
Raphael Michel
185fc6c73d Fix a test incompatibility 2019-03-15 11:31:32 +01:00
Raphael Michel
a21ea34944 Bank transfer: Properly deal with fees of aborted payment methods 2019-03-15 11:31:32 +01:00
Raphael Michel
130ba3c217 Merge pull request #1215 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-03-15 11:30:09 +01:00
Raphael Michel
dd39131942 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (71 of 71 strings)

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

powered by weblate
2019-03-15 07:55:50 +00:00
Raphael Michel
295ad9e9c3 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (71 of 71 strings)

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

powered by weblate
2019-03-15 07:55:50 +00:00
oocf
a0969dc7fa Translated on translate.pretix.eu (Spanish)
Currently translated at 99.9% (3018 of 3019 strings)

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

powered by weblate
2019-03-15 07:55:50 +00:00
Serge Bazanski
81a2e0c71c Translated on translate.pretix.eu (Polish)
Currently translated at 15.5% (467 of 3019 strings)

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

powered by weblate
2019-03-15 07:55:50 +00:00
徐志能
71f69c9afb Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 50.2% (1517 of 3019 strings)

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

powered by weblate
2019-03-15 07:55:50 +00:00
徐志能
cfff6f1605 Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 50.2% (1517 of 3019 strings)

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

powered by weblate
2019-03-15 07:55:50 +00:00
Raphael Michel
84ccaed94a Fix German strings
How did that happen?
2019-03-15 08:55:35 +01:00
Raphael Michel
ec61e07ab6 Merge pull request #1214 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-03-14 17:51:31 +01:00
Raphael Michel
4d5fb67b02 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3019 of 3019 strings)

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

powered by weblate
2019-03-14 16:51:12 +00:00
Raphael Michel
c885807fa5 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3019 of 3019 strings)

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

powered by weblate
2019-03-14 16:48:11 +00:00
Raphael Michel
88ec54809a Added translation on translate.pretix.eu (Polish (informal)) 2019-03-14 16:46:26 +00:00
Raphael Michel
5f96152d57 Added translation on translate.pretix.eu (Polish (informal)) 2019-03-14 16:46:17 +00:00
Raphael Michel
a9b08660c6 Added translation on translate.pretix.eu (Polish) 2019-03-14 16:44:24 +00:00
Raphael Michel
68e901f76d Added translation on translate.pretix.eu (Polish) 2019-03-14 16:44:12 +00:00
Raphael Michel
e10083379c Merge pull request #1212 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-03-14 15:38:47 +01:00
pretix Translation Platform
d705102cbb Merge branch 'master' of https://github.com/pretix/pretix 2019-03-14 15:37:44 +01:00
Raphael Michel
122fda27c4 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-03-14 15:35:29 +01:00
Raphael Michel
b83752005a Voucher: Do not show URL if subevent is required 2019-03-14 15:35:01 +01:00
Raphael Michel
7fc926f23e Bank transfer: Send email for underpayments 2019-03-14 15:35:01 +01:00
Raphael Michel
d90686f352 Bank transfer: Ask people if they understood how it works 2019-03-14 15:35:01 +01:00
Raphael Michel
8523f4dfa2 Do not print canceled add-ons on orders 2019-03-14 15:35:01 +01:00
徐志能
9c13676349 Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 39.9% (1201 of 3010 strings)

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

powered by weblate
2019-03-14 14:34:52 +00:00
Raphael Michel
19ee8e9802 Update from Weblate (#1201) 2019-03-14 15:34:47 +01:00
徐志能
bbe5ff249b Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 38.6% (1161 of 3010 strings)

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

powered by weblate
2019-03-14 08:43:14 +00:00
Maarten van den Berg
7470cda17f Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (69 of 69 strings)

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

powered by weblate
2019-03-14 08:43:14 +00:00
Maarten van den Berg
a4d50ae4c5 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (69 of 69 strings)

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

powered by weblate
2019-03-14 08:43:14 +00:00
Maarten van den Berg
e0d7a9d2da Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3010 of 3010 strings)

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

powered by weblate
2019-03-14 08:43:14 +00:00
Maarten van den Berg
2159a65643 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3010 of 3010 strings)

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

powered by weblate
2019-03-14 08:43:14 +00:00
Alvaro Enrique Ruano
3f05c92602 Translated on translate.pretix.eu (Spanish)
Currently translated at 100.0% (3010 of 3010 strings)

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

powered by weblate
2019-03-14 08:43:14 +00:00
yichengsd
d764cdb338 Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 30.8% (926 of 3010 strings)

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

powered by weblate
2019-03-14 08:43:14 +00:00
yichengsd
7b7bd67ae9 Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 30.7% (924 of 3010 strings)

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

powered by weblate
2019-03-14 08:43:14 +00:00
徐志能
dc502618dd Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 30.7% (924 of 3010 strings)

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

powered by weblate
2019-03-14 08:43:14 +00:00
徐志能
cb6b4c96f8 Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 24.8% (746 of 3010 strings)

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

powered by weblate
2019-03-14 08:43:14 +00:00
徐志能
de5f094f73 Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 24.4% (733 of 3010 strings)

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

powered by weblate
2019-03-14 08:43:14 +00:00
徐志能
6b85e89e62 Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 24.3% (730 of 3010 strings)

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

powered by weblate
2019-03-14 08:43:14 +00:00
Alvaro Enrique Ruano
8cfc8bc152 Correct documentation for payment forms (#1209) 2019-03-14 09:43:05 +01:00
Raphael Michel
27990b3fbb Prevent users from setting up dependencies for check-in questions 2019-03-13 17:10:23 +01:00
Raphael Michel
307ee36e52 Do not show invisible questions in order overview 2019-03-13 17:06:49 +01:00
Raphael Michel
f95e8f374d Allow dependencies between questions (#1202)
- [x] data model
- [x] api
- [x] backend editor
- [x] backend validation logic
- [x] frontend display logic
- [x] frontend validation logic
- [x] test checkout step
- [x] test modify order in frontend
- [x] test modify order in backend
- [x] validation tests
- [x] correctly evaluate dependency tree in frontend?
- [x] copy events
2019-03-13 16:49:20 +01:00
Raphael Michel
d10cbd07a7 Delete cart positiosn during bulk deletion of subevents 2019-03-13 11:54:58 +01:00
Raphael Michel
5519643782 Voucher redemption: Show a checkbox if max_per_order=1 2019-03-13 11:46:19 +01:00
Raphael Michel
2c91a17927 Item form: Smaller description field 2019-03-13 11:26:13 +01:00
Raphael Michel
875d79536b Re-group voucher options 2019-03-13 11:24:50 +01:00
Raphael Michel
4bf0d2d229 Show voucher link in voucher detail view 2019-03-13 11:22:50 +01:00
Raphael Michel
068983004a ReportlabExportMixin: Encapsule header strings 2019-03-13 11:18:34 +01:00
Martin Gross
0365a1c68d Show SumUp payment details for boxoffice transactions 2019-03-12 14:20:45 +01:00
Raphael Michel
5024fae5ed Improve performance of bulk-generation of ticket PDFs 2019-03-12 09:53:28 +01:00
Raphael Michel
affc6254a8 Fix potential XSS in questions [not a vulnerability, thanks to CSP] 2019-03-12 09:20:48 +01:00
Raphael Michel
bb956c13ba Bump version to 2.6.0.dev0 2019-03-11 18:13:16 +01:00
Raphael Michel
ee70104735 Bump version to 2.5.0 2019-03-11 18:12:29 +01:00
Raphael Michel
8ba38a0254 Order.meta_info_data: Expose null values as an empty dict 2019-03-11 18:12:29 +01:00
Raphael Michel
761a03abdc Merge pull request #1200 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-03-11 16:42:02 +01:00
Raphael Michel
f3b63acd40 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3010 of 3010 strings)

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

powered by weblate
2019-03-11 15:41:46 +00:00
Raphael Michel
9eee967050 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3010 of 3010 strings)

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

powered by weblate
2019-03-11 15:41:24 +00:00
Raphael Michel
02aee0637a Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2019-03-11 16:02:41 +01:00
Raphael Michel
dde99c45f3 Merge pull request #1194 from pretix-translations/weblate-pretix-pretix
Update from Weblate
2019-03-11 16:02:01 +01:00
徐志能
74292535ad Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 21.2% (635 of 3000 strings)

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

powered by weblate
2019-03-11 13:36:08 +00:00
徐志能
d1e3ba778d Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 10.7% (320 of 3000 strings)

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

powered by weblate
2019-03-11 13:36:08 +00:00
yichengsd
475835959d Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 10.7% (320 of 3000 strings)

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

powered by weblate
2019-03-11 13:36:08 +00:00
yichengsd
efdeaeac83 Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 100.0% (68 of 68 strings)

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

powered by weblate
2019-03-11 13:36:08 +00:00
yichengsd
56fe37dd67 Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 6.2% (186 of 3000 strings)

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

powered by weblate
2019-03-11 13:36:08 +00:00
yichengsd
ca07f48afd Translated on translate.pretix.eu (Chinese (Simplified))
Currently translated at 6.2% (185 of 3000 strings)

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

powered by weblate
2019-03-11 13:36:08 +00:00
Raphael Michel
fa706549ce Added translation on translate.pretix.eu (Chinese (Simplified)) 2019-03-11 13:36:08 +00:00
Raphael Michel
989b28c2f6 Added translation on translate.pretix.eu (Chinese (Simplified)) 2019-03-11 13:36:08 +00:00
Raphael Michel
1c84660c42 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3000 of 3000 strings)

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

powered by weblate
2019-03-11 13:36:08 +00:00
Alvaro Enrique Ruano
cf58447cd4 Translated on translate.pretix.eu (Spanish)
Currently translated at 100.0% (3000 of 3000 strings)

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

powered by weblate
2019-03-11 13:36:08 +00:00
arabestia
305a3aaf9f Translated on translate.pretix.eu (Spanish)
Currently translated at 100.0% (3000 of 3000 strings)

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

powered by weblate
2019-03-11 13:36:08 +00:00
Alvaro Enrique Ruano
b54a8c120f Translated on translate.pretix.eu (Spanish)
Currently translated at 96.1% (2884 of 3000 strings)

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

powered by weblate
2019-03-11 13:36:08 +00:00
Maarten van den Berg
89684c8e0f Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3000 of 3000 strings)

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

powered by weblate
2019-03-11 13:36:08 +00:00
Maarten van den Berg
c5566dfee7 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3000 of 3000 strings)

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

powered by weblate
2019-03-11 13:36:08 +00:00
Raphael Michel
5e7ee3c047 Delete cart positions if deleting subevent
Fixes sentry issue PRETIXEU-Y4
2019-03-11 14:35:49 +01:00
Raphael Michel
815ee29a50 Sendmail: Backwards compatbility of from_log
Fixes sentry issue PRETIXEU-Y3
2019-03-11 14:34:41 +01:00
Raphael Michel
13ee691133 Banktransfer: CSV import of Mac CSV files 2019-03-11 14:30:43 +01:00
Raphael Michel
4e3dd24209 Order list: Prevent type error on empty result set 2019-03-08 13:53:18 +01:00
Raphael Michel
7ef4adeb73 Adjust to new isort version 2019-03-08 12:50:35 +01:00
Raphael Michel
7be5331da5 Show ticket code in check-in list 2019-03-08 12:50:25 +01:00
Raphael Michel
12fc02b2e4 Pagination: Remove hover effect of current page indicator 2019-03-08 12:30:13 +01:00
Raphael Michel
86b4835273 Go to order: Allow to pass an invoice number 2019-03-08 12:22:19 +01:00
Raphael Michel
e53818b025 Sendmail history: Show selected items 2019-03-08 12:18:13 +01:00
Raphael Michel
206a0a28c7 Render markdown in all e-mail previews 2019-03-08 12:15:06 +01:00
Raphael Michel
461b0b639c Sendmail: Use multi-select for product selection 2019-03-08 12:14:40 +01:00
Raphael Michel
2e6f5d0f32 E-Mail rendering: Consistent markdown evaluation between preview and mail 2019-03-08 11:58:26 +01:00
Raphael Michel
12b48948e3 Add a new notification category for overpayments 2019-03-08 11:40:22 +01:00
Raphael Michel
87c7a3d26f PayPal: Even a canceled payment can still succeed 2019-03-08 11:33:03 +01:00
Raphael Michel
4c0789ac20 Fix inconsistent naming of option 2019-03-08 11:30:04 +01:00
Raphael Michel
bc4e6fa549 Add new API endpoints to documentation 2019-03-06 09:39:58 +01:00
Raphael Michel
2b8949dea4 Add API for badge and ticket layout assignments 2019-03-06 09:35:08 +01:00
Raphael Michel
f3ef00e3b7 Set an update_check_id even if update checks are disabled 2019-03-06 09:09:18 +01:00
Martin Gross
c5499df0b4 Improve display of date/time-format in PDF-ticket preview vs. actual rendering (Z#2344558) 2019-03-05 12:10:05 +01:00
Raphael Michel
68dbfedfdf Add database-level uniqueness constraint for check-ins
We measured that this creates a ~10% performance loss on MySQL, but
believe that correctness is more important. Also, in case on concurrent
check-ins on MySQL with default transaction isolation level, this might
lead to Internal Server Errors on all but one check-ins, which is still
better than to show green.
2019-03-04 18:51:52 +01:00
Raphael Michel
e70738ae0c Fix percentage bar in list of check-in lists 2019-03-04 18:44:21 +01:00
Raphael Michel
5750201bc3 Position API: search in attendee_email 2019-03-04 11:04:39 +01:00
564 changed files with 256480 additions and 68773 deletions

View File

@@ -1,4 +1,5 @@
language: python
dist: xenial
sudo: false
install:
- pip install -U pip wheel setuptools
@@ -12,23 +13,21 @@ services:
- postgresql
matrix:
include:
- python: 3.6
- python: 3.7
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
- python: 3.6
- python: 3.7
env: JOB=tests-cov PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.6
- python: 3.7
env: JOB=style
- python: 3.6
- python: 3.7
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
- python: 3.6
- python: 3.7
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.5
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.6
env: JOB=plugins
- python: 3.6
- python: 3.7
env: JOB=doc-spelling
- python: 3.6
- python: 3.7
env: JOB=translation-spelling
addons:
postgresql: "9.4"

View File

@@ -57,6 +57,8 @@ COPY deployment/docker/nginx.conf /etc/nginx/nginx.conf
COPY deployment/docker/production_settings.py /pretix/src/production_settings.py
COPY src /pretix/src
RUN cd /pretix/src && pip3 install .
RUN chmod +x /usr/local/bin/pretix && \
rm /etc/nginx/sites-enabled/default && \
cd /pretix/src && \

View File

@@ -78,6 +78,15 @@ Example::
Enables or disables nagging staff users for leaving comments on their sessions for auditability.
Defaults to ``off``.
``obligatory_2fa``
Enables or disables obligatory usage of Two-Factor Authentication for users of the pretix backend.
Defaults to ``False``
``trust_x_forwarded_for``
Specifies whether the ``X-Forwarded-For`` header can be trusted. Only set to ``on`` if you have a reverse
proxy that actively removes and re-adds the header to make sure the correct client IP is the first value.
Defaults to ``off``.
Locale settings
---------------
@@ -125,6 +134,8 @@ Example::
Indicates if the database backend is a MySQL/MariaDB Galera cluster and
turns on some optimizations/special case handlers. Default: ``False``
.. _`config-replica`:
Database replica settings
-------------------------
@@ -142,6 +153,8 @@ Example::
[replica]
host=192.168.0.2
.. _`config-urls`:
URLs
----
@@ -269,6 +282,24 @@ to speed up various operations::
If redis is not configured, pretix will store sessions and locks in the database. If memcached
is configured, memcached will be used for caching instead of redis.
Translations
------------
pretix comes with a number of translations. Some of them are marked as "incubating", which means
they can usually only be selected in development mode. If you want to use them nevertheless, you
can activate them like this::
[languages]
allow_incubating=pt-br,da
You can also tell pretix about additional paths where it will search for translations::
[languages]
path=/path/to/my/translations
For a given language (e.g. ``pt-br``), pretix will then look in the
specific sub-folder, e.g. ``/path/to/my/translations/pt_BR/LC_MESSAGES/django.po``.
Celery task queue
-----------------

View File

@@ -11,3 +11,4 @@ This documentation is for everyone who wants to install pretix on a server.
installation/index
config
maintainance
scaling

View File

@@ -58,16 +58,29 @@ Database
--------
Next, we need a database and a database user. We can create these with any kind of database managing tool or directly on
our database's shell, e.g. for MySQL::
our database's shell. For PostgreSQL, we would do::
$ mysql -u root -p
mysql> CREATE DATABASE pretix DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
mysql> GRANT ALL PRIVILEGES ON pretix.* TO pretix@'localhost' IDENTIFIED BY '*********';
mysql> FLUSH PRIVILEGES;
# sudo -u postgres createuser -P pretix
# sudo -u postgres createdb -O pretix pretix
Replace the asterisks with a password of your own. For MySQL, we will use a unix domain socket to connect to the
database. For PostgreSQL, be sure to configure the interface binding and your firewall so that the docker container
can reach PostgreSQL.
Make sure that your database listens on the network. If PostgreSQL on the same same host as docker, but not inside a docker container, we recommend that you just listen on the Docker interface by changing the following line in ``/etc/postgresql/<version>/main/postgresql.conf``::
listen_addresses = 'localhost,172.17.0.1'
You also need to add a new line to ``/etc/postgresql/<version>/main/pg_hba.conf`` to allow network connections to this user and database::
host pretix pretix 172.17.0.1/16 md5
Restart PostgreSQL after you changed these files::
# systemctl restart postgresql
If you have a firewall running, you should also make sure that port 5432 is reachable from the ``172.17.0.1/16`` subnet.
For MySQL, you can either also use network-based connections or mount the ``/var/run/mysqld/mysqld.sock`` socket into the docker container.
When using MySQL, make sure you set the character set of the database to ``utf8mb4``, e.g. like this::
mysql > CREATE DATABASE pretix DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
Redis
-----
@@ -114,13 +127,16 @@ Fill the configuration file ``/etc/pretix/pretix.cfg`` with the following conten
datadir=/data
[database]
; Replace mysql with postgresql_psycopg2 for PostgreSQL
backend=mysql
; Replace postgresql with mysql for MySQL
backend=postgresql
name=pretix
user=pretix
; Replace with the password you chose above
password=*********
; Replace with host IP address for PostgreSQL
host=/var/run/mysqld/mysqld.sock
; In most docker setups, 172.17.0.1 is the address of the docker host. Adjuts
; this to wherever your database is running, e.g. the name of a linked container
; or of a mounted MySQL socket.
host=172.17.0.1
[mail]
; See config file documentation for more options
@@ -164,14 +180,15 @@ named ``/etc/systemd/system/pretix.service`` with the following content::
-v /var/pretix-data:/data \
-v /etc/pretix:/etc/pretix \
-v /var/run/redis:/var/run/redis \
-v /var/run/mysqld:/var/run/mysqld \
pretix/standalone:stable all
ExecStop=/usr/bin/docker stop %n
[Install]
WantedBy=multi-user.target
You can leave the MySQL socket volume out if you're using PostgreSQL. You can now run the following commands
When using MySQL and socket mounting, you'll need the additional flag ``-v /var/run/mysqld:/var/run/mysqld`` in the command.
You can now run the following commands
to enable and start the service::
# systemctl daemon-reload

View File

@@ -1,3 +1,5 @@
.. _`installation`:
Installation guide
==================

View File

@@ -50,21 +50,23 @@ Database
--------
Having the database server installed, we still need a database and a database user. We can create these with any kind
of database managing tool or directly on our database's shell, e.g. for MySQL::
of database managing tool or directly on our database's shell. For PostgreSQL, we would do::
$ mysql -u root -p
mysql> CREATE DATABASE pretix DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
mysql> GRANT ALL PRIVILEGES ON pretix.* TO pretix@'localhost' IDENTIFIED BY '*********';
mysql> FLUSH PRIVILEGES;
# sudo -u postgres createuser pretix
# sudo -u postgres createdb -O pretix pretix
When using MySQL, make sure you set the character set of the database to ``utf8mb4``, e.g. like this::
mysql > CREATE DATABASE pretix DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
Package dependencies
--------------------
To build and run pretix, you will need the following debian packages::
# apt-get install git build-essential python-dev python-virtualenv python3 python3-pip \
# apt-get install git build-essential python-dev python3-venv python3 python3-pip \
python3-dev libxml2-dev libxslt1-dev libffi-dev zlib1g-dev libssl-dev \
gettext libpq-dev libmysqlclient-dev libjpeg-dev libopenjp2-7-dev
gettext libpq-dev libmariadbclient-dev libjpeg-dev libopenjp2-7-dev
Config file
-----------
@@ -85,13 +87,18 @@ Fill the configuration file ``/etc/pretix/pretix.cfg`` with the following conten
datadir=/var/pretix/data
[database]
; Replace mysql with postgresql_psycopg2 for PostgreSQL
backend=mysql
; For MySQL, replace with "mysql"
backend=postgresql
name=pretix
user=pretix
password=*********
; Replace with host IP address for PostgreSQL
host=/var/run/mysqld/mysqld.sock
; For MySQL, enter the user password. For PostgreSQL on the same host,
; we don't need one because we can use peer authentification if our
; PostgreSQL user matches our unix user.
password=
; For MySQL, use local socket, e.g. /var/run/mysqld/mysqld.sock
; For a remote host, supply an IP address
; For local postgres authentication, you can leave it empty
host=
[mail]
; See config file documentation for more options
@@ -115,14 +122,14 @@ Now we will install pretix itself. The following steps are to be executed as the
actually install pretix, we will create a virtual environment to isolate the python packages from your global
python installation::
$ virtualenv -p python3 /var/pretix/venv
$ python3 -m venv /var/pretix/venv
$ source /var/pretix/venv/bin/activate
(venv)$ pip3 install -U pip setuptools wheel
We now install pretix, its direct dependencies and gunicorn. Replace ``mysql`` with ``postgres`` in the following
command if you're running PostgreSQL::
We now install pretix, its direct dependencies and gunicorn. Replace ``postgres`` with ``mysql`` in the following
command if you're running MySQL::
(venv)$ pip3 install "pretix[mysql]" gunicorn
(venv)$ pip3 install "pretix[postgres]" gunicorn
Note that you need Python 3.5 or newer. You can find out your Python version using ``python -V``.
@@ -268,10 +275,10 @@ Updates
.. warning:: While we try hard not to break things, **please perform a backup before every upgrade**.
To upgrade to a new pretix release, pull the latest code changes and run the following commands (again, replace
``mysql`` with ``postgres`` if necessary)::
``postgres`` with ``mysql`` if necessary)::
$ source /var/pretix/venv/bin/activate
(venv)$ pip3 install -U pretix[mysql] gunicorn
(venv)$ pip3 install -U pretix[postgres] gunicorn
(venv)$ python -m pretix migrate
(venv)$ python -m pretix rebuild
(venv)$ python -m pretix updatestyles

236
doc/admin/scaling.rst Normal file
View File

@@ -0,0 +1,236 @@
.. _`scaling`:
Scaling guide
=============
Our :ref:`installation guide <installation>` only covers "small-scale" setups, by which we mostly mean
setups that run on a **single (virtual) machine** and do not encounter large traffic peaks.
We do not offer an installation guide for larger-scale setups of pretix, mostly because we believe that
there is no one-size-fits-all solution for this and the desired setup highly depends on your use case,
the platform you run pretix on, and your technical capabilities. We do not recommend trying set up pretix
in a multi-server environment if you do not already have experience with managing server clusters.
This document is intended to give you a general idea on what issues you will encounter when you scale up
and what you should think of.
.. tip::
If you require more help on this, we're happy to help. Our pretix Enterprise support team has built
and helped building, scaling and load-testing pretix installations at any scale and we're looking
forward to work with you on fine-tuning your system. If you intend to sell **more than a thousand
tickets in a very short amount of time**, we highly recommend reaching out and at least talking this
through. Just get in touch at sales@pretix.eu!
Scaling reasons
---------------
There's mainly two reasons to scale up a pretix installation beyond a single server:
* **Availability:** Distributing pretix over multiple servers can allow you to survive failure of one or more single machines, leading to a higher uptime and reliability of your system.
* **Traffic and throughput:** Distributing pretix over multiple servers can allow you to process more web requests and ticket sales at the same time.
You are very unlikely to require scaling for other reasons, such as having too much data in your database.
Components
----------
A pretix installation usually consists of the following components which run performance-relevant processes:
* ``pretix-web`` is the Django-based web application that serves all user interaction.
* ``pretix-worker`` is a Celery-based application that processes tasks that should be run asynchronously outside of the web application process.
* A **SQL database** keeps all the important data and processes the actual transactions. We recommend using PostgreSQL, but MySQL/MariaDB works as well.
* A **web server** that terminates TLS and HTTP connections and forwards them to ``pretix-web``. In some cases, e.g. when serving static files, the web servers might return a response directly. We recommend using ``nginx``.
* A **redis** server responsible for the communication between ``pretix-web`` and ``pretix-worker``, as well as for caching.
* A directory of **media files** such as user-uploaded files or generated files (tickets, invoices, …) that are created and used by ``pretix-web``, ``pretix-worker`` and the web server.
In the following, we will discuss the scaling behavior of every component individually. In general, you can run all of the components
on the same server, but you can just as well distribute every component to its own server, or even use multiple servers for some single
components.
.. warning::
When setting up your system, don't forget about security. In a multi-server environment,
you need to take special care to ensure that no unauthorized access to your database
is possible through the network and that it's not easy to wiretap your connections. We
recommend a rigorous use of firewalls and encryption on all communications. You can
ensure this either on an application level (such as using the TLS support in your
database) or on a network level with a VPN solution.
Web server
""""""""""
Your web server is at the very front of your installation. It will need to absorb all of the traffic, and it should be able to
at least show a decent error message, even when everything else fails. Luckily, web servers are really fast these days, so this
can be achieved without too much work.
We recommend reading up on tuning your web server for high concurrency. For nginx, this means thinking about the number of worker
processes and the number of connections each worker process accepts. Double-check that TLS session caching works, because TLS
handshakes can get really expensive.
During a traffic peak, your web server will be able to make us of more CPU resources, while memory usage will stay comparatively low,
so if you invest in more hardware here, invest in more and faster CPU cores.
Make sure that pretix' static files (such as CSS and JavaScript assets) as well as user-uploaded media files (event logos, etc)
are served directly by your web server and your web server caches them in-memory (nginx does it by default) and sets useful
headers for client-side caching. As an additional performance improvement, you can turn of access logging for these types of files.
If you want, you can even farm out serving static files to a different web server entirely and :ref:`configure pretix to reference
them from a different URL <config-urls>`.
.. tip::
If you expect *really high traffic* for your very popular event, you might want to do some rate limiting on this layer, or,
if you want to ensure a fair and robust first-come-first-served experience and prefer letting users wait over showing them
errors, consider a queuing solution. We're happy to provide you with such systems, just get in touch at sales@pretix.eu.
pretix-web
""""""""""
The ``pretix-web`` process does not carry any internal state can be easily started on as many machines as you like, and you can
use the load balancing features of your frontend web server to redirect to all of them.
You can adjust the number of processes in the ``gunicorn`` command line, and we recommend choosing roughly two times the number
of CPU cores available. Under load, the memory consumption of ``pretix-web`` will stay comparatively constant, while the CPU usage
will increase a lot. Therefore, if you can add more or faster CPU cores, you will be able to serve more users.
pretix-worker
"""""""""""""
The ``pretix-worker`` process performs all operations that are not directly executed in the request-response-cycle of ``pretix-web``.
Just like ``pretix-web`` you can easily start up as many instances as you want on different machines to share the work. As long as they
all talk to the same redis server, they will all receive tasks from ``pretix-web``, work on them and post their result back.
You can configure the number of threads that run tasks in parallel through the ``--concurrency`` command line option of ``celery``.
Just like ``pretix-web``, this process is mostly heavy on CPU, disk IO and network IO, although memory peaks can occur e.g. during the
generation of large PDF files, so we recommend having some reserves here.
``pretix-worker`` performs a variety of tasks which are of different importance.
Some of them are mission-critical and need to be run quickly even during high load (such as
creating a cart or an order), others are irrelevant and can easily run later (such as
distributing tickets on the waiting list). You can fine-tune the capacity you assign to each
of these tasks by running ``pretix-worker`` processes that only work on a specific **queue**.
For example, you could have three servers dedicated only to process order creations and one
server dedicated only to sending emails. This allows you to set priorities and also protects
you from e.g. a slow email server lowering your ticket throughput.
You can do so by specifying one or more queues on the ``celery`` command line of this process, such as ``celery -A pretix.celery_app worker -Q notifications,mail``. Currently,
the following queues exist:
* ``checkout`` -- This queue handles everything related to carts and orders and thereby everything required to process a sale. This includes adding and deleting items from carts as well as creating and canceling orders.
* ``mail`` -- This queue handles sending of outgoing emails.
* ``notifications`` -- This queue handles the processing of any outgoing notifications, such as email notifications to admin users (except for the actual sending) or API notifications to registered webhooks.
* ``background`` -- This queue handles tasks that are expected to take long or have no human waiting for their result immediately, such as refreshing caches, re-generating CSS files, assigning tickets on the waiting list or parsing bank data files.
* ``default`` -- This queue handles everything else with "medium" or unassigned priority, most prominently the generation of files for tickets, invoices, badges, admin exports, etc.
Media files
"""""""""""
Both ``pretix-web``, ``pretix-worker`` and in some cases your webserver need to work with
media files. Media files are all files generated *at runtime* by the software. This can
include files uploaded by the event organizers, such as the event logo, files uploaded by
ticket buyers (if you use such features) or files generated by the software, such as
ticket files, invoice PDFs, data exports or customized CSS files.
Those files are by default stored to the ``media/`` sub-folder of the data directory given
in the ``pretix.cfg`` configuration file. Inside that ``media/`` folder, you will find a
``pub/`` folder containing the subset of files that should be publicly accessible through
the web server. Everything else only needs to be accessible by ``pretix-web`` and
``pretix-worker`` themselves.
If you distribute ``pretix-web`` or ``pretix-worker`` across more than one machine, you
**must** make sure that they all have access to a shared storage to read and write these
files, otherwise you **will** run into errors with the user interface.
The easiest solution for this is probably to store them on a NFS server that you mount
on each of the other servers.
Since we use Django's file storage mechanism internally, you can in theory also use a object-storage solution like Amazon S3, Ceph, or Minio to store these files, although we currently do not expose this through pretix' configuration file and this would require you to ship your own variant of ``pretix/settings.py`` and reference it through the ``DJANGO_SETTINGS_MODULE`` environment variable.
At pretix.eu, we use a custom-built `object storage cluster`_.
SQL database
""""""""""""
One of the most critical parts of the whole setup is the SQL database -- and certainly the
hardest to scale. Tuning relational databases is an art form, and while there's lots of
material on it on the internet, there's not a single recipe that you can apply to every case.
As a general rule of thumb, the more resources you can give your databases, the better.
Most databases will happily use all CPU cores available, but only use memory up to an amount
you configure, so make sure to set this memory usage as high as you can afford. Having more
memory available allows your database to make more use of caching, which is usually good.
Scaling your database to multiple machines needs to be treated with great caution. It's a
good to have a replica of your database for availability reasons. In case your primary
database server fails, you can easily switch over to the replica and continue working.
However, using database replicas for performance gains is much more complicated. When using
replicated database systems, you are always trading in consistency or availability to get
additional performance and the consequences of this can be subtle and it is important
that you have a deep understanding of the semantics of your replication mechanism.
.. warning::
Using an off-the-shelf database proxy solution that redirects read queries to your
replicas and write queries to your primary database **will lead to very nasty bugs.**
As an example, if you buy a ticket, pretix first needs to calculate how many tickets
are left to sell. If this calculation is done on a database replica that lags behind
even for fractions of a second, the decision to allow selling the ticket will be made
on out-of-data data and you can end up with more tickets sold than configured. Similarly,
you could imagine situations leading to double payments etc.
If you do have a replica, you *can* tell pretix about it :ref:`in your configuration <config-replica>`.
This way, pretix can offload complex read-only queries to the replica when it is safe to do so.
As of pretix 2.7, this is mainly used for search queries in the backend and for rendering the
product list and event lists in the frontend, but we plan on expanding this in the future.
Therefore, for now our clear recommendation is: Try to scale your database vertically and put
it on the most powerful machine you have available.
redis
"""""
While redis is a very important part that glues together some of the components, it isn't used
heavily and can usually handle a fairly large pretix installation easily on a single modern
CPU core.
Having some memory available is good in case of e.g. lots of tasks queuing up during a traffic peak, but we wouldn't expect ever needing more than a gigabyte of it.
Feel free to set up a redis cluster for availability but you won't need it for performance in a long time.
The limitations
---------------
Up to a certain point, pretix scales really well. However, there are a few things that we consider
even more important than scalability, and those are correctness and reliability. We want you to be
able to trust that pretix will not sell more tickets than you intended or run into similar error
cases.
Combined with pretix' flexibility and complexity, especially around vouchers and quotas, this creates
some hard issues. In many cases, we need to fall back to event-global locking for some actions which
are likely to run with high concurrency and cause harm.
For every event, only one of these locking actions can be run at the same time. Examples for this are
adding products limited by a quota to a cart, adding items to a cart using a voucher or placing an order
consisting of cart positions that don't have a valid reservation for much longer. In these cases, it is
currently not realistically possible to exceed selling **approx. 500 orders per minute per event**, even
if you add more hardware.
If you have an unlimited number of tickets, we can apply fewer locking and we've reached **approx.
1500 orders per minute per event** in benchmarks, although even more should be possible.
We're working to reduce the number of cases in which this is relevant and thereby improve the possible
throughput. If you want to use pretix for an event with 10,000+ tickets that are likely to be sold out
within minutes, please get in touch to discuss possible solutions. We'll work something out for you!
.. _object storage cluster: https://behind.pretix.eu/2018/03/20/high-available-cdn/

View File

@@ -181,4 +181,37 @@ as the string values ``true`` and ``false``.
If the ``ordering`` parameter is documented for a resource, you can use it to sort the result set by one of the allowed
fields. Prepend a ``-`` to the field name to reverse the sort order.
Idempotency
-----------
Our API supports an idempotency mechanism to make sure you can safely retry operations without accidentally performing
them twice. This is useful if an API call experiences interruptions in transit, e.g. due to a network failure, and you
do not know if it completed successfully.
To perform an idempotent request, add a ``X-Idempotency-Key`` header with a random string value (we recommend a version
4 UUID) to your request. If we see a second request with the same ``X-Idempotency-Key`` and the same ``Authorization``
and ``Cookie`` headers, we will not perform the action for a second time but return the exact same response instead.
Please note that this also goes for most error responses. For example, if we returned you a ``403 Permission Denied``
error and you retry with the same ``X-Idempotency-Key``, you will get the same error again, even if you were granted
permission in the meantime! This includes internal server errors on our side that might have been fixed in the meantime.
There are only three exceptions to the rule:
* Responses with status code ``409 Conflict`` are not cached. If you send the request again, it will be executed as a
new request, since these responses are intended to be retried.
* Rate-limited responses with status code ``429 Too Many Requests`` are not cached and you can safely retry them.
* Responses with status code ``503 Service Unavailable`` are not cached and you can safely retry them.
If you send a request with an ``X-Idempotency-Key`` header that we have seen before but that has not yet received a
response, you will receive a response with status code ``409 Conflict`` and are asked to retry after five seconds.
We store idempotency keys for 24 hours, so you should never retry a request after a longer time period.
All ``POST``, ``PUT``, ``PATCH``, or ``DELETE`` api calls support idempotency keys. Adding an idempotency key to a
``GET``, ``HEAD``, or ``OPTIONS`` request has no effect.
.. _CSRF policies: https://docs.djangoproject.com/en/1.11/ref/csrf/#ajax

View File

@@ -0,0 +1,132 @@
Creating an external checkout process
=====================================
Occasionally, we get asked whether it is possible to just use pretix' powerful backend as a ticketing engine but use
a fully-customized checkout process that only communicates via the API. This is possible, but with a few limitations.
If you go down this route, you will miss out on many of pretix features and safeguards, as well as the added flexibility
by most of pretix' plugins. We strongly recommend to talk this through with us before you decide this is the way to go.
However, this is really useful if you need to tightly integrate pretix into existing web applications that e.g. control
the pricing of your products in a way that cannot be mapped to pretix' product structures.
Creating orders
---------------
After letting your user select the products to buy in your application, you should create a new order object inside
pretix. Below, you can see an example of such an order, but most fields are optional and there are some more features
supported. Read :ref:`rest-orders-create` to learn more about this endpoint.
Please note that this endpoint assumes trustworthy input for the most part. By default, the endpoint checks that
you do not exceed any quotas, do not sell any seats twice, or do not use any redeemed vouchers. However, it will not
complain about violation of any other availability constraints, such as violation of time frames or minimum/maximum
amounts of either your product or event. Bundled products will not be added in automatically and fees will not be
calculated automatically.
.. sourcecode:: http
POST /api/v1/organizers/democon/events/3vjrh/orders/ HTTP/1.1
Host: test.pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Authorization: …
{
"email": "dummy@example.org",
"locale": "en",
"sales_channel": "web",
"payment_provider": "banktransfer",
"invoice_address": {
"is_business": false,
"company": "Sample company",
"name_parts": {"full_name": "John Doe"},
"street": "Sesam Street 12",
"zipcode": "12345",
"city": "Sample City",
"country": "US",
"state": "NY",
"internal_reference": "",
"vat_id": ""
},
"positions": [
{
"item": 21,
"variation": null,
"attendee_name_parts": {
"full_name": "Peter"
},
"answers": [
{
"question": 1,
"answer": "23",
"options": []
}
],
"subevent": null
}
],
"fees": []
}
You will be returned a full order object that you can inspect, store, or use to build emails or confirmation pages for
the user. If you don't want to do that yourself, it will also contain the URL to our confirmation page in the ``url``
attribute. If you pass the ``"send_mail": true`` option, pretix will also send order confirmations for you.
Handling payments yourself
--------------------------
If you want to handle payments in your application, you can either just create the orders with status "paid" or you can
create them in "pending" state (the default) and later confirm the payment. We strongly advise to use the payment
provider ``"manual"`` in this case to avoid interference with payment code with pretix.
However, it is often unfeasible to implement the payment process yourself, and it also requires you to give up a
lot of pretix functionality, such as automatic refunds. Therefore, it is also possible to utilize pretix' native
payment process even in this case:
Using pretix payment providers
------------------------------
If you passed a ``payment_provider`` during order creation above, pretix will have created a payment object with state
``created`` that you can see in the returned order object. This payment object will have an attribute ``payment_url``
that you can use to let the user pay. For example, you could link or redirect to this page.
If you want the user to return to your application after the payment is complete, you can pass a query parameter
``return_url``. To prepare your event for this, open your event in the pretix backend and go to "Settings", then
"Plugins". Enable the plugin "Redirection from order page". Then, go to the new page "Settings", then "Redirection".
Enter the base URL of your web application. This will allow you to redirect to pages under this base URL later on.
For example, if you want users to be redirected to ``https://example.org/order/return?tx_id=1234``, you could now
either enter ``https://example.org`` or ``https://example.org/order/``.
The user will be redirected back to your page instead of pretix' order confirmation page after the payment,
**regardless of whether it was successful or not**. Make sure you use our API to check if the payment actually
worked! Your final URL could look like this::
https://test.pretix.eu/democon/3vjrh/order/NSLEZ/ujbrnsjzbq4dzhck/pay/123/?return_url=https%3A%2F%2Fexample.org%2Forder%2Freturn%3Ftx_id%3D1234
You can also embed this page in an ``<iframe>`` instead. Note, however, that this causes problems with some payment
methods such as PayPal which do not allow being opened in an iframe. pretix can partly work around these issues by
opening a new window, but will only to so if you also append an ``iframe=1`` parameter to the URL::
https://test.pretix.eu/democon/3vjrh/order/NSLEZ/ujbrnsjzbq4dzhck/pay/123/?return_url=https%3A%2F%2Fexample.org%2Forder%2Freturn%3Ftx_id%3D1234&iframe=1
If you did **not** pass a payment method since you want us to ask the user which payment method they want to use, you
need to construct the URL from the ``url`` attribute of the order and the sub-path ``pay/change```. For example, you
would end up with the following URL::
https://test.pretix.eu/democon/3vjrh/order/NSLEZ/ujbrnsjzbq4dzhck/pay/change
Of course, you can also use the ``iframe`` and ``return_url`` parameters here.
Optional: Cart reservations
---------------------------
Creating orders is an atomic operation: The order is either created as a whole or not at all. However, pretix'
built-in checkout automatically reserves tickets in a user's cart for a configurable amount of time to ensure users
will actually get their tickets once they started entering all their details. If you want a similar behavior in your
application, you need to create :ref:`rest-carts` through the API.
When creating your order, you can pass a ``consume_carts`` parameter with the cart ID(s) of your user. This way, the
quota reserved by the cart will be credited towards the order and the carts will be destroyed if (and only if) the
order creation succeeds.
Cart creation is currently even more limited than the order creation endpoints, as cart creation currently does not
support vouchers or automatic price calculation. If you require these features, please get in touch with us.

11
doc/api/guides/index.rst Normal file
View File

@@ -0,0 +1,11 @@
.. _`rest-api-guides`:
API Usage Guides
================
This part of the documentation contains how-to guides on some special use cases of our API.
.. toctree::
:maxdepth: 2
custom_checkout

View File

@@ -18,3 +18,4 @@ in functionality over time.
resources/index
ratelimit
webhooks
guides/index

View File

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

View File

@@ -36,12 +36,20 @@ answers list of objects Answers to user
├ 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
seat objects The assigned seat. Can be ``null``.
├ id integer Internal ID of the seat instance
├ name string Human-readable seat name
└ seat_guid string Identifier of the seat within the seating plan
===================================== ========================== =======================================================
.. versionchanged:: 1.17
This resource has been added.
.. versionchanged:: 3.0
This ``seat`` attribute has been added.
Cart position endpoints
-----------------------
@@ -87,6 +95,7 @@ Cart position endpoints
"datetime": "2018-06-11T10:00:00Z",
"expires": "2018-06-11T10:00:00Z",
"includes_tax": true,
"seat": null,
"answers": []
}
]
@@ -132,6 +141,7 @@ Cart position endpoints
"datetime": "2018-06-11T10:00:00Z",
"expires": "2018-06-11T10:00:00Z",
"includes_tax": true,
"seat": null,
"answers": []
}
@@ -178,6 +188,7 @@ Cart position endpoints
* ``item``
* ``variation`` (optional)
* ``price``
* ``seat`` (The ``seat_guid`` attribute of a seat. Required when the specified ``item`` requires a seat, otherwise must be ``null``.)
* ``attendee_name`` **or** ``attendee_name_parts`` (optional)
* ``attendee_email`` (optional)
* ``subevent`` (optional)
@@ -196,7 +207,7 @@ Cart position endpoints
POST /api/v1/organizers/bigevents/events/sampleconf/cartpositions/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content: application/json
Content-Type: application/json
{
"item": 1,

View File

@@ -20,7 +20,7 @@ internal_name string An optional nam
description multi-lingual string A public description (might include markdown, can
be ``null``)
position integer An integer, used for sorting the categories
is_addon boolean If ``True``, items within this category are not on sale
is_addon boolean If ``true``, items within this category are not on sale
on their own but the category provides a source for
defining add-ons for other products.
===================================== ========================== =======================================================
@@ -131,7 +131,7 @@ Endpoints
POST /api/v1/organizers/bigevents/events/sampleconf/categories/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content: application/json
Content-Type: application/json
{
"name": {"en": "Tickets"},

View File

@@ -1,3 +1,5 @@
.. spelling:: checkin
Check-in lists
==============
@@ -27,6 +29,7 @@ subevent integer ID of the date
position_count integer Number of tickets that match this list (read-only).
checkin_count integer Number of check-ins performed on this list (read-only).
include_pending boolean If ``true``, the check-in list also contains tickets from orders in pending state.
auto_checkin_sales_channels list of strings All items on the check-in list will be automatically marked as checked-in when purchased through any of the listed sales channels.
===================================== ========================== =======================================================
.. versionchanged:: 1.10
@@ -41,6 +44,10 @@ include_pending boolean If ``true``, th
The ``include_pending`` field has been added.
.. versionchanged:: 3.2
The ``auto_checkin_sales_channels`` field has been added.
Endpoints
---------
@@ -81,7 +88,10 @@ Endpoints
"all_products": true,
"limit_products": [],
"include_pending": false,
"subevent": null
"subevent": null,
"auto_checkin_sales_channels": [
"pretixpos"
]
}
]
}
@@ -122,7 +132,10 @@ Endpoints
"all_products": true,
"limit_products": [],
"include_pending": false,
"subevent": null
"subevent": null,
"auto_checkin_sales_channels": [
"pretixpos"
]
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -156,14 +169,14 @@ Endpoints
"checkin_count": 17,
"position_count": 42,
"event": {
"name": "Demo Converence",
"name": "Demo Conference"
},
"items": [
{
"name": "T-Shirt",
"id": 1,
"checkin_count": 1,
"admission": False,
"admission": false,
"position_count": 1,
"variations": [
{
@@ -184,7 +197,7 @@ Endpoints
"name": "Ticket",
"id": 2,
"checkin_count": 15,
"admission": True,
"admission": true,
"position_count": 22,
"variations": []
}
@@ -209,13 +222,16 @@ Endpoints
POST /api/v1/organizers/bigevents/events/sampleconf/checkinlists/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content: application/json
Content-Type: application/json
{
"name": "VIP entry",
"all_products": false,
"limit_products": [1, 2],
"subevent": null
"subevent": null,
"auto_checkin_sales_channels": [
"pretixpos"
]
}
**Example response**:
@@ -234,7 +250,10 @@ Endpoints
"all_products": false,
"limit_products": [1, 2],
"include_pending": false,
"subevent": null
"subevent": null,
"auto_checkin_sales_channels": [
"pretixpos"
]
}
:param organizer: The ``slug`` field of the organizer of the event/item to create a list for
@@ -283,7 +302,10 @@ Endpoints
"all_products": false,
"limit_products": [1, 2],
"include_pending": false,
"subevent": null
"subevent": null,
"auto_checkin_sales_channels": [
"pretixpos"
]
}
:param organizer: The ``slug`` field of the organizer to modify
@@ -336,11 +358,29 @@ Order position endpoints
The order positions endpoint has been extended by the filter queries ``voucher`` and ``voucher__code``.
.. versionchanged:: 2.7
The resource now contains the new attributes ``require_attention`` and ``order__status`` and accepts the new
``ignore_status`` filter. The ``attendee_name`` field is now "smart" (see below) and the redemption endpoint
returns ``400`` instead of ``404`` on tickets which are known but not paid.
.. versionchanged:: 3.2
The ``checkins`` dict now also contains a ``auto_checked_in`` value to indicate if the check-in has been performed
automatically by the system.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(list)/positions/
Returns a list of all order positions within a given event. The result is the same as
the :ref:`order-position-resource`, with one important difference: the ``checkins`` value will only include
check-ins for the selected list.
the :ref:`order-position-resource`, with the following differences:
* The ``checkins`` value will only include check-ins for the selected list.
* An additional boolean property ``require_attention`` will inform you whether either the order or the item
have the ``checkin_attention`` flag set.
* If ``attendee_name`` is empty, it will automatically fall back to values from a parent product or from invoice
addresses.
**Example request**:
@@ -383,10 +423,12 @@ Order position endpoints
"addon_to": null,
"subevent": null,
"pseudonymization_id": "MQLJvANO3B",
"seat": null,
"checkins": [
{
"list": 1,
"datetime": "2017-12-25T12:45:23Z"
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": true
}
],
"answers": [
@@ -407,6 +449,8 @@ Order position endpoints
}
:query integer page: The page number in case of a multi-page result set, default is 1
:query string ignore_status: If set to ``true``, results will be returned regardless of the state of
the order they belong to and you will need to do your own filtering by order status.
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``order__code``,
``order__datetime``, ``positionid``, ``attendee_name``, ``last_checked_in`` and ``order__email``. Default:
``attendee_name,positionid``
@@ -442,8 +486,15 @@ Order position endpoints
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(list)/positions/(id)/
Returns information on one order position, identified by its internal ID.
The result format is the same as the :ref:`order-position-resource`, with one important difference: the
``checkins`` value will only include check-ins for the selected list.
The result is the same as the :ref:`order-position-resource`, with the following differences:
* The ``checkins`` value will only include check-ins for the selected list.
* An additional boolean property ``require_attention`` will inform you whether either the order or the item
have the ``checkin_attention`` flag set.
* If ``attendee_name`` is empty, it will automatically fall back to values from a parent product or from invoice
addresses.
**Instead of an ID, you can also use the ``secret`` field as the lookup parameter.**
@@ -483,10 +534,12 @@ Order position endpoints
"addon_to": null,
"subevent": null,
"pseudonymization_id": "MQLJvANO3B",
"seat": null,
"checkins": [
{
"list": 1,
"datetime": "2017-12-25T12:45:23Z"
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": true
}
],
"answers": [
@@ -524,11 +577,14 @@ Order position endpoints
you do not implement question handling in your user interface, you **must**
set this to ``false``. In that case, questions will just be ignored. Defaults
to ``true``.
:<json boolean canceled_supported: When this parameter is set to ``true``, the response code ``canceled`` may be
returned. Otherwise, canceled orders will return ``unpaid``.
:<json datetime datetime: Specifies the datetime of the check-in. If not supplied, the current time will be used.
:<json boolean force: Specifies that the check-in should succeed regardless of previous check-ins or required
questions that have not been filled. Defaults to ``false``.
:<json boolean ignore_unpaid: Specifies that the check-in should succeed even if the order is in pending state.
Defaults to ``false``.
Defaults to ``false`` and only works when ``include_pending`` is set on the check-in
list.
:<json string nonce: You can set this parameter to a unique random value to identify this check-in. If you're sending
this request twice with the same nonce, the second request will also succeed but will always
create only one check-in object even when the previous request was successful as well. This
@@ -551,6 +607,7 @@ Order position endpoints
"nonce": "Pvrk50vUzQd0DhdpNRL4I4OcXsvg70uA",
"datetime": null,
"questions_supported": true,
"canceled_supported": true,
"answers": {
"4": "XS"
}
@@ -634,7 +691,9 @@ Order position endpoints
Possible error reasons:
* ``unpaid`` - Ticket is not paid for or has been refunded
* ``unpaid`` - Ticket is not paid for
* ``canceled`` Ticket is canceled or expired. This reason is only sent when your request sets
``canceled_supported`` to ``true``, otherwise these orders return ``unpaid``.
* ``already_redeemed`` - Ticket already has been redeemed
* ``product`` - Tickets with this product may not be scanned at this device

View File

@@ -25,11 +25,15 @@ is_public boolean If ``true``, th
presale_start datetime The date at which the ticket shop opens (or ``null``)
presale_end datetime The date at which the ticket shop closes (or ``null``)
location multi-lingual string The event location (or ``null``)
has_subevents boolean ``True`` if the event series feature is active for this
has_subevents boolean ``true`` if the event series feature is active for this
event. Cannot change after event is created.
meta_data dict Values set for organizer-specific meta data parameters.
meta_data object Values set for organizer-specific meta data parameters.
plugins list A list of package names of the enabled plugins for this
event.
seating_plan integer If reserved seating is in use, the ID of a seating
plan. Otherwise ``null``.
seat_category_mapping object An object mapping categories of the seating plan
(strings) to items in the event (integers or ``null``).
===================================== ========================== =======================================================
@@ -50,6 +54,14 @@ plugins list A list of packa
The ``testmode`` attribute has been added.
.. versionchanged:: 2.8
When cloning events, the ``testmode`` attribute will now be cloned, too.
.. versionchanged:: 3.0
The attributes ``seating_plan`` and ``seat_category_mapping`` have been added.
Endpoints
---------
@@ -95,6 +107,8 @@ Endpoints
"location": null,
"has_subevents": false,
"meta_data": {},
"seating_plan": null,
"seat_category_mapping": {},
"plugins": [
"pretix.plugins.banktransfer"
"pretix.plugins.stripe"
@@ -112,6 +126,9 @@ Endpoints
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned. Event series are never (always) returned.
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned. Event series are never (always) returned.
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned. Event series are never returned.
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``date_from`` and
``slug``. Keep in mind that ``date_from`` of event series does not really tell you anything.
Default: ``slug``.
:param organizer: The ``slug`` field of a valid organizer
:statuscode 200: no error
:statuscode 401: Authentication failure
@@ -153,6 +170,8 @@ Endpoints
"presale_end": null,
"location": null,
"has_subevents": false,
"seating_plan": null,
"seat_category_mapping": {},
"meta_data": {},
"plugins": [
"pretix.plugins.banktransfer"
@@ -184,7 +203,7 @@ Endpoints
POST /api/v1/organizers/bigevents/events/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content: application/json
Content-Type: application/json
{
"name": {"en": "Sample Conference"},
@@ -198,6 +217,8 @@ Endpoints
"is_public": false,
"presale_start": null,
"presale_end": null,
"seating_plan": null,
"seat_category_mapping": {},
"location": null,
"has_subevents": false,
"meta_data": {},
@@ -228,6 +249,8 @@ Endpoints
"presale_start": null,
"presale_end": null,
"location": null,
"seating_plan": null,
"seat_category_mapping": {},
"has_subevents": false,
"meta_data": {},
"plugins": [
@@ -246,7 +269,7 @@ Endpoints
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/clone/
Creates a new event with properties as set in the request body. The properties that are copied are: 'is_public',
settings, plugin settings, items, variations, add-ons, quotas, categories, tax rules, questions.
`testmode`, settings, plugin settings, items, variations, add-ons, quotas, categories, tax rules, questions.
If the 'plugins' and/or 'is_public' fields are present in the post body this will determine their value. Otherwise
their value will be copied from the existing event.
@@ -262,7 +285,7 @@ Endpoints
POST /api/v1/organizers/bigevents/events/sampleconf/clone/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content: application/json
Content-Type: application/json
{
"name": {"en": "Sample Conference"},
@@ -277,6 +300,8 @@ Endpoints
"presale_start": null,
"presale_end": null,
"location": null,
"seating_plan": null,
"seat_category_mapping": {},
"has_subevents": false,
"meta_data": {},
"plugins": [
@@ -307,6 +332,8 @@ Endpoints
"presale_end": null,
"location": null,
"has_subevents": false,
"seating_plan": null,
"seat_category_mapping": {},
"meta_data": {},
"plugins": [
"pretix.plugins.stripe",
@@ -335,7 +362,7 @@ Endpoints
PATCH /api/v1/organizers/bigevents/events/sampleconf/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content: application/json
Content-Type: application/json
{
"plugins": [
@@ -368,6 +395,8 @@ Endpoints
"presale_end": null,
"location": null,
"has_subevents": false,
"seating_plan": null,
"seat_category_mapping": {},
"meta_data": {},
"plugins": [
"pretix.plugins.banktransfer",
@@ -379,7 +408,7 @@ Endpoints
:param organizer: The ``slug`` field of the organizer of the event to update
:param event: The ``slug`` field of the event to update
:statuscode 201: no error
:statuscode 200: no error
:statuscode 400: The event 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.

View File

@@ -11,6 +11,7 @@ Resources and endpoints
categories
items
item_variations
item_bundles
item_add-ons
questions
question_options
@@ -22,3 +23,5 @@ Resources and endpoints
waitinglist
carts
webhooks
seatingplans
billing_invoices

View File

@@ -13,7 +13,7 @@ Field Type Description
===================================== ========================== =======================================================
number string Invoice number (with prefix)
order string Order code of the order this invoice belongs to
is_cancellation boolean ``True``, if this invoice is the cancellation of a
is_cancellation boolean ``true``, if this invoice is the cancellation of a
different invoice.
invoice_from string Sender address
invoice_to string Receiver address

View File

@@ -134,7 +134,7 @@ Endpoints
POST /api/v1/organizers/(organizer)/events/(event)/items/(item)/addons/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content: application/json
Content-Type: application/json
{
"addon_category": 1,
@@ -189,7 +189,7 @@ Endpoints
{
"min_count": 0,
"max_count": 10,
"max_count": 10
}
**Example response**:

View File

@@ -0,0 +1,242 @@
Item bundles
============
Resource description
--------------------
With bundles, you can specify products that are included within other products. There are two premier use cases of this:
* Package discounts. For example, you could offer a discounted package that includes three tickets but can only be
bought as a whole. With a bundle including three times the usual product, the package will automatically pull three
sub-items into the cart, making sure of correct quota calculation and issuance of the correct number of tickets.
* Tax splitting. For example, if your conference ticket includes a part that is subject to different taxation and that
you need to put on the invoice separately. When you putting a "designated price" on a bundled sub-item, pretix will
use that price to show a split taxation.
The bundles resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the bundling configuration
bundled_item integer Internal ID of the item that is included.
bundled_variation integer Internal ID of the variation of the item (or ``null``).
count integer Number of items included
designated_price money (string) Designated price of the bundled product. This will be
used to split the price of the base item e.g. for mixed
taxation. This is not added to the price.
===================================== ========================== =======================================================
.. versionchanged:: 2.6
This resource has been added.
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/bundles/
Returns a list of all bundles for a given item.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/items/11/bundles/ 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": 3,
"bundled_item": 3,
"bundled_variation": null,
"count": 1,
"designated_price": "0.00"
},
{
"id": 3,
"bundled_item": 3,
"bundled_variation": null,
"count": 2,
"designated_price": "1.50"
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param item: The ``id`` field of the item to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/item does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/bundles/(id)/
Returns information on one bundle configuration, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/items/1/bundles/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": 3,
"bundled_item": 3,
"bundled_variation": null,
"count": 2,
"designated_price": "1.50"
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param item: The ``id`` field of the item to fetch
:param id: The ``id`` field of the bundle 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/bigevents/events/sampleconf/items/1/bundles/
Creates a new bundle configuration
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/(organizer)/events/(event)/items/(item)/bundles/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"bundled_item": 3,
"bundled_variation": null,
"count": 2,
"designated_price": "1.50"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 3,
"bundled_item": 3,
"bundled_variation": null,
"count": 2,
"designated_price": "1.50"
}
:param organizer: The ``slug`` field of the organizer of the event/item to create a bundle-configuration for
:param event: The ``slug`` field of the event to create a bundle configuration for
:param item: The ``id`` field of the item to create a bundle configuration for
:statuscode 201: no error
:statuscode 400: The bundle 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)/items/(item)/bundles/(id)/
Update a bundle configuration. 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/items/1/bundles/3/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"count": 2
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 3,
"bundled_item": 3,
"bundled_variation": null,
"count": 2,
"designated_price": "1.50"
}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param item: The ``id`` field of the item to modify
:param id: The ``id`` field of the bundle to modify
:statuscode 200: no error
:statuscode 400: The bundle configuration 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)/items/(id)/bundles/(id)/
Delete a bundle configuration.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/items/1/bundles/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 modify
:param id: The ``id`` field of the bundle 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

@@ -18,12 +18,18 @@ default_price money (string) The price set d
price money (string) The price used for this variation. This is either the
same as ``default_price`` if that value is set or equal
to the item's ``default_price`` (read-only).
active boolean If ``False``, this variation will not be sold or shown.
original_price money (string) An original price, shown for comparison, not used
for price calculations (or ``null``).
active boolean If ``false``, this variation will not be sold or shown.
description multi-lingual string A public description of the variation. May contain
Markdown syntax or can be ``null``.
position integer An integer, used for sorting
===================================== ========================== =======================================================
.. versionchanged:: 2.7
The attribute ``original_price`` has been added.
.. versionchanged:: 1.12
This resource has been added.
@@ -67,7 +73,8 @@ Endpoints
},
"position": 0,
"default_price": "223.00",
"price": 223.0
"price": 223.0,
"original_price": null,
},
{
"id": 3,
@@ -120,6 +127,7 @@ Endpoints
},
"default_price": "10.00",
"price": "10.00",
"original_price": null,
"active": true,
"description": null,
"position": 0
@@ -144,7 +152,7 @@ Endpoints
POST /api/v1/organizers/bigevents/events/sampleconf/items/1/variations/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content: application/json
Content-Type: application/json
{
"value": {"en": "Student"},
@@ -167,6 +175,7 @@ Endpoints
"value": {"en": "Student"},
"default_price": "10.00",
"price": "10.00",
"original_price": null,
"active": true,
"description": null,
"position": 0
@@ -216,6 +225,7 @@ Endpoints
"value": {"en": "Student"},
"default_price": "10.00",
"price": "10.00",
"original_price": null,
"active": false,
"description": null,
"position": 1

View File

@@ -21,34 +21,38 @@ default_price money (string) The item price
overwritten by variations or other options.
category integer The ID of the category this item belongs to
(or ``null``).
active boolean If ``False``, the item is hidden from all public lists
active boolean If ``false``, the item is hidden from all public lists
and will not be sold.
description multi-lingual string A public description of the item. May contain Markdown
syntax or can be ``null``.
free_price boolean If ``True``, customers can change the price at which
free_price boolean If ``true``, customers can change the price at which
they buy the product (however, the price can't be set
lower than the price defined by ``default_price`` or
otherwise).
tax_rate decimal (string) The VAT rate to be applied for this item.
tax_rate decimal (string) The VAT rate to be applied for this item (read-only,
set through ``tax_rule``).
tax_rule integer The internal ID of the applied tax rule (or ``null``).
admission boolean ``True`` for items that grant admission to the event
(such as primary tickets) and ``False`` for others
admission boolean ``true`` for items that grant admission to the event
(such as primary tickets) and ``false`` for others
(such as add-ons or merchandise).
position integer An integer, used for sorting
picture string A product picture to be displayed in the shop
(read-only).
(read-only, can be ``null``).
sales_channels list of strings Sales channels this product is available on, such as
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
available_from datetime The first date time at which this item can be bought
(or ``null``).
available_until datetime The last date time at which this item can be bought
(or ``null``).
require_voucher boolean If ``True``, this item can only be bought using a
hidden_if_available integer The internal ID of a quota object, or ``null``. If
set, this item won't be shown publicly as long as this
quota is available.
require_voucher boolean If ``true``, this item can only be bought using a
voucher that is specifically assigned to this item.
hide_without_voucher boolean If ``True``, this item is only shown during the voucher
hide_without_voucher boolean If ``true``, this item is only shown during the voucher
redemption process, but not in the normal shop
frontend.
allow_cancel boolean If ``False``, customers cannot cancel orders containing
allow_cancel boolean If ``false``, customers cannot cancel orders containing
this item.
min_per_order integer This product can only be bought if it is included at
least this many times in the order (or ``null`` for no
@@ -56,20 +60,25 @@ min_per_order integer This product ca
max_per_order integer This product can only be bought if it is included at
most this many times in the order (or ``null`` for no
limitation).
checkin_attention boolean If ``True``, the check-in app should show a warning
checkin_attention boolean If ``true``, the check-in app should show a warning
that this ticket requires special attention if such
a product is being scanned.
original_price money (string) An original price, shown for comparison, not used
for price calculations.
require_approval boolean If ``True``, orders with this product will need to be
for price calculations (or ``null``).
require_approval boolean If ``true``, orders with this product will need to be
approved by the event organizer before they can be
paid.
generate_tickets boolean If ``False``, tickets are never generated for this
product, regardless of other settings. If ``True``,
require_bundling boolean If ``true``, this item is only available as part of bundles.
generate_tickets boolean If ``false``, tickets are never generated for this
product, regardless of other settings. If ``true``,
tickets are generated even if this is a
non-admission or add-on product, regardless of event
settings. If this is ``null``, regular ticketing
rules apply.
allow_waitinglist boolean If ``false``, no waiting list will be shown for this
product when it is sold out.
show_quota_left boolean Publicly show how many tickets are still available.
If this is ``null``, the event default is used.
has_variations boolean Shows whether or not this item has variations.
variations list of objects A list with one object for each variation of this item.
Can be empty. Only writable during creation,
@@ -80,7 +89,9 @@ variations list of objects A list with one
├ price money (string) The price used for this variation. This is either the
same as ``default_price`` if that value is set or equal
to the item's ``default_price``.
active boolean If ``False``, this variation will not be sold or shown.
original_price money (string) An original price, shown for comparison, not used
for price calculations (or ``null``).
├ active boolean If ``false``, this variation will not be sold or shown.
├ description multi-lingual string A public description of the variation. May contain
Markdown syntax or can be ``null``.
└ position integer An integer, used for sorting
@@ -91,10 +102,23 @@ addons list of objects Definition of a
chosen from.
├ min_count integer The minimal number of add-ons that need to be chosen.
├ max_count integer The maximal number of add-ons that can be chosen.
position integer An integer, used for sorting
position integer An integer, used for sorting
└ price_included boolean Adding this add-on to the item is free
bundles list of objects Definition of bundles that are included in this item.
Only writable during creation,
use separate endpoint to modify this later.
├ bundled_item integer Internal ID of the item that is included.
├ bundled_variation integer Internal ID of the variation of the item (or ``null``).
├ count integer Number of items included
└ designated_price money (string) Designated price of the bundled product. This will be
used to split the price of the base item e.g. for mixed
taxation. This is not added to the price.
===================================== ========================== =======================================================
.. versionchanged:: 2.7
The attribute ``original_price`` has been added for ``variations``.
.. versionchanged:: 1.7
The attribute ``tax_rule`` has been added. ``tax_rate`` is kept for compatibility. The attribute
@@ -121,15 +145,24 @@ addons list of objects Definition of a
The ``generate_tickets`` attribute has been added.
.. versionchanged:: 2.6
The ``bundles`` and ``require_bundling`` attributes have been added.
.. versionchanged:: 3.0
The ``show_quota_left``, ``allow_waitinglist``, and ``hidden_if_available`` attributes have been added.
Notes
-----
Please note that an item either always has variations or never has. Once created with variations the item can never
change to an item without and vice versa. To create an item with variations ensure that you POST an item with at least
one variation.
Also note that ``variations`` and ``addons`` are only supported on ``POST``. To update/delete variations and add-ons please
use the dedicated nested endpoints. By design this endpoint does not support ``PATCH`` and ``PUT`` with nested
``variations`` and/or ``addons``.
Also note that ``variations``, ``bundles``, and ``addons`` are only supported on ``POST``. To update/delete variations,
bundles, and add-ons please use the dedicated nested endpoints. By design this endpoint does not support ``PATCH`` and ``PUT``
with nested ``variations``, ``bundles`` and/or ``addons``.
Endpoints
---------
@@ -177,6 +210,7 @@ Endpoints
"picture": null,
"available_from": null,
"available_until": null,
"hidden_if_available": null,
"require_voucher": false,
"hide_without_voucher": false,
"allow_cancel": true,
@@ -185,12 +219,16 @@ Endpoints
"checkin_attention": false,
"has_variations": false,
"generate_tickets": null,
"allow_waitinglist": true,
"show_quota_left": null,
"require_approval": false,
"require_bundling": false,
"variations": [
{
"value": {"en": "Student"},
"default_price": "10.00",
"price": "10.00",
"original_price": null,
"active": true,
"description": null,
"position": 0
@@ -199,12 +237,14 @@ Endpoints
"value": {"en": "Regular"},
"default_price": null,
"price": "23.00",
"original_price": null,
"active": true,
"description": null,
"position": 1
}
],
"addons": []
"addons": [],
"bundles": []
}
]
}
@@ -264,20 +304,25 @@ Endpoints
"picture": null,
"available_from": null,
"available_until": null,
"hidden_if_available": null,
"require_voucher": false,
"hide_without_voucher": false,
"allow_cancel": true,
"generate_tickets": null,
"allow_waitinglist": true,
"show_quota_left": null,
"min_per_order": null,
"max_per_order": null,
"checkin_attention": false,
"has_variations": false,
"require_approval": false,
"require_bundling": false,
"variations": [
{
"value": {"en": "Student"},
"default_price": "10.00",
"price": "10.00",
"original_price": null,
"active": true,
"description": null,
"position": 0
@@ -286,12 +331,14 @@ Endpoints
"value": {"en": "Regular"},
"default_price": null,
"price": "23.00",
"original_price": null,
"active": true,
"description": null,
"position": 1
}
],
"addons": []
"addons": [],
"bundles": []
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -312,7 +359,7 @@ Endpoints
POST /api/v1/organizers/bigevents/events/sampleconf/items/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content: application/json
Content-Type: application/json
{
"id": 1,
@@ -332,19 +379,24 @@ Endpoints
"picture": null,
"available_from": null,
"available_until": null,
"hidden_if_available": null,
"require_voucher": false,
"hide_without_voucher": false,
"allow_cancel": true,
"generate_tickets": null,
"allow_waitinglist": true,
"show_quota_left": null,
"min_per_order": null,
"max_per_order": null,
"checkin_attention": false,
"require_approval": false,
"require_bundling": false,
"variations": [
{
"value": {"en": "Student"},
"default_price": "10.00",
"price": "10.00",
"original_price": null,
"active": true,
"description": null,
"position": 0
@@ -353,12 +405,14 @@ Endpoints
"value": {"en": "Regular"},
"default_price": null,
"price": "23.00",
"original_price": null,
"active": true,
"description": null,
"position": 1
}
],
"addons": []
"addons": [],
"bundles": []
}
**Example response**:
@@ -387,20 +441,25 @@ Endpoints
"picture": null,
"available_from": null,
"available_until": null,
"hidden_if_available": null,
"require_voucher": false,
"hide_without_voucher": false,
"allow_cancel": true,
"min_per_order": null,
"max_per_order": null,
"generate_tickets": null,
"allow_waitinglist": true,
"show_quota_left": null,
"checkin_attention": false,
"has_variations": true,
"require_approval": false,
"require_bundling": false,
"variations": [
{
"value": {"en": "Student"},
"default_price": "10.00",
"price": "10.00",
"original_price": null,
"active": true,
"description": null,
"position": 0
@@ -409,12 +468,14 @@ Endpoints
"value": {"en": "Regular"},
"default_price": null,
"price": "23.00",
"original_price": null,
"active": true,
"description": null,
"position": 1
}
],
"addons": []
"addons": [],
"bundles": []
}
:param organizer: The ``slug`` field of the organizer of the event to create an item for
@@ -474,20 +535,25 @@ Endpoints
"picture": null,
"available_from": null,
"available_until": null,
"hidden_if_available": null,
"require_voucher": false,
"hide_without_voucher": false,
"generate_tickets": null,
"allow_waitinglist": true,
"show_quota_left": null,
"allow_cancel": true,
"min_per_order": null,
"max_per_order": null,
"checkin_attention": false,
"has_variations": true,
"require_approval": false,
"require_bundling": false,
"variations": [
{
"value": {"en": "Student"},
"default_price": "10.00",
"price": "10.00",
"original_price": null,
"active": true,
"description": null,
"position": 0
@@ -496,12 +562,14 @@ Endpoints
"value": {"en": "Regular"},
"default_price": null,
"price": "23.00",
"original_price": null,
"active": true,
"description": null,
"position": 1
}
],
"addons": []
"addons": [],
"bundles": []
}
:param organizer: The ``slug`` field of the organizer to modify

View File

@@ -26,7 +26,7 @@ status string Order status, o
* ``p`` paid
* ``e`` expired
* ``c`` canceled
testmode boolean If ``True``, this order was created when the event was in
testmode boolean If ``true``, this order was created when the event was in
test mode. Only orders in test mode can be deleted.
secret string The secret contained in the link sent to the customer
email string The customer email address
@@ -39,13 +39,13 @@ payment_date date **DEPRECATED AN
payment_provider string **DEPRECATED AND INACCURATE** Payment provider used for this order
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
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
├ is_business boolean Business or individual customers (always ``False``
├ is_business boolean Business or individual customers (always ``false``
for orders created before pretix 1.7, do not rely on
it).
├ name string Customer name
@@ -53,10 +53,12 @@ invoice_address object Invoice address
├ street string Customer street
├ zipcode string Customer ZIP code
├ city string Customer city
├ country string Customer country
├ country string Customer country code
├ state string Customer state (ISO 3166-2 code). Only supported in
AU, BR, CA, CN, MY, MX, and US.
├ internal_reference string Customer's internal reference to be printed on the invoice
├ vat_id string Customer VAT ID
└ vat_id_validated string ``True``, if the VAT ID has been validated against the
└ vat_id_validated string ``true``, if the VAT ID has been validated against the
EU VAT service and validation was successful. This only
happens in rare cases.
positions list of objects List of non-canceled order positions (see below)
@@ -78,10 +80,11 @@ downloads list of objects List of ticket
download options.
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
└ url string Download URL
require_approval boolean If ``True`` and the order is pending, this order
require_approval boolean If ``true`` and the order is pending, this order
needs approval by an organizer before it can
continue. If ``True`` and the order is canceled,
continue. If ``true`` and the order is canceled,
this order has been denied by the event organizer.
url string The full URL to the order confirmation page
payments list of objects List of payment processes (see below)
refunds list of objects List of refund processes (see below)
last_modified datetime Last modification of this object
@@ -137,6 +140,12 @@ last_modified datetime Last modificati
The ``testmode`` attribute has been added and ``DELETE`` has been implemented for orders.
.. versionchanged:: 3.1:
The ``invoice_address.state`` and ``url`` attributes have been added. When creating orders through the API,
vouchers are now supported and many fields are now optional.
.. _order-position-resource:
Order position resource
@@ -166,7 +175,8 @@ subevent integer ID of the date
pseudonymization_id string A random ID, e.g. for use in lead scanning apps
checkins list of objects List of check-ins with this ticket
├ list integer Internal ID of the check-in list
datetime datetime Time of check-in
datetime datetime Time of check-in
└ auto_checked_in boolean Indicates if this check-in been performed automatically by the system
downloads list of objects List of ticket download options
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
└ url string Download URL
@@ -176,6 +186,10 @@ answers list of objects Answers to user
├ 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
seat objects The assigned seat. Can be ``null``.
├ id integer Internal ID of the seat instance
├ name string Human-readable seat name
└ seat_guid string Identifier of the seat within the seating plan
pdf_data object Data object required for ticket PDF generation. By default,
this field is missing. It will be added only if you add the
``pdf_data=true`` query parameter to your request.
@@ -197,6 +211,14 @@ pdf_data object Data object req
The attributes ``pseudonymization_id`` and ``pdf_data`` have been added.
.. versionchanged:: 3.0
The attribute ``seat`` has been added.
.. versionchanged:: 3.2
The value ``auto_checked_in`` has been added to the ``checkins``-attribute.
.. _order-payment-resource:
Order payment resource
@@ -213,13 +235,27 @@ amount money (string) Payment amount
created datetime Date and time of creation of this payment
payment_date datetime Date and time of completion of this payment (or ``null``)
provider string Identification string of the payment provider
payment_url string The URL where an user can continue with the payment (or ``null``)
details object Payment-specific information. This is a dictionary
with various fields that can be different between
payment providers, versions, payment states, etc. If
you read this field, you always need to be able to
deal with situations where values that you expect are
missing. Mostly, the field contains various IDs that
can be used for matching with other systems. If a
payment provider does not implement this feature,
the object is empty.
===================================== ========================== =======================================================
.. versionchanged:: 2.0
This resource has been added.
.. _order-payment-resource:
.. versionchanged:: 3.1
The attributes ``payment_url`` and ``details`` have been added.
.. _order-refund-resource:
Order refund resource
---------------------
@@ -280,6 +316,7 @@ List of all orders
"status": "p",
"testmode": false,
"secret": "k24fiuwvu8kxz3y1",
"url": "https://test.pretix.eu/dummy/dummy/order/ABC12/k24fiuwvu8kxz3y1/",
"email": "tester@example.org",
"locale": "en",
"sales_channel": "web",
@@ -295,17 +332,18 @@ List of all orders
"require_approval": false,
"invoice_address": {
"last_modified": "2017-12-01T10:00:00Z",
"is_business": True,
"is_business": true,
"company": "Sample company",
"name": "John Doe",
"name_parts": {"full_name": "John Doe"},
"street": "Test street 12",
"zipcode": "12345",
"city": "Testington",
"country": "Testikistan",
"country": "DE",
"state": "",
"internal_reference": "",
"vat_id": "EU123456789",
"vat_id_validated": False
"vat_id_validated": false
},
"positions": [
{
@@ -328,10 +366,12 @@ List of all orders
"addon_to": null,
"subevent": null,
"pseudonymization_id": "MQLJvANO3B",
"seat": null,
"checkins": [
{
"list": 44,
"datetime": "2017-12-25T12:45:23Z"
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}
],
"answers": [
@@ -364,6 +404,8 @@ List of all orders
"amount": "23.00",
"created": "2017-12-01T10:00:00Z",
"payment_date": "2017-12-04T12:13:12Z",
"payment_url": null,
"details": {},
"provider": "banktransfer"
}
],
@@ -373,8 +415,8 @@ List of all orders
}
:query integer page: The page number in case of a multi-page result set, default is 1
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``datetime``, ``code`` and
``status``. Default: ``datetime``
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``datetime``, ``code``,
``last_modified``, and ``status``. Default: ``datetime``
:query string code: Only return orders that match the given order code
:query string status: Only return orders in the given order status (see above)
:query boolean testmode: Only return orders with ``testmode`` set to ``true`` or ``false``
@@ -385,6 +427,7 @@ List of all orders
:query datetime modified_since: Only return orders that have changed since the given date. Be careful: We only
recommend using this in combination with ``testmode=false``, since test mode orders can vanish at any time and
you will not notice it using this method.
:query datetime created_since: Only return orders that have been created since the given date.
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:resheader X-Page-Generated: The server time at the beginning of the operation. If you're using this API to fetch
@@ -421,6 +464,7 @@ Fetching individual orders
"status": "p",
"testmode": false,
"secret": "k24fiuwvu8kxz3y1",
"url": "https://test.pretix.eu/dummy/dummy/order/ABC12/k24fiuwvu8kxz3y1/",
"email": "tester@example.org",
"locale": "en",
"sales_channel": "web",
@@ -437,16 +481,17 @@ Fetching individual orders
"invoice_address": {
"last_modified": "2017-12-01T10:00:00Z",
"company": "Sample company",
"is_business": True,
"is_business": true,
"name": "John Doe",
"name_parts": {"full_name": "John Doe"},
"street": "Test street 12",
"zipcode": "12345",
"city": "Testington",
"country": "Testikistan",
"country": "DE",
"state": "",
"internal_reference": "",
"vat_id": "EU123456789",
"vat_id_validated": False
"vat_id_validated": false
},
"positions": [
{
@@ -469,10 +514,12 @@ Fetching individual orders
"addon_to": null,
"subevent": null,
"pseudonymization_id": "MQLJvANO3B",
"seat": null,
"checkins": [
{
"list": 44,
"datetime": "2017-12-25T12:45:23Z"
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}
],
"answers": [
@@ -505,6 +552,8 @@ Fetching individual orders
"amount": "23.00",
"created": "2017-12-01T10:00:00Z",
"payment_date": "2017-12-04T12:13:12Z",
"payment_url": null,
"details": {},
"provider": "banktransfer"
}
],
@@ -563,6 +612,92 @@ Order ticket download
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
seconds.
Updating order fields
---------------------
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/
Updates specific fields on an order. Currently, only the following fields are supported:
* ``email``
* ``checkin_attention``
* ``locale``
* ``comment``
* ``invoice_address`` (you always need to supply the full object, or ``null`` to delete the current address)
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"email": "other@example.org",
"locale": "de",
"comment": "Foo",
"checkin_attention": true
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
(Full order resource, see above.)
:param organizer: The ``slug`` field of the organizer of the event
:param event: The ``slug`` field of the event
:param code: The ``code`` field of the order to update
:statuscode 200: no error
:statuscode 400: The order could not be updated due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order.
Generating new secrets
----------------------
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/regenerate_secrets/
Triggers generation of new ``secret`` attributes for both the order and all order positions.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/regenerate_secrets/ 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
(Full order resource, see above.)
:param organizer: The ``slug`` field of the organizer of the event
:param event: The ``slug`` field of the event
:param code: The ``code`` field of the order to update
:statuscode 200: no error
:statuscode 400: The order could not be updated due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order.
Deleting orders
---------------
@@ -594,6 +729,8 @@ Deleting orders
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource **or** the order may not be deleted.
:statuscode 404: The requested order does not exist.
.. _rest-orders-create:
Creating orders
---------------
@@ -601,8 +738,6 @@ Creating orders
Creates a new order.
.. warning:: This endpoint is considered **experimental**. It might change at any time without prior notice.
.. warning::
This endpoint is intended for advanced users. It is not designed to be used to build your own shop frontend,
@@ -621,23 +756,17 @@ Creating orders
* does not validate the number of items per order or the number of times an item can be included in an order
* does not validate any requirements related to add-on products
* does not validate any requirements related to add-on products and does not add bundled products automatically
* does not check or calculate prices but believes any prices you send
* does not support the redemption of vouchers
* does not check prices but believes any prices you send
* does not prevent you from buying items that can only be bought with a voucher
* does not calculate fees
* does not calculate fees automatically
* does not allow to pass data to plugins and will therefore cause issues with some plugins like the shipping
module
* does not send order confirmations via email
* does not support reverse charge taxation
* does not support file upload questions
You can supply the following fields of the resource:
@@ -650,19 +779,20 @@ Creating orders
then call the ``mark_paid`` API method.
* ``testmode`` (optional) Defaults to ``false``
* ``consume_carts`` (optional) A list of cart IDs. All cart positions with these IDs will be deleted if the
order creation is successful. Any quotas that become free by this operation will be credited to your order
order creation is successful. Any quotas or seats that become free by this operation will be credited to your order
creation.
* ``email``
* ``locale``
* ``sales_channel``
* ``payment_provider`` The identifier of the payment provider set for this order. This needs to be an existing
payment provider. You should use ``"free"`` for free orders, and we strongly advise to use ``"manual"`` for all
orders you create as paid.
* ``payment_provider`` (optional) The identifier of the payment provider set for this order. This needs to be an
existing payment provider. You should use ``"free"`` for free orders, and we strongly advise to use ``"manual"``
for all orders you create as paid.
* ``payment_info`` (optional) You can pass a nested JSON object that will be set as the internal ``info``
value of the payment object that will be created. How this value is handled is up to the payment provider and you
should only use this if you know the specific payment provider in detail. Please keep in mind that the payment
provider will not be called to do anything about this (i.e. if you pass a bank account to a debit provider, *no*
charge will be created), this is just informative in case you *handled the payment already*.
* ``payment_date`` (optional) Date and time of the completion of the payment.
* ``comment`` (optional)
* ``checkin_attention`` (optional)
* ``invoice_address`` (optional)
@@ -674,16 +804,22 @@ Creating orders
* ``zipcode``
* ``city``
* ``country``
* ``state``
* ``internal_reference``
* ``vat_id``
* ``vat_id_validated`` (optional) If you need support for reverse charge (rarely the case), you need to check
yourself if the passed VAT ID is a valid EU VAT ID. In that case, set this to ``true``. Only valid VAT IDs will
trigger reverse charge taxation. Don't forget to set ``is_business`` as well!
* ``positions``
* ``positionid`` (optional, see below)
* ``item``
* ``variation``
* ``price``
* ``price`` (optional, if set to ``null`` or missing the price will be computed from the given product)
* ``seat`` (The ``seat_guid`` attribute of a seat. Required when the specified ``item`` requires a seat, otherwise must be ``null``.)
* ``attendee_name`` **or** ``attendee_name_parts``
* ``voucher`` (optional, the ``code`` attribute of a valid voucher)
* ``attendee_email``
* ``secret`` (optional)
* ``addon_to`` (optional, see below)
@@ -702,6 +838,10 @@ Creating orders
* ``internal_type``
* ``tax_rule``
* ``force`` (optional). If set to ``true``, quotas will be ignored.
* ``send_mail`` (optional). If set to ``true``, the same emails will be sent as for a regular order. Defaults to
``false``.
If you want to use add-on products, you need to set the ``positionid`` fields of all positions manually
to incrementing integers starting with ``1``. Then, you can reference one of these
IDs in the ``addon_to`` field of another position. Note that all add_ons for a specific position need to come
@@ -714,7 +854,7 @@ Creating orders
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content: application/json
Content-Type: application/json
{
"email": "dummy@example.org",
@@ -731,13 +871,14 @@ Creating orders
],
"payment_provider": "banktransfer",
"invoice_address": {
"is_business": False,
"is_business": false,
"company": "Sample company",
"name_parts": {"full_name": "John Doe"},
"street": "Sesam Street 12",
"zipcode": "12345",
"city": "Sample City",
"country": "UK",
"state": "",
"internal_reference": "",
"vat_id": ""
},
@@ -761,7 +902,7 @@ Creating orders
],
"subevent": null
}
],
]
}
**Example response**:
@@ -774,10 +915,10 @@ Creating orders
(Full order resource, see above.)
: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
:param organizer: The ``slug`` field of the organizer of the event to create an order for
:param event: The ``slug`` field of the event to create an order for
:statuscode 201: no error
:statuscode 400: The item could not be created due to invalid submitted data or lack of quota.
:statuscode 400: The order could not be created due to invalid submitted data or lack of quota.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this
order.
@@ -902,7 +1043,7 @@ Order state operations
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_expired/
Marks a unpaid order as expired.
Marks an unpaid order as expired.
**Example request**:
@@ -1061,9 +1202,82 @@ Order state operations
:statuscode 200: no error
:statuscode 400: The order cannot be marked as denied since the current order status does not allow it.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this resource.
:statuscode 404: The requested order does not exist.
Generating invoices
-------------------
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/create_invoice/
Creates an invoice for an order which currently does not have an invoice. Returns the
invoice object.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/create_invoice/ 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
{
"order": "FOO",
"number": "DUMMY-00001",
"is_cancellation": false,
...
}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param code: The ``code`` field of the order to create an invoice for
:statuscode 200: no error
:statuscode 400: The invoice can not be created (invoicing disabled, the order already has an invoice, …)
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order does not exist.
Sending e-mails
---------------
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/resend_link/
Sends an email to the buyer with the link to the order page.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/resend_link/ 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 code: The ``code`` field of the order to send an email for
:statuscode 200: no error
:statuscode 400: The order does not have an email address associated
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order does not exist.
:statuscode 503: The email could not be sent.
List of all order positions
---------------------------
@@ -1079,6 +1293,11 @@ List of all order positions
The order positions endpoint has been extended by the filter queries ``voucher``, ``voucher__code`` and
``pseudonymization_id``.
.. versionchanged:: 3.2
The value ``auto_checked_in`` has been added to the ``checkins``-attribute.
.. note:: Individually canceled order positions are currently not visible via the API at all.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/
@@ -1124,12 +1343,14 @@ List of all order positions
"tax_value": "0.00",
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
"pseudonymization_id": "MQLJvANO3B",
"seat": null,
"addon_to": null,
"subevent": null,
"checkins": [
{
"list": 44,
"datetime": "2017-12-25T12:45:23Z"
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}
],
"answers": [
@@ -1226,10 +1447,12 @@ Fetching individual positions
"addon_to": null,
"subevent": null,
"pseudonymization_id": "MQLJvANO3B",
"seat": null,
"checkins": [
{
"list": 44,
"datetime": "2017-12-25T12:45:23Z"
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}
],
"answers": [
@@ -1372,6 +1595,8 @@ Order payment endpoints
"amount": "23.00",
"created": "2017-12-01T10:00:00Z",
"payment_date": "2017-12-04T12:13:12Z",
"payment_url": null,
"details": {},
"provider": "banktransfer"
}
]
@@ -1412,6 +1637,8 @@ Order payment endpoints
"amount": "23.00",
"created": "2017-12-01T10:00:00Z",
"payment_date": "2017-12-04T12:13:12Z",
"payment_url": null,
"details": {},
"provider": "banktransfer"
}

View File

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

View File

@@ -30,14 +30,19 @@ type string The expected ty
* ``D`` date
* ``H`` time
* ``W`` date and time
required boolean If ``True``, the question needs to be filled out.
* ``CC`` country code (ISO 3666-1 alpha-2)
required boolean If ``true``, the question needs to be filled out.
position integer An integer, used for sorting
items list of integers List of item IDs this question is assigned to.
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
ask_during_checkin boolean If ``true``, this question will not be asked while
buying the ticket, but will show up when redeeming
the ticket instead.
hidden boolean If ``true``, the question will only be shown in the
backend.
print_on_invoice boolean If ``true``, the question will only be shown on
invoices.
options list of objects In case of question type ``C`` or ``M``, this lists the
available objects. Only writable during creation,
use separate endpoint to modify this later.
@@ -46,6 +51,17 @@ options list of objects In case of ques
├ identifier string An arbitrary string that can be used for matching with
other sources.
└ answer multi-lingual string The displayed value of this option
dependency_question integer Internal ID of a different question. The current
question will only be shown if the question given in
this attribute is set to the value given in
``dependency_value``. This cannot be combined with
``ask_during_checkin``.
dependency_values list of strings If ``dependency_question`` is set to a boolean
question, this should be ``["True"]`` or ``["False"]``.
Otherwise, it should be a list of ``identifier`` values
of question options.
dependency_value string An old version of ``dependency_values`` that only allows
for one value. **Deprecated.**
===================================== ========================== =======================================================
.. versionchanged:: 1.12
@@ -58,6 +74,18 @@ options list of objects In case of ques
Write methods have been added. The attribute ``identifier`` has been added to both the resource itself and the
options resource. The ``position`` attribute has been added to the options resource.
.. versionchanged:: 2.7
The attribute ``hidden`` and the question type ``CC`` have been added.
.. versionchanged:: 3.0
The attribute ``dependency_values`` has been added.
.. versionchanged:: 3.1
The attribute ``print_on_invoice`` has been added.
Endpoints
---------
@@ -100,6 +128,11 @@ Endpoints
"position": 1,
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"dependency_question": null,
"dependency_value": null,
"dependency_values": [],
"options": [
{
"id": 1,
@@ -165,6 +198,11 @@ Endpoints
"position": 1,
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"dependency_question": null,
"dependency_value": null,
"dependency_values": [],
"options": [
{
"id": 1,
@@ -205,7 +243,7 @@ Endpoints
POST /api/v1/organizers/bigevents/events/sampleconf/questions/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content: application/json
Content-Type: application/json
{
"question": {"en": "T-Shirt size"},
@@ -214,6 +252,10 @@ Endpoints
"items": [1, 2],
"position": 1,
"ask_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"dependency_question": null,
"dependency_values": [],
"options": [
{
"answer": {"en": "S"}
@@ -245,6 +287,11 @@ Endpoints
"position": 1,
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"dependency_question": null,
"dependency_value": null,
"dependency_values": [],
"options": [
{
"id": 1,
@@ -314,6 +361,11 @@ Endpoints
"position": 2,
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"dependency_question": null,
"dependency_value": null,
"dependency_values": [],
"options": [
{
"id": 1,

View File

@@ -20,12 +20,22 @@ size integer The size of the
items list of integers List of item IDs this quota acts on.
variations list of integers List of item variation IDs this quota acts on.
subevent integer ID of the date inside an event series this quota belongs to (or ``null``).
close_when_sold_out boolean If ``true``, the quota will "close" as soon as it is
sold out once. Even if tickets become available again,
they will not be sold unless the quota is set to open
again.
closed boolean Whether the quota is currently closed (see above
field).
===================================== ========================== =======================================================
.. versionchanged:: 1.10
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
.. versionchanged:: 3.0
The attributes ``close_when_sold_out`` and ``closed`` have been added.
Endpoints
---------
@@ -61,7 +71,9 @@ Endpoints
"size": 200,
"items": [1, 2],
"variations": [1, 4, 5, 7],
"subevent": null
"subevent": null,
"close_when_sold_out": false,
"closed": false
}
]
}
@@ -102,7 +114,9 @@ Endpoints
"size": 200,
"items": [1, 2],
"variations": [1, 4, 5, 7],
"subevent": null
"subevent": null,
"close_when_sold_out": false,
"closed": false
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -123,14 +137,16 @@ Endpoints
POST /api/v1/organizers/bigevents/events/sampleconf/quotas/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content: application/json
Content-Type: application/json
{
"name": "Ticket Quota",
"size": 200,
"items": [1, 2],
"variations": [1, 4, 5, 7],
"subevent": null
"subevent": null,
"close_when_sold_out": false,
"closed": false
}
**Example response**:
@@ -147,7 +163,9 @@ Endpoints
"size": 200,
"items": [1, 2],
"variations": [1, 4, 5, 7],
"subevent": null
"subevent": null,
"close_when_sold_out": false,
"closed": false
}
:param organizer: The ``slug`` field of the organizer of the event/item to create a quota for
@@ -200,7 +218,9 @@ Endpoints
1,
2
],
"subevent": null
"subevent": null,
"close_when_sold_out": false,
"closed": false
}
:param organizer: The ``slug`` field of the organizer to modify

View File

@@ -0,0 +1,209 @@
.. _`rest-seatingplans`:
Seating plans
=============
Resource description
--------------------
The seating plan resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the plan
name string Human-readable name of the plan
layout object JSON representation of the seating plan. These
representations follow a JSON schema that currently
still evolves. The version in use can be found `here`_.
===================================== ========================== =======================================================
.. versionchanged:: 3.0
This endpoint has been added.
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/seatingplans/
Returns a list of all seating plans within a given organizer.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/seatingplans/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 2,
"name": "Main plan",
"layout": { … }
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/seatingplans/(id)/
Returns information on one plan, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/seatingplans/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": 2,
"name": "Main plan",
"layout": { … }
}
:param organizer: The ``slug`` field of the organizer to fetch
:param id: The ``id`` field of the seating plan to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/seatingplans/
Creates a new seating plan
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/seatingplans/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"name": "Main plan",
"layout": { … }
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 3,
"name": "Main plan",
"layout": { … }
}
:param organizer: The ``slug`` field of the organizer to create a seating plan for
:statuscode 201: no error
:statuscode 400: The seating plan could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/seatingplans/(id)/
Update a plan. 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. **You can not change a plan while it is in use for
any events.**
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/seatingplans/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"name": "Old plan"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"name": "Old plan",
"layout": { … }
}
:param organizer: The ``slug`` field of the organizer to modify
:param id: The ``id`` field of the plan to modify
:statuscode 200: no error
:statuscode 400: The plan could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource **or** the plan is currently in use.
.. http:delete:: /api/v1/organizers/(organizer)/seatingplans/(id)/
Delete a plan. You can not delete plans which are currently in use by any events.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/seatingplans/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 id: The ``id`` field of the plan to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource **or** the plan is currently in use.
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/seating/seating-plan.schema.json

View File

@@ -20,6 +20,8 @@ name multi-lingual string The sub-event's
event string The slug of the parent event
active boolean If ``true``, the sub-event ticket shop is publicly
available.
is_public boolean If ``true``, the sub-event ticket shop is publicly
shown in lists.
date_from datetime The sub-event's start date
date_to datetime The sub-event's end date (or ``null``)
date_admission datetime The sub-event's admission date (or ``null``)
@@ -34,7 +36,11 @@ variation_price_overrides list of objects List of variati
the default price
├ variation integer The internal variation ID
└ price money (string) The price or ``null`` for the default price
meta_data dict Values set for organizer-specific meta data parameters.
meta_data object Values set for organizer-specific meta data parameters.
seating_plan integer If reserved seating is in use, the ID of a seating
plan. Otherwise ``null``.
seat_category_mapping object An object mapping categories of the seating plan
(strings) to items in the event (integers or ``null``).
===================================== ========================== =======================================================
.. versionchanged:: 1.7
@@ -45,6 +51,16 @@ meta_data dict Values set for
The ``event`` field has been added, together with filters on the list of dates and an organizer-level list.
.. versionchanged:: 2.6
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
.. versionchanged:: 2.7
The attribute ``is_public`` has been added.
.. versionchanged:: 3.0
The attributes ``seating_plan`` and ``seat_category_mapping`` have been added.
Endpoints
---------
@@ -79,11 +95,14 @@ Endpoints
"name": {"en": "First Sample Conference"},
"event": "sampleconf",
"active": false,
"is_public": true,
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
"date_admission": null,
"presale_start": null,
"presale_end": null,
"seating_plan": null,
"seat_category_mapping": {},
"location": null,
"item_price_overrides": [
{
@@ -103,11 +122,89 @@ Endpoints
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned.
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned.
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of the event to fetch
:param event: The ``slug`` field of the main event
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/subevents/
Creates a new subevent.
Permission required: "Can create events"
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/subevents/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"name": {"en": "First Sample Conference"},
"active": false,
"is_public": true,
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
"date_admission": null,
"presale_start": null,
"presale_end": null,
"location": null,
"seating_plan": null,
"seat_category_mapping": {},
"item_price_overrides": [
{
"item": 2,
"price": "12.00"
}
],
"variation_price_overrides": [],
"meta_data": {}
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 1,
"name": {"en": "First Sample Conference"},
"active": false,
"is_public": true,
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
"date_admission": null,
"presale_start": null,
"presale_end": null,
"location": null,
"seating_plan": null,
"seat_category_mapping": {},
"item_price_overrides": [
{
"item": 2,
"price": "12.00"
}
],
"variation_price_overrides": [],
"meta_data": {}
}
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of the main event
:statuscode 201: no error
:statuscode 400: The sub-event could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/
Returns information on one sub-event, identified by its ID.
@@ -133,12 +230,15 @@ Endpoints
"name": {"en": "First Sample Conference"},
"event": "sampleconf",
"active": false,
"is_public": true,
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
"date_admission": null,
"presale_start": null,
"presale_end": null,
"location": null,
"seating_plan": null,
"seat_category_mapping": {},
"item_price_overrides": [
{
"item": 2,
@@ -149,13 +249,109 @@ Endpoints
"meta_data": {}
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param id: The ``slug`` field of the sub-event to fetch
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of the main event
:param id: The ``id`` field of the sub-event 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 it.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/
Updates a sub-event, identified by its ID. 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.
Permission required: "Can change event settings"
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/subevents/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"name": {"en": "New Subevent Name"},
"item_price_overrides": [
{
"item": 2,
"price": "23.42"
}
],
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"name": {"en": "New Subevent Name"},
"event": "sampleconf",
"active": false,
"is_public": true,
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
"date_admission": null,
"presale_start": null,
"presale_end": null,
"location": null,
"seating_plan": null,
"seat_category_mapping": {},
"item_price_overrides": [
{
"item": 2,
"price": "23.42"
}
],
"variation_price_overrides": [],
"meta_data": {}
}
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of the main event
:param id: The ``id`` field of the sub-event to update
:statuscode 200: no error
:statuscode 400: The sub-event could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/sub-event does not exist **or** you have no permission to update this resource.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/
Delete a sub-event. Note that events with orders cannot be deleted to ensure data integrity.
Permission required: "Can change event settings"
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/subevents/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 a valid organizer
:param event: The ``slug`` field of the main event
:param id: The ``id`` field of the sub-event to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/sub-event does not exist **or** you have no permission to delete this resource.
.. http:get:: /api/v1/organizers/(organizer)/subevents/
Returns a list of all sub-events of any event series you have access to within an organizer account.
@@ -186,12 +382,15 @@ Endpoints
"name": {"en": "First Sample Conference"},
"event": "sampleconf",
"active": false,
"is_public": true,
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
"date_admission": null,
"presale_start": null,
"presale_end": null,
"location": null,
"seating_plan": null,
"seat_category_mapping": {},
"item_price_overrides": [
{
"item": 2,

View File

@@ -18,8 +18,8 @@ max_usages integer The maximum num
redeemed integer The number of times this voucher already has been
redeemed.
valid_until datetime The voucher expiration date (or ``null``).
block_quota boolean If ``True``, quota is blocked for this voucher.
allow_ignore_quota boolean If ``True``, this voucher can be redeemed even if a
block_quota boolean If ``true``, quota is blocked for this voucher.
allow_ignore_quota boolean If ``true``, this voucher can be redeemed even if a
product is sold out and even if quota is not blocked
for this voucher.
price_mode string Determines how this voucher affects product prices.
@@ -41,6 +41,7 @@ quota integer An ID of a quot
tag string A string that is used for grouping vouchers
comment string An internal comment on the voucher
subevent integer ID of the date inside an event series this voucher belongs to (or ``null``).
show_hidden_items boolean Only if set to ``true``, this voucher allows to buy products with the property ``hide_without_voucher``. Defaults to ``true``.
===================================== ========================== =======================================================
@@ -48,6 +49,10 @@ subevent integer ID of the date
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
.. versionchanged:: 3.0
The attribute ``show_hidden_items`` has been added.
Endpoints
---------

View File

@@ -17,11 +17,11 @@ The webhook resource contains the following public fields:
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the webhook
enabled boolean If ``False``, this webhook will not receive any notifications
enabled boolean If ``false``, this webhook will not receive any notifications
target_url string The URL to call
all_events boolean If ``True``, this webhook will receive notifications
all_events boolean If ``true``, this webhook will receive notifications
on all events of this organizer
limit_events list of strings If ``all_events`` is ``False``, this is a list of
limit_events list of strings If ``all_events`` is ``false``, this is a list of
event slugs this webhook is active for
action_types list of strings A list of action type filters that limit the
notifications sent to this webhook. See below for
@@ -137,7 +137,7 @@ Endpoints
POST /api/v1/organizers/bigevents/webhooks/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content: application/json
Content-Type: application/json
{
"enabled": true,

View File

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

View File

@@ -101,9 +101,12 @@ The template is passed the following context variables:
The ``Event`` object
``signature`` (optional, only if configured)
The body as markdown (render with ``{{ signature|safe }}``)
The signature with event organizer contact details as markdown (render with ``{{ signature|safe }}``)
``order`` (optional, only if applicable)
The ``Order`` object
``position`` (optional, only if applicable)
The ``OrderPosition`` object
.. _inlinestyler: https://pypi.org/project/inlinestyler/

View File

@@ -12,7 +12,7 @@ Core
.. automodule:: pretix.base.signals
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types,
item_copy_data, register_sales_channels
item_copy_data, register_sales_channels, register_global_settings, quota_availability, global_email_filter
Order events
""""""""""""
@@ -20,13 +20,13 @@ Order events
There are multiple signals that will be sent out in the ordering cycle:
.. automodule:: pretix.base.signals
:members: validate_cart, order_fee_calculation, order_paid, order_placed, order_fee_type_name, allow_ticket_download
:members: validate_cart, validate_cart_addons, validate_order, order_fee_calculation, order_paid, order_placed, order_canceled, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split
Frontend
--------
.. automodule:: pretix.presale.signals
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info
.. automodule:: pretix.presale.signals
@@ -49,11 +49,11 @@ Backend
.. automodule:: pretix.control.signals
:members: nav_event, html_head, html_page_start, quota_detail_html, nav_topbar, nav_global, nav_organizer, nav_event_settings,
order_info, event_settings_widget, oauth_application_registered, order_position_buttons
order_info, event_settings_widget, oauth_application_registered, order_position_buttons, subevent_forms, item_formsets
.. automodule:: pretix.base.signals
:members: logentry_display, logentry_object_link, requiredaction_display
:members: logentry_display, logentry_object_link, requiredaction_display, timeline_events
Vouchers
""""""""

View File

@@ -12,6 +12,7 @@ Contents:
payment
payment_2.0
email
placeholder
invoice
shredder
customview

View File

@@ -108,6 +108,8 @@ The provider class
.. automethod:: execute_refund
.. automethod:: api_payment_details
.. automethod:: shred_payment_info
.. autoattribute:: is_implicit

View File

@@ -0,0 +1,79 @@
.. highlight:: python
:linenothreshold: 5
Writing an HTML e-mail placeholder plugin
=========================================
An email placeholder is a dynamic value that pretix users can use in their email templates.
Please read :ref:`Creating a plugin <pluginsetup>` first, if you haven't already.
Placeholder registration
------------------------
The placeholder API does not make a lot of usage from signals, however, it
does use a signal to get a list of all available email placeholders. Your plugin
should listen for this signal and return an instance of a subclass of ``pretix.base.email.BaseMailTextPlaceholder``::
from django.dispatch import receiver
from pretix.base.signals import register_mail_placeholders
@receiver(register_mail_placeholders, dispatch_uid="placeholder_custom")
def register_mail_renderers(sender, **kwargs):
from .email import MyPlaceholderClass
return MyPlaceholder()
Context mechanism
-----------------
Emails are sent in different "contexts" within pretix. For example, many emails are sent in the
the context of an order, but some are not, such as the notification of a waiting list voucher.
Not all placeholders make sense in every email, and placeholders usually depend some parameters
themselves, such as the ``Order`` object. Therefore, placeholders are expected to explicitly declare
what values they depend on and they will only be available in an email if all those dependencies are
met. Currently, placeholders can depend on the following context parameters:
* ``event``
* ``order``
* ``position``
* ``waiting_list_entry``
* ``invoice_address``
* ``payment``
There are a few more that are only to be used internally but not by plugins.
The placeholder class
---------------------
.. class:: pretix.base.email.BaseMailTextPlaceholder
.. autoattribute:: identifier
This is an abstract attribute, you **must** override this!
.. autoattribute:: required_context
This is an abstract attribute, you **must** override this!
.. automethod:: render
This is an abstract method, you **must** implement this!
.. automethod:: render_sample
This is an abstract method, you **must** implement this!
Helper class for simple placeholders
------------------------------------
pretix ships with a helper class that makes it easy to provide placeholders based on simple
functions::
placeholder = SimpleFunctionalMailTextPlaceholder(
'code', ['order'], lambda order: order.code, sample='F8VVL'
)

View File

@@ -49,15 +49,19 @@ description string A more verbose description of what your
visible boolean (optional) ``True`` by default, can hide a plugin so it cannot be normally activated.
restricted boolean (optional) ``False`` by default, restricts a plugin such that it can only be enabled
for an event by system administrators / superusers.
compatibility string Specifier for compatible pretix versions.
================== ==================== ===========================================================
A working example would be::
from django.apps import AppConfig
try:
from pretix.base.plugins import PluginConfig
except ImportError:
raise RuntimeError("Please use pretix 2.7 or above to run this plugin!")
from django.utils.translation import ugettext_lazy as _
class PaypalApp(AppConfig):
class PaypalApp(PluginConfig):
name = 'pretix_paypal'
verbose_name = _("PayPal")
@@ -68,6 +72,7 @@ A working example would be::
visible = True
restricted = False
description = _("This plugin allows you to receive payments via PayPal")
compatibility = "pretix>=2.7.0"
default_app_config = 'pretix_paypal.PaypalApp'

View File

@@ -35,9 +35,9 @@ The shredder class
.. class:: pretix.base.shredder.BaseDataShredder
The central object of each invoice renderer is the subclass of ``BaseInvoiceRenderer``.
The central object of each data shredder is the subclass of ``BaseDataShredder``.
.. py:attribute:: BaseInvoiceRenderer.event
.. py:attribute:: BaseDataShredder.event
The default constructor sets this property to the event we are currently
working for.

View File

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

View File

@@ -21,10 +21,12 @@ Your should install the following on your system:
* Python 3.5 or newer
* ``pip`` for Python 3 (Debian package: ``python3-pip``)
* ``python-dev`` for Python 3 (Debian package: ``python3-dev``)
* On Debian/Ubuntu: ``python-venv`` for Python 3 (Debian package: ``python3-venv``)
* ``libffi`` (Debian package: ``libffi-dev``)
* ``libssl`` (Debian package: ``libssl-dev``)
* ``libxml2`` (Debian package ``libxml2-dev``)
* ``libxslt`` (Debian package ``libxslt1-dev``)
* ``libenchant1c2a`` (Debian package ``libenchant1c2a``)
* ``msgfmt`` (Debian package ``gettext``)
* ``git``
@@ -63,9 +65,7 @@ Then, create the local database::
python manage.py migrate
A first user with username ``admin@localhost`` and password ``admin`` will be automatically
created. If you want to generate more test data, run::
python make_testdata.py
created.
If you want to see pretix in a different language than English, you have to compile our language
files::
@@ -81,8 +81,7 @@ To run the local development webserver, execute::
and head to http://localhost:8000/
As we did not implement an overall front page yet, you need to go directly to
http://localhost:8000/control/ for the admin view or, if you imported the test
data as suggested above, to the event page at http://localhost:8000/bigevents/2019/
http://localhost:8000/control/ for the admin view.
.. note:: If you want the development server to listen on a different interface or
port (for example because you develop on `pretixdroid`_), you can check

View File

@@ -108,3 +108,43 @@ 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 it.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/badgeitems/
Returns a list of all assignments of items to layouts
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/democon/badgeitems/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"layout": 2,
"item": 3,
}
]
}
:query page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of a valid event
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.

View File

@@ -316,7 +316,7 @@ uses to communicate with the pretix server.
"total": 42,
"version": 3,
"event": {
"name": "Demo Converence",
"name": "Demo Conference",
"slug": "democon",
"date_from": "2016-12-27T17:00:00Z",
"date_to": "2016-12-30T18:00:00Z",

View File

@@ -114,3 +114,44 @@ 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 it.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/ticketlayoutitems/
Returns a list of all assignments of items to layouts
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/democon/ticketlayoutitems/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"layout": 2,
"item": 3,
"sales_channel": web
}
]
}
:query page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of a valid event
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.

View File

@@ -10,10 +10,12 @@ availabilities
backend
backends
banktransfer
Bcc
boolean
booleans
cancelled
casted
Ceph
checkbox
checksum
config
@@ -34,13 +36,17 @@ eu
filename
filesystem
fontawesome
formset
formsets
frontend
frontpage
gettext
gunicorn
guid
hardcoded
hostname
idempotency
iframe
incrementing
inofficial
invalidations
@@ -52,6 +58,7 @@ linters
memcached
metadata
middleware
Minio
mixin
mixins
multi
@@ -93,8 +100,12 @@ renderer
renderers
reportlab
SaaS
scalability
screenshot
scss
searchable
selectable
serializable
serializers
serializers
sexualized
@@ -108,6 +119,7 @@ subdomains
subevent
subevents
submodule
subnet
subpath
Symfony
systemd
@@ -120,12 +132,14 @@ unconfigured
unix
unprefixed
untrusted
uptime
username
url
versa
versioning
viewset
viewsets
waitinglist
webhook
webhooks
webserver

View File

@@ -21,11 +21,18 @@ Frontpage text
your product types, give more information on the event or for other general notices.
You can use :ref:`Markdown syntax <markdown-guide>` in this field.
Voucher explanation
This text will be shown above the voucher input box. You can use it to explain how to obtain a voucher and use it.
Show variations of a product expanded by default
If this is not checked, a product with variations will be shown as one row in the show by default and will expand
into multiple rows once it is clicked on. With this box checked, the variations will be shown as multiple rows
right from the beginning.
Ask search engines not to index the ticket shop
If this is checked, we will set a HTML meta attribute asking search engines by Google not to put this ticket shop
into their searchable index.
The lower part of the page contains settings that you can **either** set on organizer-level for all your events **or**
override for this single event individually. Those are:
@@ -35,6 +42,12 @@ Primary color
customers. We suggest not choosing something to light, since text in that color should be readable on a white
background and white text should be readable on a background of this color.
Accent color for success
This color will be used for success messages. We suggest to choose a dark shade of green.
Accent color for errors
This color will be used for error messages. We suggest to choose a dark shade of red.
Font
Choose one of multiple fonts to use for your web shop.

View File

@@ -8,8 +8,8 @@ event.
:align: center
:class: screenshot
The page is separated into three parts: "E-mail settings", "E-mail content" and "SMTP settings". We will explain all
of them in detail on this page.
The page is separated into four parts: "E-mail settings", "E-mail design", "E-mail content" and "SMTP settings".
We will explain all of them in detail on this page.
E-mail settings
---------------
@@ -30,10 +30,18 @@ Signature
This text will be appended to all e-mails in form of a signature. This might be useful e.g. to add your contact
details or any legal information that needs to be included with the e-mails.
Bcc address
This email address will receive a copy of every event-related email.
E-mail design
-------------
In this part, you can choose and preview the layout of your emails. More layouts can be added by pretix plugins.
E-mail content
--------------
The middle part of the page allows you to customize the exact texts of all e-mails sent by the system automatically.
The next part of the page allows you to customize the exact texts of all e-mails sent by the system automatically.
You can click on the different boxes to expand them and see the texts.
Within the texts, you can use placeholders that will later by replaced by values depending on the event or order. Below
@@ -45,6 +53,7 @@ Placeholder Description
============================== ===============================================================================
event The event name
total The order's total value
total_with_currency The order's total value with a localized currency sign
currency The currency used for the event (three-letter code)
payment_info Information text specific to the payment method (e.g. banking details)
url An URL pointing to the download/status page of the order
@@ -112,6 +121,22 @@ Reminder to download tickets
attendees to download their tickets. The e-mail should include a link to the ticket download. This e-mail will only
ever be sent if you specify a number of days.
Order approval process
If you configure one of your products to "require approval", orders of that product will not immediately be confirmed
but only after you approved them manually. In this case, the following e-mail templates will be sent out.
Received order
After an order has been received, this e-mail will be sent automatically instead of the "order placed" e-mail from
above.
Approved order
This e-mail will be sent after you manually approved an order. This should include instructions to pay for the order,
which is why this will only be used for a paid order. For a free order, the "free order" e-mail from above will
be sent.
Denied order
This e-mail will be sent out to customers when their order has been denied.
SMTP settings
-------------

View File

@@ -11,18 +11,6 @@ The settings at "Settings" → "Invoice" allow you to specify if and how pretix
In particular, you can configure the following things:
Ask for invoice address
If this checkbox is enabled, customers will be able to enter an invoice address during checkout. If you only enable
this box, the invoice address will be optional to fill in.
Require invoice address
If this checkbox is enabled, entering an invoice address will be obligatory for all customers and it will not be
able to create an order without entering an address.
Require customer name
If this checkbox is enabled, the street, city, and country fields of the invoice address will still be optional but
the name field will be obligatory.
Generate invoices
This field controls whether pretix should generate an invoice for an order. You have the following options:
@@ -51,6 +39,51 @@ Attach invoices to emails
"Automatically for all created orders" or to the payment confirmation e-mails if it is set to "Automatically on
payment".
Invoice number prefix
This is the prefix that will be prepended to all your invoice numbers. For example, if you set this to "Inv", your
invoices will be numbered Inv00001, Inv00002, etc. If you leave this field empty, your event slug will be used,
followed by a dash, e.g. DEMOCON-00001.
Within one organizer account, events with the same number prefix will share their number range. For example, if you
set this to "Inv" for all of your events, there will be only one invoice numbered Inv00007 across all your events
and the numbers will have gaps within one event.
Generate invoices with consecutive numbers
If enabled, invoices will be created with numerical invoice numbers in the order of their creation, i.e.
PREFIX-00001, PREFIX-00002, and so on. If disabled, invoice numbers will instead be generated from the order code,
i.e. PREFIX-YHASD-1. When in doubt, keep this option enabled since it might be legally required in your country,
but disabling it has the advantage that your customers can not estimate the number of tickets sold by looking at
the invoice numbers.
Invoice language
This setting allows you to configure the language of all invoices. You can either set it to one of your event
language or dynamically to the language used by the customer.
Show free products on invoices
If enabled, products that do not cost anything will still show up on invoices. Note that the order needs to contain
at least one non-free product in order to generate an invoice.
Show attendee names on invoices
If enabled, the attendee name will be printed on the invoice for admission tickets.
Ask for invoice address
If this checkbox is enabled, customers will be able to enter an invoice address during checkout. If you only enable
this box, the invoice address will be optional to fill in.
Require invoice address
If this checkbox is enabled, entering an invoice address will be obligatory for all customers and it will not be
able to create an order without entering an address.
Require customer name
If this checkbox is enabled, the street, city, and country fields of the invoice address will still be optional but
the name field will be obligatory.
Require a business address
If enabled, the invoice address form will require a company name and do not allow personal addresses.
Ask for beneficiary
If enabled, the invoice address form will contain an additional field to input the beneficiary of the transaction.
Ask for VAT ID
If enabled, the invoice address form will not only ask for a postal address, but also for a VAT ID. The VAT ID will
always be an optional field.
@@ -62,26 +95,13 @@ Generate invoices with consecutive numbers
but disabling it has the advantage that your customers can not estimate the number of tickets sold by looking at
the invoice numbers.
Invoice number prefix
This is the prefix that will be prepended to all your invoice numbers. For example, if you set this to "Inv", your
invoices will be numbered Inv00001, Inv00002, etc. If you leave this field empty, your event slug will be used,
followed by a dash, e.g. DEMOCON-00001.
Within one organizer account, events with the same number prefix will share their number range. For example, if you
set this to "Inv" for all of your events, there will be only one invoice numbered Inv00007 across all your events
and the numbers will have gaps within one event.
Show free products on invoices
If enabled, products that do not cost anything will still show up on invoices. Note that the order needs to contain
at least one non-free product in order to generate an invoice.
Show attendee names on invoices
If enabled, the attendee name will be printed on the invoice for admission tickets.
Your address
This should be set to the address of the entity issuing the invoice (read: you) and will be printed inside
Your invoice details
These fields should be set to the address of the entity issuing the invoice (read: you) and will be printed inside
the header of the invoice.
Invoice style
This setting allows you to choose the design of your invoice. Additional designs can be added by pretix plugins.
Introductory text
A free custom text that will be printed above the list of products on the invoice.

View File

@@ -0,0 +1,346 @@
Product structure guide
=======================
Between products, categories, variations, add-ons, bundles, and quotas, pretix provides a wide range of features that allow you to set up your event in the way you want it.
However, it is easy to get lost in the process or to get started with building your event right.
Often times, there are multiple ways to do something that come with different advantages and disadvantages.
This guide will walk you through a number of typical examples of pretix event structures and will explain how to set them up feel free to just skip ahead to a section relevant for you.
Terminology
-----------
Products
A product is a basic entity that can be bought. You can think of it as a ticket type, but it can be more things than just a ticket, it can also be a piece of merchandise, a parking slot, etc.
You might find some places where they are called "items" instead, but we're trying to get rid of that.
Product categories
Products can be sorted into categories. Each product can only be in one category. Category are mostly used for grouping related products together to make your event page easier to read for buyers. However, we'll need categories as well to set up some of the structures outlined below.
Product variations
During creation of a product, you can decide that your product should have multiple variations. Variations of a product can differ in price, description, and availability, but are otherwise the same. You could use this e.g. for differentiating between a regular ticket and a discounted ticket for students, or when selling merchandise to differentiate the different sizes of a t-shirt.
Product add-ons
Add-ons are products that are sold together with another product (which we will call the base product in this case). For example, you could have a base product "Conference ticket" and then define multiple workshops that can be chosen as an add-on.
Product bundles
Bundles work very similarly to add-ons, but are different in the way that they are always automatically included with the base product and cannot be optional. In contrast to add-on products, the same product can be included multiple times in a bundle.
Quotas
Quotas define the availability of products. A quota has a size (i.e. the number of products in the inventory) and is mapped to one or multiple products or variations.
Questions
Questions are user-defined form fields that buyers will need to fill out when purchasing a product.
Use case: Multiple price levels
-------------------------------
Imagine you're running a concert with general admission that sells a total of 200 tickets for two prices:
* Regular: € 25.00
* Students: € 19.00
You can either set up two different products called e.g. "Regular ticket" and "Student ticket" with the respective prices, or two variations within the same product. In this simple case, it really doesn't matter.
In addition, you will need quotas. If you do not care how many of your tickets are sold to students, you should set up just **one quota** of 200 called e.g. "General admission" that you link to **both products**.
If you want to limit the number of student tickets to 50 to ensure a certain minimum revenue, but do not want to limit the number of regular tickets artificially, we suggest you to create the same quota of 200 that is linked to both products, and then create a **second quota** of 50 that is only linked to the student ticket. This way, the system will reduce both quotas whenever a student ticket is sold and only the larger quota when a regular ticket is sold.
Use case: Early-bird tiers based on dates
-----------------------------------------
Let's say you run a conference that has the following pricing scheme:
* 12 to 6 months before the event: € 450
* 6 to 3 months before the event: € 550
* closer than 3 months to the event: € 650
Of course, you could just set up one product and change its price at the given dates manually, but if you want to set this up automatically, here's how:
Create three products (e.g. "super early bird", "early bird", "regular ticket") with the respective prices and one shared quota of your total event capacity. Then, set the **available from** and **available until** configuration fields of the products to automatically turn them on and off based on the current date.
Use case: Early-bird tiers based on ticket numbers
--------------------------------------------------
Let's say you run a conference with 400 tickets that has the following pricing scheme:
* First 100 tickets ("super early bird"): € 450
* Next 100 tickets ("early bird"): € 550
* Remaining tickets ("regular"): € 650
First of all, create three products:
* "Super early bird ticket"
* "Early bird ticket"
* "Regular ticket"
Then, create three quotas:
* "Super early bird" with a **size of 100** and the "Super early bird ticket" product selected. At "Advanced options",
select the box "Close this quota permanently once it is sold out".
* "Early bird and lower" with a **size of 200** and both of the "Super early bird ticket" and "Early bird ticket"
products selected. At "Advanced options", select the box "Close this quota permanently once it is sold out".
* "All participants" with a **size of 400**, all three products selected and **no additional options**.
Next, modify the product "Regular ticket". In the section "Availability", you should look for the option "Only show
after sellout of" and select your quota "Early bird and lower". Do the same for the "Early bird ticket" with the quota
"Super early bird ticket".
This will ensure the following things:
* Each ticket level is only visible after the previous level is sold out.
* As soon as one level is really sold out, it's not coming back, because the quota "closes", i.e. locks in place.
* By creating a total quota of 400 with all tickets included, you can still make sure to sell the maximum number of
tickets, even if e.g. early-bird tickets are canceled.
Optionally, if you want to hide the early bird prices once they are sold out, go to "Settings", then "Display" and
select "Hide all products that are sold out". Of course, it might be a nice idea to keep showing the prices to remind
people to buy earlier next time ;)
Please note that there might be short time intervals where the prices switch back and forth: When the last early bird
tickets are in someone's cart (but not yet sold!), the early bird tickets will show as "Reserved" and the regular
tickets start showing up. However, if the customers holding the reservations do not complete their order,
the early bird tickets will become available again. This is not avoidable if we want to prevent malicious users
from blocking all the cheap tickets without an actual sale happening.
Use case: Up-selling of ticket extras
-------------------------------------
Let's assume you're putting up a great music festival, and to save trouble with handling payments on-site, you want to sell parking spaces together with your ticket. By using our add-on feature, you can prompt all users to book the parking space (to make sure they see it) and ensure that only people with a ticket can book a parking space. You can set it up like this:
* Create a base product "Festival admission"
* Create a quota for the base product
* Create a category "Ticket extras" and check "Products in this category are add-on products"
* Create a product "Parking space" within that category
* Create a quota for the parking space product
* Go to the base product and select the tab "Add-Ons" at the top. Click "Add a new add-on" and choose the "Ticket extras" category. You can keep the numbers at 0 and 1.
During checkout, all buyers of the base product will now be prompted if they want to add the parking space.
.. tip::
You can also use add-on products for free things, just to keep tabs on capacity.
Use case: Conference with workshops
-----------------------------------
When running a conference, you might also organize a number of workshops with smaller capacity. To be able to plan, it would be great to know which workshops an attendee plans to attend.
Option A: Questions
"""""""""""""""""""
Your first and simplest option is to just create a multiple-choice question. This has the upside of making it easy for users to change their mind later on, but will not allow you to restrict the number of attendees signing up for a given workshop or even charge extra for a given workshop.
Option B: Add-on products with fixed time slots
"""""""""""""""""""""""""""""""""""""""""""""""
The usually better option is to go with add-on products. Let's take for example the following conference schedule, in which the lecture can be attended by anyone, but the workshops only have space for 20 persons each:
==================== =================================== ===================================
Time Room A Room B
==================== =================================== ===================================
Wednesday morning Lecture
Wednesday afternoon Workshop A Workshop B
Thursday morning Workshop C Workshop D (20 € extra charge)
==================== =================================== ===================================
Assuming you already created one or more products for your general conference admission, we suggest that you additionally create:
* A category called "Workshops" with the checkbox "Products in this category are add-on products" activated
* A free product called "Wednesday afternoon" within the category "Workshops" and with two variations:
* Workshop A
* Workshop B
* A free product called "Thursday morning" within the category "Workshops" and with two variations:
* Workshop C
* Workshop D with a price of 20 €
* Four quotas for each of the workshops
* One add-on configuration on your base product that allows users to choose between 0 and 2 products from the category "Workshops"
Option C: Add-on products with variable time slots
""""""""""""""""""""""""""""""""""""""""""""""""""
The above option only works if your conference uses fixed time slots and every workshop uses exactly one time slot. If
your schedule looks like this, it's not going to work great:
+-------------+------------+-----------+
| Time | Room A | Room B |
+=============+============+===========+
| 09:00-11:00 | Talk 1 | Long |
+-------------+------------+ Workshop 1|
| 11:00-13:00 | Talk 2 | |
+-------------+------------+-----------+
| 14:00-16:00 | Long | Talk 3 |
+-------------+ workshop 2 +-----------+
| 16:00-18:00 | | Talk 4 |
+-------------+------------+-----------+
In this case, we recommend that you go to *Settings*, then *Plugins* and activate the plugin **Agenda constraints**.
Then, create a product (without variations) for every single part that should be bookable (talks 1-4 and long workshops
1 and 2) as well as appropriate quotas for each of them.
All of these products should be part of the same category. In your base product (e.g. your conference ticket), you
can then create an add-on product configuration allowing users to add products from this category.
If you edit these products, you will be able to enter the "Start date" and "End date" of the talk or workshop close
to the bottom of the page. If you fill in these values, pretix will automatically ensure no overlapping talks are
booked.
.. note::
This option is currently only available on pretix Hosted. If you are interested in using it with pretix Enterprise,
please contact sales@pretix.eu.
Use case: Discounted packages
-----------------------------
Imagine you run a trade show that opens on three consecutive days and you want to have the following pricing:
* Single day: € 10
* Any two days: € 17
* All three days: € 25
In this case, there are multiple different ways you could set this up with pretix.
Option A: Combination products
""""""""""""""""""""""""""""""
With this option, you just set up all the different combinations someone could by as a separate product. In this case, you would need 7 products:
* Day 1 pass
* Day 2 pass
* Day 3 pass
* Day 1+2 pass
* Day 2+3 pass
* Day 1+3 pass
* All-day pass
Then, you create three quotas, each one with the maximum capacity of your venue on any given day:
* Day 1 quota, linked to "Day 1 pass", "Day 1+2 pass", "Day 1+3 pass", and "All-day pass"
* Day 2 quota, linked to "Day 2 pass", "Day 1+2 pass", "Day 2+3 pass", and "All-day pass"
* Day 3 quota, linked to "Day 3 pass", "Day 2+3 pass", "Day 1+3 pass", and "All-day pass"
This way, every person gets exactly one ticket that they can use for all days that they attend. You can later set up check-in lists appropriately to make sure only tickets valid for a certain day can be scanned on that day.
The benefit of this option is that your product structure and order structure stays very simple. However, the two-day packages scale badly when you need many products.
We recommend this setup for most setups in which the number of possible combinations does not exceed the number of parts (here: number of days) by much.
Option B: Add-ons and bundles
"""""""""""""""""""""""""""""
We can combine the two features "product add-ons" and "product bundles" to set this up in a different way. Here, you would create the following five products:
* Day 1 pass in a category called "Day passes"
* Day 2 pass in a category called "Day passes"
* Day 3 pass in a category called "Day passes"
* Two-day pass
* All-day pass
This time, you will need five quotas:
* Day 1 quota, linked to "Day 1 pass"
* Day 2 quota, linked to "Day 2 pass"
* Day 3 quota, linked to "Day 3 pass"
* Two-day pass quota, linked to "Two-day pass" (can be unlimited)
* All-day pass quota, linked to "All-day pass" (can be unlimited)
Then, you open the "Add-On" tab in the settings of the **Two-day pass** product and create a new add-on configuration specifying the following options:
* Category: "Day passes"
* Minimum number: 2
* Maximum number: 2
* Add-Ons are included in the price: Yes
This way, when buying a two-day pass, the user will be able to select *exactly* two days for free, which will then be added to the cart. Depending on your specific configuration, the user will now receive *two separate* tickets, one for each day.
For the all-day pass, you open the "Bundled products" tab in the settings of the **All-day pass** product and add **three** new bundled items with the following options:
* Bundled product: "Day 1/2/3"
* Bundled variation: None
* Count: 1
* Designated price: 0
This way, when buying an all-day pass, three free day passes will *automatically* be added to the cart. Depending on your specific configuration, the user will now receive *three separate* tickets, one for each day.
This approach makes your order data more complicated, since e.g. someone who buys an all-day pass now technically bought **four products**. However, this option allows for more flexibility when you have lots of options to choose from.
.. tip::
Depending on the packages you offer, you **might not need both the add-on and the bundle feature**, i.e. you only need the add-on feature for the two-day pass and only the bundle feature for the all-day pass. You could also set up the two-day pass like we showed here, but the all-day pass like in option A!
Use case: Group discounts
-------------------------
Often times, you want to give discounts for whole groups attending your event. pretix can't automatically discount based on volume, but there's still some ways you can set up group tickets.
Flexible group sizes
""""""""""""""""""""
If you want to give out discounted tickets to groups starting at a given size, but still billed per person, you can do so by creating a special **Group ticket** at the per-person price and set the **Minimum amount per order** option of the ticket to the minimal group size.
This way, your ticket can be bought an arbitrary number of times but no less than the given minimal amount per order.
Fixed group sizes
"""""""""""""""""
If you want to sell group tickets in fixed sizes, e.g. a table of eight at your gala dinner, you can use product bundles. Assuming you already set up a ticket for admission of single persons, you then set up a second product **Table (8 persons)** with a discounted full price. Then, head to the **Bundled products** tab of that product and add one bundle configuration to include the single admission product **eight times**. Next, create an unlimited quota mapped to the new product.
This way, the purchase of a table will automatically create eight tickets, leading to a correct calculation of your total quota and, as expected, eight persons on your check-in list. You can even ask for the individual names of the persons during checkout.
Use case: Restricted audience
-----------------------------
Not all events are for everyone. Sometimes, there is a good reason to restrict access to your event or parts of your event only to a specific, invited group. There's two ways to implement this with pretix:
Option A: Required voucher codes
""""""""""""""""""""""""""""""""
If you check the option "**This product can only be bought using a voucher**" of one or multiple products, only people holding an applicable voucher code will be able to buy the product.
You can then generate voucher codes for the respective product and send them out to the group of possible attendees. If the recipients should still be able to choose between different products, you can create an additional quota and map the voucher to that quota instead of the products themselves.
There's also the second option "**This product will only be shown if a voucher matching the product is redeemed**". In this case, the existence of the product won't even be shown before a voucher code is entered useful for a VIP option in a shop where you also sell other products to the general public. Please note that this option does **not** work with vouchers assigned to a quota, only with vouchers assigned directly to the product.
This option is appropriate if you know the group of people beforehand, e.g. members of a club, and you can mail them their access codes.
Option B: Order approvals
"""""""""""""""""""""""""
If you do not know your audience already, but still want to restrict it to a certain group, e.g. people with a given profession, you can check the "**Buying this product requires approval**" in the settings of your product. If a customer tries to buy such a product, they will be able to place their order but can not proceed to payment. Instead, you will be asked to approve or deny the order and only if you approve it, we will send a payment link to the customer.
This requires the customer to interact with the ticket shop twice (once for the order, once for the payment) which adds a little more friction, but gives you full control over who attends the event.
Use case: Mixed taxation
------------------------
Let's say you are a charitable organization in Germany and are allowed to charge a reduced tax rate of 7% for your educational event. However, your event includes a significant amount of food, you might need to charge a 19% tax rate on that portion. For example, your desired tax structure might then look like this:
* Conference ticket price: € 450 (incl. € 150 for food)
* incl. € 19.63 VAT at 7%
* incl. € 23.95 VAT at 19%
You can implement this in pretix using product bundles. In order to do so, you should create the following two products:
* Conference ticket at € 450 with a 7% tax rule
* Conference food at € 150 with a 19% tax rule and the option "**Only sell this product as part of a bundle**" set
In addition to your normal conference quota, you need to create an unlimited quota for the food product.
Then, head to the **Bundled products** tab of the "conference ticket" and add the "conference food" as a bundled product with a **designated price** of € 150.
Once a customer tries to buy the € 450 conference ticket, a sub-product will be added and the price will automatically be split into the two components, leading to a correct computation of taxes.

View File

@@ -25,6 +25,10 @@ Generate tickets for non-admission products
By default, tickets will only be generated for products that are marked as admission products. Enable this option to
generate tickets for all products instead.
Offer to download tickets even before an order is paid
By default, ticket download is only possible for paid orders. If you run an event where people usually pay only after
the event, you can check this box to enable ticket download even before.
Below these settings, the detail settings for the various ticket file formats are offered. They differ from format to
format and only share the common "Enable" setting that can be used to turn them on. By default, pretix ships with
a PDF output plugin that you can configure through a visual design editor.

View File

@@ -114,6 +114,40 @@ If you want to disable voucher input in the widget, you can pass the ``disable-v
<pretix-widget event="https://pretix.eu/demo/democon/" disable-vouchers></pretix-widget>
Multi-event selection
---------------------
If you want to embed multiple events in a single widget, you can do so. If it's multiple dates of an event series, just leave off the ``series`` attribute::
<pretix-widget event="https://pretix.eu/demo/series/"></pretix-widget>
If you want to include all your public events, you can just reference your organizer::
<pretix-widget event="https://pretix.eu/demo/"></pretix-widget>
There is an optional ``style`` parameter that let's you choose between a calendar view and a list view. If you do not set it, the choice will be taken from your organizer settings::
<pretix-widget event="https://pretix.eu/demo/series/" style="list"></pretix-widget>
<pretix-widget event="https://pretix.eu/demo/series/" style="calendar"></pretix-widget>
You can see an example here:
.. raw:: html
<pretix-widget event="https://pretix.eu/demo/series/" style="calendar"></pretix-widget>
<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/series/">click here</a>.
</div>
</div>
</noscript>
You can filter events by meta data attributes. You can create those attributes in your order profile and set their values in both event and series date
settings. For example, if you set up a meta data property called "Promoted" that you set to "Yes" on some events, you can pass a filter like this::
<pretix-widget event="https://pretix.eu/demo/series/" style="list" filter="attr[Promoted]=Yes"></pretix-widget>
pretix Button
-------------
@@ -240,6 +274,9 @@ Hosted or pretix Enterprise are active, you can pass the following fields:
};
</script>
In some combinations with Google Tag Manager, the widget does not load this way. In this case, try replacing
``tracker.get('clientId')`` with ``ga.getAll()[0].get('clientId')``.
.. versionchanged:: 2.3

View File

@@ -10,6 +10,7 @@ wanting to use pretix to sell tickets.
organizers/index
events/create
events/settings
events/structureguide
events/widget
faq
markdown

View File

@@ -3,6 +3,13 @@
PayPal
======
.. note::
If you use pretix Hosted, you do not longer need to go through this tedious process! You can
just open the PayPal settings in the payment section of your event, click "Connect to PayPal"
and log in to your PayPal account. The following guide is only required for self-hosted
versions of pretix.
To integrate PayPal with pretix, you first need to have an active PayPal merchant account. If you do not already have a
PayPal account, you can create one on `paypal.com`_.
If you look into pretix' settings, you are required to fill in two keys:

View File

@@ -22,3 +22,5 @@ recursive-include pretix/plugins/ticketoutputpdf/templates *
recursive-include pretix/plugins/ticketoutputpdf/static *
recursive-include pretix/plugins/badges/templates *
recursive-include pretix/plugins/badges/static *
recursive-include pretix/plugins/returnurl/templates *
recursive-include pretix/plugins/returnurl/static *

View File

@@ -1,71 +0,0 @@
#!/usr/bin/env python
import os
import sys
from datetime import datetime
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pretix.settings")
import django
django.setup()
from pretix.base.models import * # NOQA
from django.utils.timezone import now
if Organizer.objects.exists():
print("There already is data in your DB!")
sys.exit(0)
user = User.objects.get_or_create(
email='admin@localhost',
)[0]
user.set_password('admin')
user.save()
organizer = Organizer.objects.create(
name='BigEvents LLC', slug='bigevents'
)
year = now().year + 1
event = Event.objects.create(
organizer=organizer, name='Demo Conference {}'.format(year),
slug=year, currency='EUR', live=True,
date_from=datetime(year, 9, 4, 17, 0, 0),
date_to=datetime(year, 9, 6, 17, 0, 0),
)
t = Team.objects.get_or_create(
organizer=organizer, name='Admin Team',
all_events=True, can_create_events=True, can_change_teams=True,
can_change_organizer_settings=True, can_change_event_settings=True, can_change_items=True,
can_view_orders=True, can_change_orders=True, can_view_vouchers=True, can_change_vouchers=True
)
t[0].members.add(user)
cat_tickets = ItemCategory.objects.create(
event=event, name='Tickets'
)
cat_merch = ItemCategory.objects.create(
event=event, name='Merchandise'
)
question = Question.objects.create(
event=event, question='Age',
type=Question.TYPE_NUMBER, required=False
)
tr19 = event.tax_rules.create(rate=19)
item_ticket = Item.objects.create(
event=event, category=cat_tickets, name='Ticket',
default_price=23, tax_rule=tr19, admission=True
)
item_ticket.questions.add(question)
item_shirt = Item.objects.create(
event=event, category=cat_merch, name='T-Shirt',
default_price=15, tax_rule=tr19
)
var_s = ItemVariation.objects.create(item=item_shirt, value='S')
var_m = ItemVariation.objects.create(item=item_shirt, value='M')
var_l = ItemVariation.objects.create(item=item_shirt, value='L')
ticket_quota = Quota.objects.create(
event=event, name='Ticket quota', size=400,
)
ticket_quota.items.add(item_ticket)
ticket_shirts = Quota.objects.create(
event=event, name='Shirt quota', size=200,
)
ticket_quota.items.add(item_shirt)
ticket_quota.variations.add(var_s, var_m, var_l)

View File

@@ -1 +1 @@
__version__ = "2.5.0.dev0"
__version__ = "3.2.0"

View File

@@ -1,4 +1,5 @@
from django.contrib.auth.models import AnonymousUser
from django_scopes import scopes_disabled
from rest_framework import exceptions
from rest_framework.authentication import TokenAuthentication
@@ -12,14 +13,15 @@ class DeviceTokenAuthentication(TokenAuthentication):
def authenticate_credentials(self, key):
model = self.get_model()
try:
device = model.objects.select_related('organizer').get(api_token=key)
with scopes_disabled():
device = model.objects.select_related('organizer').get(api_token=key)
except model.DoesNotExist:
raise exceptions.AuthenticationFailed('Invalid token.')
if not device.initialized:
raise exceptions.AuthenticationFailed('Device has not been initialized.')
if not device.api_token:
if device.revoked:
raise exceptions.AuthenticationFailed('Device access has been revoked.')
return AnonymousUser(), device

View File

@@ -1,8 +1,9 @@
from rest_framework.permissions import SAFE_METHODS, BasePermission
from pretix.api.models import OAuthAccessToken
from pretix.base.models import Device, Event
from pretix.base.models.organizer import Organizer, TeamAPIToken
from pretix.base.models import Device, Event, User
from pretix.base.models.auth import SuperuserPermissionSet
from pretix.base.models.organizer import TeamAPIToken
from pretix.helpers.security import (
SessionInvalid, SessionReauthRequired, assert_session_valid,
)
@@ -37,21 +38,24 @@ class EventPermission(BasePermission):
slug=request.resolver_match.kwargs['event'],
organizer__slug=request.resolver_match.kwargs['organizer'],
).select_related('organizer').first()
if not request.event or not perm_holder.has_event_permission(request.event.organizer, request.event):
if not request.event or not perm_holder.has_event_permission(request.event.organizer, request.event, request=request):
return False
request.organizer = request.event.organizer
request.eventpermset = perm_holder.get_event_permission_set(request.organizer, request.event)
if isinstance(perm_holder, User) and perm_holder.has_active_staff_session(request.session.session_key):
request.eventpermset = SuperuserPermissionSet()
else:
request.eventpermset = perm_holder.get_event_permission_set(request.organizer, request.event)
if required_permission and required_permission not in request.eventpermset:
return False
elif 'organizer' in request.resolver_match.kwargs:
request.organizer = Organizer.objects.filter(
slug=request.resolver_match.kwargs['organizer'],
).first()
if not request.organizer or not perm_holder.has_organizer_permission(request.organizer):
if not request.organizer or not perm_holder.has_organizer_permission(request.organizer, request=request):
return False
request.orgapermset = perm_holder.get_organizer_permission_set(request.organizer)
if isinstance(perm_holder, User) and perm_holder.has_active_staff_session(request.session.session_key):
request.orgapermset = SuperuserPermissionSet()
else:
request.orgapermset = perm_holder.get_organizer_permission_set(request.organizer)
if required_permission and required_permission not in request.orgapermset:
return False

View File

@@ -0,0 +1,112 @@
import json
from hashlib import sha1
from django.conf import settings
from django.db import transaction
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.urls import resolve
from django.utils.timezone import now
from django_scopes import scope
from rest_framework import status
from pretix.api.models import ApiCall
from pretix.base.models import Organizer
class IdempotencyMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request: HttpRequest):
if request.method in ('GET', 'HEAD', 'OPTIONS'):
return self.get_response(request)
if not request.path.startswith('/api/'):
return self.get_response(request)
if not request.headers.get('X-Idempotency-Key'):
return self.get_response(request)
auth_hash_parts = '{}:{}'.format(
request.headers.get('Authorization', ''),
request.COOKIES.get(settings.SESSION_COOKIE_NAME, '')
)
auth_hash = sha1(auth_hash_parts.encode()).hexdigest()
idempotency_key = request.headers.get('X-Idempotency-Key', '')
with transaction.atomic():
call, created = ApiCall.objects.select_for_update().get_or_create(
auth_hash=auth_hash,
idempotency_key=idempotency_key,
defaults={
'locked': now(),
'request_method': request.method,
'request_path': request.path,
'response_code': 0,
'response_headers': '{}',
'response_body': b''
}
)
if created:
resp = self.get_response(request)
with transaction.atomic():
if resp.status_code in (409, 429, 503):
# This is the exception: These calls are *meant* to be retried!
call.delete()
else:
call.response_code = resp.status_code
if isinstance(resp.content, str):
call.response_body = resp.content.encode()
elif isinstance(resp.content, memoryview):
call.response_body = resp.content.tobytes()
elif isinstance(resp.content, bytes):
call.response_body = resp.content
elif hasattr(resp.content, 'read'):
call.response_body = resp.read()
elif hasattr(resp, 'data'):
call.response_body = json.dumps(resp.data)
else:
call.response_body = repr(resp).encode()
call.response_headers = json.dumps(resp._headers)
call.locked = None
call.save(update_fields=['locked', 'response_code', 'response_headers',
'response_body'])
return resp
else:
if call.locked:
r = JsonResponse(
{'detail': 'Concurrent request with idempotency key.'},
status=status.HTTP_409_CONFLICT,
)
r['Retry-After'] = 5
return r
content = call.response_body
if isinstance(content, memoryview):
content = content.tobytes()
r = HttpResponse(
content=content,
status=call.response_code,
)
for k, v in json.loads(call.response_headers).values():
r[k] = v
return r
class ApiScopeMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request: HttpRequest):
if not request.path.startswith('/api/'):
return self.get_response(request)
url = resolve(request.path_info)
if 'organizer' in url.kwargs:
request.organizer = Organizer.objects.filter(
slug=url.kwargs['organizer'],
).first()
with scope(organizer=getattr(request, 'organizer', None)):
return self.get_response(request)

View File

@@ -0,0 +1,44 @@
# Generated by Django 2.1.5 on 2019-04-05 10:48
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('pretixbase', '0116_auto_20190402_0722'),
('pretixapi', '0003_webhook_webhookcall_webhookeventlistener'),
]
operations = [
migrations.CreateModel(
name='ApiCall',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('idempotency_key', models.CharField(db_index=True, max_length=190)),
('auth_hash', models.CharField(db_index=True, max_length=190)),
('created', models.DateTimeField(auto_now_add=True)),
('locked', models.DateTimeField(null=True)),
('request_method', models.CharField(max_length=20)),
('request_path', models.CharField(max_length=255)),
('response_code', models.PositiveIntegerField()),
('response_headers', models.TextField()),
('response_body', models.BinaryField()),
],
),
migrations.AlterModelOptions(
name='webhookcall',
options={'ordering': ('-datetime',)},
),
migrations.AlterModelOptions(
name='webhookeventlistener',
options={'ordering': ('action_type',)},
),
migrations.AlterUniqueTogether(
name='apicall',
unique_together={('idempotency_key', 'auth_hash')},
),
]

View File

@@ -77,6 +77,9 @@ class WebHook(models.Model):
all_events = models.BooleanField(default=True, verbose_name=_("All events (including newly created ones)"))
limit_events = models.ManyToManyField('pretixbase.Event', verbose_name=_("Limit to events"), blank=True)
class Meta:
ordering = ('id',)
@property
def action_types(self):
return [
@@ -106,3 +109,20 @@ class WebHookCall(models.Model):
class Meta:
ordering = ("-datetime",)
class ApiCall(models.Model):
idempotency_key = models.CharField(max_length=190, db_index=True)
auth_hash = models.CharField(max_length=190, db_index=True)
created = models.DateTimeField(auto_now_add=True)
locked = models.DateTimeField(null=True)
request_method = models.CharField(max_length=20)
request_path = models.CharField(max_length=255)
response_code = models.PositiveIntegerField()
response_headers = models.TextField()
response_body = models.BinaryField()
class Meta:
unique_together = (('idempotency_key', 'auth_hash'),)

View File

@@ -8,31 +8,33 @@ from rest_framework.exceptions import ValidationError
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.order import (
AnswerCreateSerializer, AnswerSerializer,
AnswerCreateSerializer, AnswerSerializer, InlineSeatSerializer,
)
from pretix.base.models import Quota
from pretix.base.models import Quota, Seat
from pretix.base.models.orders import CartPosition
class CartPositionSerializer(I18nAwareModelSerializer):
answers = AnswerSerializer(many=True)
seat = InlineSeatSerializer()
class Meta:
model = CartPosition
fields = ('id', 'cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
'attendee_email', 'voucher', 'addon_to', 'subevent', 'datetime', 'expires', 'includes_tax',
'answers',)
'answers', 'seat')
class CartPositionCreateSerializer(I18nAwareModelSerializer):
answers = AnswerCreateSerializer(many=True, required=False)
expires = serializers.DateTimeField(required=False)
attendee_name = serializers.CharField(required=False, allow_null=True)
seat = serializers.CharField(required=False, allow_null=True)
class Meta:
model = CartPosition
fields = ('cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
'subevent', 'expires', 'includes_tax', 'answers',)
'subevent', 'expires', 'includes_tax', 'answers', 'seat')
def create(self, validated_data):
answers_data = validated_data.pop('answers')
@@ -71,6 +73,24 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
validated_data['attendee_name_parts'] = {
'_legacy': attendee_name
}
seated = validated_data.get('item').seat_category_mappings.filter(subevent=validated_data.get('subevent')).exists()
if validated_data.get('seat'):
if not seated:
raise ValidationError('The specified product does not allow to choose a seat.')
try:
seat = self.context['event'].seats.get(seat_guid=validated_data['seat'], subevent=validated_data.get('subevent'))
except Seat.DoesNotExist:
raise ValidationError('The specified seat does not exist.')
except Seat.MultipleObjectsReturned:
raise ValidationError('The specified seat ID is not unique.')
else:
validated_data['seat'] = seat
if not seat.is_available():
raise ValidationError(ugettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
elif seated:
raise ValidationError('The specified product requires to choose a seat.')
cp = CartPosition.objects.create(event=self.context['event'], **validated_data)
for answ_data in answers_data:

View File

@@ -3,6 +3,7 @@ from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.channels import get_all_sales_channels
from pretix.base.models import CheckinList
@@ -13,7 +14,7 @@ class CheckinListSerializer(I18nAwareModelSerializer):
class Meta:
model = CheckinList
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
'include_pending')
'include_pending', 'auto_checkin_sales_channels')
def validate(self, data):
data = super().validate(data)
@@ -35,4 +36,8 @@ class CheckinListSerializer(I18nAwareModelSerializer):
if full_data.get('subevent'):
raise ValidationError(_('The subevent does not belong to this event.'))
for channel in full_data.get('auto_checkin_sales_channels') or []:
if channel not in get_all_sales_channels():
raise ValidationError(_('Unknown sales channel.'))
return data

View File

@@ -11,6 +11,9 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import Event, TaxRule
from pretix.base.models.event import SubEvent
from pretix.base.models.items import SubEventItem, SubEventItemVariation
from pretix.base.services.seating import (
SeatProtected, generate_seats, validate_plan_change,
)
class MetaDataField(Field):
@@ -26,15 +29,31 @@ class MetaDataField(Field):
}
class SeatCategoryMappingField(Field):
def to_representation(self, value):
qs = value.seat_category_mappings.all()
if isinstance(value, Event):
qs = qs.filter(subevent=None)
return {
v.layout_category: v.product_id for v in qs
}
def to_internal_value(self, data):
return {
'seat_category_mapping': data or {}
}
class PluginsField(Field):
def to_representation(self, obj):
from pretix.base.plugins import get_all_plugins
return {
return sorted([
p.module for p in get_all_plugins()
if not p.name.startswith('.') and getattr(p, 'visible', True) and p.module in obj.get_plugins()
}
])
def to_internal_value(self, data):
return {
@@ -45,12 +64,14 @@ class PluginsField(Field):
class EventSerializer(I18nAwareModelSerializer):
meta_data = MetaDataField(required=False, source='*')
plugins = PluginsField(required=False, source='*')
seat_category_mapping = SeatCategoryMappingField(source='*', required=False)
class Meta:
model = Event
fields = ('name', 'slug', 'live', 'testmode', 'currency', 'date_from',
'date_to', 'date_admission', 'is_public', 'presale_start',
'presale_end', 'location', 'has_subevents', 'meta_data', 'plugins')
'presale_end', 'location', 'has_subevents', 'meta_data', 'seating_plan',
'plugins', 'seat_category_mapping')
def validate(self, data):
data = super().validate(data)
@@ -61,6 +82,9 @@ class EventSerializer(I18nAwareModelSerializer):
Event.clean_dates(data.get('date_from'), data.get('date_to'))
Event.clean_presale(data.get('presale_start'), data.get('presale_end'))
if full_data.get('has_subevents') and full_data.get('seating_plan'):
raise ValidationError('Event series should not directly be assigned a seating plan.')
return data
def validate_has_subevents(self, value):
@@ -92,6 +116,27 @@ class EventSerializer(I18nAwareModelSerializer):
raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key))
return value
def validate_seating_plan(self, value):
if value and value.organizer != self.context['request'].organizer:
raise ValidationError('Invalid seating plan.')
if self.instance and self.instance.pk:
try:
validate_plan_change(self.instance, None, value)
except SeatProtected as e:
raise ValidationError(str(e))
return value
def validate_seat_category_mapping(self, value):
if value and value['seat_category_mapping'] and (not self.instance or not self.instance.pk):
raise ValidationError('You cannot specify seat category mappings on event creation.')
item_cache = {i.pk: i for i in self.instance.items.all()}
result = {}
for k, item in value['seat_category_mapping'].items():
if item not in item_cache:
raise ValidationError('Item \'{id}\' does not exist.'.format(id=item))
result[k] = item_cache[item]
return {'seat_category_mapping': result}
def validate_plugins(self, value):
from pretix.base.plugins import get_all_plugins
@@ -109,6 +154,7 @@ class EventSerializer(I18nAwareModelSerializer):
@transaction.atomic
def create(self, validated_data):
meta_data = validated_data.pop('meta_data', None)
validated_data.pop('seat_category_mapping', None)
plugins = validated_data.pop('plugins', settings.PRETIX_PLUGINS_DEFAULT.split(','))
event = super().create(validated_data)
@@ -120,6 +166,10 @@ class EventSerializer(I18nAwareModelSerializer):
value=value
)
# Seats
if event.seating_plan:
generate_seats(event, None, event.seating_plan, {})
# Plugins
if plugins is not None:
event.set_active_plugins(plugins)
@@ -131,6 +181,7 @@ class EventSerializer(I18nAwareModelSerializer):
def update(self, instance, validated_data):
meta_data = validated_data.pop('meta_data', None)
plugins = validated_data.pop('plugins', None)
seat_category_mapping = validated_data.pop('seat_category_mapping', None)
event = super().update(instance, validated_data)
# Meta data
@@ -151,6 +202,29 @@ class EventSerializer(I18nAwareModelSerializer):
if prop.name not in meta_data:
current_object.delete()
# Seats
if seat_category_mapping is not None or ('seating_plan' in validated_data and validated_data['seating_plan'] is None):
current_mappings = {
m.layout_category: m
for m in event.seat_category_mappings.filter(subevent=None)
}
if not event.seating_plan:
seat_category_mapping = {}
for key, value in seat_category_mapping.items():
if key in current_mappings:
m = current_mappings.pop(key)
m.product = value
m.save()
else:
event.seat_category_mappings.create(product=value, layout_category=key)
for m in current_mappings.values():
m.delete()
if 'seating_plan' in validated_data or seat_category_mapping is not None:
generate_seats(event, None, event.seating_plan, {
m.layout_category: m.product
for m in event.seat_category_mappings.select_related('product').filter(subevent=None)
})
# Plugins
if plugins is not None:
event.set_active_plugins(plugins)
@@ -164,6 +238,7 @@ class CloneEventSerializer(EventSerializer):
def create(self, validated_data):
plugins = validated_data.pop('plugins', None)
is_public = validated_data.pop('is_public', None)
testmode = validated_data.pop('testmode', None)
new_event = super().create(validated_data)
event = Event.objects.filter(slug=self.context['event'], organizer=self.context['organizer'].pk).first()
@@ -173,6 +248,8 @@ class CloneEventSerializer(EventSerializer):
new_event.set_active_plugins(plugins)
if is_public is not None:
new_event.is_public = is_public
if testmode is not None:
new_event.testmode = testmode
new_event.save()
return new_event
@@ -191,16 +268,172 @@ class SubEventItemVariationSerializer(I18nAwareModelSerializer):
class SubEventSerializer(I18nAwareModelSerializer):
item_price_overrides = SubEventItemSerializer(source='subeventitem_set', many=True)
variation_price_overrides = SubEventItemVariationSerializer(source='subeventitemvariation_set', many=True)
item_price_overrides = SubEventItemSerializer(source='subeventitem_set', many=True, required=False)
variation_price_overrides = SubEventItemVariationSerializer(source='subeventitemvariation_set', many=True, required=False)
seat_category_mapping = SeatCategoryMappingField(source='*', required=False)
event = SlugRelatedField(slug_field='slug', read_only=True)
meta_data = MetaDataField(source='*')
class Meta:
model = SubEvent
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
'presale_start', 'presale_end', 'location', 'event',
'item_price_overrides', 'variation_price_overrides', 'meta_data')
'presale_start', 'presale_end', 'location', 'event', 'is_public', 'seating_plan',
'item_price_overrides', 'variation_price_overrides', 'meta_data', 'seat_category_mapping')
def validate(self, data):
data = super().validate(data)
event = self.context['request'].event
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
full_data.update(data)
Event.clean_dates(data.get('date_from'), data.get('date_to'))
Event.clean_presale(data.get('presale_start'), data.get('presale_end'))
SubEvent.clean_items(event, [item['item'] for item in full_data.get('subeventitem_set', [])])
SubEvent.clean_variations(event, [item['variation'] for item in full_data.get('subeventitemvariation_set', [])])
return data
def validate_item_price_overrides(self, data):
return list(filter(lambda i: 'item' in i, data))
def validate_variation_price_overrides(self, data):
return list(filter(lambda i: 'variation' in i, data))
def validate_seating_plan(self, value):
if value and value.organizer != self.context['request'].organizer:
raise ValidationError('Invalid seating plan.')
if self.instance and self.instance.pk:
try:
validate_plan_change(self.context['request'].event, self.instance, value)
except SeatProtected as e:
raise ValidationError(str(e))
return value
def validate_seat_category_mapping(self, value):
item_cache = {i.pk: i for i in self.context['request'].event.items.all()}
result = {}
for k, item in value['seat_category_mapping'].items():
if item not in item_cache:
raise ValidationError('Item \'{id}\' does not exist.'.format(id=item))
result[k] = item_cache[item]
return {'seat_category_mapping': result}
@cached_property
def meta_properties(self):
return {
p.name: p for p in self.context['request'].organizer.meta_properties.all()
}
def validate_meta_data(self, value):
for key in value['meta_data'].keys():
if key not in self.meta_properties:
raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key))
return value
@transaction.atomic
def create(self, validated_data):
item_price_overrides_data = validated_data.pop('subeventitem_set') if 'subeventitem_set' in validated_data else {}
variation_price_overrides_data = validated_data.pop('subeventitemvariation_set') if 'subeventitemvariation_set' in validated_data else {}
meta_data = validated_data.pop('meta_data', None)
seat_category_mapping = validated_data.pop('seat_category_mapping', None)
subevent = super().create(validated_data)
for item_price_override_data in item_price_overrides_data:
SubEventItem.objects.create(subevent=subevent, **item_price_override_data)
for variation_price_override_data in variation_price_overrides_data:
SubEventItemVariation.objects.create(subevent=subevent, **variation_price_override_data)
# Meta data
if meta_data is not None:
for key, value in meta_data.items():
subevent.meta_values.create(
property=self.meta_properties.get(key),
value=value
)
# Seats
if subevent.seating_plan:
if seat_category_mapping is not None:
for key, value in seat_category_mapping.items():
self.context['request'].event.seat_category_mappings.create(
product=value, layout_category=key, subevent=subevent
)
generate_seats(self.context['request'].event, subevent, subevent.seating_plan, {
m.layout_category: m.product
for m in self.context['request'].event.seat_category_mappings.select_related('product').filter(subevent=subevent)
})
return subevent
@transaction.atomic
def update(self, instance, validated_data):
item_price_overrides_data = validated_data.pop('subeventitem_set') if 'subeventitem_set' in validated_data else {}
variation_price_overrides_data = validated_data.pop('subeventitemvariation_set') if 'subeventitemvariation_set' in validated_data else {}
meta_data = validated_data.pop('meta_data', None)
seat_category_mapping = validated_data.pop('seat_category_mapping', None)
subevent = super().update(instance, validated_data)
existing_item_overrides = {item.item: item.id for item in SubEventItem.objects.filter(subevent=subevent)}
for item_price_override_data in item_price_overrides_data:
id = existing_item_overrides.pop(item_price_override_data['item'], None)
SubEventItem(id=id, subevent=subevent, **item_price_override_data).save()
SubEventItem.objects.filter(id__in=existing_item_overrides.values()).delete()
existing_variation_overrides = {item.variation: item.id for item in SubEventItemVariation.objects.filter(subevent=subevent)}
for variation_price_override_data in variation_price_overrides_data:
id = existing_variation_overrides.pop(variation_price_override_data['variation'], None)
SubEventItemVariation(id=id, subevent=subevent, **variation_price_override_data).save()
SubEventItemVariation.objects.filter(id__in=existing_variation_overrides.values()).delete()
# Meta data
if meta_data is not None:
current = {mv.property: mv for mv in subevent.meta_values.select_related('property')}
for key, value in meta_data.items():
prop = self.meta_properties.get(key)
if prop in current:
current[prop].value = value
current[prop].save()
else:
subevent.meta_values.create(
property=self.meta_properties.get(key),
value=value
)
for prop, current_object in current.items():
if prop.name not in meta_data:
current_object.delete()
# Seats
if seat_category_mapping is not None or ('seating_plan' in validated_data and validated_data['seating_plan'] is None):
current_mappings = {
m.layout_category: m
for m in self.context['request'].event.seat_category_mappings.filter(subevent=subevent)
}
if not subevent.seating_plan:
seat_category_mapping = {}
for key, value in seat_category_mapping.items():
if key in current_mappings:
m = current_mappings.pop(key)
m.product = value
m.save()
else:
self.context['request'].event.seat_category_mappings.create(
product=value, layout_category=key, subevent=subevent
)
for m in current_mappings.values():
m.delete()
if 'seating_plan' in validated_data or seat_category_mapping is not None:
generate_seats(self.context['request'].event, subevent, subevent.seating_plan, {
m.layout_category: m.product
for m in self.context['request'].event.seat_category_mappings.select_related('product').filter(subevent=subevent)
})
return subevent
class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):

View File

@@ -7,23 +7,36 @@ from rest_framework import serializers
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import (
Item, ItemAddOn, ItemCategory, ItemVariation, Question, QuestionOption,
Quota,
Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation, Question,
QuestionOption, Quota,
)
class InlineItemVariationSerializer(I18nAwareModelSerializer):
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=10,
coerce_to_string=True)
class Meta:
model = ItemVariation
fields = ('id', 'value', 'active', 'description',
'position', 'default_price', 'price')
'position', 'default_price', 'price', 'original_price')
class ItemVariationSerializer(I18nAwareModelSerializer):
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=10,
coerce_to_string=True)
class Meta:
model = ItemVariation
fields = ('id', 'value', 'active', 'description',
'position', 'default_price', 'price')
'position', 'default_price', 'price', 'original_price')
class InlineItemBundleSerializer(serializers.ModelSerializer):
class Meta:
model = ItemBundle
fields = ('bundled_item', 'bundled_variation', 'count',
'designated_price')
class InlineItemAddOnSerializer(serializers.ModelSerializer):
@@ -33,6 +46,31 @@ class InlineItemAddOnSerializer(serializers.ModelSerializer):
'position', 'price_included')
class ItemBundleSerializer(serializers.ModelSerializer):
class Meta:
model = ItemBundle
fields = ('id', 'bundled_item', 'bundled_variation', 'count',
'designated_price')
def validate(self, data):
data = super().validate(data)
event = self.context['event']
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
full_data.update(data)
ItemBundle.clean_itemvar(event, full_data.get('bundled_item'), full_data.get('bundled_variation'))
item = self.context['item']
if item == full_data.get('bundled_item'):
raise ValidationError(_("The bundled item must not be the same item as the bundling one."))
if full_data.get('bundled_item'):
if full_data['bundled_item'].bundles.exists():
raise ValidationError(_("The bundled item must not have bundles on its own."))
return data
class ItemAddOnSerializer(serializers.ModelSerializer):
class Meta:
model = ItemAddOn
@@ -69,6 +107,7 @@ class ItemTaxRateField(serializers.Field):
class ItemSerializer(I18nAwareModelSerializer):
addons = InlineItemAddOnSerializer(many=True, required=False)
bundles = InlineItemBundleSerializer(many=True, required=False)
variations = InlineItemVariationSerializer(many=True, required=False)
tax_rate = ItemTaxRateField(source='*', read_only=True)
@@ -77,9 +116,10 @@ class ItemSerializer(I18nAwareModelSerializer):
fields = ('id', 'category', 'name', 'internal_name', 'active', 'sales_channels', 'description',
'default_price', 'free_price', 'tax_rate', 'tax_rule', 'admission',
'position', 'picture', 'available_from', 'available_until',
'require_voucher', 'hide_without_voucher', 'allow_cancel',
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations',
'variations', 'addons', 'original_price', 'require_approval', 'generate_tickets')
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
'show_quota_left', 'hidden_if_available', 'allow_waitinglist')
read_only_fields = ('has_variations', 'picture')
def get_serializer_context(self):
@@ -87,8 +127,8 @@ 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 '
if self.instance and ('addons' in data or 'variations' in data or 'bundles' in data):
raise ValidationError(_('Updating add-ons, bundles, 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'))
@@ -104,6 +144,12 @@ class ItemSerializer(I18nAwareModelSerializer):
Item.clean_tax_rule(value, self.context['event'])
return value
def validate_bundles(self, value):
if not self.instance:
for b_data in value:
ItemBundle.clean_itemvar(self.context['event'], b_data['bundled_item'], b_data['bundled_variation'])
return value
def validate_addons(self, value):
if not self.instance:
for addon_data in value:
@@ -117,11 +163,14 @@ class ItemSerializer(I18nAwareModelSerializer):
def create(self, validated_data):
variations_data = validated_data.pop('variations') if 'variations' in validated_data else {}
addons_data = validated_data.pop('addons') if 'addons' in validated_data else {}
bundles_data = validated_data.pop('bundles') if 'bundles' in validated_data else {}
item = Item.objects.create(**validated_data)
for variation_data in variations_data:
ItemVariation.objects.create(item=item, **variation_data)
for addon_data in addons_data:
ItemAddOn.objects.create(base_item=item, **addon_data)
for bundle_data in bundles_data:
ItemBundle.objects.create(base_item=item, **bundle_data)
return item
@@ -152,19 +201,38 @@ class InlineQuestionOptionSerializer(I18nAwareModelSerializer):
fields = ('id', 'identifier', 'answer', 'position')
class LegacyDependencyValueField(serializers.CharField):
def to_representation(self, obj):
return obj[0] if obj else None
def to_internal_value(self, data):
return [data] if data else []
class QuestionSerializer(I18nAwareModelSerializer):
options = InlineQuestionOptionSerializer(many=True, required=False)
identifier = serializers.CharField(allow_null=True)
dependency_value = LegacyDependencyValueField(source='dependency_values', required=False, allow_null=True)
class Meta:
model = Question
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
'ask_during_checkin', 'identifier')
'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
'hidden', 'dependency_value', 'print_on_invoice')
def validate_identifier(self, value):
Question._clean_identifier(self.context['event'], value, self.instance)
return value
def validate_dependency_question(self, value):
if value:
if value.type not in (Question.TYPE_CHOICE, Question.TYPE_BOOLEAN, Question.TYPE_CHOICE_MULTIPLE):
raise ValidationError('Question dependencies can only be set to boolean or choice questions.')
if value == self.instance:
raise ValidationError('A question cannot depend on itself.')
return value
def validate(self, data):
data = super().validate(data)
if self.instance and 'options' in data:
@@ -176,6 +244,18 @@ class QuestionSerializer(I18nAwareModelSerializer):
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
full_data.update(data)
if full_data.get('ask_during_checkin') and full_data.get('dependency_question'):
raise ValidationError('Dependencies are not supported during check-in.')
dep = full_data.get('dependency_question')
if dep:
seen_ids = {self.instance.pk} if self.instance else set()
while dep:
if dep.pk in seen_ids:
raise ValidationError(_('Circular dependency between questions detected.'))
seen_ids.add(dep.pk)
dep = dep.dependency_question
Question.clean_items(event, full_data.get('items'))
return data
@@ -193,6 +273,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
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:
@@ -204,7 +285,7 @@ class QuotaSerializer(I18nAwareModelSerializer):
class Meta:
model = Quota
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent')
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent', 'closed', 'close_when_sold_out')
def validate(self, data):
data = super().validate(data)

View File

@@ -2,6 +2,8 @@ import json
from collections import Counter
from decimal import Decimal
import pycountry
from django.db.models import F, Q
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy
from django_countries.fields import Country
@@ -14,14 +16,18 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.channels import get_all_sales_channels
from pretix.base.i18n import language
from pretix.base.models import (
Checkin, Invoice, InvoiceAddress, InvoiceLine, Order, OrderPosition,
Question, QuestionAnswer,
Checkin, Invoice, InvoiceAddress, InvoiceLine, Item, ItemVariation, Order,
OrderPosition, Question, QuestionAnswer, Seat, SubEvent, Voucher,
)
from pretix.base.models.orders import (
CartPosition, OrderFee, OrderPayment, OrderRefund,
)
from pretix.base.pdf import get_variables
from pretix.base.services.cart import error_messages
from pretix.base.services.pricing import get_price
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
from pretix.base.signals import register_ticket_outputs
from pretix.multidomain.urlreverse import build_absolute_uri
class CompatibleCountryField(serializers.Field):
@@ -42,8 +48,8 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
class Meta:
model = InvoiceAddress
fields = ('last_modified', 'is_business', 'company', 'name', 'name_parts', 'street', 'zipcode', 'city', 'country',
'vat_id', 'vat_id_validated', 'internal_reference')
read_only_fields = ('last_modified', 'vat_id_validated')
'state', 'vat_id', 'vat_id_validated', 'internal_reference')
read_only_fields = ('last_modified',)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -58,6 +64,24 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
)
if data.get('name_parts') and '_scheme' not in data.get('name_parts'):
data['name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
if data.get('country'):
if not pycountry.countries.get(alpha_2=data.get('country')):
raise ValidationError(
{'country': ['Invalid country code.']}
)
if data.get('state'):
cc = str(data.get('country') or self.instance.country or '')
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
raise ValidationError(
{'state': ['States are not supported in country "{}".'.format(cc)]}
)
if not pycountry.subdivisions.get(code=cc + '-' + data.get('state')):
raise ValidationError(
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
)
return data
@@ -71,6 +95,13 @@ class AnswerQuestionOptionsIdentifierField(serializers.Field):
return [o.identifier for o in instance.options.all()]
class InlineSeatSerializer(I18nAwareModelSerializer):
class Meta:
model = Seat
fields = ('id', 'name', 'seat_guid')
class AnswerSerializer(I18nAwareModelSerializer):
question_identifier = AnswerQuestionIdentifierField(source='*', read_only=True)
option_identifiers = AnswerQuestionOptionsIdentifierField(source='*', read_only=True)
@@ -83,7 +114,7 @@ class AnswerSerializer(I18nAwareModelSerializer):
class CheckinSerializer(I18nAwareModelSerializer):
class Meta:
model = Checkin
fields = ('datetime', 'list')
fields = ('datetime', 'list', 'auto_checked_in')
class OrderDownloadsField(serializers.Field):
@@ -166,12 +197,13 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
downloads = PositionDownloadsField(source='*')
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
pdf_data = PdfDataSerializer(source='*')
seat = InlineSeatSerializer(read_only=True)
class Meta:
model = OrderPosition
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data')
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -179,6 +211,55 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
self.fields.pop('pdf_data')
class RequireAttentionField(serializers.Field):
def to_representation(self, instance: OrderPosition):
return instance.order.checkin_attention or instance.item.checkin_attention
class AttendeeNameField(serializers.Field):
def to_representation(self, instance: OrderPosition):
an = instance.attendee_name
if not an:
if instance.addon_to_id:
an = instance.addon_to.attendee_name
if not an:
try:
an = instance.order.invoice_address.name
except InvoiceAddress.DoesNotExist:
pass
return an
class AttendeeNamePartsField(serializers.Field):
def to_representation(self, instance: OrderPosition):
an = instance.attendee_name
p = instance.attendee_name_parts
if not an:
if instance.addon_to_id:
an = instance.addon_to.attendee_name
p = instance.addon_to.attendee_name_parts
if not an:
try:
p = instance.order.invoice_address.name_parts
except InvoiceAddress.DoesNotExist:
pass
return p
class CheckinListOrderPositionSerializer(OrderPositionSerializer):
require_attention = RequireAttentionField(source='*')
attendee_name = AttendeeNameField(source='*')
attendee_name_parts = AttendeeNamePartsField(source='*')
order__status = serializers.SlugRelatedField(read_only=True, slug_field='status', source='order')
class Meta:
model = OrderPosition
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'require_attention',
'order__status')
class OrderPaymentTypeField(serializers.Field):
# TODO: Remove after pretix 2.2
def to_representation(self, instance: Order):
@@ -205,10 +286,33 @@ class OrderFeeSerializer(I18nAwareModelSerializer):
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule')
class PaymentURLField(serializers.URLField):
def to_representation(self, instance: OrderPayment):
if instance.state != OrderPayment.PAYMENT_STATE_CREATED:
return None
return build_absolute_uri(self.context['event'], 'presale:event.order.pay', kwargs={
'order': instance.order.code,
'secret': instance.order.secret,
'payment': instance.pk,
})
class PaymentDetailsField(serializers.Field):
def to_representation(self, value: OrderPayment):
pp = value.payment_provider
if not pp:
return {}
return pp.api_payment_details(value)
class OrderPaymentSerializer(I18nAwareModelSerializer):
payment_url = PaymentURLField(source='*', allow_null=True, read_only=True)
details = PaymentDetailsField(source='*', allow_null=True, read_only=True)
class Meta:
model = OrderPayment
fields = ('local_id', 'state', 'amount', 'created', 'payment_date', 'provider')
fields = ('local_id', 'state', 'amount', 'created', 'payment_date', 'provider', 'payment_url',
'details')
class OrderRefundSerializer(I18nAwareModelSerializer):
@@ -219,27 +323,100 @@ class OrderRefundSerializer(I18nAwareModelSerializer):
fields = ('local_id', 'state', 'source', 'amount', 'payment', 'created', 'execution_date', 'provider')
class OrderURLField(serializers.URLField):
def to_representation(self, instance: Order):
return build_absolute_uri(self.context['event'], 'presale:event.order', kwargs={
'order': instance.code,
'secret': instance.secret,
})
class OrderSerializer(I18nAwareModelSerializer):
invoice_address = InvoiceAddressSerializer()
positions = OrderPositionSerializer(many=True)
fees = OrderFeeSerializer(many=True)
downloads = OrderDownloadsField(source='*')
payments = OrderPaymentSerializer(many=True)
refunds = OrderRefundSerializer(many=True)
payment_date = OrderPaymentDateField(source='*')
payment_provider = OrderPaymentTypeField(source='*')
invoice_address = InvoiceAddressSerializer(allow_null=True)
positions = OrderPositionSerializer(many=True, read_only=True)
fees = OrderFeeSerializer(many=True, read_only=True)
downloads = OrderDownloadsField(source='*', read_only=True)
payments = OrderPaymentSerializer(many=True, read_only=True)
refunds = OrderRefundSerializer(many=True, read_only=True)
payment_date = OrderPaymentDateField(source='*', read_only=True)
payment_provider = OrderPaymentTypeField(source='*', read_only=True)
url = OrderURLField(source='*', read_only=True)
class Meta:
model = Order
fields = ('code', 'status', 'testmode', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel')
fields = (
'code', 'status', 'testmode', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
'url'
)
read_only_fields = (
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
'payment_provider', 'fees', 'total', 'positions', 'downloads',
'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel'
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.context['request'].query_params.get('pdf_data', 'false') == 'true':
self.fields['positions'].child.fields.pop('pdf_data')
def validate_locale(self, l):
if l not in set(k for k in self.instance.event.settings.locales):
raise ValidationError('"{}" is not a supported locale for this event.'.format(l))
return l
def update(self, instance, validated_data):
# Even though all fields that shouldn't be edited are marked as read_only in the serializer
# (hopefully), we'll be extra careful here and be explicit about the model fields we update.
update_fields = ['comment', 'checkin_attention', 'email', 'locale']
if 'invoice_address' in validated_data:
iadata = validated_data.pop('invoice_address')
if not iadata:
try:
instance.invoice_address.delete()
except InvoiceAddress.DoesNotExist:
pass
else:
name = iadata.pop('name', '')
if name and not iadata.get('name_parts'):
iadata['name_parts'] = {
'_legacy': name
}
try:
ia = instance.invoice_address
if iadata.get('vat_id') != ia.vat_id and 'vat_id_validated' not in iadata:
ia.vat_id_validated = False
self.fields['invoice_address'].update(ia, iadata)
except InvoiceAddress.DoesNotExist:
InvoiceAddress.objects.create(order=instance, **iadata)
for attr, value in validated_data.items():
if attr in update_fields:
setattr(instance, attr, value)
instance.save(update_fields=update_fields)
return instance
class PriceCalcSerializer(serializers.Serializer):
item = serializers.PrimaryKeyRelatedField(queryset=Item.objects.none(), required=False, allow_null=True)
variation = serializers.PrimaryKeyRelatedField(queryset=ItemVariation.objects.none(), required=False, allow_null=True)
subevent = serializers.PrimaryKeyRelatedField(queryset=SubEvent.objects.none(), required=False, allow_null=True)
locale = serializers.CharField(allow_null=True, required=False)
def __init__(self, *args, **kwargs):
event = kwargs.pop('event')
super().__init__(*args, **kwargs)
self.fields['item'].queryset = event.items.all()
self.fields['variation'].queryset = ItemVariation.objects.filter(item__event=event)
if event.has_subevents:
self.fields['subevent'].queryset = event.subevents.all()
else:
del self.fields['subevent']
class AnswerCreateSerializer(I18nAwareModelSerializer):
@@ -317,11 +494,16 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
addon_to = serializers.IntegerField(required=False, allow_null=True)
secret = serializers.CharField(required=False)
attendee_name = serializers.CharField(required=False, allow_null=True)
seat = serializers.CharField(required=False, allow_null=True)
price = serializers.DecimalField(required=False, allow_null=True, decimal_places=2,
max_digits=10)
voucher = serializers.SlugRelatedField(slug_field='code', queryset=Voucher.objects.none(),
required=False, allow_null=True)
class Meta:
model = OrderPosition
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
'secret', 'addon_to', 'subevent', 'answers')
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher')
def validate_secret(self, secret):
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
@@ -395,7 +577,7 @@ class CompatibleJSONField(serializers.JSONField):
class OrderCreateSerializer(I18nAwareModelSerializer):
invoice_address = InvoiceAddressSerializer(required=False)
positions = OrderPositionCreateSerializer(many=True, required=False)
positions = OrderPositionCreateSerializer(many=True, required=True)
fees = OrderFeeCreateSerializer(many=True, required=False)
status = serializers.ChoiceField(choices=(
('n', Order.STATUS_PENDING),
@@ -407,16 +589,26 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
min_length=5
)
comment = serializers.CharField(required=False, allow_blank=True)
payment_provider = serializers.CharField(required=True)
payment_provider = serializers.CharField(required=False, allow_null=True)
payment_info = CompatibleJSONField(required=False)
consume_carts = serializers.ListField(child=serializers.CharField(), required=False)
force = serializers.BooleanField(default=False, required=False)
payment_date = serializers.DateTimeField(required=False, allow_null=True)
send_mail = serializers.BooleanField(default=False, required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['positions'].child.fields['voucher'].queryset = self.context['event'].vouchers.all()
class Meta:
model = Order
fields = ('code', 'status', 'testmode', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'consume_carts')
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts',
'force', 'send_mail')
def validate_payment_provider(self, pp):
if pp is None:
return None
if pp not in self.context['event'].get_payment_providers():
raise ValidationError('The given payment provider is not known.')
return pp
@@ -475,6 +667,9 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
{'positionid': ["If you set addon_to on any position, you need to specify position IDs manually."]}
for p in data
]
else:
for i, p in enumerate(data):
p['positionid'] = i + 1
if any(errs):
raise ValidationError(errs)
@@ -483,8 +678,11 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
def create(self, validated_data):
fees_data = validated_data.pop('fees') if 'fees' in validated_data else []
positions_data = validated_data.pop('positions') if 'positions' in validated_data else []
payment_provider = validated_data.pop('payment_provider')
payment_provider = validated_data.pop('payment_provider', None)
payment_info = validated_data.pop('payment_info', '{}')
payment_date = validated_data.pop('payment_date', now())
force = validated_data.pop('force', False)
self._send_mail = validated_data.pop('send_mail', False)
if 'invoice_address' in validated_data:
iadata = validated_data.pop('invoice_address')
@@ -498,13 +696,16 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
ia = None
with self.context['event'].lock() as now_dt:
quotadiff = Counter()
free_seats = set()
seats_seen = set()
consume_carts = validated_data.pop('consume_carts', [])
delete_cps = []
quota_avail_cache = {}
voucher_usage = Counter()
if consume_carts:
for cp in CartPosition.objects.filter(event=self.context['event'], cart_id__in=consume_carts):
for cp in CartPosition.objects.filter(
event=self.context['event'], cart_id__in=consume_carts, expires__gt=now()
):
quotas = (cp.variation.quotas.filter(subevent=cp.subevent)
if cp.variation else cp.item.quotas.filter(subevent=cp.subevent))
for quota in quotas:
@@ -512,35 +713,84 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
quota_avail_cache[quota] = list(quota.availability())
if quota_avail_cache[quota][1] is not None:
quota_avail_cache[quota][1] += 1
if cp.voucher:
voucher_usage[cp.voucher] -= 1
if cp.expires > now_dt:
quotadiff.subtract(quotas)
if cp.seat:
free_seats.add(cp.seat)
delete_cps.append(cp)
errs = [{} for p in positions_data]
for i, pos_data in enumerate(positions_data):
new_quotas = (pos_data.get('variation').quotas.filter(subevent=pos_data.get('subevent'))
if pos_data.get('variation')
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
if len(new_quotas) == 0:
errs[i]['item'] = [ugettext_lazy('The product "{}" is not assigned to a quota.').format(
str(pos_data.get('item'))
)]
else:
for quota in new_quotas:
if quota not in quota_avail_cache:
quota_avail_cache[quota] = list(quota.availability())
if pos_data.get('voucher'):
v = pos_data['voucher']
if quota_avail_cache[quota][1] is not None:
quota_avail_cache[quota][1] -= 1
if quota_avail_cache[quota][1] < 0:
errs[i]['item'] = [
ugettext_lazy('There is not enough quota available on quota "{}" to perform the operation.').format(
quota.name
)
]
if not v.applies_to(pos_data['item'], pos_data.get('variation')):
errs[i]['voucher'] = [error_messages['voucher_invalid_item']]
continue
quotadiff.update(new_quotas)
if v.subevent_id and pos_data.get('subevent').pk != v.subevent_id:
errs[i]['voucher'] = [error_messages['voucher_invalid_subevent']]
continue
if v.valid_until is not None and v.valid_until < now_dt:
errs[i]['voucher'] = [error_messages['voucher_expired']]
continue
voucher_usage[v] += 1
if voucher_usage[v] > 0:
redeemed_in_carts = CartPosition.objects.filter(
Q(voucher=pos_data['voucher']) & Q(event=self.context['event']) & Q(expires__gte=now_dt)
).exclude(pk__in=[cp.pk for cp in delete_cps])
v_avail = v.max_usages - v.redeemed - redeemed_in_carts.count()
if v_avail < voucher_usage[v]:
errs[i]['voucher'] = [
'The voucher has already been used the maximum number of times.'
]
seated = pos_data.get('item').seat_category_mappings.filter(subevent=pos_data.get('subevent')).exists()
if pos_data.get('seat'):
if not seated:
errs[i]['seat'] = ['The specified product does not allow to choose a seat.']
try:
seat = self.context['event'].seats.get(seat_guid=pos_data['seat'], subevent=pos_data.get('subevent'))
except Seat.DoesNotExist:
errs[i]['seat'] = ['The specified seat does not exist.']
else:
pos_data['seat'] = seat
if (seat not in free_seats and not seat.is_available()) or seat in seats_seen:
errs[i]['seat'] = [ugettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
seats_seen.add(seat)
elif seated:
errs[i]['seat'] = ['The specified product requires to choose a seat.']
if not force:
for i, pos_data in enumerate(positions_data):
if pos_data.get('voucher'):
if pos_data['voucher'].allow_ignore_quota or pos_data['voucher'].block_quota:
continue
new_quotas = (pos_data.get('variation').quotas.filter(subevent=pos_data.get('subevent'))
if pos_data.get('variation')
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
if len(new_quotas) == 0:
errs[i]['item'] = [ugettext_lazy('The product "{}" is not assigned to a quota.').format(
str(pos_data.get('item'))
)]
else:
for quota in new_quotas:
if quota not in quota_avail_cache:
quota_avail_cache[quota] = list(quota.availability())
if quota_avail_cache[quota][1] is not None:
quota_avail_cache[quota][1] -= 1
if quota_avail_cache[quota][1] < 0:
errs[i]['item'] = [
ugettext_lazy('There is not enough quota available on quota "{}" to perform the operation.').format(
quota.name
)
]
if any(errs):
raise ValidationError({'positions': errs})
@@ -549,38 +799,14 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
validated_data['locale'] = self.context['event'].settings.locale
order = Order(event=self.context['event'], **validated_data)
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
order.total = sum([p['price'] for p in positions_data]) + sum([f['value'] for f in fees_data], Decimal('0.00'))
order.meta_info = "{}"
order.total = Decimal('0.00')
order.save()
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID:
order.status = Order.STATUS_PAID
order.save()
order.payments.create(
amount=order.total, provider='free', state=OrderPayment.PAYMENT_STATE_CONFIRMED,
payment_date=now()
)
elif payment_provider == "free" and order.total != Decimal('0.00'):
raise ValidationError('You cannot use the "free" payment provider for non-free orders.')
elif validated_data.get('status') == Order.STATUS_PAID:
order.payments.create(
amount=order.total,
provider=payment_provider,
info=payment_info,
payment_date=now(),
state=OrderPayment.PAYMENT_STATE_CONFIRMED
)
elif payment_provider:
order.payments.create(
amount=order.total,
provider=payment_provider,
info=payment_info,
state=OrderPayment.PAYMENT_STATE_CREATED
)
if ia:
ia.order = order
ia.save()
pos_map = {}
for pos_data in positions_data:
answers_data = pos_data.pop('answers', [])
@@ -592,9 +818,27 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
}
pos = OrderPosition(**pos_data)
pos.order = order
pos._calculate_tax()
if addon_to:
pos.addon_to = pos_map[addon_to]
if pos.price is None:
price = get_price(
item=pos.item,
variation=pos.variation,
voucher=pos.voucher,
custom_price=None,
subevent=pos.subevent,
addon_to=pos.addon_to,
invoice_address=ia,
)
pos.price = price.gross
pos.tax_rate = price.rate
pos.tax_value = price.tax
pos.tax_rule = pos.item.tax_rule
else:
pos._calculate_tax()
if pos.voucher:
Voucher.objects.filter(pk=pos.voucher.pk).update(redeemed=F('redeemed') + 1)
pos.save()
pos_map[pos.positionid] = pos
for answ_data in answers_data:
@@ -604,12 +848,43 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
for cp in delete_cps:
cp.delete()
for fee_data in fees_data:
f = OrderFee(**fee_data)
f.order = order
f._calculate_tax()
f.save()
order.total = sum([p.price for p in order.positions.all()]) + sum([f.value for f in order.fees.all()])
order.save(update_fields=['total'])
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID:
order.status = Order.STATUS_PAID
order.save()
order.payments.create(
amount=order.total, provider='free', state=OrderPayment.PAYMENT_STATE_CONFIRMED,
payment_date=now()
)
elif payment_provider == "free" and order.total != Decimal('0.00'):
raise ValidationError('You cannot use the "free" payment provider for non-free orders.')
elif validated_data.get('status') == Order.STATUS_PAID:
if not payment_provider:
raise ValidationError('You cannot create a paid order without a payment provider.')
order.payments.create(
amount=order.total,
provider=payment_provider,
info=payment_info,
payment_date=payment_date,
state=OrderPayment.PAYMENT_STATE_CONFIRMED
)
elif payment_provider:
order.payments.create(
amount=order.total,
provider=payment_provider,
info=payment_info,
state=OrderPayment.PAYMENT_STATE_CREATED
)
return order

View File

@@ -1,8 +1,20 @@
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import Organizer
from pretix.api.serializers.order import CompatibleJSONField
from pretix.base.models import Organizer, SeatingPlan
from pretix.base.models.seating import SeatingPlanLayoutValidator
class OrganizerSerializer(I18nAwareModelSerializer):
class Meta:
model = Organizer
fields = ('name', 'slug')
class SeatingPlanSerializer(I18nAwareModelSerializer):
layout = CompatibleJSONField(
validators=[SeatingPlanLayoutValidator()]
)
class Meta:
model = SeatingPlan
fields = ('id', 'name', 'layout')

View File

@@ -27,7 +27,7 @@ class VoucherSerializer(I18nAwareModelSerializer):
model = Voucher
fields = ('id', 'code', 'max_usages', 'redeemed', 'valid_until', 'block_quota',
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
'tag', 'comment', 'subevent')
'tag', 'comment', 'subevent', 'show_hidden_items')
read_only_fields = ('id', 'redeemed')
list_serializer_class = VoucherListSerializer

View File

@@ -2,8 +2,9 @@ from datetime import timedelta
from django.dispatch import Signal, receiver
from django.utils.timezone import now
from django_scopes import scopes_disabled
from pretix.api.models import WebHookCall
from pretix.api.models import ApiCall, WebHookCall
from pretix.base.signals import periodic_task
register_webhook_events = Signal(
@@ -17,5 +18,12 @@ instances.
@receiver(periodic_task)
@scopes_disabled()
def cleanup_webhook_logs(sender, **kwargs):
WebHookCall.objects.filter(datetime__lte=now() - timedelta(days=30)).delete()
@receiver(periodic_task)
@scopes_disabled()
def cleanup_api_logs(sender, **kwargs):
ApiCall.objects.filter(created__lte=now() - timedelta(hours=24)).delete()

View File

@@ -18,6 +18,7 @@ orga_router = routers.DefaultRouter()
orga_router.register(r'events', event.EventViewSet)
orga_router.register(r'subevents', event.SubEventViewSet)
orga_router.register(r'webhooks', webhooks.WebHookViewSet)
orga_router.register(r'seatingplans', organizer.SeatingPlanViewSet)
event_router = routers.DefaultRouter()
event_router.register(r'subevents', event.SubEventViewSet)
@@ -44,6 +45,7 @@ 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)
item_router.register(r'bundles', item.ItemBundleViewSet)
order_router = routers.DefaultRouter()
order_router.register(r'payments', order.PaymentViewSet)

View File

@@ -31,10 +31,10 @@ class RichOrderingFilter(OrderingFilter):
class ConditionalListView:
def list(self, request, **kwargs):
if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
if_modified_since = request.headers.get('If-Modified-Since')
if if_modified_since:
if_modified_since = parse_http_date_safe(if_modified_since)
if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
if_unmodified_since = request.headers.get('If-Unmodified-Since')
if if_unmodified_since:
if_unmodified_since = parse_http_date_safe(if_unmodified_since)
if not hasattr(request, 'event'):

View File

@@ -24,7 +24,7 @@ class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
return CartPosition.objects.filter(
event=self.request.event,
cart_id__endswith="@api"
)
).select_related('seat').prefetch_related('answers')
def get_serializer_context(self):
ctx = super().get_serializer_context()

View File

@@ -6,14 +6,15 @@ from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property
from django.utils.timezone import now
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import viewsets
from rest_framework.decorators import detail_route
from rest_framework.decorators import action
from rest_framework.fields import DateTimeField
from rest_framework.response import Response
from pretix.api.serializers.checkin import CheckinListSerializer
from pretix.api.serializers.item import QuestionSerializer
from pretix.api.serializers.order import OrderPositionSerializer
from pretix.api.serializers.order import CheckinListOrderPositionSerializer
from pretix.api.views import RichOrderingFilter
from pretix.api.views.order import OrderPositionFilter
from pretix.base.models import (
@@ -24,11 +25,11 @@ from pretix.base.services.checkin import (
)
from pretix.helpers.database import FixedOrderBy
class CheckinListFilter(FilterSet):
class Meta:
model = CheckinList
fields = ['subevent']
with scopes_disabled():
class CheckinListFilter(FilterSet):
class Meta:
model = CheckinList
fields = ['subevent']
class CheckinListViewSet(viewsets.ModelViewSet):
@@ -43,7 +44,6 @@ class CheckinListViewSet(viewsets.ModelViewSet):
qs = self.request.event.checkin_lists.prefetch_related(
'limit_products',
)
qs = CheckinList.annotate_with_numbers(qs, self.request.event)
return qs
def perform_create(self, serializer):
@@ -77,7 +77,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
)
super().perform_destroy(instance)
@detail_route(methods=['GET'])
@action(detail=True, methods=['GET'])
def status(self, *args, **kwargs):
clist = self.get_object()
cqs = Checkin.objects.filter(
@@ -92,6 +92,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
)
if not clist.all_products:
pqs = pqs.filter(item__in=clist.limit_products.values_list('id', flat=True))
cqs = cqs.filter(position__item__in=clist.limit_products.values_list('id', flat=True))
ev = clist.subevent or clist.event
response = {
@@ -146,15 +147,16 @@ class CheckinListViewSet(viewsets.ModelViewSet):
return Response(response)
class CheckinOrderPositionFilter(OrderPositionFilter):
with scopes_disabled():
class CheckinOrderPositionFilter(OrderPositionFilter):
def has_checkin_qs(self, queryset, name, value):
return queryset.filter(last_checked_in__isnull=not value)
def has_checkin_qs(self, queryset, name, value):
return queryset.filter(last_checked_in__isnull=not value)
class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = OrderPositionSerializer
queryset = OrderPosition.objects.none()
serializer_class = CheckinListOrderPositionSerializer
queryset = OrderPosition.all.none()
filter_backends = (DjangoFilterBackend, RichOrderingFilter)
ordering = ('attendee_name_cached', 'positionid')
ordering_fields = (
@@ -189,7 +191,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
except ValueError:
raise Http404()
def get_queryset(self):
def get_queryset(self, ignore_status=False):
cqs = Checkin.objects.filter(
position_id=OuterRef('pk'),
list_id=self.checkinlist.pk
@@ -199,11 +201,15 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
qs = OrderPosition.objects.filter(
order__event=self.request.event,
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)
)
if self.request.query_params.get('ignore_status', 'false') != 'true' and not ignore_status:
qs = qs.filter(
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.checkinlist.include_pending else [Order.STATUS_PAID]
)
if self.request.query_params.get('pdf_data', 'false') == 'true':
qs = qs.prefetch_related(
Prefetch(
@@ -225,7 +231,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
)
))
).select_related(
'item', 'variation', 'item__category', 'addon_to'
'item', 'variation', 'item__category', 'addon_to', 'order', 'order__invoice_address', 'seat'
)
else:
qs = qs.prefetch_related(
@@ -235,19 +241,19 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
),
'answers', 'answers__options', 'answers__question',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address')
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat')
if not self.checkinlist.all_products:
qs = qs.filter(item__in=self.checkinlist.limit_products.values_list('id', flat=True))
return qs
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def redeem(self, *args, **kwargs):
force = bool(self.request.data.get('force', False))
ignore_unpaid = bool(self.request.data.get('ignore_unpaid', False))
nonce = self.request.data.get('nonce')
op = self.get_object()
op = self.get_object(ignore_status=True)
if 'datetime' in self.request.data:
dt = DateTimeField().to_internal_value(self.request.data.get('datetime'))
@@ -274,6 +280,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
nonce=nonce,
datetime=dt,
questions_supported=self.request.data.get('questions_supported', True),
canceled_supported=self.request.data.get('canceled_supported', False),
user=self.request.user,
auth=self.request.auth,
)
@@ -281,7 +288,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
return Response({
'status': 'incomplete',
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': OrderPositionSerializer(op, context=self.get_serializer_context()).data,
'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data,
'questions': [
QuestionSerializer(q).data for q in e.questions
]
@@ -291,17 +298,17 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
'status': 'error',
'reason': e.code,
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': OrderPositionSerializer(op, context=self.get_serializer_context()).data
'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data
}, status=400)
else:
return Response({
'status': 'ok',
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': OrderPositionSerializer(op, context=self.get_serializer_context()).data
'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data
}, status=201)
def get_object(self):
queryset = self.filter_queryset(self.get_queryset())
def get_object(self, ignore_status=False):
queryset = self.filter_queryset(self.get_queryset(ignore_status=ignore_status))
if self.kwargs['pk'].isnumeric():
obj = get_object_or_404(queryset, Q(pk=self.kwargs['pk']) | Q(secret=self.kwargs['pk']))
else:

View File

@@ -105,7 +105,7 @@ class RevokeKeyView(APIView):
def post(self, request, format=None):
device = request.auth
device.api_token = None
device.revoked = True
device.save()
device.log_action('pretix.device.revoked', auth=device)

View File

@@ -3,6 +3,7 @@ from django.db import transaction
from django.db.models import ProtectedError, Q
from django.utils.timezone import now
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import filters, viewsets
from rest_framework.exceptions import PermissionDenied
@@ -13,56 +14,56 @@ from pretix.api.serializers.event import (
)
from pretix.api.views import ConditionalListView
from pretix.base.models import (
Device, Event, ItemCategory, TaxRule, TeamAPIToken,
CartPosition, Device, Event, ItemCategory, TaxRule, TeamAPIToken,
)
from pretix.base.models.event import SubEvent
from pretix.helpers.dicts import merge_dicts
with scopes_disabled():
class EventFilter(FilterSet):
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
class EventFilter(FilterSet):
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
class Meta:
model = Event
fields = ['is_public', 'live', 'has_subevents']
class Meta:
model = Event
fields = ['is_public', 'live', 'has_subevents']
def ends_after_qs(self, queryset, name, value):
expr = (
Q(has_subevents=False) &
Q(
Q(Q(date_to__isnull=True) & Q(date_from__gte=value))
| Q(Q(date_to__isnull=False) & Q(date_to__gte=value))
def ends_after_qs(self, queryset, name, value):
expr = (
Q(has_subevents=False) &
Q(
Q(Q(date_to__isnull=True) & Q(date_from__gte=value))
| Q(Q(date_to__isnull=False) & Q(date_to__gte=value))
)
)
)
return queryset.filter(expr)
def is_past_qs(self, queryset, name, value):
expr = (
Q(has_subevents=False) &
Q(
Q(Q(date_to__isnull=True) & Q(date_from__lt=now()))
| Q(Q(date_to__isnull=False) & Q(date_to__lt=now()))
)
)
if value:
return queryset.filter(expr)
else:
return queryset.exclude(expr)
def is_future_qs(self, queryset, name, value):
expr = (
Q(has_subevents=False) &
Q(
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
| Q(Q(date_to__isnull=False) & Q(date_to__gte=now()))
def is_past_qs(self, queryset, name, value):
expr = (
Q(has_subevents=False) &
Q(
Q(Q(date_to__isnull=True) & Q(date_from__lt=now()))
| Q(Q(date_to__isnull=False) & Q(date_to__lt=now()))
)
)
)
if value:
return queryset.filter(expr)
else:
return queryset.exclude(expr)
if value:
return queryset.filter(expr)
else:
return queryset.exclude(expr)
def is_future_qs(self, queryset, name, value):
expr = (
Q(has_subevents=False) &
Q(
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
| Q(Q(date_to__isnull=False) & Q(date_to__gte=now()))
)
)
if value:
return queryset.filter(expr)
else:
return queryset.exclude(expr)
class EventViewSet(viewsets.ModelViewSet):
@@ -72,6 +73,8 @@ class EventViewSet(viewsets.ModelViewSet):
lookup_url_kwarg = 'event'
permission_classes = (EventCRUDPermission,)
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
ordering = ('slug',)
ordering_fields = ('date_from', 'slug')
filterset_class = EventFilter
def get_queryset(self):
@@ -83,7 +86,7 @@ class EventViewSet(viewsets.ModelViewSet):
)
return qs.prefetch_related(
'meta_values', 'meta_values__property'
'meta_values', 'meta_values__property', 'seat_category_mappings'
)
def perform_update(self, serializer):
@@ -180,46 +183,48 @@ class CloneEventViewSet(viewsets.ModelViewSet):
)
class SubEventFilter(FilterSet):
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
with scopes_disabled():
class SubEventFilter(FilterSet):
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
class Meta:
model = SubEvent
fields = ['active', 'event__live']
class Meta:
model = SubEvent
fields = ['active', 'event__live']
def ends_after_qs(self, queryset, name, value):
expr = Q(
Q(Q(date_to__isnull=True) & Q(date_from__gte=value))
| Q(Q(date_to__isnull=False) & Q(date_to__gte=value))
)
return queryset.filter(expr)
def is_past_qs(self, queryset, name, value):
expr = Q(
Q(Q(date_to__isnull=True) & Q(date_from__lt=now()))
| Q(Q(date_to__isnull=False) & Q(date_to__lt=now()))
)
if value:
def ends_after_qs(self, queryset, name, value):
expr = Q(
Q(Q(date_to__isnull=True) & Q(date_from__gte=value))
| Q(Q(date_to__isnull=False) & Q(date_to__gte=value))
)
return queryset.filter(expr)
else:
return queryset.exclude(expr)
def is_future_qs(self, queryset, name, value):
expr = Q(
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
| Q(Q(date_to__isnull=False) & Q(date_to__gte=now()))
)
if value:
return queryset.filter(expr)
else:
return queryset.exclude(expr)
def is_past_qs(self, queryset, name, value):
expr = Q(
Q(Q(date_to__isnull=True) & Q(date_from__lt=now()))
| Q(Q(date_to__isnull=False) & Q(date_to__lt=now()))
)
if value:
return queryset.filter(expr)
else:
return queryset.exclude(expr)
def is_future_qs(self, queryset, name, value):
expr = Q(
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
| Q(Q(date_to__isnull=False) & Q(date_to__gte=now()))
)
if value:
return queryset.filter(expr)
else:
return queryset.exclude(expr)
class SubEventViewSet(ConditionalListView, viewsets.ReadOnlyModelViewSet):
class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer_class = SubEventSerializer
queryset = ItemCategory.objects.none()
write_permission = 'can_change_event_settings'
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
filterset_class = SubEventFilter
@@ -237,9 +242,53 @@ class SubEventViewSet(ConditionalListView, viewsets.ReadOnlyModelViewSet):
event__in=self.request.user.get_events_with_any_permission()
)
return qs.prefetch_related(
'subeventitem_set', 'subeventitemvariation_set'
'subeventitem_set', 'subeventitemvariation_set', 'seat_category_mappings'
)
def perform_update(self, serializer):
original_data = self.get_serializer(instance=serializer.instance).data
super().perform_update(serializer)
if serializer.data == original_data:
# Performance optimization: If nothing was changed, we do not need to save or log anything.
# This costs us a few cycles on save, but avoids thousands of lines in our log.
return
serializer.instance.log_action(
'pretix.subevent.changed',
user=self.request.user,
auth=self.request.auth,
data=self.request.data
)
def perform_create(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
'pretix.subevent.added',
user=self.request.user,
auth=self.request.auth,
data=self.request.data
)
def perform_destroy(self, instance):
if not instance.allow_delete():
raise PermissionDenied('The sub-event can not be deleted as it has already been used in orders. Please set'
' \'active\' to false instead to hide it from users.')
try:
with transaction.atomic():
instance.log_action(
'pretix.subevent.deleted',
user=self.request.user,
auth=self.request.auth,
data=self.request.data
)
CartPosition.objects.filter(addon_to__subevent=instance).delete()
instance.cartposition_set.all().delete()
super().perform_destroy(instance)
except ProtectedError:
raise PermissionDenied('The sub-event could not be deleted as some constraints (e.g. data created by '
'plug-ins) do not allow it.')
class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer_class = TaxRuleSerializer

View File

@@ -1,38 +1,40 @@
import django_filters
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import viewsets
from rest_framework.decorators import detail_route
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.filters import OrderingFilter
from rest_framework.response import Response
from pretix.api.serializers.item import (
ItemAddOnSerializer, ItemCategorySerializer, ItemSerializer,
ItemVariationSerializer, QuestionOptionSerializer, QuestionSerializer,
QuotaSerializer,
ItemAddOnSerializer, ItemBundleSerializer, ItemCategorySerializer,
ItemSerializer, ItemVariationSerializer, QuestionOptionSerializer,
QuestionSerializer, QuotaSerializer,
)
from pretix.api.views import ConditionalListView
from pretix.base.models import (
Item, ItemAddOn, ItemCategory, ItemVariation, Question, QuestionOption,
Quota,
CartPosition, Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation,
Question, QuestionOption, Quota,
)
from pretix.helpers.dicts import merge_dicts
with scopes_disabled():
class ItemFilter(FilterSet):
tax_rate = django_filters.CharFilter(method='tax_rate_qs')
class ItemFilter(FilterSet):
tax_rate = django_filters.CharFilter(method='tax_rate_qs')
def tax_rate_qs(self, queryset, name, value):
if value in ("0", "None", "0.00"):
return queryset.filter(Q(tax_rule__isnull=True) | Q(tax_rule__rate=0))
else:
return queryset.filter(tax_rule__rate=value)
def tax_rate_qs(self, queryset, name, value):
if value in ("0", "None", "0.00"):
return queryset.filter(Q(tax_rule__isnull=True) | Q(tax_rule__rate=0))
else:
return queryset.filter(tax_rule__rate=value)
class Meta:
model = Item
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
class Meta:
model = Item
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
@@ -46,7 +48,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
write_permission = 'can_change_items'
def get_queryset(self):
return self.request.event.items.select_related('tax_rule').prefetch_related('variations', 'addons').all()
return self.request.event.items.select_related('tax_rule').prefetch_related('variations', 'addons', 'bundles').all()
def perform_create(self, serializer):
serializer.save(event=self.request.event)
@@ -64,7 +66,14 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
return ctx
def perform_update(self, serializer):
original_data = self.get_serializer(instance=serializer.instance).data
serializer.save(event=self.request.event)
if serializer.data == original_data:
# Performance optimization: If nothing was changed, we do not need to save or log anything.
# This costs us a few cycles on save, but avoids thousands of lines in our log.
return
serializer.instance.log_action(
'pretix.event.item.changed',
user=self.request.user,
@@ -83,7 +92,8 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
user=self.request.user,
auth=self.request.auth,
)
self.get_object().cartposition_set.all().delete()
CartPosition.objects.filter(addon_to__item=instance).delete()
instance.cartposition_set.all().delete()
super().perform_destroy(instance)
@@ -96,17 +106,20 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
permission = None
write_permission = 'can_change_items'
@cached_property
def item(self):
return get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
def get_queryset(self):
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
return item.variations.all()
return self.item.variations.all()
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['item'] = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
ctx['item'] = self.item
return ctx
def perform_create(self, serializer):
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
item = self.item
if not item.has_variations:
raise PermissionDenied('This variation cannot be created because the item does not have variations. '
'Changing a product without variations to a product with variations is not allowed.')
@@ -149,6 +162,58 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
)
class ItemBundleViewSet(viewsets.ModelViewSet):
serializer_class = ItemBundleSerializer
queryset = ItemBundle.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter,)
ordering_fields = ('id',)
ordering = ('id',)
permission = None
write_permission = 'can_change_items'
@cached_property
def item(self):
return get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
def get_queryset(self):
return self.item.bundles.all()
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['item'] = self.item
return ctx
def perform_create(self, serializer):
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
serializer.save(base_item=item)
item.log_action(
'pretix.event.item.bundles.added',
user=self.request.user,
auth=self.request.auth,
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
)
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.base_item.log_action(
'pretix.event.item.bundles.changed',
user=self.request.user,
auth=self.request.auth,
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
)
def perform_destroy(self, instance):
super().perform_destroy(instance)
instance.base_item.log_action(
'pretix.event.item.bundles.removed',
user=self.request.user,
auth=self.request.auth,
data={'bundled_item': instance.bundled_item.pk, 'bundled_variation': instance.bundled_variation.pk if instance.bundled_variation else None,
'count': instance.count, 'designated_price': instance.designated_price}
)
class ItemAddOnViewSet(viewsets.ModelViewSet):
serializer_class = ItemAddOnSerializer
queryset = ItemAddOn.objects.none()
@@ -158,18 +223,21 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
permission = None
write_permission = 'can_change_items'
@cached_property
def item(self):
return get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
def get_queryset(self):
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
return item.addons.all()
return self.item.addons.all()
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['item'] = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
ctx['item'] = self.item
return ctx
def perform_create(self, serializer):
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
item = self.item
category = get_object_or_404(ItemCategory, pk=self.request.data['addon_category'])
serializer.save(base_item=item, addon_category=category)
item.log_action(
@@ -252,10 +320,11 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
super().perform_destroy(instance)
class QuestionFilter(FilterSet):
class Meta:
model = Question
fields = ['ask_during_checkin', 'required', 'identifier']
with scopes_disabled():
class QuestionFilter(FilterSet):
class Meta:
model = Question
fields = ['ask_during_checkin', 'required', 'identifier']
class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
@@ -351,10 +420,11 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
super().perform_destroy(instance)
class QuotaFilter(FilterSet):
class Meta:
model = Quota
fields = ['subevent']
with scopes_disabled():
class QuotaFilter(FilterSet):
class Meta:
model = Quota
fields = ['subevent']
class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
@@ -392,9 +462,30 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
return ctx
def perform_update(self, serializer):
original_data = self.get_serializer(instance=serializer.instance).data
current_subevent = serializer.instance.subevent
serializer.save(event=self.request.event)
request_subevent = serializer.instance.subevent
if serializer.data == original_data:
# Performance optimization: If nothing was changed, we do not need to save or log anything.
# This costs us a few cycles on save, but avoids thousands of lines in our log.
return
if original_data['closed'] is True and serializer.instance.closed is False:
serializer.instance.log_action(
'pretix.event.quota.opened',
user=self.request.user,
auth=self.request.auth,
)
elif original_data['closed'] is False and serializer.instance.closed is True:
serializer.instance.log_action(
'pretix.event.quota.closed',
user=self.request.user,
auth=self.request.auth,
)
serializer.instance.log_action(
'pretix.event.quota.changed',
user=self.request.user,
@@ -439,7 +530,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
)
super().perform_destroy(instance)
@detail_route(methods=['get'])
@action(detail=True, methods=['get'])
def availability(self, request, *args, **kwargs):
quota = self.get_object()

View File

@@ -9,52 +9,63 @@ from django.db.models.functions import Coalesce, Concat
from django.http import FileResponse
from django.shortcuts import get_object_or_404
from django.utils.timezone import make_aware, now
from django.utils.translation import ugettext as _
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import mixins, serializers, status, viewsets
from rest_framework.decorators import detail_route
from rest_framework.decorators import action
from rest_framework.exceptions import (
APIException, NotFound, PermissionDenied, ValidationError,
)
from rest_framework.filters import OrderingFilter
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin
from rest_framework.mixins import CreateModelMixin
from rest_framework.response import Response
from pretix.api.models import OAuthAccessToken
from pretix.api.serializers.order import (
InvoiceSerializer, OrderCreateSerializer, OrderPaymentSerializer,
OrderPositionSerializer, OrderRefundCreateSerializer,
OrderRefundSerializer, OrderSerializer,
OrderRefundSerializer, OrderSerializer, PriceCalcSerializer,
)
from pretix.base.i18n import language
from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Device, Event, Invoice, Order,
OrderPayment, OrderPosition, OrderRefund, Quota, TeamAPIToken,
CachedCombinedTicket, CachedTicket, Device, Event, Invoice, InvoiceAddress,
Order, OrderPayment, OrderPosition, OrderRefund, Quota, TeamAPIToken,
generate_position_secret, generate_secret,
)
from pretix.base.payment import PaymentException
from pretix.base.services import tickets
from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
regenerate_invoice,
)
from pretix.base.services.mail import SendMailException
from pretix.base.services.orders import (
OrderChangeManager, OrderError, approve_order, cancel_order, deny_order,
OrderChangeManager, OrderError, _order_placed_email,
_order_placed_email_attendee, approve_order, cancel_order, deny_order,
extend_order, mark_order_expired, mark_order_refunded,
)
from pretix.base.services.pricing import get_price
from pretix.base.services.tickets import generate
from pretix.base.signals import order_placed, register_ticket_outputs
from pretix.base.signals import (
order_modified, order_placed, register_ticket_outputs,
)
from pretix.base.templatetags.money import money_filter
with scopes_disabled():
class OrderFilter(FilterSet):
email = django_filters.CharFilter(field_name='email', lookup_expr='iexact')
code = django_filters.CharFilter(field_name='code', lookup_expr='iexact')
status = django_filters.CharFilter(field_name='status', lookup_expr='iexact')
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
created_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte')
class Meta:
model = Order
fields = ['code', 'status', 'email', 'locale', 'testmode', 'require_approval']
class OrderFilter(FilterSet):
email = django_filters.CharFilter(field_name='email', lookup_expr='iexact')
code = django_filters.CharFilter(field_name='code', lookup_expr='iexact')
status = django_filters.CharFilter(field_name='status', lookup_expr='iexact')
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
class Meta:
model = Order
fields = ['code', 'status', 'email', 'locale', 'testmode', 'require_approval']
class OrderViewSet(DestroyModelMixin, CreateModelMixin, viewsets.ReadOnlyModelViewSet):
class OrderViewSet(viewsets.ModelViewSet):
serializer_class = OrderSerializer
queryset = Order.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
@@ -83,8 +94,8 @@ class OrderViewSet(DestroyModelMixin, CreateModelMixin, viewsets.ReadOnlyModelVi
'positions',
OrderPosition.objects.all().prefetch_related(
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
'item__category', 'addon_to',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
'item__category', 'addon_to', 'seat',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation', 'seat'))
)
)
)
@@ -93,7 +104,7 @@ class OrderViewSet(DestroyModelMixin, CreateModelMixin, viewsets.ReadOnlyModelVi
Prefetch(
'positions',
OrderPosition.objects.all().prefetch_related(
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question', 'seat',
)
)
)
@@ -122,7 +133,7 @@ class OrderViewSet(DestroyModelMixin, CreateModelMixin, viewsets.ReadOnlyModelVi
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data, headers={'X-Page-Generated': date})
@detail_route(url_name='download', url_path='download/(?P<output>[^/]+)')
@action(detail=True, url_name='download', url_path='download/(?P<output>[^/]+)')
def download(self, request, output, **kwargs):
provider = self._get_output_provider(output)
order = self.get_object()
@@ -144,7 +155,7 @@ class OrderViewSet(DestroyModelMixin, CreateModelMixin, viewsets.ReadOnlyModelVi
)
return resp
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def mark_paid(self, request, **kwargs):
order = self.get_object()
@@ -185,7 +196,7 @@ class OrderViewSet(DestroyModelMixin, CreateModelMixin, viewsets.ReadOnlyModelVi
status=status.HTTP_400_BAD_REQUEST
)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def mark_canceled(self, request, **kwargs):
send_mail = request.data.get('send_email', True)
cancellation_fee = request.data.get('cancellation_fee', None)
@@ -219,7 +230,7 @@ class OrderViewSet(DestroyModelMixin, CreateModelMixin, viewsets.ReadOnlyModelVi
)
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def approve(self, request, **kwargs):
send_mail = request.data.get('send_email', True)
@@ -237,7 +248,7 @@ class OrderViewSet(DestroyModelMixin, CreateModelMixin, viewsets.ReadOnlyModelVi
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def deny(self, request, **kwargs):
send_mail = request.data.get('send_email', True)
comment = request.data.get('comment', '')
@@ -255,7 +266,7 @@ class OrderViewSet(DestroyModelMixin, CreateModelMixin, viewsets.ReadOnlyModelVi
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def mark_pending(self, request, **kwargs):
order = self.get_object()
@@ -274,7 +285,7 @@ class OrderViewSet(DestroyModelMixin, CreateModelMixin, viewsets.ReadOnlyModelVi
)
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def mark_expired(self, request, **kwargs):
order = self.get_object()
@@ -291,7 +302,7 @@ class OrderViewSet(DestroyModelMixin, CreateModelMixin, viewsets.ReadOnlyModelVi
)
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def mark_refunded(self, request, **kwargs):
order = self.get_object()
@@ -308,7 +319,73 @@ class OrderViewSet(DestroyModelMixin, CreateModelMixin, viewsets.ReadOnlyModelVi
)
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def create_invoice(self, request, **kwargs):
order = self.get_object()
has_inv = order.invoices.exists() and not (
order.status in (Order.STATUS_PAID, Order.STATUS_PENDING)
and order.invoices.filter(is_cancellation=True).count() >= order.invoices.filter(is_cancellation=False).count()
)
if self.request.event.settings.get('invoice_generate') not in ('admin', 'user', 'paid', 'True') or not invoice_qualified(order):
return Response(
{'detail': _('You cannot generate an invoice for this order.')},
status=status.HTTP_400_BAD_REQUEST
)
elif has_inv:
return Response(
{'detail': _('An invoice for this order already exists.')},
status=status.HTTP_400_BAD_REQUEST
)
inv = generate_invoice(order)
order.log_action(
'pretix.event.order.invoice.generated',
user=self.request.user,
auth=self.request.auth,
data={
'invoice': inv.pk
}
)
return Response(
InvoiceSerializer(inv).data,
status=status.HTTP_201_CREATED
)
@action(detail=True, methods=['POST'])
def resend_link(self, request, **kwargs):
order = self.get_object()
if not order.email:
return Response({'detail': 'There is no email address associated with this order.'}, status=status.HTTP_400_BAD_REQUEST)
try:
order.resend_link(user=self.request.user, auth=self.request.auth)
except SendMailException:
return Response({'detail': _('There was an error sending the mail. Please try again later.')}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
return Response(
status=status.HTTP_204_NO_CONTENT
)
@action(detail=True, methods=['POST'])
@transaction.atomic
def regenerate_secrets(self, request, **kwargs):
order = self.get_object()
order.secret = generate_secret()
for op in order.all_positions.all():
op.secret = generate_position_secret()
op.save()
order.save(update_fields=['secret'])
CachedTicket.objects.filter(order_position__order=order).delete()
CachedCombinedTicket.objects.filter(order=order).delete()
tickets.invalidate_cache.apply_async(kwargs={'event': self.request.event.pk,
'order': order.pk})
order.log_action(
'pretix.event.order.secret.changed',
user=self.request.user,
auth=self.request.auth,
)
return self.retrieve(request, [], **kwargs)
@action(detail=True, methods=['POST'])
def extend(self, request, **kwargs):
new_date = request.data.get('expires', None)
force = request.data.get('force', False)
@@ -355,6 +432,7 @@ class OrderViewSet(DestroyModelMixin, CreateModelMixin, viewsets.ReadOnlyModelVi
serializer.is_valid(raise_exception=True)
with transaction.atomic():
self.perform_create(serializer)
send_mail = serializer._send_mail
order = serializer.instance
serializer = OrderSerializer(order, context=serializer.context)
@@ -369,12 +447,116 @@ class OrderViewSet(DestroyModelMixin, CreateModelMixin, viewsets.ReadOnlyModelVi
(order.event.settings.get('invoice_generate') == 'True') or
(order.event.settings.get('invoice_generate') == 'paid' and order.status == Order.STATUS_PAID)
) and not order.invoices.last()
invoice = None
if gen_invoice:
generate_invoice(order, trigger_pdf=True)
invoice = generate_invoice(order, trigger_pdf=True)
if send_mail:
payment = order.payments.last()
free_flow = (
payment and order.total == Decimal('0.00') and order.status == Order.STATUS_PAID and
not order.require_approval and payment.provider == "free"
)
if free_flow:
email_template = request.event.settings.mail_text_order_free
log_entry = 'pretix.event.order.email.order_free'
email_attendees = request.event.settings.mail_send_order_free_attendee
email_attendees_template = request.event.settings.mail_text_order_free_attendee
else:
email_template = request.event.settings.mail_text_order_placed
log_entry = 'pretix.event.order.email.order_placed'
email_attendees = request.event.settings.mail_send_order_placed_attendee
email_attendees_template = request.event.settings.mail_text_order_placed_attendee
_order_placed_email(
request.event, order, payment.payment_provider if payment else None, email_template,
log_entry, invoice, payment
)
if email_attendees:
for p in order.positions.all():
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
_order_placed_email_attendee(request.event, order, p, email_attendees_template, log_entry)
if not free_flow and order.status == Order.STATUS_PAID and payment:
payment._send_paid_mail(invoice, None, '')
if self.request.event.settings.mail_send_order_paid_attendee:
for p in order.positions.all():
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
payment._send_paid_mail_attendee(p, None)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def update(self, request, *args, **kwargs):
partial = kwargs.get('partial', False)
if not partial:
return Response(
{"detail": "Method \"PUT\" not allowed."},
status=status.HTTP_405_METHOD_NOT_ALLOWED,
)
return super().update(request, *args, **kwargs)
def perform_update(self, serializer):
with transaction.atomic():
if 'comment' in self.request.data and serializer.instance.comment != self.request.data.get('comment'):
serializer.instance.log_action(
'pretix.event.order.comment',
user=self.request.user,
auth=self.request.auth,
data={
'new_comment': self.request.data.get('comment')
}
)
if 'checkin_attention' in self.request.data and serializer.instance.checkin_attention != self.request.data.get('checkin_attention'):
serializer.instance.log_action(
'pretix.event.order.checkin_attention',
user=self.request.user,
auth=self.request.auth,
data={
'new_value': self.request.data.get('checkin_attention')
}
)
if 'email' in self.request.data and serializer.instance.email != self.request.data.get('email'):
serializer.instance.email_known_to_work = False
serializer.instance.log_action(
'pretix.event.order.contact.changed',
user=self.request.user,
auth=self.request.auth,
data={
'old_email': serializer.instance.email,
'new_email': self.request.data.get('email'),
}
)
if 'locale' in self.request.data and serializer.instance.locale != self.request.data.get('locale'):
serializer.instance.log_action(
'pretix.event.order.locale.changed',
user=self.request.user,
auth=self.request.auth,
data={
'old_locale': serializer.instance.locale,
'new_locale': self.request.data.get('locale'),
}
)
if 'invoice_address' in self.request.data:
serializer.instance.log_action(
'pretix.event.order.modified',
user=self.request.user,
auth=self.request.auth,
data={
'invoice_data': self.request.data.get('invoice_address'),
}
)
serializer.save()
tickets.invalidate_cache.apply_async(kwargs={'event': serializer.instance.event.pk, 'order': serializer.instance.pk})
if 'invoice_address' in self.request.data:
order_modified.send(sender=serializer.instance.event, order=serializer.instance)
def perform_create(self, serializer):
serializer.save()
@@ -386,46 +568,49 @@ class OrderViewSet(DestroyModelMixin, CreateModelMixin, viewsets.ReadOnlyModelVi
self.get_object().gracefully_delete(user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
class OrderPositionFilter(FilterSet):
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
has_checkin = django_filters.rest_framework.BooleanFilter(method='has_checkin_qs')
attendee_name = django_filters.CharFilter(method='attendee_name_qs')
search = django_filters.CharFilter(method='search_qs')
with scopes_disabled():
class OrderPositionFilter(FilterSet):
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
has_checkin = django_filters.rest_framework.BooleanFilter(method='has_checkin_qs')
attendee_name = django_filters.CharFilter(method='attendee_name_qs')
search = django_filters.CharFilter(method='search_qs')
def search_qs(self, queryset, name, value):
return queryset.filter(
Q(secret__istartswith=value)
| Q(attendee_name_cached__icontains=value)
| Q(addon_to__attendee_name_cached__icontains=value)
| Q(order__code__istartswith=value)
| Q(order__invoice_address__name_cached__icontains=value)
| Q(order__email__icontains=value)
)
def search_qs(self, queryset, name, value):
return queryset.filter(
Q(secret__istartswith=value)
| Q(attendee_name_cached__icontains=value)
| Q(addon_to__attendee_name_cached__icontains=value)
| Q(attendee_email__icontains=value)
| Q(addon_to__attendee_email__icontains=value)
| Q(order__code__istartswith=value)
| Q(order__invoice_address__name_cached__icontains=value)
| Q(order__email__icontains=value)
)
def has_checkin_qs(self, queryset, name, value):
return queryset.filter(checkins__isnull=not value)
def has_checkin_qs(self, queryset, name, value):
return queryset.filter(checkins__isnull=not value)
def attendee_name_qs(self, queryset, name, value):
return queryset.filter(Q(attendee_name_cached__iexact=value) | Q(addon_to__attendee_name_cached__iexact=value))
def attendee_name_qs(self, queryset, name, value):
return queryset.filter(Q(attendee_name_cached__iexact=value) | Q(addon_to__attendee_name_cached__iexact=value))
class Meta:
model = OrderPosition
fields = {
'item': ['exact', 'in'],
'variation': ['exact', 'in'],
'secret': ['exact'],
'order__status': ['exact', 'in'],
'addon_to': ['exact', 'in'],
'subevent': ['exact', 'in'],
'pseudonymization_id': ['exact'],
'voucher__code': ['exact'],
'voucher': ['exact'],
}
class Meta:
model = OrderPosition
fields = {
'item': ['exact', 'in'],
'variation': ['exact', 'in'],
'secret': ['exact'],
'order__status': ['exact', 'in'],
'addon_to': ['exact', 'in'],
'subevent': ['exact', 'in'],
'pseudonymization_id': ['exact'],
'voucher__code': ['exact'],
'voucher': ['exact'],
}
class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = OrderPositionSerializer
queryset = OrderPosition.objects.none()
queryset = OrderPosition.all.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
ordering = ('order__datetime', 'positionid')
ordering_fields = ('order__code', 'order__datetime', 'positionid', 'attendee_name', 'order__status',)
@@ -462,13 +647,13 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
)
))
).select_related(
'item', 'variation', 'item__category', 'addon_to'
'item', 'variation', 'item__category', 'addon_to', 'seat'
)
else:
qs = qs.prefetch_related(
'checkins', 'answers', 'answers__options', 'answers__question'
).select_related(
'item', 'order', 'order__event', 'order__event__organizer'
'item', 'order', 'order__event', 'order__event__organizer', 'seat'
)
return qs
@@ -480,7 +665,84 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
return prov
raise NotFound('Unknown output provider.')
@detail_route(url_name='download', url_path='download/(?P<output>[^/]+)')
@action(detail=True, methods=['POST'], url_name='price_calc')
def price_calc(self, request, *args, **kwargs):
"""
This calculates the price assuming a change of product or subevent. This endpoint
is deliberately not documented and considered a private API, only to be used by
pretix' web interface.
Sample input:
{
"item": 2,
"variation": null,
"subevent": 3
}
Sample output:
{
"gross": "2.34",
"gross_formatted": "2,34",
"net": "2.34",
"tax": "0.00",
"rate": "0.00",
"name": "VAT"
}
"""
serializer = PriceCalcSerializer(data=request.data, event=request.event)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
pos = self.get_object()
try:
ia = pos.order.invoice_address
except InvoiceAddress.DoesNotExist:
ia = InvoiceAddress()
kwargs = {
'item': pos.item,
'variation': pos.variation,
'voucher': pos.voucher,
'subevent': pos.subevent,
'addon_to': pos.addon_to,
'invoice_address': ia,
}
if data.get('item'):
item = data.get('item')
kwargs['item'] = item
if item.has_variations:
variation = data.get('variation') or pos.variation
if not variation:
raise ValidationError('No variation given')
if variation.item != item:
raise ValidationError('Variation does not belong to item')
kwargs['variation'] = variation
else:
variation = None
kwargs['variation'] = None
if pos.voucher and not pos.voucher.applies_to(item, variation):
kwargs['voucher'] = None
if data.get('subevent'):
kwargs['subevent'] = data.get('subevent')
price = get_price(**kwargs)
with language(data.get('locale') or self.request.event.settings.locale):
return Response({
'gross': price.gross,
'gross_formatted': money_filter(price.gross, self.request.event.currency, hide_currency=True),
'net': price.net,
'rate': price.rate,
'name': str(price.name),
'tax': price.tax,
})
@action(detail=True, url_name='download', url_path='download/(?P<output>[^/]+)')
def download(self, request, output, **kwargs):
provider = self._get_output_provider(output)
pos = self.get_object()
@@ -531,7 +793,7 @@ class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
order = get_object_or_404(Order, code=self.kwargs['order'], event=self.request.event)
return order.payments.all()
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def confirm(self, request, **kwargs):
payment = self.get_object()
force = request.data.get('force', False)
@@ -552,7 +814,7 @@ class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
pass
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def refund(self, request, **kwargs):
payment = self.get_object()
amount = serializers.DecimalField(max_digits=10, decimal_places=2).to_internal_value(
@@ -617,7 +879,7 @@ class PaymentViewSet(viewsets.ReadOnlyModelViewSet):
payment.order.save(update_fields=['status', 'expires'])
return Response(OrderRefundSerializer(r).data, status=status.HTTP_200_OK)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def cancel(self, request, **kwargs):
payment = self.get_object()
@@ -645,7 +907,7 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
order = get_object_or_404(Order, code=self.kwargs['order'], event=self.request.event)
return order.refunds.all()
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def cancel(self, request, **kwargs):
refund = self.get_object()
@@ -662,7 +924,7 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
}, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def process(self, request, **kwargs):
refund = self.get_object()
@@ -687,7 +949,7 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
refund.order.save(update_fields=['status', 'expires'])
return self.retrieve(request, [], **kwargs)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def done(self, request, **kwargs):
refund = self.get_object()
@@ -736,22 +998,23 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
serializer.save()
class InvoiceFilter(FilterSet):
refers = django_filters.CharFilter(method='refers_qs')
number = django_filters.CharFilter(method='nr_qs')
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
with scopes_disabled():
class InvoiceFilter(FilterSet):
refers = django_filters.CharFilter(method='refers_qs')
number = django_filters.CharFilter(method='nr_qs')
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
def refers_qs(self, queryset, name, value):
return queryset.annotate(
refers_nr=Concat('refers__prefix', 'refers__invoice_no')
).filter(refers_nr__iexact=value)
def refers_qs(self, queryset, name, value):
return queryset.annotate(
refers_nr=Concat('refers__prefix', 'refers__invoice_no')
).filter(refers_nr__iexact=value)
def nr_qs(self, queryset, name, value):
return queryset.filter(nr__iexact=value)
def nr_qs(self, queryset, name, value):
return queryset.filter(nr__iexact=value)
class Meta:
model = Invoice
fields = ['order', 'number', 'is_cancellation', 'refers', 'locale']
class Meta:
model = Invoice
fields = ['order', 'number', 'is_cancellation', 'refers', 'locale']
class RetryException(APIException):
@@ -777,7 +1040,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
nr=Concat('prefix', 'invoice_no')
)
@detail_route()
@action(detail=True, )
def download(self, request, **kwargs):
invoice = self.get_object()
@@ -795,7 +1058,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
resp['Content-Disposition'] = 'attachment; filename="{}.pdf"'.format(invoice.number)
return resp
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def regenerate(self, request, **kwarts):
inv = self.get_object()
if inv.canceled:
@@ -814,7 +1077,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
)
return Response(status=204)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def reissue(self, request, **kwarts):
inv = self.get_object()
if inv.canceled:

View File

@@ -1,8 +1,12 @@
from rest_framework import viewsets
from rest_framework import filters, viewsets
from rest_framework.exceptions import PermissionDenied
from pretix.api.models import OAuthAccessToken
from pretix.api.serializers.organizer import OrganizerSerializer
from pretix.base.models import Organizer
from pretix.api.serializers.organizer import (
OrganizerSerializer, SeatingPlanSerializer,
)
from pretix.base.models import Organizer, SeatingPlan
from pretix.helpers.dicts import merge_dicts
class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
@@ -10,6 +14,9 @@ class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Organizer.objects.none()
lookup_field = 'slug'
lookup_url_kwarg = 'organizer'
filter_backends = (filters.OrderingFilter,)
ordering = ('slug',)
ordering_fields = ('name', 'slug')
def get_queryset(self):
if self.request.user.is_authenticated:
@@ -27,3 +34,50 @@ class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
return Organizer.objects.filter(pk=self.request.auth.organizer_id)
else:
return Organizer.objects.filter(pk=self.request.auth.team.organizer_id)
class SeatingPlanViewSet(viewsets.ModelViewSet):
serializer_class = SeatingPlanSerializer
queryset = SeatingPlan.objects.none()
permission = 'can_change_organizer_settings'
write_permission = 'can_change_organizer_settings'
def get_queryset(self):
return self.request.organizer.seating_plans.all()
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['organizer'] = self.request.organizer
return ctx
def perform_create(self, serializer):
inst = serializer.save(organizer=self.request.organizer)
self.request.organizer.log_action(
'pretix.seatingplan.added',
user=self.request.user,
auth=self.request.auth,
data=merge_dicts(self.request.data, {'id': inst.pk})
)
def perform_update(self, serializer):
if serializer.instance.events.exists() or serializer.instance.subevents.exists():
raise PermissionDenied('This plan can not be changed while it is in use for an event.')
inst = serializer.save(organizer=self.request.organizer)
self.request.organizer.log_action(
'pretix.seatingplan.changed',
user=self.request.user,
auth=self.request.auth,
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
)
return inst
def perform_destroy(self, instance):
if instance.events.exists() or instance.subevents.exists():
raise PermissionDenied('This plan can not be deleted while it is in use for an event.')
instance.log_action(
'pretix.seatingplan.deleted',
user=self.request.user,
auth=self.request.auth,
data={'id': instance.pk}
)
instance.delete()

View File

@@ -6,8 +6,9 @@ from django.utils.timezone import now
from django_filters.rest_framework import (
BooleanFilter, DjangoFilterBackend, FilterSet,
)
from django_scopes import scopes_disabled
from rest_framework import status, viewsets
from rest_framework.decorators import list_route
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.filters import OrderingFilter
from rest_framework.response import Response
@@ -15,22 +16,22 @@ from rest_framework.response import Response
from pretix.api.serializers.voucher import VoucherSerializer
from pretix.base.models import Voucher
with scopes_disabled():
class VoucherFilter(FilterSet):
active = BooleanFilter(method='filter_active')
class VoucherFilter(FilterSet):
active = BooleanFilter(method='filter_active')
class Meta:
model = Voucher
fields = ['code', 'max_usages', 'redeemed', 'block_quota', 'allow_ignore_quota',
'price_mode', 'value', 'item', 'variation', 'quota', 'tag', 'subevent']
class Meta:
model = Voucher
fields = ['code', 'max_usages', 'redeemed', 'block_quota', 'allow_ignore_quota',
'price_mode', 'value', 'item', 'variation', 'quota', 'tag', 'subevent']
def filter_active(self, queryset, name, value):
if value:
return queryset.filter(Q(redeemed__lt=F('max_usages')) &
(Q(valid_until__isnull=True) | Q(valid_until__gt=now())))
else:
return queryset.filter(Q(redeemed__gte=F('max_usages')) |
(Q(valid_until__isnull=False) & Q(valid_until__lte=now())))
def filter_active(self, queryset, name, value):
if value:
return queryset.filter(Q(redeemed__lt=F('max_usages')) &
(Q(valid_until__isnull=True) | Q(valid_until__gt=now())))
else:
return queryset.filter(Q(redeemed__gte=F('max_usages')) |
(Q(valid_until__isnull=False) & Q(valid_until__lte=now())))
class VoucherViewSet(viewsets.ModelViewSet):
@@ -111,9 +112,12 @@ class VoucherViewSet(viewsets.ModelViewSet):
user=self.request.user,
auth=self.request.auth,
)
super().perform_destroy(instance)
with transaction.atomic():
instance.cartposition_set.filter(addon_to__isnull=False).delete()
instance.cartposition_set.all().delete()
super().perform_destroy(instance)
@list_route(methods=['POST'])
@action(detail=False, methods=['POST'])
def batch_create(self, request, *args, **kwargs):
if any(self._predict_quota_check(d, None) for d in request.data):
lockfn = request.event.lock

View File

@@ -1,7 +1,8 @@
import django_filters
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import viewsets
from rest_framework.decorators import detail_route
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.filters import OrderingFilter
from rest_framework.response import Response
@@ -10,16 +11,16 @@ from pretix.api.serializers.waitinglist import WaitingListSerializer
from pretix.base.models import WaitingListEntry
from pretix.base.models.waitinglist import WaitingListException
with scopes_disabled():
class WaitingListFilter(FilterSet):
has_voucher = django_filters.rest_framework.BooleanFilter(method='has_voucher_qs')
class WaitingListFilter(FilterSet):
has_voucher = django_filters.rest_framework.BooleanFilter(method='has_voucher_qs')
def has_voucher_qs(self, queryset, name, value):
return queryset.filter(voucher__isnull=not value)
def has_voucher_qs(self, queryset, name, value):
return queryset.filter(voucher__isnull=not value)
class Meta:
model = WaitingListEntry
fields = ['item', 'variation', 'email', 'locale', 'has_voucher', 'subevent']
class Meta:
model = WaitingListEntry
fields = ['item', 'variation', 'email', 'locale', 'has_voucher', 'subevent']
class WaitingListViewSet(viewsets.ModelViewSet):
@@ -69,7 +70,7 @@ class WaitingListViewSet(viewsets.ModelViewSet):
)
super().perform_destroy(instance)
@detail_route(methods=['POST'])
@action(detail=True, methods=['POST'])
def send_voucher(self, *args, **kwargs):
try:
self.get_object().send_voucher(

View File

@@ -8,6 +8,7 @@ from celery.exceptions import MaxRetriesExceededError
from django.db.models import Exists, OuterRef, Q
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from django_scopes import scope, scopes_disabled
from requests import RequestException
from pretix.api.models import WebHook, WebHookCall, WebHookEventListener
@@ -203,51 +204,52 @@ def notify_webhooks(logentry_id: int):
@app.task(base=ProfiledTask, bind=True, max_retries=9)
def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int):
# 9 retries with 2**(2*x) timing is roughly 72 hours
logentry = LogEntry.all.get(id=logentry_id)
webhook = WebHook.objects.get(id=webhook_id)
with scopes_disabled():
webhook = WebHook.objects.get(id=webhook_id)
with scope(organizer=webhook.organizer):
logentry = LogEntry.all.get(id=logentry_id)
types = get_all_webhook_events()
event_type = types.get(action_type)
if not event_type or not webhook.enabled:
return # Ignore, e.g. plugin not installed
types = get_all_webhook_events()
event_type = types.get(action_type)
if not event_type or not webhook.enabled:
return # Ignore, e.g. plugin not installed
payload = event_type.build_payload(logentry)
t = time.time()
payload = event_type.build_payload(logentry)
t = time.time()
try:
try:
resp = requests.post(
webhook.target_url,
json=payload,
allow_redirects=False
)
WebHookCall.objects.create(
webhook=webhook,
action_type=logentry.action_type,
target_url=webhook.target_url,
is_retry=self.request.retries > 0,
execution_time=time.time() - t,
return_code=resp.status_code,
payload=json.dumps(payload),
response_body=resp.text[:1024 * 1024],
success=200 <= resp.status_code <= 299
)
if resp.status_code == 410:
webhook.enabled = False
webhook.save()
elif resp.status_code > 299:
try:
resp = requests.post(
webhook.target_url,
json=payload,
allow_redirects=False
)
WebHookCall.objects.create(
webhook=webhook,
action_type=logentry.action_type,
target_url=webhook.target_url,
is_retry=self.request.retries > 0,
execution_time=time.time() - t,
return_code=resp.status_code,
payload=json.dumps(payload),
response_body=resp.text[:1024 * 1024],
success=200 <= resp.status_code <= 299
)
if resp.status_code == 410:
webhook.enabled = False
webhook.save()
elif resp.status_code > 299:
raise self.retry(countdown=2 ** (self.request.retries * 2))
except RequestException as e:
WebHookCall.objects.create(
webhook=webhook,
action_type=logentry.action_type,
target_url=webhook.target_url,
is_retry=self.request.retries > 0,
execution_time=time.time() - t,
return_code=0,
payload=json.dumps(payload),
response_body=str(e)[:1024 * 1024]
)
raise self.retry(countdown=2 ** (self.request.retries * 2))
except RequestException as e:
WebHookCall.objects.create(
webhook=webhook,
action_type=logentry.action_type,
target_url=webhook.target_url,
is_retry=self.request.retries > 0,
execution_time=time.time() - t,
return_code=0,
payload=json.dumps(payload),
response_body=str(e)[:1024 * 1024]
)
raise self.retry(countdown=2 ** (self.request.retries * 2))
except MaxRetriesExceededError:
pass
except MaxRetriesExceededError:
pass

View File

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

View File

@@ -0,0 +1,80 @@
import re
# banlist based on http://www.bannedwordlist.com/lists/swearWords.txt
banlist = [
"anal",
"anus",
"arse",
"ass",
"balls",
"bastard",
"bitch",
"biatch",
"bloody",
"blowjob",
"bollock",
"bollok",
"boner",
"boob",
"bugger",
"bum",
"butt",
"clitoris",
"cock",
"coon",
"crap",
"cunt",
"damn",
"dick",
"dildo",
"dyke",
"fag",
"feck",
"fellate",
"fellatio",
"felching",
"fuck",
"fudgepacker",
"flange",
"goddamn",
"hell",
"homo",
"jerk",
"jizz",
"knobend",
"labia",
"lmao",
"lmfao",
"muff",
"nigger",
"nigga",
"omg",
"penis",
"piss",
"poop",
"prick",
"pube",
"pussy",
"queer",
"scrotum",
"sex",
"shit",
"sh1t",
"slut",
"smegma",
"spunk",
"tit",
"tosser",
"turd",
"twat",
"vagina",
"wank",
"whore",
"wtf"
]
blacklist_regex = re.compile('(' + '|'.join(banlist) + ')')
def banned(string):
return bool(blacklist_regex.search(string.lower()))

View File

@@ -0,0 +1,13 @@
import sys
from django.conf import settings
def contextprocessor(request):
ctx = {}
if settings.DEBUG and 'runserver' not in sys.argv:
ctx['debug_warning'] = True
elif 'runserver' in sys.argv:
ctx['development_warning'] = True
return ctx

View File

@@ -1,18 +1,24 @@
import inspect
import logging
from datetime import timedelta
from decimal import Decimal
from smtplib import SMTPResponseException
import bleach
import markdown
from django.conf import settings
from django.core.mail.backends.smtp import EmailBackend
from django.dispatch import receiver
from django.template.loader import get_template
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from inlinestyler.utils import inline_css
from pretix.base.models import Event, Order
from pretix.base.signals import register_html_mail_renderers
from pretix.base.templatetags.rich_text import markdown_compile
from pretix.base.i18n import LazyCurrencyNumber, LazyDate, LazyNumber
from pretix.base.models import Event
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import (
register_html_mail_renderers, register_mail_placeholders,
)
from pretix.base.templatetags.rich_text import markdown_compile_email
logger = logging.getLogger('pretix.base.email')
@@ -46,7 +52,8 @@ class BaseHTMLMailRenderer:
def __str__(self):
return self.identifier
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order=None) -> str:
def render(self, plain_body: str, plain_signature: str, subject: str, order=None,
position=None) -> str:
"""
This method should generate the HTML part of the email.
@@ -54,6 +61,7 @@ class BaseHTMLMailRenderer:
:param plain_signature: The signature with event organizer contact details in plain text.
:param subject: The email subject.
:param order: The order if this email is connected to one, otherwise ``None``.
:param position: The order position if this email is connected to one, otherwise ``None``.
:return: An HTML string
"""
raise NotImplementedError()
@@ -97,8 +105,8 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
def template_name(self):
raise NotImplementedError()
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order) -> str:
body_md = bleach.linkify(markdown_compile(plain_body))
def render(self, plain_body: str, plain_signature: str, subject: str, order, position) -> str:
body_md = markdown_compile_email(plain_body)
htmlctx = {
'site': settings.PRETIX_INSTANCE_NAME,
'site_url': settings.SITE_URL,
@@ -112,12 +120,15 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
if plain_signature:
signature_md = plain_signature.replace('\n', '<br>\n')
signature_md = bleach.linkify(bleach.clean(markdown.markdown(signature_md), tags=bleach.ALLOWED_TAGS + ['p', 'br']))
signature_md = markdown_compile_email(signature_md)
htmlctx['signature'] = signature_md
if order:
htmlctx['order'] = order
if position:
htmlctx['position'] = position
tpl = get_template(self.template_name)
body_html = inline_css(tpl.render(htmlctx))
return body_html
@@ -133,3 +144,285 @@ class ClassicMailRenderer(TemplateBasedMailRenderer):
@receiver(register_html_mail_renderers, dispatch_uid="pretixbase_email_renderers")
def base_renderers(sender, **kwargs):
return [ClassicMailRenderer]
class BaseMailTextPlaceholder:
"""
This is the base class for for all email text placeholders.
"""
@property
def required_context(self):
"""
This property should return a list of all attribute names that need to be
contained in the base context so that this placeholder is available. By default,
it returns a list containing the string "event".
"""
return ["event"]
@property
def identifier(self):
"""
This should return the identifier of this placeholder in the email.
"""
raise NotImplementedError()
def render(self, context):
"""
This method is called to generate the actual text that is being
used in the email. You will be passed a context dictionary with the
base context attributes specified in ``required_context``. You are
expected to return a plain-text string.
"""
raise NotImplementedError()
def render_sample(self, event):
"""
This method is called to generate a text to be used in email previews.
This may only depend on the event.
"""
raise NotImplementedError()
class SimpleFunctionalMailTextPlaceholder(BaseMailTextPlaceholder):
def __init__(self, identifier, args, func, sample):
self._identifier = identifier
self._args = args
self._func = func
self._sample = sample
@property
def identifier(self):
return self._identifier
@property
def required_context(self):
return self._args
def render(self, context):
return self._func(**{k: context[k] for k in self._args})
def render_sample(self, event):
if callable(self._sample):
return self._sample(event)
else:
return self._sample
def get_available_placeholders(event, base_parameters):
if 'order' in base_parameters:
base_parameters.append('invoice_address')
params = {}
for r, val in register_mail_placeholders.send(sender=event):
if not isinstance(val, (list, tuple)):
val = [val]
for v in val:
if all(rp in base_parameters for rp in v.required_context):
params[v.identifier] = v
return params
def get_email_context(**kwargs):
from pretix.base.models import InvoiceAddress
event = kwargs['event']
if 'order' in kwargs:
try:
kwargs['invoice_address'] = kwargs['order'].invoice_address
except InvoiceAddress.DoesNotExist:
kwargs['invoice_address'] = InvoiceAddress()
ctx = {}
for r, val in register_mail_placeholders.send(sender=event):
if not isinstance(val, (list, tuple)):
val = [val]
for v in val:
if all(rp in kwargs for rp in v.required_context):
ctx[v.identifier] = v.render(kwargs)
return ctx
def _placeholder_payment(order, payment):
if not payment:
return None
if 'payment' in inspect.signature(payment.payment_provider.order_pending_mail_render).parameters:
return str(payment.payment_provider.order_pending_mail_render(order, payment))
else:
return str(payment.payment_provider.order_pending_mail_render(order))
@receiver(register_mail_placeholders, dispatch_uid="pretixbase_register_mail_placeholders")
def base_placeholders(sender, **kwargs):
from pretix.base.models import InvoiceAddress
from pretix.multidomain.urlreverse import build_absolute_uri
ph = [
SimpleFunctionalMailTextPlaceholder(
'event', ['event'], lambda event: event.name, lambda event: event.name
),
SimpleFunctionalMailTextPlaceholder(
'code', ['order'], lambda order: order.code, 'F8VVL'
),
SimpleFunctionalMailTextPlaceholder(
'total', ['order'], lambda order: LazyNumber(order.total), lambda event: LazyNumber(Decimal('42.23'))
),
SimpleFunctionalMailTextPlaceholder(
'currency', ['event'], lambda event: event.currency, lambda event: event.currency
),
SimpleFunctionalMailTextPlaceholder(
'total_with_currency', ['event', 'order'], lambda event, order: LazyCurrencyNumber(order.total,
event.currency),
lambda event: LazyCurrencyNumber(Decimal('42.23'), event.currency)
),
SimpleFunctionalMailTextPlaceholder(
'expire_date', ['event', 'order'], lambda event, order: LazyDate(order.expires.astimezone(event.timezone)),
lambda event: LazyDate(now() + timedelta(days=15))
# TODO: This used to be "date" in some placeholders, add a migration!
),
SimpleFunctionalMailTextPlaceholder(
'url', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash()
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.open', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
'hash': '98kusd8ofsj8dnkd'
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url', ['event', 'position'], lambda event, position: build_absolute_uri(
event,
'presale:event.order.position',
kwargs={
'order': position.order.code,
'secret': position.web_secret,
'position': position.positionid
}
),
lambda event: build_absolute_uri(
event,
'presale:event.order.position', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
'position': '123'
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url', ['waiting_list_entry', 'event'],
lambda waiting_list_entry, event: build_absolute_uri(
event, 'presale:event.redeem'
) + '?voucher=' + waiting_list_entry.voucher.code,
lambda event: build_absolute_uri(
event,
'presale:event.redeem',
) + '?voucher=68CYU2H6ZTP3WLK5',
),
SimpleFunctionalMailTextPlaceholder(
'invoice_name', ['invoice_address'], lambda invoice_address: invoice_address.name or '',
_('John Doe')
),
SimpleFunctionalMailTextPlaceholder(
'invoice_company', ['invoice_address'], lambda invoice_address: invoice_address.company or '',
_('Sample Corporation')
),
SimpleFunctionalMailTextPlaceholder(
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
'* {} - {}'.format(
order.full_code,
build_absolute_uri(event, 'presale:event.order', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'order': order.code,
'secret': order.secret
}),
)
for order in orders
), lambda event: '\n' + '\n\n'.join(
'* {} - {}'.format(
'{}-{}'.format(event.slug.upper(), order['code']),
build_absolute_uri(event, 'presale:event.order', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'order': order['code'],
'secret': order['secret']
}),
)
for order in [
{'code': 'F8VVL', 'secret': '6zzjnumtsx136ddy'},
{'code': 'HIDHK', 'secret': '98kusd8ofsj8dnkd'},
{'code': 'OPKSB', 'secret': '09pjdksflosk3njd'}
]
),
),
SimpleFunctionalMailTextPlaceholder(
'hours', ['event', 'waiting_list_entry'], lambda event, waiting_list_entry:
event.settings.waiting_list_hours,
lambda event: event.settings.waiting_list_hours
),
SimpleFunctionalMailTextPlaceholder(
'product', ['waiting_list_entry'], lambda waiting_list_entry: waiting_list_entry.item.name,
_('Sample Admission Ticket')
),
SimpleFunctionalMailTextPlaceholder(
'code', ['waiting_list_entry'], lambda waiting_list_entry: waiting_list_entry.voucher.code,
'68CYU2H6ZTP3WLK5'
),
SimpleFunctionalMailTextPlaceholder(
'comment', ['comment'], lambda comment: comment,
_('An individual text with a reason can be inserted here.'),
),
SimpleFunctionalMailTextPlaceholder(
'payment_info', ['order', 'payment'], _placeholder_payment,
_('The amount has been charged to your card.'),
),
SimpleFunctionalMailTextPlaceholder(
'payment_info', ['payment_info'], lambda payment_info: payment_info,
_('Please transfer money to this bank account: 9999-9999-9999-9999'),
),
SimpleFunctionalMailTextPlaceholder(
'attendee_name', ['position'], lambda position: position.attendee_name,
_('John Doe'),
),
SimpleFunctionalMailTextPlaceholder(
'name', ['position_or_address'],
lambda position_or_address: (
position_or_address.name
if isinstance(position_or_address, InvoiceAddress)
else position_or_address.attendee_name
),
_('John Doe'),
),
]
name_scheme = PERSON_NAME_SCHEMES[sender.settings.name_scheme]
for f, l, w in name_scheme['fields']:
if f == 'full_name':
continue
ph.append(SimpleFunctionalMailTextPlaceholder(
'attendee_name_%s' % f, ['position'], lambda position, f=f: position.attendee_name_parts.get(f, ''),
name_scheme['sample'][f]
))
ph.append(SimpleFunctionalMailTextPlaceholder(
'name_%s' % f, ['position_or_address'],
lambda position_or_address, f=f: (
position_or_address.name_parts.get(f, '')
if isinstance(position_or_address, InvoiceAddress)
else position_or_address.attendee_name_parts.get(f, '')
),
name_scheme['sample'][f]
))
for k, v in sender.meta_data.items():
ph.append(SimpleFunctionalMailTextPlaceholder(
'meta_%s' % k, ['event'], lambda event, k=k: event.meta_data[k],
v
))
return ph

View File

@@ -71,6 +71,8 @@ class BaseExporter:
:type form_data: dict
:param form_data: The form data of the export details form
:param output_file: You can optionally accept a parameter that will be given a file handle to write the
output to. In this case, you can return None instead of the file content.
Note: If you use a ``ModelChoiceField`` (or a ``ModelMultipleChoiceField``), the
``form_data`` will not contain the model instance but only it's primary key (or
@@ -92,7 +94,7 @@ class ListExporter(BaseExporter):
choices=(
('xlsx', _('Excel (.xlsx)')),
('default', _('CSV (with commas)')),
('excel', _('CSV (Excel-style)')),
('csv-excel', _('CSV (Excel-style)')),
('semicolon', _('CSV (with semicolons)')),
),
)),
@@ -109,16 +111,22 @@ class ListExporter(BaseExporter):
raise NotImplementedError() # noqa
def get_filename(self):
return 'export.csv'
return 'export'
def _render_csv(self, form_data, **kwargs):
output = io.StringIO()
writer = csv.writer(output, **kwargs)
for line in self.iterate_list(form_data):
writer.writerow(line)
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
def _render_csv(self, form_data, output_file=None, **kwargs):
if output_file:
writer = csv.writer(output_file, **kwargs)
for line in self.iterate_list(form_data):
writer.writerow(line)
return self.get_filename() + '.csv', 'text/csv', None
else:
output = io.StringIO()
writer = csv.writer(output, **kwargs)
for line in self.iterate_list(form_data):
writer.writerow(line)
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
def _render_xlsx(self, form_data):
def _render_xlsx(self, form_data, output_file=None):
wb = Workbook()
ws = wb.get_active_sheet()
try:
@@ -129,20 +137,24 @@ class ListExporter(BaseExporter):
for j, val in enumerate(line):
ws.cell(row=i + 1, column=j + 1).value = str(val) if not isinstance(val, KNOWN_TYPES) else val
with tempfile.NamedTemporaryFile(suffix='.xlsx') as f:
wb.save(f.name)
f.seek(0)
return self.get_filename() + '.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', f.read()
if output_file:
wb.save(output_file)
return self.get_filename() + '.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', None
else:
with tempfile.NamedTemporaryFile(suffix='.xlsx') as f:
wb.save(f.name)
f.seek(0)
return self.get_filename() + '.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', f.read()
def render(self, form_data: dict) -> Tuple[str, str, bytes]:
def render(self, form_data: dict, output_file=None) -> Tuple[str, str, bytes]:
if form_data.get('_format') == 'xlsx':
return self._render_xlsx(form_data)
return self._render_xlsx(form_data, output_file=output_file)
elif form_data.get('_format') == 'default':
return self._render_csv(form_data, quoting=csv.QUOTE_NONNUMERIC, delimiter=',')
return self._render_csv(form_data, quoting=csv.QUOTE_NONNUMERIC, delimiter=',', output_file=output_file)
elif form_data.get('_format') == 'csv-excel':
return self._render_csv(form_data, dialect='excel')
return self._render_csv(form_data, dialect='excel', output_file=output_file)
elif form_data.get('_format') == 'semicolon':
return self._render_csv(form_data, dialect='excel', delimiter=';')
return self._render_csv(form_data, dialect='excel', delimiter=';', output_file=output_file)
class MultiSheetListExporter(ListExporter):
@@ -180,14 +192,20 @@ class MultiSheetListExporter(ListExporter):
def iterate_sheet(self, form_data, sheet):
raise NotImplementedError() # noqa
def _render_sheet_csv(self, form_data, sheet, **kwargs):
output = io.StringIO()
writer = csv.writer(output, **kwargs)
for line in self.iterate_sheet(form_data, sheet):
writer.writerow(line)
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
def _render_sheet_csv(self, form_data, sheet, output_file=None, **kwargs):
if output_file:
writer = csv.writer(output_file, **kwargs)
for line in self.iterate_sheet(form_data, sheet):
writer.writerow(line)
return self.get_filename() + '.csv', 'text/csv', None
else:
output = io.StringIO()
writer = csv.writer(output, **kwargs)
for line in self.iterate_sheet(form_data, sheet):
writer.writerow(line)
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
def _render_xlsx(self, form_data):
def _render_xlsx(self, form_data, output_file=None):
wb = Workbook()
ws = wb.get_active_sheet()
wb.remove(ws)
@@ -197,19 +215,24 @@ class MultiSheetListExporter(ListExporter):
for j, val in enumerate(line):
ws.cell(row=i + 1, column=j + 1).value = str(val) if not isinstance(val, KNOWN_TYPES) else val
with tempfile.NamedTemporaryFile(suffix='.xlsx') as f:
wb.save(f.name)
f.seek(0)
return self.get_filename() + '.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', f.read()
if output_file:
wb.save(output_file)
return self.get_filename() + '.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', None
else:
with tempfile.NamedTemporaryFile(suffix='.xlsx') as f:
wb.save(f.name)
f.seek(0)
return self.get_filename() + '.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', f.read()
def render(self, form_data: dict) -> Tuple[str, str, bytes]:
def render(self, form_data: dict, output_file=None) -> Tuple[str, str, bytes]:
if form_data.get('_format') == 'xlsx':
return self._render_xlsx(form_data)
return self._render_xlsx(form_data, output_file=output_file)
elif ':' in form_data.get('_format'):
sheet, f = form_data.get('_format').split(':')
if f == 'default':
return self._render_sheet_csv(form_data, sheet, quoting=csv.QUOTE_NONNUMERIC, delimiter=',')
return self._render_sheet_csv(form_data, sheet, quoting=csv.QUOTE_NONNUMERIC, delimiter=',',
output_file=output_file)
elif f == 'excel':
return self._render_sheet_csv(form_data, sheet, dialect='excel')
return self._render_sheet_csv(form_data, sheet, dialect='excel', output_file=output_file)
elif f == 'semicolon':
return self._render_sheet_csv(form_data, sheet, dialect='excel', delimiter=';')
return self._render_sheet_csv(form_data, sheet, dialect='excel', delimiter=';', output_file=output_file)

View File

@@ -1,4 +1,5 @@
from .answers import * # noqa
from .dekodi import * # noqa
from .invoices import * # noqa
from .json import * # noqa
from .mail import * # noqa

View File

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

View File

@@ -0,0 +1,222 @@
import json
from collections import OrderedDict
from decimal import Decimal
import dateutil
from django import forms
from django.core.serializers.json import DjangoJSONEncoder
from django.dispatch import receiver
from django.utils.translation import ugettext, ugettext_lazy
from pretix.base.i18n import language
from pretix.base.models import Invoice, OrderPayment
from ..exporter import BaseExporter
from ..signals import register_data_exporters
class DekodiNREIExporter(BaseExporter):
identifier = 'dekodi_nrei'
verbose_name = 'dekodi NREI (JSON)'
# Specification: http://manuals.dekodi.de/nexuspub/schnittstellenbuch/
def _encode_invoice(self, invoice: Invoice):
p_last = invoice.order.payments.filter(state=[OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED]).last()
gross_total = Decimal('0.00')
net_total = Decimal('0.00')
positions = []
for l in invoice.lines.all():
positions.append({
'ADes': l.description.replace("<br />", "\n"),
'ANetA': round(float((-1 if invoice.is_cancellation else 1) * l.net_value), 2),
'ANo': self.event.slug,
'AQ': -1 if invoice.is_cancellation else 1,
'AVatP': round(float(l.tax_rate), 2),
'DIDt': (l.subevent or invoice.order.event).date_from.isoformat().replace('Z', '+00:00'),
'PosGrossA': round(float(l.gross_value), 2),
'PosNetA': round(float(l.net_value), 2),
})
gross_total += l.gross_value
net_total += l.net_value
payments = []
paypal_email = None
for p in invoice.order.payments.filter(
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_PENDING,
OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_REFUNDED)
):
if p.provider == 'paypal':
paypal_email = p.info_data.get('payer', {}).get('payer_info', {}).get('email')
try:
ppid = p.info_data['transactions'][0]['related_resources'][0]['sale']['id']
except:
ppid = p.info_data.get('id')
payments.append({
'PTID': '1',
'PTN': 'PayPal',
'PTNo1': ppid,
'PTNo2': p.info_data.get('id'),
'PTNo7': round(float(p.amount), 2),
'PTNo8': str(self.event.currency),
'PTNo11': paypal_email or '',
'PTNo15': p.full_id or '',
})
elif p.provider == 'banktransfer':
payments.append({
'PTID': '4',
'PTN': 'Vorkasse',
'PTNo4': p.info_data.get('reference') or p.payment_provider._code(invoice.order),
'PTNo7': round(float(p.amount), 2),
'PTNo8': str(self.event.currency),
'PTNo10': p.info_data.get('payer') or '',
'PTNo14': p.info_data.get('date') or '',
'PTNo15': p.full_id or '',
})
elif p.provider == 'sepadebit':
with language(invoice.order.locale):
payments.append({
'PTID': '5',
'PTN': 'Lastschrift',
'PTNo4': ugettext('Event ticket {event}-{code}').format(
event=self.event.slug.upper(),
code=invoice.order.code
),
'PTNo5': p.info_data.get('iban') or '',
'PTNo6': p.info_data.get('bic') or '',
'PTNo7': round(float(p.amount), 2),
'PTNo8': str(self.event.currency) or '',
'PTNo9': p.info_data.get('date') or '',
'PTNo10': p.info_data.get('account') or '',
'PTNo14': p.info_data.get('reference') or '',
'PTNo15': p.full_id or '',
})
elif p.provider.startswith('stripe'):
src = p.info_data.get("source", p.info_data)
payments.append({
'PTID': '81',
'PTN': 'Stripe',
'PTNo1': p.info_data.get("id") or '',
'PTNo5': src.get("card", {}).get("last4") or '',
'PTNo7': round(float(p.amount), 2) or '',
'PTNo8': str(self.event.currency) or '',
'PTNo10': src.get('owner', {}).get('verified_name') or src.get('owner', {}).get('name') or '',
'PTNo15': p.full_id or '',
})
else:
payments.append({
'PTID': '0',
'PTN': p.provider,
'PTNo7': round(float(p.amount), 2) or '',
'PTNo8': str(self.event.currency) or '',
'PTNo15': p.full_id or '',
})
payments = [
{
k: v for k, v in p.items() if v is not None
} for p in payments
]
hdr = {
'C': str(invoice.invoice_to_country) or self.event.settings.invoice_address_from_country,
'CC': self.event.currency,
'City': invoice.invoice_to_city,
'CN': invoice.invoice_to_company,
'DIC': self.event.settings.invoice_address_from_country,
# DIC is a little bit unclean, should be the event location's country
'DIDt': invoice.order.datetime.isoformat().replace('Z', '+00:00'),
'DT': '30' if invoice.is_cancellation else '10',
'EM': invoice.order.email,
'FamN': invoice.invoice_to_name.rsplit(' ', 1)[-1] if invoice.invoice_to_name else '',
'FN': (
invoice.invoice_to_name.rsplit(' ', 1)[0]
if invoice.invoice_to_name and ' ' in invoice.invoice_to_name else ''
),
'IDt': invoice.date.isoformat() + 'T08:00:00+01:00',
'INo': invoice.full_invoice_no,
'IsNet': invoice.reverse_charge,
'ODt': invoice.order.datetime.isoformat().replace('Z', '+00:00'),
'OID': invoice.order.code,
'SID': self.event.slug,
'SN': str(self.event),
'Str': invoice.invoice_to_street or '',
'TGrossA': round(float(gross_total), 2),
'TNetA': round(float(net_total), 2),
'TVatA': round(float(gross_total - net_total), 2),
'VatDp': False,
'Zip': invoice.invoice_to_zipcode
}
if not hdr['FamN'] and not hdr['CN']:
hdr['CN'] = "Unbekannter Kunde"
if invoice.refers:
hdr['PvrINo'] = invoice.refers.full_invoice_no
if p_last:
hdr['PmDt'] = p_last.payment_date.isoformat().replace('Z', '+00:00')
if paypal_email:
hdr['PPEm'] = paypal_email
if invoice.invoice_to_vat_id:
hdr['VatID'] = invoice.invoice_to_vat_id
return {
'IsValid': True,
'Hdr': hdr,
'InvcPstns': positions,
'PmIs': payments,
'ValidationMessage': ''
}
def render(self, form_data):
qs = self.event.invoices.select_related('order').prefetch_related('lines', 'lines__subevent')
if form_data.get('date_from'):
date_value = form_data.get('date_from')
if isinstance(date_value, str):
date_value = dateutil.parser.parse(date_value).date()
qs = qs.filter(date__gte=date_value)
if form_data.get('date_to'):
date_value = form_data.get('date_to')
if isinstance(date_value, str):
date_value = dateutil.parser.parse(date_value).date()
qs = qs.filter(date__lte=date_value)
jo = {
'Format': 'NREI',
'Version': '18.10.2.0',
'SourceSystem': 'pretix',
'Data': [
self._encode_invoice(i) for i in qs
]
}
return '{}_nrei.json'.format(self.event.slug), 'application/json', json.dumps(jo, cls=DjangoJSONEncoder, indent=4)
@property
def export_form_fields(self):
return OrderedDict(
[
('date_from',
forms.DateField(
label=ugettext_lazy('Start date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=ugettext_lazy('Only include invoices issued on or after this date. Note that the invoice date does '
'not always correspond to the order or payment date.')
)),
('date_to',
forms.DateField(
label=ugettext_lazy('End date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=ugettext_lazy('Only include invoices issued on or before this date. Note that the invoice date '
'does not always correspond to the order or payment date.')
)),
]
)
@receiver(register_data_exporters, dispatch_uid="exporter_dekodi_nrei")
def register_dekodi_export(sender, **kwargs):
return DekodiNREIExporter

View File

@@ -20,7 +20,7 @@ class InvoiceExporter(BaseExporter):
identifier = 'invoices'
verbose_name = _('All invoices')
def render(self, form_data: dict):
def render(self, form_data: dict, output_file=None):
qs = self.event.invoices.filter(shredded=False)
if form_data.get('payment_provider'):
@@ -46,7 +46,8 @@ class InvoiceExporter(BaseExporter):
qs = qs.filter(date__lte=date_value)
with tempfile.TemporaryDirectory() as d:
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
any = False
with ZipFile(output_file or os.path.join(d, 'tmp.zip'), 'w') as zipf:
for i in qs:
try:
if not i.file:
@@ -54,16 +55,24 @@ class InvoiceExporter(BaseExporter):
i.refresh_from_db()
i.file.open('rb')
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
any = True
i.file.close()
except FileNotFoundError:
invoice_pdf_task.apply(args=(i.pk,))
i.refresh_from_db()
i.file.open('rb')
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
any = True
i.file.close()
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
return '{}_invoices.zip'.format(self.event.slug), 'application/zip', zipf.read()
if not any:
return None
if output_file:
return '{}_invoices.zip'.format(self.event.slug), 'application/zip', None
else:
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
return '{}_invoices.zip'.format(self.event.slug), 'application/zip', zipf.read()
@property
def export_form_fields(self):

View File

@@ -6,10 +6,10 @@ from django import forms
from django.db.models import DateTimeField, F, Max, OuterRef, Subquery, Sum
from django.dispatch import receiver
from django.utils.formats import date_format, localize
from django.utils.translation import ugettext as _, ugettext_lazy
from django.utils.translation import pgettext, ugettext as _, ugettext_lazy
from pretix.base.models import (
InvoiceAddress, InvoiceLine, Order, OrderPosition,
InvoiceAddress, InvoiceLine, Order, OrderPosition, Question,
)
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
from pretix.base.settings import PERSON_NAME_SCHEMES
@@ -96,7 +96,7 @@ class OrderListExporter(MultiSheetListExporter):
for k, label, w in name_scheme['fields']:
headers.append(label)
headers += [
_('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
_('Address'), _('ZIP code'), _('City'), _('Country'), pgettext('address', 'State'), _('VAT ID'),
_('Date of last payment'), _('Fees'), _('Order locale')
]
@@ -108,6 +108,9 @@ class OrderListExporter(MultiSheetListExporter):
]
headers.append(_('Invoice numbers'))
headers.append(_('Sales channel'))
headers.append(_('Requires special attention'))
headers.append(_('Comment'))
yield headers
@@ -152,10 +155,11 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.city,
order.invoice_address.country if order.invoice_address.country else
order.invoice_address.country_old,
order.invoice_address.state,
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
row += [''] * (7 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row += [''] * (8 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row += [
order.payment_date.astimezone(tz).strftime('%Y-%m-%d') if order.payment_date else '',
@@ -176,6 +180,9 @@ class OrderListExporter(MultiSheetListExporter):
]
row.append(', '.join([i.number for i in order.invoices.all()]))
row.append(order.sales_channel)
row.append(_('Yes') if order.checkin_attention else _('No'))
row.append(order.comment or "")
yield row
def iterate_fees(self, form_data: dict):
@@ -206,7 +213,7 @@ class OrderListExporter(MultiSheetListExporter):
for k, label, w in name_scheme['fields']:
headers.append(_('Invoice address name') + ': ' + str(label))
headers += [
_('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
_('Address'), _('ZIP code'), _('City'), _('Country'), pgettext('address', 'State'), _('VAT ID'),
]
yield headers
@@ -241,10 +248,11 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.city,
order.invoice_address.country if order.invoice_address.country else
order.invoice_address.country_old,
order.invoice_address.state,
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
row += [''] * (7 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row += [''] * (8 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
yield row
def iterate_positions(self, form_data: dict):
@@ -267,6 +275,10 @@ class OrderListExporter(MultiSheetListExporter):
_('Status'),
_('Email'),
_('Order date'),
]
if self.event.has_subevents:
headers.append(pgettext('subevent', 'Date'))
headers += [
_('Product'),
_('Variation'),
_('Price'),
@@ -295,8 +307,9 @@ class OrderListExporter(MultiSheetListExporter):
for k, label, w in name_scheme['fields']:
headers.append(_('Invoice address name') + ': ' + str(label))
headers += [
_('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
_('Address'), _('ZIP code'), _('City'), _('Country'), pgettext('address', 'State'), _('VAT ID'),
]
headers.append(_('Sales channel'))
yield headers
@@ -308,6 +321,10 @@ class OrderListExporter(MultiSheetListExporter):
order.get_status_display(),
order.email,
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
]
if self.event.has_subevents:
row.append(op.subevent)
row += [
str(op.item),
str(op.variation) if op.variation else '',
op.price,
@@ -328,7 +345,12 @@ class OrderListExporter(MultiSheetListExporter):
]
acache = {}
for a in op.answers.all():
acache[a.question_id] = str(a)
# We do not want to localize Date, Time and Datetime question answers, as those can lead
# to difficulties parsing the data (for example 2019-02-01 may become Février, 2019 01 in French).
if a.question.type in Question.UNLOCALIZED_TYPES:
acache[a.question_id] = a.answer
else:
acache[a.question_id] = str(a)
for q in questions:
row.append(acache.get(q.pk, ''))
try:
@@ -347,10 +369,12 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.city,
order.invoice_address.country if order.invoice_address.country else
order.invoice_address.country_old,
order.invoice_address.state,
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
row += [''] * (7 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row += [''] * (8 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row.append(order.sales_channel)
yield row
def get_filename(self):
@@ -491,6 +515,7 @@ class InvoiceDataExporter(MultiSheetListExporter):
_('Invoice recipient:') + ' ' + _('ZIP code'),
_('Invoice recipient:') + ' ' + _('City'),
_('Invoice recipient:') + ' ' + _('Country'),
_('Invoice recipient:') + ' ' + pgettext('address', 'State'),
_('Invoice recipient:') + ' ' + _('VAT ID'),
_('Invoice recipient:') + ' ' + _('Beneficiary'),
_('Invoice recipient:') + ' ' + _('Internal reference'),
@@ -540,6 +565,7 @@ class InvoiceDataExporter(MultiSheetListExporter):
i.invoice_to_zipcode,
i.invoice_to_city,
i.invoice_to_country,
i.invoice_to_state,
i.invoice_to_vat_id,
i.invoice_to_beneficiary,
i.internal_reference,
@@ -579,6 +605,7 @@ class InvoiceDataExporter(MultiSheetListExporter):
_('Invoice recipient:') + ' ' + _('ZIP code'),
_('Invoice recipient:') + ' ' + _('City'),
_('Invoice recipient:') + ' ' + _('Country'),
_('Invoice recipient:') + ' ' + pgettext('address', 'State'),
_('Invoice recipient:') + ' ' + _('VAT ID'),
_('Invoice recipient:') + ' ' + _('Beneficiary'),
_('Invoice recipient:') + ' ' + _('Internal reference'),
@@ -618,6 +645,7 @@ class InvoiceDataExporter(MultiSheetListExporter):
i.invoice_to_zipcode,
i.invoice_to_city,
i.invoice_to_country,
i.invoice_to_state,
i.invoice_to_vat_id,
i.invoice_to_beneficiary,
i.internal_reference,

View File

@@ -14,7 +14,7 @@ class LoginForm(forms.Form):
Base class for authenticating users. Extend this to get a form that accepts
username/password logins.
"""
email = forms.EmailField(label=_("E-mail"), max_length=254)
email = forms.EmailField(label=_("E-mail"), max_length=254, widget=forms.EmailInput(attrs={'autofocus': 'autofocus'}))
password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
keep_logged_in = forms.BooleanField(label=_("Keep me logged in"), required=False)

View File

@@ -1,26 +1,40 @@
import copy
import json
import logging
from decimal import Decimal
from urllib.error import HTTPError
import dateutil.parser
import pycountry
import pytz
import vat_moss.errors
import vat_moss.id
from django import forms
from django.contrib import messages
from django.core.exceptions import ValidationError
from django.db.models import QuerySet
from django.forms import Select
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import (
get_language, pgettext_lazy, ugettext_lazy as _,
)
from django_countries import countries
from django_countries.fields import Country, CountryField
from pretix.base.forms.widgets import (
BusinessBooleanRadio, DatePickerWidget, SplitDateTimePickerWidget,
TimePickerWidget, UploadedFileWidget,
)
from pretix.base.models import InvoiceAddress, Question
from pretix.base.models.tax import EU_COUNTRIES
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.models import InvoiceAddress, Question, QuestionOption
from pretix.base.models.tax import EU_COUNTRIES, cc_to_vat_prefix
from pretix.base.settings import (
COUNTRIES_WITH_STATE_IN_ADDRESS, PERSON_NAME_SCHEMES,
PERSON_NAME_TITLE_GROUPS,
)
from pretix.base.templatetags.rich_text import rich_text
from pretix.control.forms import SplitDateTimeField
from pretix.helpers.escapejson import escapejson_attr
from pretix.helpers.i18n import get_format_without_seconds
from pretix.presale.signals import question_form_fields
@@ -29,15 +43,27 @@ logger = logging.getLogger(__name__)
class NamePartsWidget(forms.MultiWidget):
widget = forms.TextInput
autofill_map = {
'given_name': 'given-name',
'family_name': 'family-name',
'middle_name': 'additional-name',
'title': 'honorific-prefix',
'full_name': 'name',
'calling_name': 'nickname',
}
def __init__(self, scheme: dict, field: forms.Field, attrs=None):
def __init__(self, scheme: dict, field: forms.Field, attrs=None, titles: list=None):
widgets = []
self.scheme = scheme
self.field = field
self.titles = titles
for fname, label, size in self.scheme['fields']:
a = copy.copy(attrs) or {}
a['data-fname'] = fname
widgets.append(self.widget(attrs=a))
if fname == 'title' and self.titles:
widgets.append(Select(attrs=a, choices=[('', '')] + [(d, d) for d in self.titles[1]]))
else:
widgets.append(self.widget(attrs=a))
super().__init__(widgets, attrs)
def decompress(self, value):
@@ -71,6 +97,7 @@ class NamePartsWidget(forms.MultiWidget):
title=self.scheme['fields'][i][1],
placeholder=self.scheme['fields'][i][1],
)
final_attrs['autocomplete'] = (self.attrs.get('autocomplete', '') + ' ' + self.autofill_map.get(self.scheme['fields'][i][0], 'off')).strip()
final_attrs['data-size'] = self.scheme['fields'][i][2]
output.append(widget.render(name + '_%s' % i, widget_value, final_attrs, renderer=renderer))
return mark_safe(self.format_output(output))
@@ -96,19 +123,34 @@ class NamePartsFormField(forms.MultiValueField):
'max_length': kwargs.pop('max_length', None),
}
self.scheme_name = kwargs.pop('scheme')
self.titles = kwargs.pop('titles')
self.scheme = PERSON_NAME_SCHEMES.get(self.scheme_name)
if self.titles:
self.scheme_titles = PERSON_NAME_TITLE_GROUPS.get(self.titles)
else:
self.scheme_titles = None
self.one_required = kwargs.get('required', True)
require_all_fields = kwargs.pop('require_all_fields', False)
kwargs['required'] = False
kwargs['widget'] = (kwargs.get('widget') or self.widget)(
scheme=self.scheme, field=self, **kwargs.pop('widget_kwargs', {})
scheme=self.scheme, titles=self.scheme_titles, field=self, **kwargs.pop('widget_kwargs', {})
)
defaults.update(**kwargs)
for fname, label, size in self.scheme['fields']:
defaults['label'] = label
field = forms.CharField(**defaults)
field.part_name = fname
fields.append(field)
if fname == 'title' and self.scheme_titles:
d = dict(defaults)
d.pop('max_length', None)
field = forms.ChoiceField(
**d,
choices=[('', '')] + [(d, d) for d in self.scheme_titles[1]]
)
field.part_name = fname
fields.append(field)
else:
field = forms.CharField(**defaults)
field.part_name = fname
fields.append(field)
super().__init__(
fields=fields, require_all_fields=False, *args, **kwargs
)
@@ -144,6 +186,7 @@ class BaseQuestionsForm(forms.Form):
item = pos.item
questions = pos.item.questions_to_ask
event = kwargs.pop('event')
self.all_optional = kwargs.pop('all_optional', False)
super().__init__(*args, **kwargs)
@@ -152,6 +195,7 @@ class BaseQuestionsForm(forms.Form):
max_length=255,
required=event.settings.attendee_names_required,
scheme=event.settings.name_scheme,
titles=event.settings.name_scheme_titles,
label=_('Attendee name'),
initial=(cartpos.attendee_name_parts if cartpos else orderpos.attendee_name_parts),
)
@@ -159,7 +203,12 @@ class BaseQuestionsForm(forms.Form):
self.fields['attendee_email'] = forms.EmailField(
required=event.settings.attendee_emails_required,
label=_('Attendee email'),
initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email)
initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email),
widget=forms.EmailInput(
attrs={
'autocomplete': 'email'
}
)
)
for q in questions:
@@ -171,6 +220,8 @@ class BaseQuestionsForm(forms.Form):
initial = None
tz = pytz.timezone(event.settings.timezone)
help_text = rich_text(q.help_text)
label = escape(q.question) # django-bootstrap3 calls mark_safe
required = q.required and not self.all_optional
if q.type == Question.TYPE_BOOLEAN:
if q.required:
# For some reason, django-bootstrap3 does not set the required attribute
@@ -185,71 +236,81 @@ class BaseQuestionsForm(forms.Form):
initialbool = False
field = forms.BooleanField(
label=q.question, required=q.required,
label=label, required=required,
help_text=help_text,
initial=initialbool, widget=widget,
)
elif q.type == Question.TYPE_NUMBER:
field = forms.DecimalField(
label=q.question, required=q.required,
label=label, required=required,
help_text=q.help_text,
initial=initial.answer if initial else None,
min_value=Decimal('0.00'),
)
elif q.type == Question.TYPE_STRING:
field = forms.CharField(
label=q.question, required=q.required,
label=label, required=required,
help_text=help_text,
initial=initial.answer if initial else None,
)
elif q.type == Question.TYPE_TEXT:
field = forms.CharField(
label=q.question, required=q.required,
label=label, required=required,
help_text=help_text,
widget=forms.Textarea,
initial=initial.answer if initial else None,
)
elif q.type == Question.TYPE_COUNTRYCODE:
field = CountryField().formfield(
label=label, required=required,
help_text=help_text,
widget=forms.Select,
empty_label='',
initial=initial.answer if initial else None,
)
elif q.type == Question.TYPE_CHOICE:
field = forms.ModelChoiceField(
queryset=q.options,
label=q.question, required=q.required,
label=label, required=required,
help_text=help_text,
widget=forms.Select,
to_field_name='identifier',
empty_label='',
initial=initial.options.first() if initial else None,
)
elif q.type == Question.TYPE_CHOICE_MULTIPLE:
field = forms.ModelMultipleChoiceField(
queryset=q.options,
label=q.question, required=q.required,
label=label, required=required,
help_text=help_text,
to_field_name='identifier',
widget=forms.CheckboxSelectMultiple,
initial=initial.options.all() if initial else None,
)
elif q.type == Question.TYPE_FILE:
field = forms.FileField(
label=q.question, required=q.required,
label=label, required=required,
help_text=help_text,
initial=initial.file if initial else None,
widget=UploadedFileWidget(position=pos, event=event, answer=initial),
)
elif q.type == Question.TYPE_DATE:
field = forms.DateField(
label=q.question, required=q.required,
label=label, required=required,
help_text=help_text,
initial=dateutil.parser.parse(initial.answer).date() if initial and initial.answer else None,
widget=DatePickerWidget(),
)
elif q.type == Question.TYPE_TIME:
field = forms.TimeField(
label=q.question, required=q.required,
label=label, required=required,
help_text=help_text,
initial=dateutil.parser.parse(initial.answer).time() if initial and initial.answer else None,
widget=TimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
)
elif q.type == Question.TYPE_DATETIME:
field = SplitDateTimeField(
label=q.question, required=q.required,
label=label, required=required,
help_text=help_text,
initial=dateutil.parser.parse(initial.answer).astimezone(tz) if initial and initial.answer else None,
widget=SplitDateTimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
@@ -258,6 +319,15 @@ class BaseQuestionsForm(forms.Form):
if answers:
# Cache the answer object for later use
field.answer = answers[0]
if q.dependency_question_id:
field.widget.attrs['data-question-dependency'] = q.dependency_question_id
field.widget.attrs['data-question-dependency-values'] = escapejson_attr(json.dumps(q.dependency_values))
if q.type != 'M':
field.widget.attrs['required'] = q.required and not self.all_optional
field._required = q.required and not self.all_optional
field.required = False
self.fields['question_%s' % q.id] = field
responses = question_form_fields.send(sender=event, position=pos)
@@ -268,19 +338,71 @@ class BaseQuestionsForm(forms.Form):
self.fields[key] = value
value.initial = data.get('question_form_data', {}).get(key)
for k, v in self.fields.items():
if v.widget.attrs.get('autocomplete') or k == 'attendee_name_parts':
v.widget.attrs['autocomplete'] = 'section-{} '.format(self.prefix) + v.widget.attrs.get('autocomplete', '')
def clean(self):
d = super().clean()
question_cache = {f.question.pk: f.question for f in self.fields.values() if getattr(f, 'question', None)}
def question_is_visible(parentid, qvals):
parentq = question_cache[parentid]
if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id, parentq.dependency_values):
return False
if 'question_%d' % parentid not in d:
return False
dval = d.get('question_%d' % parentid)
return (
('True' in qvals and dval)
or ('False' in qvals and not dval)
or (isinstance(dval, QuestionOption) and dval.identifier in qvals)
or (isinstance(dval, (list, QuerySet)) and any(qval in [o.identifier for o in dval] for qval in qvals))
)
def question_is_required(q):
return (
q.required and
(not q.dependency_question_id or question_is_visible(q.dependency_question_id, q.dependency_values))
)
if not self.all_optional:
for q in question_cache.values():
if question_is_required(q) and not d.get('question_%d' % q.pk):
raise ValidationError({'question_%d' % q.pk: [_('This field is required')]})
return d
class BaseInvoiceAddressForm(forms.ModelForm):
vat_warning = False
class Meta:
model = InvoiceAddress
fields = ('is_business', 'company', 'name_parts', 'street', 'zipcode', 'city', 'country', 'vat_id',
'internal_reference', 'beneficiary')
fields = ('is_business', 'company', 'name_parts', 'street', 'zipcode', 'city', 'country', 'state',
'vat_id', 'internal_reference', 'beneficiary')
widgets = {
'is_business': BusinessBooleanRadio,
'street': forms.Textarea(attrs={'rows': 2, 'placeholder': _('Street and Number')}),
'street': forms.Textarea(attrs={
'rows': 2,
'placeholder': _('Street and Number'),
'autocomplete': 'street-address'
}),
'beneficiary': forms.Textarea(attrs={'rows': 3}),
'company': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
'country': forms.Select(attrs={
'autocomplete': 'country',
}),
'zipcode': forms.TextInput(attrs={
'autocomplete': 'postal-code',
}),
'city': forms.TextInput(attrs={
'autocomplete': 'address-level2',
}),
'company': forms.TextInput(attrs={
'data-display-dependency': '#id_is_business_1',
'autocomplete': 'organization',
}),
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
'internal_reference': forms.TextInput,
}
@@ -293,10 +415,58 @@ class BaseInvoiceAddressForm(forms.ModelForm):
self.request = kwargs.pop('request', None)
self.validate_vat_id = kwargs.pop('validate_vat_id')
self.all_optional = kwargs.pop('all_optional', False)
kwargs.setdefault('initial', {})
if not kwargs.get('instance') or not kwargs['instance'].country:
# Try to guess the initial country from either the country of the merchant
# or the locale. This will hopefully save at least some users some scrolling :)
locale = get_language()
country = event.settings.invoice_address_from_country
if not country:
valid_countries = countries.countries
if '-' in locale:
parts = locale.split('-')
if parts[1].upper() in valid_countries:
country = Country(parts[1].upper())
elif parts[0].upper() in valid_countries:
country = Country(parts[0].upper())
else:
if locale in valid_countries:
country = Country(locale.upper())
kwargs['initial']['country'] = country
super().__init__(*args, **kwargs)
if not event.settings.invoice_address_vatid:
del self.fields['vat_id']
c = [('', pgettext_lazy('address', 'Select state'))]
fprefix = self.prefix + '-' if self.prefix else ''
cc = None
if fprefix + 'country' in self.data:
cc = str(self.data[fprefix + 'country'])
elif 'country' in self.initial:
cc = str(self.initial['country'])
elif self.instance and self.instance.country:
cc = str(self.instance.country)
if cc and cc in COUNTRIES_WITH_STATE_IN_ADDRESS:
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc]
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
c += sorted([(s.code[3:], s.name) for s in statelist], key=lambda s: s[1])
elif fprefix + 'state' in self.data:
self.data = self.data.copy()
del self.data[fprefix + 'state']
self.fields['state'] = forms.ChoiceField(
label=pgettext_lazy('address', 'State'),
required=False,
choices=c,
widget=forms.Select(attrs={
'autocomplete': 'address-level1',
}),
)
self.fields['state'].widget.is_required = True
if not event.settings.invoice_address_required or self.all_optional:
for k, f in self.fields.items():
f.required = False
@@ -318,6 +488,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
max_length=255,
required=event.settings.invoice_name_required and not self.all_optional,
scheme=event.settings.name_scheme,
titles=event.settings.name_scheme_titles,
label=_('Name'),
initial=(self.instance.name_parts if self.instance else self.instance.name_parts),
)
@@ -329,6 +500,10 @@ class BaseInvoiceAddressForm(forms.ModelForm):
if not event.settings.invoice_address_beneficiary:
del self.fields['beneficiary']
for k, v in self.fields.items():
if v.widget.attrs.get('autocomplete') or k == 'name_parts':
v.widget.attrs['autocomplete'] = 'section-invoice billing ' + v.widget.attrs.get('autocomplete', '')
def clean(self):
data = self.cleaned_data
if not data.get('is_business'):
@@ -342,12 +517,22 @@ class BaseInvoiceAddressForm(forms.ModelForm):
if 'vat_id' in self.changed_data or not data.get('vat_id'):
self.instance.vat_id_validated = False
if data.get('city') and data.get('country') and str(data['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
if not data.get('state'):
self.add_error('state', _('This field is required.'))
self.instance.name_parts = data.get('name_parts')
if all(
not v for k, v in data.items() if k not in ('is_business', 'country', 'name_parts')
) and len(data.get('name_parts', {})) == 1:
# Do not save the country if it is the only field set -- we don't know the user even checked it!
self.cleaned_data['country'] = ''
if self.validate_vat_id and self.instance.vat_id_validated and 'vat_id' not in self.changed_data:
pass
elif self.validate_vat_id and data.get('is_business') and data.get('country') in EU_COUNTRIES and data.get('vat_id'):
if data.get('vat_id')[:2] != str(data.get('country')):
if data.get('vat_id')[:2] != cc_to_vat_prefix(str(data.get('country'))):
raise ValidationError(_('Your VAT ID does not match the selected country.'))
try:
result = vat_moss.id.validate(data.get('vat_id'))
@@ -365,7 +550,7 @@ 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:
except (vat_moss.errors.WebServiceError, HTTPError):
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:
@@ -378,9 +563,8 @@ class BaseInvoiceAddressForm(forms.ModelForm):
class BaseInvoiceNameForm(BaseInvoiceAddressForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for f in list(self.fields.keys()):
if f != 'name':
if f != 'name_parts':
del self.fields[f]

View File

@@ -115,5 +115,5 @@ class User2FADeviceAddForm(forms.Form):
name = forms.CharField(label=_('Device name'), max_length=64)
devicetype = forms.ChoiceField(label=_('Device type'), widget=forms.RadioSelect, choices=(
('totp', _('Smartphone with the Authenticator application')),
('u2f', _('U2F-compatible hardware token (e.g. Yubikey)')),
('webauthn', _('WebAuthn-compatible hardware token (e.g. Yubikey)')),
))

View File

@@ -26,7 +26,7 @@ class PlaceholderValidator(BaseValidator):
if value.count('{') != value.count('}'):
raise ValidationError(
_('Invalid placeholder syntax: You used a different number of "{" than of "}".'),
code='invalid',
code='invalid_placeholder_syntax',
)
data_placeholders = list(re.findall(r'({[^}]*})', value, re.X))
@@ -37,7 +37,7 @@ class PlaceholderValidator(BaseValidator):
if invalid_placeholders:
raise ValidationError(
_('Invalid placeholder(s): %(value)s'),
code='invalid',
code='invalid_placeholders',
params={'value': ", ".join(invalid_placeholders,)})
def clean(self, x):

View File

@@ -9,14 +9,17 @@ import vat_moss.exchange_rates
from django.contrib.staticfiles import finders
from django.dispatch import receiver
from django.utils.formats import date_format, localize
from django.utils.translation import pgettext, ugettext
from django.utils.translation import (
get_language, pgettext, ugettext, ugettext_lazy,
)
from PIL.Image import BICUBIC
from reportlab.lib import pagesizes
from reportlab.lib.enums import TA_LEFT
from reportlab.lib.enums import TA_LEFT, TA_RIGHT
from reportlab.lib.styles import ParagraphStyle, StyleSheet1
from reportlab.lib.units import mm
from reportlab.lib.utils import ImageReader
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.pdfmetrics import stringWidth
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import (
@@ -122,6 +125,7 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
"""
stylesheet = StyleSheet1()
stylesheet.add(ParagraphStyle(name='Normal', fontName=self.font_regular, fontSize=10, leading=12))
stylesheet.add(ParagraphStyle(name='InvoiceFrom', parent=stylesheet['Normal']))
stylesheet.add(ParagraphStyle(name='Heading1', fontName=self.font_bold, fontSize=15, leading=15 * 1.2))
stylesheet.add(ParagraphStyle(name='FineprintHeading', fontName=self.font_bold, fontSize=8, leading=12))
stylesheet.add(ParagraphStyle(name='Fineprint', fontName=self.font_regular, fontSize=8, leading=10))
@@ -138,6 +142,12 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
pdfmetrics.registerFontFamily('OpenSans', normal='OpenSans', bold='OpenSansBd',
italic='OpenSansIt', boldItalic='OpenSansBI')
def _upper(self, val):
# We uppercase labels, but not in every language
if get_language() == 'el':
return val
return val.upper()
def _on_other_page(self, canvas: Canvas, doc):
"""
Called when a new page is rendered that is *not* the first page.
@@ -248,52 +258,67 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
canvas.restoreState()
invoice_to_width = 85 * mm
invoice_to_height = 50 * mm
invoice_to_left = 25 * mm
invoice_to_top = 52 * mm
def _draw_invoice_to(self, canvas):
p = Paragraph(self.invoice.invoice_to.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p.wrapOn(canvas, 85 * mm, 50 * mm)
p_size = p.wrap(85 * mm, 50 * mm)
p.drawOn(canvas, 25 * mm, (297 - 52) * mm - p_size[1])
p = Paragraph(self.invoice.address_invoice_to.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height)
p_size = p.wrap(self.invoice_to_width, self.invoice_to_height)
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - p_size[1] - self.invoice_to_top)
invoice_from_width = 70 * mm
invoice_from_height = 50 * mm
invoice_from_left = 25 * mm
invoice_from_top = 17 * mm
def _draw_invoice_from(self, canvas):
p = Paragraph(self.invoice.full_invoice_from.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p.wrapOn(canvas, 70 * mm, 50 * mm)
p_size = p.wrap(70 * mm, 50 * mm)
p.drawOn(canvas, 25 * mm, (297 - 17) * mm - p_size[1])
def _on_first_page(self, canvas: Canvas, doc):
canvas.setCreator('pretix.eu')
canvas.setTitle(pgettext('invoice', 'Invoice {num}').format(num=self.invoice.number))
canvas.saveState()
canvas.setFont(self.font_regular, 8)
if self.invoice.order.testmode:
canvas.saveState()
canvas.setFont('OpenSansBd', 30)
canvas.setFillColorRGB(32, 0, 0)
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, ugettext('TEST MODE'))
canvas.restoreState()
for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]):
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip())
p = Paragraph(self.invoice.full_invoice_from.strip().replace('\n', '<br />\n'), style=self.stylesheet[
'InvoiceFrom'])
p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height)
p_size = p.wrap(self.invoice_from_width, self.invoice_from_height)
p.drawOn(canvas, self.invoice_from_left, self.pagesize[1] - p_size[1] - self.invoice_from_top)
def _draw_invoice_from_label(self, canvas):
textobject = canvas.beginText(25 * mm, (297 - 15) * mm)
textobject.setFont(self.font_bold, 8)
textobject.textLine(pgettext('invoice', 'Invoice from').upper())
textobject.textLine(self._upper(pgettext('invoice', 'Invoice from')))
canvas.drawText(textobject)
self._draw_invoice_from(canvas)
def _draw_invoice_to_label(self, canvas):
textobject = canvas.beginText(25 * mm, (297 - 50) * mm)
textobject.setFont(self.font_bold, 8)
textobject.textLine(pgettext('invoice', 'Invoice to').upper())
textobject.textLine(self._upper(pgettext('invoice', 'Invoice to')))
canvas.drawText(textobject)
self._draw_invoice_to(canvas)
logo_width = 25 * mm
logo_height = 25 * mm
logo_left = 95 * mm
logo_top = 13 * mm
logo_anchor = 'n'
def _draw_logo(self, canvas):
if self.invoice.event.settings.invoice_logo_image:
logo_file = self.invoice.event.settings.get('invoice_logo_image', binary_file=True)
ir = ThumbnailingImageReader(logo_file)
try:
ir.resize(self.logo_width, self.logo_height, 300)
except:
logger.exception("Can not resize image")
pass
canvas.drawImage(ir,
self.logo_left,
self.pagesize[1] - self.logo_height - self.logo_top,
width=self.logo_width, height=self.logo_height,
preserveAspectRatio=True, anchor=self.logo_anchor,
mask='auto')
def _draw_metadata(self, canvas):
textobject = canvas.beginText(125 * mm, (297 - 38) * mm)
textobject.setFont(self.font_bold, 8)
textobject.textLine(pgettext('invoice', 'Order code').upper())
textobject.textLine(self._upper(pgettext('invoice', 'Order code')))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.order.full_code)
@@ -302,18 +327,18 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
textobject = canvas.beginText(125 * mm, (297 - 50) * mm)
textobject.setFont(self.font_bold, 8)
if self.invoice.is_cancellation:
textobject.textLine(pgettext('invoice', 'Cancellation number').upper())
textobject.textLine(self._upper(pgettext('invoice', 'Cancellation number')))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.number)
textobject.moveCursor(0, 5)
textobject.setFont(self.font_bold, 8)
textobject.textLine(pgettext('invoice', 'Original invoice').upper())
textobject.textLine(self._upper(pgettext('invoice', 'Original invoice')))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.refers.number)
else:
textobject.textLine(pgettext('invoice', 'Invoice number').upper())
textobject.textLine(self._upper(pgettext('invoice', 'Invoice number')))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.number)
@@ -321,20 +346,20 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if self.invoice.is_cancellation:
textobject.setFont(self.font_bold, 8)
textobject.textLine(pgettext('invoice', 'Cancellation date').upper())
textobject.textLine(self._upper(pgettext('invoice', 'Cancellation date')))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(date_format(self.invoice.date, "DATE_FORMAT"))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_bold, 8)
textobject.textLine(pgettext('invoice', 'Original invoice date').upper())
textobject.textLine(self._upper(pgettext('invoice', 'Original invoice date')))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(date_format(self.invoice.refers.date, "DATE_FORMAT"))
textobject.moveCursor(0, 5)
else:
textobject.setFont(self.font_bold, 8)
textobject.textLine(pgettext('invoice', 'Invoice date').upper())
textobject.textLine(self._upper(pgettext('invoice', 'Invoice date')))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(date_format(self.invoice.date, "DATE_FORMAT"))
@@ -342,37 +367,37 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
canvas.drawText(textobject)
if self.invoice.event.settings.invoice_logo_image:
logo_file = self.invoice.event.settings.get('invoice_logo_image', binary_file=True)
ir = ThumbnailingImageReader(logo_file)
try:
ir.resize(25 * mm, 25 * mm, 300)
except:
logger.exception("Can not resize image")
pass
canvas.drawImage(ir,
95 * mm, (297 - 38) * mm,
width=25 * mm, height=25 * mm,
preserveAspectRatio=True, anchor='n',
mask='auto')
event_left = 125 * mm
event_top = 17 * mm
event_width = 65 * mm
event_height = 50 * mm
def _draw_event_label(self, canvas):
textobject = canvas.beginText(125 * mm, (297 - 15) * mm)
textobject.setFont(self.font_bold, 8)
textobject.textLine(self._upper(pgettext('invoice', 'Event')))
canvas.drawText(textobject)
def _draw_event(self, canvas):
def shorten(txt):
txt = str(txt)
p = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p_size = p.wrap(65 * mm, 50 * mm)
p_size = p.wrap(self.event_width, self.event_height)
while p_size[1] > 2 * self.stylesheet['Normal'].leading:
txt = ' '.join(txt.replace('', '').split()[:-1]) + ''
p = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p_size = p.wrap(65 * mm, 50 * mm)
p_size = p.wrap(self.event_width, self.event_height)
return txt
if not self.invoice.event.has_subevents:
if self.invoice.event.settings.show_date_to and self.invoice.event.date_to:
p_str = (
shorten(self.invoice.event.name) + '\n' + pgettext('invoice', '{from_date}\nuntil {to_date}').format(
shorten(self.invoice.event.name) + '\n' +
pgettext('invoice', '{from_date}\nuntil {to_date}').format(
from_date=self.invoice.event.get_date_from_display(),
to_date=self.invoice.event.get_date_to_display())
to_date=self.invoice.event.get_date_to_display()
)
)
else:
p_str = (
@@ -382,15 +407,38 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
p_str = shorten(self.invoice.event.name)
p = Paragraph(p_str.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p.wrapOn(canvas, 65 * mm, 50 * mm)
p_size = p.wrap(65 * mm, 50 * mm)
p.drawOn(canvas, 125 * mm, (297 - 17) * mm - p_size[1])
p.wrapOn(canvas, self.event_width, self.event_height)
p_size = p.wrap(self.event_width, self.event_height)
p.drawOn(canvas, self.event_left, self.pagesize[1] - self.event_top - p_size[1])
self._draw_event_label(canvas)
textobject = canvas.beginText(125 * mm, (297 - 15) * mm)
textobject.setFont(self.font_bold, 8)
textobject.textLine(pgettext('invoice', 'Event').upper())
canvas.drawText(textobject)
def _draw_footer(self, canvas):
canvas.setFont(self.font_regular, 8)
for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]):
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip())
def _draw_testmode(self, canvas):
if self.invoice.order.testmode:
canvas.saveState()
canvas.setFont('OpenSansBd', 30)
canvas.setFillColorRGB(32, 0, 0)
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, ugettext('TEST MODE'))
canvas.restoreState()
def _on_first_page(self, canvas: Canvas, doc):
canvas.setCreator('pretix.eu')
canvas.setTitle(pgettext('invoice', 'Invoice {num}').format(num=self.invoice.number))
canvas.saveState()
self._draw_footer(canvas)
self._draw_testmode(canvas)
self._draw_invoice_from_label(canvas)
self._draw_invoice_from(canvas)
self._draw_invoice_to_label(canvas)
self._draw_invoice_to(canvas)
self._draw_metadata(canvas)
self._draw_logo(canvas)
self._draw_event(canvas)
canvas.restoreState()
def _get_first_page_frames(self, doc):
@@ -428,6 +476,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
self.stylesheet['Normal']
))
if self.invoice.invoice_to_vat_id:
story.append(Paragraph(
pgettext('invoice', 'Customer VAT ID') + ':<br />' +
bleach.clean(self.invoice.invoice_to_vat_id, tags=[]).replace("\n", "<br />\n"),
self.stylesheet['Normal']
))
if self.invoice.invoice_to_beneficiary:
story.append(Paragraph(
pgettext('invoice', 'Beneficiary') + ':<br />' +
@@ -548,6 +603,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
colwidths = [a * doc.width for a in (.25, .15, .15, .15, .3)]
table = Table(tdata, colWidths=colwidths, repeatRows=2, hAlign=TA_LEFT)
table.setStyle(TableStyle(tstyledata))
story.append(Spacer(5 * mm, 5 * mm))
story.append(KeepTogether([
Paragraph(pgettext('invoice', 'Included taxes'), self.stylesheet['FineprintHeading']),
table
@@ -601,6 +657,114 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
return story
class Modern1Renderer(ClassicInvoiceRenderer):
identifier = 'modern1'
verbose_name = ugettext_lazy('Modern Invoice Renderer (pretix 2.7)')
bottom_margin = 16.9 * mm
top_margin = 16.9 * mm
right_margin = 20 * mm
invoice_to_height = 27.3 * mm
invoice_to_width = 80 * mm
invoice_to_left = 25 * mm
invoice_to_top = (40 + 17.7) * mm
invoice_from_left = 125 * mm
invoice_from_top = 50 * mm
invoice_from_width = pagesizes.A4[0] - invoice_from_left - right_margin
invoice_from_height = 50 * mm
logo_width = 75 * mm
logo_height = 25 * mm
logo_left = pagesizes.A4[0] - logo_width - right_margin
logo_top = top_margin
logo_anchor = 'e'
event_left = 25 * mm
event_top = top_margin
event_width = 80 * mm
event_height = 25 * mm
def _get_stylesheet(self):
stylesheet = super()._get_stylesheet()
stylesheet.add(ParagraphStyle(name='Sender', fontName=self.font_regular, fontSize=8, leading=10))
stylesheet['InvoiceFrom'].alignment = TA_RIGHT
return stylesheet
def _draw_invoice_from(self, canvas):
if not self.invoice.invoice_from:
return
c = self.invoice.address_invoice_from.strip().split('\n')
p = Paragraph(' · '.join(c), style=self.stylesheet['Sender'])
p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm)
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm)
super()._draw_invoice_from(canvas)
def _draw_invoice_to_label(self, canvas):
pass
def _draw_invoice_from_label(self, canvas):
pass
def _draw_event_label(self, canvas):
pass
def _get_first_page_frames(self, doc):
footer_length = 3.5 * len(self.invoice.footer_text.split('\n')) * mm
return [
Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height - 95 * mm,
leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=footer_length,
id='normal')
]
def _draw_metadata(self, canvas):
begin_top = 100 * mm
textobject = canvas.beginText(self.left_margin, self.pagesize[1] - begin_top)
textobject.setFont(self.font_regular, 8)
textobject.textLine(pgettext('invoice', 'Order code'))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.order.full_code)
canvas.drawText(textobject)
if self.invoice.is_cancellation:
textobject = canvas.beginText(self.left_margin + 50 * mm, self.pagesize[1] - begin_top)
textobject.setFont(self.font_regular, 8)
textobject.textLine(pgettext('invoice', 'Cancellation number'))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.number)
canvas.drawText(textobject)
textobject = canvas.beginText(self.left_margin + 100 * mm, self.pagesize[1] - begin_top)
textobject.setFont(self.font_regular, 8)
textobject.textLine(pgettext('invoice', 'Original invoice'))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.refers.number)
canvas.drawText(textobject)
else:
textobject = canvas.beginText(self.left_margin + 70 * mm, self.pagesize[1] - begin_top)
textobject.textLine(pgettext('invoice', 'Invoice number'))
textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.number)
canvas.drawText(textobject)
p = Paragraph(date_format(self.invoice.date, "DATE_FORMAT"), style=self.stylesheet['Normal'])
w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize)
p.wrapOn(canvas, w, 15 * mm)
date_x = self.pagesize[0] - w - self.right_margin
p.drawOn(canvas, date_x, self.pagesize[1] - begin_top - 10 - 6)
textobject = canvas.beginText(date_x, self.pagesize[1] - begin_top)
textobject.setFont(self.font_regular, 8)
if self.invoice.is_cancellation:
textobject.textLine(pgettext('invoice', 'Cancellation date'))
else:
textobject.textLine(pgettext('invoice', 'Invoice date'))
canvas.drawText(textobject)
@receiver(register_invoice_renderers, dispatch_uid="invoice_renderer_classic")
def recv_classic(sender, **kwargs):
return ClassicInvoiceRenderer
return [ClassicInvoiceRenderer, Modern1Renderer]

View File

@@ -0,0 +1,58 @@
import json
import sys
from django.core.management.base import BaseCommand
from django.utils.timezone import override
from django_scopes import scope
from pretix.base.i18n import language
from pretix.base.models import Event, Organizer
from pretix.base.signals import register_data_exporters
class Command(BaseCommand):
help = "Run an exporter to get data out of pretix"
def add_arguments(self, parser):
parser.add_argument('organizer_slug', nargs=1, type=str)
parser.add_argument('event_slug', nargs=1, type=str)
parser.add_argument('export_provider', nargs=1, type=str)
parser.add_argument('output_file', nargs=1, type=str)
parser.add_argument('--parameters', action='store', type=str, help='JSON-formatted parameters')
def handle(self, *args, **options):
try:
o = Organizer.objects.get(slug=options['organizer_slug'][0])
except Organizer.DoesNotExist:
self.stderr.write(self.style.ERROR('Organizer not found.'))
sys.exit(1)
with scope(organizer=o):
try:
e = o.events.get(slug=options['event_slug'][0])
except Event.DoesNotExist:
self.stderr.write(self.style.ERROR('Event not found.'))
sys.exit(1)
with language(e.settings.locale), override(e.settings.timezone):
responses = register_data_exporters.send(e)
for receiver, response in responses:
ex = response(e)
if ex.identifier == options['export_provider'][0]:
params = json.loads(options.get('parameters') or '{}')
with open(options['output_file'][0], 'wb') as f:
try:
ex.render(form_data=params, output_file=f)
except TypeError:
self.stderr.write(self.style.WARNING(
'Provider does not support direct file writing, need to buffer export in memory.'))
d = ex.render(form_data=params)
if d is None:
self.stderr.write(self.style.ERROR('Empty export.'))
sys.exit(2)
f.write(d[2])
sys.exit(0)
self.stderr.write(self.style.ERROR('Export provider not found.'))
sys.exit(1)

View File

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

View File

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

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