Compare commits

..

277 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
280 changed files with 88493 additions and 60225 deletions

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

@@ -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

@@ -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
@@ -215,7 +228,10 @@ Endpoints
"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
@@ -342,6 +364,11 @@ Order position endpoints
``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
@@ -400,7 +427,8 @@ Order position endpoints
"checkins": [
{
"list": 1,
"datetime": "2017-12-25T12:45:23Z"
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": true
}
],
"answers": [
@@ -510,7 +538,8 @@ Order position endpoints
"checkins": [
{
"list": 1,
"datetime": "2017-12-25T12:45:23Z"
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": true
}
],
"answers": [

View File

@@ -44,6 +44,9 @@ available_from datetime The first date
(or ``null``).
available_until datetime The last date time at which this item can be bought
(or ``null``).
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
@@ -72,6 +75,8 @@ generate_tickets boolean If ``false``, t
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.
@@ -146,7 +151,7 @@ bundles list of objects Definition of b
.. versionchanged:: 3.0
The ``show_quota_left`` attribute has been added.
The ``show_quota_left``, ``allow_waitinglist``, and ``hidden_if_available`` attributes have been added.
Notes
-----
@@ -205,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,
@@ -213,6 +219,7 @@ Endpoints
"checkin_attention": false,
"has_variations": false,
"generate_tickets": null,
"allow_waitinglist": true,
"show_quota_left": null,
"require_approval": false,
"require_bundling": false,
@@ -297,10 +304,12 @@ 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,
@@ -370,10 +379,12 @@ 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,
@@ -430,12 +441,14 @@ 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,
@@ -522,9 +535,11 @@ 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,

View File

@@ -53,7 +53,9 @@ 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
@@ -82,6 +84,7 @@ require_approval boolean If ``true`` and
needs approval by an organizer before it can
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
@@ -205,6 +215,10 @@ pdf_data object Data object req
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
@@ -221,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
---------------------
@@ -288,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",
@@ -310,7 +339,8 @@ List of all orders
"street": "Test street 12",
"zipcode": "12345",
"city": "Testington",
"country": "Testikistan",
"country": "DE",
"state": "",
"internal_reference": "",
"vat_id": "EU123456789",
"vat_id_validated": false
@@ -340,7 +370,8 @@ List of all orders
"checkins": [
{
"list": 44,
"datetime": "2017-12-25T12:45:23Z"
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}
],
"answers": [
@@ -373,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"
}
],
@@ -431,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",
@@ -453,7 +487,8 @@ Fetching individual orders
"street": "Test street 12",
"zipcode": "12345",
"city": "Testington",
"country": "Testikistan",
"country": "DE",
"state": "",
"internal_reference": "",
"vat_id": "EU123456789",
"vat_id_validated": false
@@ -483,7 +518,8 @@ Fetching individual orders
"checkins": [
{
"list": 44,
"datetime": "2017-12-25T12:45:23Z"
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}
],
"answers": [
@@ -516,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"
}
],
@@ -691,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
---------------
@@ -716,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:
@@ -750,9 +784,9 @@ Creating orders
* ``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
@@ -770,17 +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)
@@ -800,6 +839,8 @@ Creating orders
* ``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
@@ -837,6 +878,7 @@ Creating orders
"zipcode": "12345",
"city": "Sample City",
"country": "UK",
"state": "",
"internal_reference": "",
"vat_id": ""
},
@@ -860,7 +902,7 @@ Creating orders
],
"subevent": null
}
],
]
}
**Example response**:
@@ -1251,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/
@@ -1302,7 +1349,8 @@ List of all order positions
"checkins": [
{
"list": 44,
"datetime": "2017-12-25T12:45:23Z"
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}
],
"answers": [
@@ -1403,7 +1451,8 @@ Fetching individual positions
"checkins": [
{
"list": 44,
"datetime": "2017-12-25T12:45:23Z"
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}
],
"answers": [
@@ -1546,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"
}
]
@@ -1586,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

@@ -41,6 +41,8 @@ ask_during_checkin boolean If ``true``, th
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.
@@ -80,6 +82,10 @@ dependency_value string An old version
The attribute ``dependency_values`` has been added.
.. versionchanged:: 3.1
The attribute ``print_on_invoice`` has been added.
Endpoints
---------
@@ -123,6 +129,7 @@ Endpoints
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"dependency_question": null,
"dependency_value": null,
"dependency_values": [],
@@ -192,6 +199,7 @@ Endpoints
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"dependency_question": null,
"dependency_value": null,
"dependency_values": [],
@@ -245,6 +253,7 @@ Endpoints
"position": 1,
"ask_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"dependency_question": null,
"dependency_values": [],
"options": [
@@ -279,6 +288,7 @@ Endpoints
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"dependency_question": null,
"dependency_value": null,
"dependency_values": [],
@@ -352,6 +362,7 @@ Endpoints
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"dependency_question": null,
"dependency_value": null,
"dependency_values": [],

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, register_global_settings, quota_availability
item_copy_data, register_sales_channels, register_global_settings, quota_availability, global_email_filter
Order events
""""""""""""
@@ -49,7 +49,7 @@ 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, nav_item, subevent_forms
order_info, event_settings_widget, oauth_application_registered, order_position_buttons, subevent_forms, item_formsets
.. automodule:: pretix.base.signals

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

@@ -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

@@ -65,7 +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.
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

@@ -36,6 +36,8 @@ eu
filename
filesystem
fontawesome
formset
formsets
frontend
frontpage
gettext
@@ -44,6 +46,7 @@ guid
hardcoded
hostname
idempotency
iframe
incrementing
inofficial
invalidations
@@ -102,6 +105,7 @@ screenshot
scss
searchable
selectable
serializable
serializers
serializers
sexualized
@@ -135,6 +139,7 @@ versa
versioning
viewset
viewsets
waitinglist
webhook
webhooks
webserver

View File

@@ -45,8 +45,8 @@ In addition, you will need quotas. If you do not care how many of your tickets a
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
--------------------------
Use case: Early-bird tiers based on dates
-----------------------------------------
Let's say you run a conference that has the following pricing scheme:
@@ -58,9 +58,53 @@ Of course, you could just set up one product and change its price at the given d
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.
.. note::
Use case: Early-bird tiers based on ticket numbers
--------------------------------------------------
pretix currently can't do early-bird tiers based on **ticket number** instead of time. We're planning this feature for later in 2019. For now, you'll need to monitor that manually.
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
-------------------------------------

View File

@@ -274,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

@@ -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 +1 @@
__version__ = "2.9.0.dev0"
__version__ = "3.2.0"

View File

@@ -82,6 +82,8 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
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():

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

@@ -119,7 +119,7 @@ class ItemSerializer(I18nAwareModelSerializer):
'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')
'show_quota_left', 'hidden_if_available', 'allow_waitinglist')
read_only_fields = ('has_variations', 'picture')
def get_serializer_context(self):
@@ -219,7 +219,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
model = Question
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
'hidden', 'dependency_value')
'hidden', 'dependency_value', 'print_on_invoice')
def validate_identifier(self, value):
Question._clean_identifier(self.context['event'], value, self.instance)

View File

@@ -1,6 +1,9 @@
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,13 +17,17 @@ from pretix.base.channels import get_all_sales_channels
from pretix.base.i18n import language
from pretix.base.models import (
Checkin, Invoice, InvoiceAddress, InvoiceLine, Item, ItemVariation, Order,
OrderPosition, Question, QuestionAnswer, Seat, SubEvent,
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):
@@ -41,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)
@@ -57,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
@@ -89,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):
@@ -261,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):
@@ -275,6 +323,14 @@ 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(allow_null=True)
positions = OrderPositionSerializer(many=True, read_only=True)
@@ -284,13 +340,15 @@ class OrderSerializer(I18nAwareModelSerializer):
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'
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
'url'
)
read_only_fields = (
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
@@ -329,7 +387,7 @@ class OrderSerializer(I18nAwareModelSerializer):
}
try:
ia = instance.invoice_address
if iadata.get('vat_id') != ia.vat_id:
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:
@@ -437,11 +495,15 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
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', 'seat')
'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():
@@ -515,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),
@@ -527,18 +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', 'payment_date', 'consume_carts', 'force')
'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
@@ -608,10 +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')
@@ -630,8 +701,11 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
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:
@@ -639,6 +713,8 @@ 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:
if cp.seat:
free_seats.add(cp.seat)
@@ -646,8 +722,55 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
errs = [{} for p in positions_data]
for i, pos_data in enumerate(positions_data):
if pos_data.get('voucher'):
v = pos_data['voucher']
if not v.applies_to(pos_data['item'], pos_data.get('variation')):
errs[i]['voucher'] = [error_messages['voucher_invalid_item']]
continue
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')))
@@ -669,23 +792,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
)
]
for i, pos_data in enumerate(positions_data):
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 any(errs):
raise ValidationError({'positions': errs})
@@ -693,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=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
)
if ia:
ia.order = order
ia.save()
pos_map = {}
for pos_data in positions_data:
answers_data = pos_data.pop('answers', [])
@@ -736,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:
@@ -748,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

@@ -44,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):

View File

@@ -41,7 +41,8 @@ from pretix.base.services.invoices import (
)
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
@@ -431,6 +432,7 @@ class OrderViewSet(viewsets.ModelViewSet):
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)
@@ -445,8 +447,42 @@ class OrderViewSet(viewsets.ModelViewSet):
(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)

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,15 +1,23 @@
import inspect
import logging
from datetime import timedelta
from decimal import Decimal
from smtplib import SMTPResponseException
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, OrderPosition
from pretix.base.signals import register_html_mail_renderers
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')
@@ -44,8 +52,8 @@ class BaseHTMLMailRenderer:
def __str__(self):
return self.identifier
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order=None,
position: OrderPosition=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.
@@ -97,7 +105,7 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
def template_name(self):
raise NotImplementedError()
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order, position: OrderPosition) -> str:
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,
@@ -136,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

@@ -111,7 +111,7 @@ class ListExporter(BaseExporter):
raise NotImplementedError() # noqa
def get_filename(self):
return 'export.csv'
return 'export'
def _render_csv(self, form_data, output_file=None, **kwargs):
if output_file:

View File

@@ -129,8 +129,11 @@ class DekodiNREIExporter(BaseExporter):
'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],
'FN': invoice.invoice_to_name.rsplit(' ', 1)[0] if ' ' in invoice.invoice_to_name else '',
'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,

View File

@@ -9,7 +9,7 @@ from django.utils.formats import date_format, localize
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')
]
@@ -109,6 +109,8 @@ class OrderListExporter(MultiSheetListExporter):
headers.append(_('Invoice numbers'))
headers.append(_('Sales channel'))
headers.append(_('Requires special attention'))
headers.append(_('Comment'))
yield headers
@@ -153,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 '',
@@ -178,6 +181,8 @@ 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):
@@ -208,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
@@ -243,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):
@@ -301,7 +307,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'),
]
headers.append(_('Sales channel'))
@@ -339,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:
@@ -358,10 +369,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.append(order.sales_channel)
yield row
@@ -503,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'),
@@ -552,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,
@@ -591,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'),
@@ -630,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

@@ -5,6 +5,7 @@ from decimal import Decimal
from urllib.error import HTTPError
import dateutil.parser
import pycountry
import pytz
import vat_moss.errors
import vat_moss.id
@@ -15,7 +16,9 @@ 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 get_language, 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
@@ -24,8 +27,11 @@ from pretix.base.forms.widgets import (
TimePickerWidget, UploadedFileWidget,
)
from pretix.base.models import InvoiceAddress, Question, QuestionOption
from pretix.base.models.tax import EU_COUNTRIES
from pretix.base.settings import PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS
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
@@ -37,6 +43,14 @@ 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, titles: list=None):
widgets = []
@@ -83,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))
@@ -188,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:
@@ -318,6 +338,10 @@ 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()
@@ -356,13 +380,29 @@ class BaseInvoiceAddressForm(forms.ModelForm):
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,
}
@@ -400,6 +440,33 @@ class BaseInvoiceAddressForm(forms.ModelForm):
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
@@ -433,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'):
@@ -446,6 +517,10 @@ 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(
@@ -457,7 +532,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
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'))
@@ -488,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

@@ -1,3 +1,4 @@
from django.conf import settings
from django.core.management import call_command
from django.core.management.base import BaseCommand
@@ -8,5 +9,12 @@ class Command(BaseCommand):
help = "Run periodic tasks"
def handle(self, *args, **options):
periodic_task.send(self)
for recv, resp in periodic_task.send_robust(self):
if isinstance(resp, Exception):
if settings.SENTRY_ENABLED:
from sentry_sdk import capture_exception
capture_exception(resp)
else:
raise resp
call_command('clearsessions')

View File

@@ -0,0 +1,21 @@
# Generated by Django 2.2.1 on 2019-07-24 15:48
import django.db.models.deletion
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0128_auto_20190715_1510'),
]
operations = [
migrations.AddField(
model_name='item',
name='hidden_if_available',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='pretixbase.Quota'),
),
]

View File

@@ -0,0 +1,31 @@
# Generated by Django 2.2.1 on 2019-07-29 13:11
import django.db.models.deletion
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0129_auto_20190724_1548'),
]
operations = [
migrations.AddField(
model_name='seat',
name='row_name',
field=models.CharField(default='', max_length=190),
),
migrations.AddField(
model_name='seat',
name='seat_number',
field=models.CharField(default='', max_length=190),
),
migrations.AddField(
model_name='seat',
name='zone_name',
field=models.CharField(default='', max_length=190),
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 2.2.1 on 2019-07-29 14:22
import django.db.models.deletion
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0130_auto_20190729_1311'),
]
operations = [
migrations.AddField(
model_name='item',
name='allow_waitinglist',
field=models.BooleanField(default=True),
),
]

View File

@@ -0,0 +1,27 @@
# Generated by Django 2.2.1 on 2019-08-08 12:53
import django.db.models.deletion
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0131_auto_20190729_1422'),
]
operations = [
migrations.AddField(
model_name='invoice',
name='invoice_to_state',
field=models.CharField(max_length=190, null=True),
),
migrations.AddField(
model_name='invoiceaddress',
name='state',
field=models.CharField(default='', max_length=255),
preserve_default=False,
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 2.2.4 on 2019-08-30 15:13
import django.db.models.deletion
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0132_auto_20190808_1253'),
]
operations = [
migrations.AddField(
model_name='question',
name='print_on_invoice',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,35 @@
# Generated by Django 2.2.4 on 2019-09-09 10:42
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0133_auto_20190830_1513'),
]
operations = [
migrations.CreateModel(
name='WebAuthnDevice',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('name', models.CharField(max_length=64)),
('confirmed', models.BooleanField(default=True)),
('credential_id', models.CharField(max_length=255, null=True)),
('rp_id', models.CharField(max_length=255, null=True)),
('icon_url', models.CharField(max_length=255, null=True)),
('ukey', models.TextField(null=True)),
('pub_key', models.TextField(null=True)),
('sign_count', models.IntegerField(default=0)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
]

View File

@@ -0,0 +1,27 @@
# Generated by Django 2.2.4 on 2019-10-07 08:03
from django.core.cache import cache
from django.db import migrations
def mail_migrator(app, schema_editor):
Event_SettingsStore = app.get_model('pretixbase', 'Event_SettingsStore')
for ss in Event_SettingsStore.objects.filter(
key__in=['mail_text_order_approved', 'mail_text_order_placed', 'mail_text_order_placed_require_approval']
):
chgd = ss.value.replace("{date}", "{expire_date}")
if chgd != ss.value:
ss.value = chgd
ss.save()
cache.delete('hierarkey_{}_{}'.format('event', ss.object_id))
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0134_auto_20190909_1042'),
]
operations = [
migrations.RunPython(mail_migrator, migrations.RunPython.noop)
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 2.2 on 2019-09-18 17:42
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0135_auto_20191007_0803'),
]
operations = [
migrations.AddField(
model_name='checkin',
name='auto_checked_in',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='checkinlist',
name='auto_checkin_sales_channels',
field=pretix.base.models.fields.MultiStringField(default=[]),
)
]

View File

@@ -1,5 +1,5 @@
from ..settings import GlobalSettingsObject_SettingsStore
from .auth import U2FDevice, User
from .auth import U2FDevice, User, WebAuthnDevice
from .base import CachedFile, LoggedModel, cachedfile_name
from .checkin import Checkin, CheckinList
from .devices import Device

View File

@@ -1,5 +1,9 @@
import binascii
import json
from datetime import timedelta
from urllib.parse import urlparse
import webauthn
from django.conf import settings
from django.contrib.auth.models import (
AbstractBaseUser, BaseUserManager, PermissionsMixin,
@@ -13,6 +17,9 @@ from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from django_otp.models import Device
from django_scopes import scopes_disabled
from u2flib_server.utils import (
pub_key_from_der, websafe_decode, websafe_encode,
)
from pretix.base.i18n import language
from pretix.helpers.urls import build_absolute_uri
@@ -176,6 +183,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
'url': build_absolute_uri('control:user.settings')
},
event=None,
user=self,
locale=self.locale
)
except SendMailException:
@@ -191,9 +199,13 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
'url': (build_absolute_uri('control:auth.forgot.recover')
+ '?id=%d&token=%s' % (self.id, default_token_generator.make_token(self)))
},
None, locale=self.locale
None, locale=self.locale, user=self
)
@property
def top_logentries(self):
return self.all_logentries
@property
def all_logentries(self):
from pretix.base.models import LogEntry
@@ -375,3 +387,49 @@ class StaffSessionAuditLog(models.Model):
class U2FDevice(Device):
json_data = models.TextField()
@property
def webauthnuser(self):
d = json.loads(self.json_data)
# We manually need to convert the pubkey from DER format (used in our
# former U2F implementation) to the format required by webauthn. This
# is based on the following example:
# https://www.w3.org/TR/webauthn/#sctn-encoded-credPubKey-examples
pub_key = pub_key_from_der(websafe_decode(d['publicKey'].replace('+', '-').replace('/', '_')))
pub_key = binascii.unhexlify(
'A5010203262001215820{:064x}225820{:064x}'.format(
pub_key.public_numbers().x, pub_key.public_numbers().y
)
)
return webauthn.WebAuthnUser(
d['keyHandle'],
self.user.email,
str(self.user),
settings.SITE_URL,
d['keyHandle'],
websafe_encode(pub_key),
1,
urlparse(settings.SITE_URL).netloc
)
class WebAuthnDevice(Device):
credential_id = models.CharField(max_length=255, null=True, blank=True)
rp_id = models.CharField(max_length=255, null=True, blank=True)
icon_url = models.CharField(max_length=255, null=True, blank=True)
ukey = models.TextField(null=True)
pub_key = models.TextField(null=True)
sign_count = models.IntegerField(default=0)
@property
def webauthnuser(self):
return webauthn.WebAuthnUser(
self.ukey,
self.user.email,
str(self.user),
settings.SITE_URL,
self.credential_id,
self.pub_key,
self.sign_count,
urlparse(settings.SITE_URL).netloc
)

View File

@@ -1,11 +1,11 @@
from django.db import models
from django.db.models import Case, Count, F, OuterRef, Q, Subquery, When
from django.db.models.functions import Coalesce
from django.db.models import Exists, OuterRef
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_scopes import ScopedManager
from pretix.base.models import LoggedModel
from pretix.base.models.fields import MultiStringField
class CheckinList(LoggedModel):
@@ -18,142 +18,69 @@ class CheckinList(LoggedModel):
include_pending = models.BooleanField(verbose_name=pgettext_lazy('checkin', 'Include pending orders'),
default=False,
help_text=_('With this option, people will be able to check in even if the '
'order have not been paid. This only works with pretixdesk '
'0.3.0 or newer or pretixdroid 1.9 or newer.'))
'order have not been paid.'))
auto_checkin_sales_channels = MultiStringField(
default=[],
blank=True,
verbose_name=_('Sales channels to automatically check in'),
help_text=_('All items on this check-in list will be automatically marked as checked-in when purchased through '
'any of the selected sales channels. This option can be useful when tickets sold at the box office '
'are not checked again before entry and should be considered validated directly upon purchase.')
)
objects = ScopedManager(organizer='event__organizer')
class Meta:
ordering = ('subevent__date_from', 'name')
@property
def positions(self):
from . import OrderPosition, Order
qs = OrderPosition.objects.filter(
order__event=self.event,
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.include_pending else [Order.STATUS_PAID],
subevent=self.subevent
)
if not self.all_products:
qs = qs.filter(item__in=self.limit_products.values_list('id', flat=True))
return qs
@property
def checkin_count(self):
return self.event.cache.get_or_set(
'checkin_list_{}_checkin_count'.format(self.pk),
lambda: self.positions.annotate(
checkedin=Exists(Checkin.objects.filter(list_id=self.pk, position=OuterRef('pk')))
).filter(
checkedin=True
).count(),
60
)
@property
def percent(self):
pc = self.position_count
return round(self.checkin_count * 100 / pc) if pc else 0
@property
def position_count(self):
return self.event.cache.get_or_set(
'checkin_list_{}_position_count'.format(self.pk),
lambda: self.positions.count(),
60
)
def touch(self):
self.event.cache.delete('checkin_list_{}_position_count'.format(self.pk))
self.event.cache.delete('checkin_list_{}_checkin_count'.format(self.pk))
@staticmethod
def annotate_with_numbers(qs, event):
"""
Modifies a queryset of checkin lists by annotating it with the number of order positions and
checkins associated with it.
"""
# Import here to prevent circular import
from . import Order, OrderPosition, Item
# This is the mother of all subqueries. Sorry. I try to explain it, at least?
# First, we prepare a subquery that for every check-in that belongs to a paid-order
# position and to the list in question. Then, we check that it also belongs to the
# correct subevent (just to be sure) and aggregate over lists (so, over everything,
# since we filtered by lists).
cqs_paid = Checkin.objects.filter(
position__order__event=event,
position__order__status=Order.STATUS_PAID,
list=OuterRef('pk')
).filter(
# This assumes that in an event with subevents, *all* positions have subevents
# and *all* checkin lists have a subevent assigned
Q(position__subevent=OuterRef('subevent'))
| (Q(position__subevent__isnull=True))
).order_by().values('list').annotate(
c=Count('*')
).values('c')
cqs_paid_and_pending = Checkin.objects.filter(
position__order__event=event,
position__order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING],
list=OuterRef('pk')
).filter(
# This assumes that in an event with subevents, *all* positions have subevents
# and *all* checkin lists have a subevent assigned
Q(position__subevent=OuterRef('subevent'))
| (Q(position__subevent__isnull=True))
).order_by().values('list').annotate(
c=Count('*')
).values('c')
# Now for the hard part: getting all order positions that contribute to this list. This
# requires us to use TWO subqueries. The first one, pqs_all, will only be used for check-in
# lists that contain all the products of the event. This is the simpler one, it basically
# looks like the check-in counter above.
pqs_all_paid = OrderPosition.objects.filter(
order__event=event,
order__status=Order.STATUS_PAID,
).filter(
# This assumes that in an event with subevents, *all* positions have subevents
# and *all* checkin lists have a subevent assigned
Q(subevent=OuterRef('subevent'))
| (Q(subevent__isnull=True))
).order_by().values('order__event').annotate(
c=Count('*')
).values('c')
pqs_all_paid_and_pending = OrderPosition.objects.filter(
order__event=event,
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING]
).filter(
# This assumes that in an event with subevents, *all* positions have subevents
# and *all* checkin lists have a subevent assigned
Q(subevent=OuterRef('subevent'))
| (Q(subevent__isnull=True))
).order_by().values('order__event').annotate(
c=Count('*')
).values('c')
# Now we need a subquery for the case of checkin lists that are limited to certain
# products. We cannot use OuterRef("limit_products") since that would do a cross-product
# with the products table and we'd get duplicate rows in the output with different annotations
# on them, which isn't useful at all. Therefore, we need to add a second layer of subqueries
# to retrieve all of those items and then check if the item_id is IN this subquery result.
pqs_limited_paid = OrderPosition.objects.filter(
order__event=event,
order__status=Order.STATUS_PAID,
item_id__in=Subquery(Item.objects.filter(checkinlist__pk=OuterRef(OuterRef('pk'))).values('pk'))
).filter(
# This assumes that in an event with subevents, *all* positions have subevents
# and *all* checkin lists have a subevent assigned
Q(subevent=OuterRef('subevent'))
| (Q(subevent__isnull=True))
).order_by().values('order__event').annotate(
c=Count('*')
).values('c')
pqs_limited_paid_and_pending = OrderPosition.objects.filter(
order__event=event,
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING],
item_id__in=Subquery(Item.objects.filter(checkinlist__pk=OuterRef(OuterRef('pk'))).values('pk'))
).filter(
# This assumes that in an event with subevents, *all* positions have subevents
# and *all* checkin lists have a subevent assigned
Q(subevent=OuterRef('subevent'))
| (Q(subevent__isnull=True))
).order_by().values('order__event').annotate(
c=Count('*')
).values('c')
# Finally, we put all of this together. We force empty subquery aggregates to 0 by using Coalesce()
# and decide which subquery to use for this row. In the end, we compute an integer percentage in case
# we want to display a progress bar.
return qs.annotate(
checkin_count=Coalesce(
Case(
When(include_pending=True, then=Subquery(cqs_paid_and_pending, output_field=models.IntegerField())),
default=Subquery(cqs_paid, output_field=models.IntegerField()),
output_field=models.IntegerField()
),
0
),
position_count=Coalesce(
Case(
When(all_products=True, include_pending=False,
then=Subquery(pqs_all_paid, output_field=models.IntegerField())),
When(all_products=True, include_pending=True,
then=Subquery(pqs_all_paid_and_pending, output_field=models.IntegerField())),
When(all_products=False, include_pending=False,
then=Subquery(pqs_limited_paid, output_field=models.IntegerField())),
default=Subquery(pqs_limited_paid_and_pending, output_field=models.IntegerField()),
output_field=models.IntegerField()
),
0
)
).annotate(
percent=Case(
When(position_count__gt=0, then=F('checkin_count') * 100 / F('position_count')),
default=0,
output_field=models.IntegerField()
)
)
# This is only kept for backwards-compatibility reasons. This method used to precompute .position_count
# and .checkin_count through a huge subquery chain, but was dropped for performance reasons.
return qs
def __str__(self):
return self.name
@@ -169,6 +96,7 @@ class Checkin(models.Model):
list = models.ForeignKey(
'pretixbase.CheckinList', related_name='checkins', on_delete=models.PROTECT,
)
auto_checked_in = models.BooleanField(default=False)
objects = ScopedManager(organizer='position__order__event__organizer')
@@ -182,8 +110,11 @@ class Checkin(models.Model):
def save(self, **kwargs):
self.position.order.touch()
self.list.event.cache.delete('checkin_count')
self.list.touch()
super().save(**kwargs)
def delete(self, **kwargs):
self.position.order.touch()
super().delete(**kwargs)
self.list.touch()

View File

@@ -65,7 +65,7 @@ class EventMixin:
"SHORT_DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT"
)
def get_date_from_display(self, tz=None, show_times=True) -> str:
def get_date_from_display(self, tz=None, show_times=True, short=False) -> str:
"""
Returns a formatted string containing the start date of the event with respect
to the current locale and to the ``show_times`` setting.
@@ -73,7 +73,7 @@ class EventMixin:
tz = tz or self.timezone
return _date(
self.date_from.astimezone(tz),
"DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT"
("SHORT_" if short else "") + ("DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT")
)
def get_time_from_display(self, tz=None) -> str:
@@ -86,7 +86,7 @@ class EventMixin:
self.date_from.astimezone(tz), "TIME_FORMAT"
)
def get_date_to_display(self, tz=None) -> str:
def get_date_to_display(self, tz=None, short=False) -> str:
"""
Returns a formatted string containing the start date of the event with respect
to the current locale and to the ``show_times`` setting. Returns an empty string
@@ -97,7 +97,7 @@ class EventMixin:
return ""
return _date(
self.date_to.astimezone(tz),
"DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT"
("SHORT_" if short else "") + ("DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT")
)
def get_date_range_display(self, tz=None, force_show_end=False) -> str:
@@ -516,6 +516,7 @@ class Event(EventMixin, LoggedModel):
for q in Quota.objects.filter(event=other, subevent__isnull=True).prefetch_related('items', 'variations'):
items = list(q.items.all())
vars = list(q.variations.all())
oldid = q.pk
q.pk = None
q.event = self
q.cached_availability_state = None
@@ -529,6 +530,7 @@ class Event(EventMixin, LoggedModel):
q.items.add(item_map[i.pk])
for v in vars:
q.variations.add(variation_map[v.pk])
self.items.filter(hidden_if_available_id=oldid).update(hidden_if_available=q)
question_map = {}
for q in Question.objects.filter(event=other).prefetch_related('items', 'options'):
@@ -606,22 +608,24 @@ class Event(EventMixin, LoggedModel):
question_map=question_map
)
def get_payment_providers(self) -> dict:
def get_payment_providers(self, cached=False) -> dict:
"""
Returns a dictionary of initialized payment providers mapped by their identifiers.
"""
from ..signals import register_payment_providers
responses = register_payment_providers.send(self)
providers = {}
for receiver, response in responses:
if not isinstance(response, list):
response = [response]
for p in response:
pp = p(self)
providers[pp.identifier] = pp
if not cached or not hasattr(self, '_cached_payment_providers'):
responses = register_payment_providers.send(self)
providers = {}
for receiver, response in responses:
if not isinstance(response, list):
response = [response]
for p in response:
pp = p(self)
providers[pp.identifier] = pp
return OrderedDict(sorted(providers.items(), key=lambda v: str(v[1].verbose_name)))
self._cached_payment_providers = OrderedDict(sorted(providers.items(), key=lambda v: str(v[1].verbose_name)))
return self._cached_payment_providers
def get_html_mail_renderer(self):
"""
@@ -822,18 +826,24 @@ class Event(EventMixin, LoggedModel):
def enable_plugin(self, module, allow_restricted=False):
plugins_active = self.get_plugins()
from pretix.presale.style import regenerate_css
if module not in plugins_active:
plugins_active.append(module)
self.set_active_plugins(plugins_active, allow_restricted=allow_restricted)
regenerate_css.apply_async(args=(self.pk,))
def disable_plugin(self, module):
plugins_active = self.get_plugins()
from pretix.presale.style import regenerate_css
if module in plugins_active:
plugins_active.remove(module)
self.set_active_plugins(plugins_active)
regenerate_css.apply_async(args=(self.pk,))
@staticmethod
def clean_has_subevents(event, has_subevents):
if event is not None and event.has_subevents is not None:

View File

@@ -1,6 +1,7 @@
import string
from decimal import Decimal
import pycountry
from django.db import DatabaseError, models, transaction
from django.db.models import Max
from django.db.models.functions import Cast
@@ -11,6 +12,8 @@ from django.utils.translation import pgettext
from django_countries.fields import CountryField
from django_scopes import ScopedManager
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
def invoice_filename(instance, filename: str) -> str:
secret = get_random_string(length=16, allowed_chars=string.ascii_letters + string.digits)
@@ -90,6 +93,7 @@ class Invoice(models.Model):
invoice_to_street = models.TextField(null=True)
invoice_to_zipcode = models.CharField(max_length=190, null=True)
invoice_to_city = models.TextField(null=True)
invoice_to_state = models.CharField(max_length=190, null=True)
invoice_to_country = CountryField(null=True)
invoice_to_vat_id = models.TextField(null=True)
invoice_to_beneficiary = models.TextField(null=True)
@@ -140,11 +144,21 @@ class Invoice(models.Model):
def address_invoice_to(self):
if self.invoice_to and not self.invoice_to_company and not self.invoice_to_name:
return self.invoice_to
state_name = ""
if self.invoice_to_state:
state_name = self.invoice_to_state
if str(self.invoice_to_country) in COUNTRIES_WITH_STATE_IN_ADDRESS:
if COUNTRIES_WITH_STATE_IN_ADDRESS[str(self.invoice_to_country)][1] == 'long':
state_name = pycountry.subdivisions.get(
code='{}-{}'.format(self.invoice_to_country, self.invoice_to_state)
).name
parts = [
self.invoice_to_company,
self.invoice_to_name,
self.invoice_to_street,
(self.invoice_to_zipcode or "") + " " + (self.invoice_to_city or ""),
((self.invoice_to_zipcode or "") + " " + (self.invoice_to_city or "") + " " + (state_name or "")).strip(),
self.invoice_to_country.name if self.invoice_to_country else "",
]
return '\n'.join([p.strip() for p in parts if p and p.strip()])
@@ -175,6 +189,8 @@ class Invoice(models.Model):
self.organizer = self.order.event.organizer
if not self.prefix:
self.prefix = self.event.settings.invoice_numbers_prefix or (self.event.slug.upper() + '-')
if self.is_cancellation:
self.prefix = self.event.settings.invoice_numbers_prefix_cancellations or self.prefix
if not self.invoice_no:
if self.order.testmode:
self.prefix += 'TEST-'

View File

@@ -311,6 +311,11 @@ class Item(LoggedModel):
verbose_name=_("Generate tickets"),
blank=True, null=True,
)
allow_waitinglist = models.BooleanField(
verbose_name=_("Show a waiting list for this ticket"),
help_text=_("This will only work of waiting lists are enabled for this event."),
default=True
)
show_quota_left = models.NullBooleanField(
verbose_name=_("Show number of tickets left"),
help_text=_("Publicly show how many tickets are still available."),
@@ -334,6 +339,17 @@ class Item(LoggedModel):
null=True, blank=True,
help_text=_('This product will not be sold after the given date.')
)
hidden_if_available = models.ForeignKey(
'Quota',
null=True, blank=True,
on_delete=models.SET_NULL,
verbose_name=_("Only show after sellout of"),
help_text=_("If you select a quota here, this product will only be shown when that quota is "
"unavailable. If combined with the option to hide sold-out products, this allows you to "
"swap out products for more expensive ones once they are sold out. There might be a short period "
"in which both products are visible while all tickets in the referenced quota are reserved, "
"but not yet sold.")
)
require_voucher = models.BooleanField(
verbose_name=_('This product can only be bought using a voucher.'),
default=False,
@@ -477,7 +493,7 @@ class Item(LoggedModel):
return check_quotas
def check_quotas(self, ignored_quotas=None, count_waitinglist=True, subevent=None, _cache=None,
include_bundled=False, trust_parameters=False):
include_bundled=False, trust_parameters=False, fail_on_no_quotas=False):
"""
This method is used to determine whether this Item is currently available
for sale.
@@ -525,6 +541,8 @@ class Item(LoggedModel):
res = (code_avail, num_avail)
if len(quotacounter) == 0:
if fail_on_no_quotas:
return Quota.AVAILABILITY_GONE, 0
return Quota.AVAILABILITY_OK, sys.maxsize # backwards compatibility
return res
@@ -680,7 +698,7 @@ class ItemVariation(models.Model):
return check_quotas
def check_quotas(self, ignored_quotas=None, count_waitinglist=True, subevent=None, _cache=None,
include_bundled=False, trust_parameters=False) -> Tuple[int, int]:
include_bundled=False, trust_parameters=False, fail_on_no_quotas=False) -> Tuple[int, int]:
"""
This method is used to determine whether this ItemVariation is currently
available for sale in terms of quotas.
@@ -722,6 +740,8 @@ class ItemVariation(models.Model):
if code_avail < res[0] or res[1] is None or num_avail < res[1]:
res = (code_avail, num_avail)
if len(quotacounter) == 0:
if fail_on_no_quotas:
return Quota.AVAILABILITY_GONE, 0
return Quota.AVAILABILITY_OK, sys.maxsize # backwards compatibility
return res
@@ -962,6 +982,7 @@ class Question(LoggedModel):
(TYPE_DATETIME, _("Date and time")),
(TYPE_COUNTRYCODE, _("Country code (ISO 3166-1 alpha-2)")),
)
UNLOCALIZED_TYPES = [TYPE_DATE, TYPE_TIME, TYPE_DATETIME]
event = models.ForeignKey(
Event,
@@ -1004,8 +1025,6 @@ class Question(LoggedModel):
)
ask_during_checkin = models.BooleanField(
verbose_name=_('Ask during check-in instead of in the ticket buying process'),
help_text=_('This will only work if you handle your check-in with pretixdroid 1.8 or newer or '
'pretixdesk 0.2 or newer.'),
default=False
)
hidden = models.BooleanField(
@@ -1013,6 +1032,10 @@ class Question(LoggedModel):
help_text=_('This question will only show up in the backend.'),
default=False
)
print_on_invoice = models.BooleanField(
verbose_name=_('Print answer on invoices'),
default=False
)
dependency_question = models.ForeignKey(
'Question', null=True, blank=True, on_delete=models.SET_NULL, related_name='dependent_questions'
)

View File

@@ -9,6 +9,7 @@ from decimal import Decimal
from typing import Any, Dict, List, Union
import dateutil
import pycountry
import pytz
from django.conf import settings
from django.db import models, transaction
@@ -30,7 +31,9 @@ from django_scopes import ScopedManager, scopes_disabled
from i18nfield.strings import LazyI18nString
from jsonfallback.fields import FallbackJSONField
from pretix.base.banlist import banned
from pretix.base.decimal import round_decimal
from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import User
from pretix.base.reldate import RelativeDateWrapper
@@ -534,16 +537,30 @@ class Order(LockModel, LoggedModel):
# handwriting (2/Z, 4/A, 5/S, 6/G). This allows for better detection e.g. in incoming wire transfers that
# might include OCR'd handwritten text
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
iteration = 0
length = settings.ENTROPY['order_code']
while True:
code = get_random_string(length=settings.ENTROPY['order_code'], allowed_chars=charset)
code = get_random_string(length=length, allowed_chars=charset)
iteration += 1
if banned(code):
continue
if self.testmode:
# Subtle way to recognize test orders while debugging: They all contain a 0 at the second place,
# even though zeros are not used outside test mode.
code = code[0] + "0" + code[2:]
if not Order.objects.filter(event__organizer=self.event.organizer, code=code).exists():
self.code = code
return
if iteration > 20:
# Safeguard: If we don't find an unused and non-blacklisted code within 20 iterations, we increase
# the length.
length += 1
iteration = 0
@property
def can_modify_answers(self) -> bool:
"""
@@ -696,7 +713,7 @@ class Order(LockModel, LoggedModel):
def send_mail(self, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent',
user: User=None, headers: dict=None, sender: str=None, invoices: list=None,
auth=None, attach_tickets=False, position: 'OrderPosition'=None):
auth=None, attach_tickets=False, position: 'OrderPosition'=None, auto_email=True):
"""
Sends an email to the user that placed this order. Basically, this method does two things:
@@ -736,7 +753,7 @@ class Order(LockModel, LoggedModel):
recipient, subject, template, context,
self.event, self.locale, self, headers=headers, sender=sender,
invoices=invoices, attach_tickets=attach_tickets,
position=position
position=position, auto_email=auto_email
)
except SendMailException:
raise
@@ -756,26 +773,9 @@ class Order(LockModel, LoggedModel):
)
def resend_link(self, user=None, auth=None):
from pretix.multidomain.urlreverse import build_absolute_uri
with language(self.locale):
try:
invoice_name = self.invoice_address.name
invoice_company = self.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
email_template = self.event.settings.mail_text_resend_link
email_context = {
'event': self.event.name,
'url': build_absolute_uri(self.event, 'presale:event.order.open', kwargs={
'order': self.code,
'secret': self.secret,
'hash': self.email_confirm_hash()
}),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
}
email_context = get_email_context(event=self.event, order=self)
email_subject = _('Your order: %(code)s') % {'code': self.code}
self.send_mail(
email_subject, email_template, email_context,
@@ -1195,7 +1195,7 @@ class OrderPayment(models.Model):
"""
Cached access to an instance of the payment provider in use.
"""
return self.order.event.get_payment_providers().get(self.provider)
return self.order.event.get_payment_providers(cached=True).get(self.provider)
def _mark_paid(self, force, count_waitinglist, user, auth, ignore_date=False, overpaid=False):
from pretix.base.signals import order_paid
@@ -1312,24 +1312,10 @@ class OrderPayment(models.Model):
def _send_paid_mail_attendee(self, position, user):
from pretix.base.services.mail import SendMailException
from pretix.multidomain.urlreverse import build_absolute_uri
with language(self.order.locale):
name_scheme = PERSON_NAME_SCHEMES[self.order.event.settings.name_scheme]
email_template = self.order.event.settings.mail_text_order_paid_attendee
email_context = {
'event': self.order.event.name,
'downloads': self.order.event.settings.get('ticket_download', as_type=bool),
'url': build_absolute_uri(self.order.event, 'presale:event.order.position', kwargs={
'order': self.order.code,
'secret': position.web_secret,
'position': position.positionid
}),
'attendee_name': position.attendee_name,
}
for f, l, w in name_scheme['fields']:
email_context['attendee_name_%s' % f] = position.attendee_name_parts.get(f, '')
email_context = get_email_context(event=self.order.event, order=self.order, position=position)
email_subject = _('Event registration confirmed: %(code)s') % {'code': self.order.code}
try:
self.order.send_mail(
@@ -1343,28 +1329,10 @@ class OrderPayment(models.Model):
def _send_paid_mail(self, invoice, user, mail_text):
from pretix.base.services.mail import SendMailException
from pretix.multidomain.urlreverse import build_absolute_uri
with language(self.order.locale):
try:
invoice_name = self.order.invoice_address.name
invoice_company = self.order.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
email_template = self.order.event.settings.mail_text_order_paid
email_context = {
'event': self.order.event.name,
'url': build_absolute_uri(self.order.event, 'presale:event.order.open', kwargs={
'order': self.order.code,
'secret': self.order.secret,
'hash': self.order.email_confirm_hash()
}),
'downloads': self.order.event.settings.get('ticket_download', as_type=bool),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
'payment_info': mail_text
}
email_context = get_email_context(event=self.order.event, order=self.order, payment_info=mail_text)
email_subject = _('Payment received for your order: %(code)s') % {'code': self.order.code}
try:
self.order.send_mail(
@@ -1913,25 +1881,26 @@ class OrderPosition(AbstractPosition):
"""
from pretix.base.services.mail import SendMailException, mail, render_mail
if not self.email:
if not self.attendee_email:
return
for k, v in self.event.meta_data.items():
context['meta_' + k] = v
with language(self.locale):
recipient = self.email
with language(self.order.locale):
recipient = self.attendee_email
try:
email_content = render_mail(template, context)
mail(
recipient, subject, template, context,
self.event, self.locale, self, headers, sender,
self.event, self.order.locale, order=self.order, headers=headers, sender=sender,
position=self,
invoices=invoices, attach_tickets=attach_tickets
)
except SendMailException:
raise
else:
self.log_action(
self.order.log_action(
log_entry_type,
user=user,
auth=auth,
@@ -1944,6 +1913,18 @@ class OrderPosition(AbstractPosition):
}
)
def resend_link(self, user=None, auth=None):
with language(self.order.locale):
email_template = self.event.settings.mail_text_resend_link
email_context = get_email_context(event=self.order.event, order=self.order, position=self)
email_subject = _('Your event registration: %(code)s') % {'code': self.order.code}
self.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.resend', user=user, auth=auth,
attach_tickets=True
)
class CartPosition(AbstractPosition):
"""
@@ -2019,6 +2000,7 @@ class InvoiceAddress(models.Model):
city = models.CharField(max_length=255, verbose_name=_('City'), blank=False)
country_old = models.CharField(max_length=255, verbose_name=_('Country'), blank=False)
country = CountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'))
state = models.CharField(max_length=255, verbose_name=pgettext_lazy('address', 'State'), blank=True)
vat_id = models.CharField(max_length=255, blank=True, verbose_name=_('VAT ID'),
help_text=_('Only for business customers within the EU.'))
vat_id_validated = models.BooleanField(default=False)
@@ -2045,6 +2027,22 @@ class InvoiceAddress(models.Model):
self.name_parts = {}
super().save(**kwargs)
@property
def state_name(self):
sd = pycountry.subdivisions.get(code='{}-{}'.format(self.country, self.state))
if sd:
return sd.name
return self.state
@property
def state_for_address(self):
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
if not self.state or str(self.country) not in COUNTRIES_WITH_STATE_IN_ADDRESS:
return ""
if COUNTRIES_WITH_STATE_IN_ADDRESS[str(self.country)][1] == 'long':
return self.state_name
return self.state
@property
def name(self):
if not self.name_parts:

View File

@@ -7,7 +7,7 @@ from django.core.exceptions import ValidationError
from django.db import models
from django.utils.deconstruct import deconstructible
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext, ugettext_lazy as _
from pretix.base.models import Event, Item, LoggedModel, Organizer, SubEvent
@@ -39,7 +39,7 @@ class SeatingPlan(LoggedModel):
layout = models.TextField(validators=[SeatingPlanLayoutValidator()])
Category = namedtuple('Categrory', 'name')
RawSeat = namedtuple('Seat', 'name guid number row category')
RawSeat = namedtuple('Seat', 'name guid number row category zone')
def __str__(self):
return self.name
@@ -67,6 +67,7 @@ class SeatingPlan(LoggedModel):
guid=s['seat_guid'],
name='{} {}'.format(r['row_number'], s['seat_number']), # TODO: Zone? Variable scheme?
row=r['row_number'],
zone=z['name'],
category=s['category']
)
@@ -90,12 +91,24 @@ class Seat(models.Model):
event = models.ForeignKey(Event, related_name='seats', on_delete=models.CASCADE)
subevent = models.ForeignKey(SubEvent, null=True, blank=True, related_name='seats', on_delete=models.CASCADE)
name = models.CharField(max_length=190)
zone_name = models.CharField(max_length=190, blank=True, default="")
row_name = models.CharField(max_length=190, blank=True, default="")
seat_number = models.CharField(max_length=190, blank=True, default="")
seat_guid = models.CharField(max_length=190, db_index=True)
product = models.ForeignKey('Item', null=True, blank=True, related_name='seats', on_delete=models.CASCADE)
blocked = models.BooleanField(default=False)
def __str__(self):
return self.name
parts = []
if self.zone_name:
parts.append(self.zone_name)
if self.row_name:
parts.append(gettext('Row {number}').format(number=self.row_name))
if self.seat_number:
parts.append(gettext('Seat {number}').format(number=self.seat_number))
if not parts:
return self.name
return ', '.join(parts)
def is_available(self, ignore_cart=None, ignore_orderpos=None):
from .orders import Order

View File

@@ -85,6 +85,12 @@ EU_CURRENCIES = {
}
def cc_to_vat_prefix(country_code):
if country_code == 'GR':
return 'EL'
return country_code
class TaxRule(LoggedModel):
event = models.ForeignKey('Event', related_name='tax_rules', on_delete=models.CASCADE)
name = I18nCharField(

View File

@@ -10,6 +10,7 @@ from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_scopes import ScopedManager, scopes_disabled
from pretix.base.banlist import banned
from pretix.base.models import SeatCategoryMapping
from ..decimal import round_decimal
@@ -21,9 +22,12 @@ from .orders import Order
def _generate_random_code(prefix=None):
charset = list('ABCDEFGHKLMNPQRSTUVWXYZ23456789')
rnd = None
while not rnd or banned(rnd):
rnd = get_random_string(length=settings.ENTROPY['voucher_code'], allowed_chars=charset)
if prefix:
return prefix + get_random_string(length=settings.ENTROPY['voucher_code'], allowed_chars=charset)
return get_random_string(length=settings.ENTROPY['voucher_code'], allowed_chars=charset)
return prefix + rnd
return rnd
@scopes_disabled()

View File

@@ -6,10 +6,10 @@ from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_scopes import ScopedManager
from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import Voucher
from pretix.base.services.mail import mail
from pretix.multidomain.urlreverse import build_absolute_uri
from .base import LoggedModel
from .event import Event, SubEvent
@@ -130,13 +130,7 @@ class WaitingListEntry(LoggedModel):
self.email,
_('You have been selected from the waitinglist for {event}').format(event=str(self.event)),
self.event.settings.mail_text_waiting_list,
{
'event': self.event.name,
'url': build_absolute_uri(self.event, 'presale:event.redeem') + '?voucher=' + self.voucher.code,
'code': self.voucher.code,
'product': str(self.item) + (' - ' + str(self.variation) if self.variation else ''),
'hours': self.event.settings.waiting_list_hours,
},
get_email_context(event=self.event, waiting_list_entry=self),
self.event,
locale=self.locale
)

View File

@@ -531,7 +531,7 @@ class BasePaymentProvider:
containing the URL the user will be redirected to. If you are done with your process
you should return the user to the order's detail page.
If the payment is completed, you should call ``payment.confirm()``. Please note that ``this`` might
If the payment is completed, you should call ``payment.confirm()``. Please note that this might
raise a ``Quota.QuotaExceededException`` if (and only if) the payment term of this order is over and
some of the items are sold out. You should use the exception message to display a meaningful error
to the user.
@@ -657,6 +657,15 @@ class BasePaymentProvider:
obj.info = '{}'
obj.save(update_fields=['info'])
def api_payment_details(self, payment: OrderPayment):
"""
Will be called to populate the ``details`` parameter of the payment in the REST API.
:param payment: The payment in question.
:return: A serializable dictionary
"""
return {}
class PaymentException(Exception):
pass
@@ -720,6 +729,12 @@ class BoxOfficeProvider(BasePaymentProvider):
def order_change_allowed(self, order: Order) -> bool:
return False
def api_payment_details(self, payment: OrderPayment):
return {
"pos_id": payment.info_data.get('pos_id', None),
"receipt_id": payment.info_data.get('receipt_id', None),
}
def payment_control_render(self, request, payment) -> str:
if not payment.info:
return
@@ -864,6 +879,11 @@ class OffsettingProvider(BasePaymentProvider):
def order_change_allowed(self, order: Order) -> bool:
return False
def api_payment_details(self, payment: OrderPayment):
return {
"orders": payment.info_data.get('orders', []),
}
def payment_control_render(self, request: HttpRequest, payment: OrderPayment) -> str:
return _('Balanced against orders: %s' % ', '.join(payment.info_data['orders']))

View File

@@ -32,7 +32,7 @@ from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import Paragraph
from pretix.base.invoice import ThumbnailingImageReader
from pretix.base.models import Order, OrderPosition, QuestionAnswer
from pretix.base.models import Order, OrderPosition
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import layout_text_variables
from pretix.base.templatetags.money import money_filter
@@ -239,9 +239,26 @@ DEFAULT_VARIABLES = OrderedDict((
) if ev.date_admission else ""
}),
("seat", {
"label": _("Seat name"),
"editor_sample": _("3, 4-5"),
"evaluate": lambda op, order, ev: str(op.seat if op.seat else _('General admission'))
"label": _("Seat: Full name"),
"editor_sample": _("Ground floor, Row 3, Seat 4"),
"evaluate": lambda op, order, ev: str(op.seat if op.seat else
_('General admission') if ev.seating_plan_id is not None else "")
}),
("seat_zone", {
"label": _("Seat: zone"),
"editor_sample": _("Ground floor"),
"evaluate": lambda op, order, ev: str(op.seat.zone_name if op.seat else
_('General admission') if ev.seating_plan_id is not None else "")
}),
("seat_row", {
"label": _("Seat: row"),
"editor_sample": "3",
"evaluate": lambda op, order, ev: str(op.seat.row_name if op.seat else "")
}),
("seat_number", {
"label": _("Seat: seat number"),
"editor_sample": 4,
"evaluate": lambda op, order, ev: str(op.seat.seat_number if op.seat else "")
}),
))
@@ -249,16 +266,28 @@ DEFAULT_VARIABLES = OrderedDict((
@receiver(layout_text_variables, dispatch_uid="pretix_base_layout_text_variables_questions")
def variables_from_questions(sender, *args, **kwargs):
def get_answer(op, order, event, question_id):
try:
if 'answers' in getattr(op, '_prefetched_objects_cache', {}):
a = [a for a in op.answers.all() if a.question_id == question_id][0]
a = None
if op.addon_to:
if 'answers' in getattr(op.addon_to, '_prefetched_objects_cache', {}):
try:
a = [a for a in op.addon_to.answers.all() if a.question_id == question_id][0]
except IndexError:
pass
else:
a = op.answers.get(question_id=question_id)
a = op.addon_to.answers.filter(question_id=question_id).first()
if 'answers' in getattr(op, '_prefetched_objects_cache', {}):
try:
a = [a for a in op.answers.all() if a.question_id == question_id][0]
except IndexError:
pass
else:
a = op.answers.filter(question_id=question_id).first()
if not a:
return ""
else:
return str(a).replace("\n", "<br/>\n")
except QuestionAnswer.DoesNotExist:
return ""
except IndexError:
return ""
d = {}
for q in sender.questions.all():

View File

@@ -226,11 +226,15 @@ class CartManager:
def _check_item_constraints(self, op):
if isinstance(op, self.AddOperation) or isinstance(op, self.ExtendOperation):
if op.item.require_voucher and op.voucher is None:
raise CartError(error_messages['voucher_required'])
if not (
(isinstance(op, self.AddOperation) and op.addon_to == 'FAKE') or
(isinstance(op, self.ExtendOperation) and op.position.is_bundled)
):
if op.item.require_voucher and op.voucher is None:
raise CartError(error_messages['voucher_required'])
if op.item.hide_without_voucher and (op.voucher is None or not op.voucher.show_hidden_items):
raise CartError(error_messages['voucher_required'])
if op.item.hide_without_voucher and (op.voucher is None or not op.voucher.show_hidden_items):
raise CartError(error_messages['voucher_required'])
if not op.item.is_available() or (op.variation and not op.variation.active):
raise CartError(error_messages['unavailable'])
@@ -432,6 +436,8 @@ class CartManager:
seat = (subevent or self.event).seats.get(seat_guid=i.get('seat'))
except Seat.DoesNotExist:
raise CartError(error_messages['seat_invalid'])
except Seat.MultipleObjectsReturned:
raise CartError(error_messages['seat_invalid'])
i['item'] = seat.product_id
if i['item'] not in self._items_cache:
self._update_items_cache([i['item']], [i['variation']])
@@ -612,7 +618,7 @@ class CartManager:
op = self.AddOperation(
count=1, item=item, variation=variation, price=price, voucher=None, quotas=quotas,
addon_to=cp, subevent=cp.subevent, includes_tax=bool(price.rate), bundled=[], seat=cp.seat
addon_to=cp, subevent=cp.subevent, includes_tax=bool(price.rate), bundled=[], seat=None
)
self._check_item_constraints(op)
operations.append(op)
@@ -789,14 +795,20 @@ class CartManager:
if isinstance(op, self.AddOperation):
for b in op.bundled:
b_quota_available_count = min(available_count * b.count, min(quotas_ok[q] for q in b.quotas))
b_quotas = list(b.quotas)
if not b_quotas:
if not op.voucher or not op.voucher.allow_ignore_quota:
err = err or error_messages['unavailable']
available_count = 0
continue
b_quota_available_count = min(available_count * b.count, min(quotas_ok[q] for q in b_quotas))
if b_quota_available_count < b.count:
err = err or error_messages['unavailable']
available_count = 0
elif b_quota_available_count < available_count * b.count:
err = err or error_messages['in_part']
available_count = b_quota_available_count // b.count
for q in b.quotas:
for q in b_quotas:
quotas_ok[q] -= available_count * b.count
# TODO: is this correct?

View File

@@ -1,11 +1,13 @@
from django.db import transaction
from django.db.models import Prefetch
from django.dispatch import receiver
from django.utils.timezone import now
from django.utils.translation import ugettext as _
from pretix.base.models import (
Checkin, CheckinList, Order, OrderPosition, Question, QuestionOption,
)
from pretix.base.signals import order_placed
class CheckInError(Exception):
@@ -155,3 +157,18 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
'datetime': dt,
'list': clist.pk
}, user=user, auth=auth)
@receiver(order_placed, dispatch_uid="autocheckin_order_placed")
def order_placed(sender, **kwargs):
order = kwargs['order']
event = sender
cls = list(event.checkin_lists.filter(auto_checkin_sales_channels__contains=order.sales_channel).prefetch_related(
'limit_products'))
if not cls:
return
for op in order.positions.all():
for cl in cls:
if cl.all_products or op.item_id in {i.pk for i in cl.limit_products.all()}:
Checkin.objects.create(position=op, list=cl, auto_checked_in=True)

View File

@@ -73,12 +73,15 @@ def build_invoice(invoice: Invoice) -> Invoice:
addr_template = pgettext("invoice", """{i.company}
{i.name}
{i.street}
{i.zipcode} {i.city}
{i.zipcode} {i.city} {state}
{country}""")
invoice.invoice_to = addr_template.format(
i=ia,
country=ia.country.name if ia.country else ia.country_old
).strip()
invoice.invoice_to = "\n".join(
a.strip() for a in addr_template.format(
i=ia,
country=ia.country.name if ia.country else ia.country_old,
state=ia.state_for_address
).split("\n") if a.strip()
)
invoice.internal_reference = ia.internal_reference
invoice.invoice_to_company = ia.company
invoice.invoice_to_name = ia.name
@@ -86,6 +89,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
invoice.invoice_to_zipcode = ia.zipcode
invoice.invoice_to_city = ia.city
invoice.invoice_to_country = ia.country
invoice.invoice_to_state = ia.state
invoice.invoice_to_beneficiary = ia.beneficiary
if ia.vat_id:
@@ -125,7 +129,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
positions = list(
invoice.order.positions.select_related('addon_to', 'item', 'tax_rule', 'subevent', 'variation').annotate(
addon_c=Count('addons')
).order_by('positionid', 'id')
).prefetch_related('answers', 'answers__question').order_by('positionid', 'id')
)
reverse_charge = False
@@ -142,6 +146,16 @@ def build_invoice(invoice: Invoice) -> Invoice:
desc = " + " + desc
if invoice.event.settings.invoice_attendee_name and p.attendee_name:
desc += "<br />" + pgettext("invoice", "Attendee: {name}").format(name=p.attendee_name)
for answ in p.answers.all():
if not answ.question.print_on_invoice:
continue
desc += "<br />{}{} {}".format(
answ.question.question,
"" if str(answ.question.question).endswith("?") else ":",
str(answ)
)
if invoice.event.has_subevents:
desc += "<br />" + pgettext("subevent", "Date: {}").format(p.subevent)
InvoiceLine.objects.create(

View File

@@ -1,11 +1,17 @@
import inspect
import logging
import os
import re
import smtplib
import warnings
from email.mime.image import MIMEImage
from email.utils import formataddr
from typing import Any, Dict, List, Union
from urllib.parse import urljoin, urlparse
import cssutils
import requests
from bs4 import BeautifulSoup
from celery import chain
from django.conf import settings
from django.core.mail import EmailMultiAlternatives, get_connection
@@ -17,12 +23,12 @@ from i18nfield.strings import LazyI18nString
from pretix.base.email import ClassicMailRenderer
from pretix.base.i18n import language
from pretix.base.models import (
Event, Invoice, InvoiceAddress, Order, OrderPosition,
Event, Invoice, InvoiceAddress, Order, OrderPosition, User,
)
from pretix.base.services.invoices import invoice_pdf_task
from pretix.base.services.tasks import TransactionAwareTask
from pretix.base.services.tickets import get_tickets_for_order
from pretix.base.signals import email_filter
from pretix.base.signals import email_filter, global_email_filter
from pretix.celery_app import app
from pretix.multidomain.urlreverse import build_absolute_uri
@@ -44,7 +50,7 @@ class SendMailException(Exception):
def mail(email: str, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, event: Event=None, locale: str=None,
order: Order=None, position: OrderPosition=None, headers: dict=None, sender: str=None,
invoices: list=None, attach_tickets=False):
invoices: list=None, attach_tickets=False, auto_email=True, user=None):
"""
Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation.
@@ -79,6 +85,10 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
:param attach_tickets: Whether to attach tickets to this email, if they are available to download.
:param auto_email: Whether this email is auto-generated
:param user: The user this email is sent to
:raises MailOrderException: on obvious, immediate failures. Not raising an exception does not necessarily mean
that the email has been sent, just that it has been queued by the email backend.
"""
@@ -86,6 +96,9 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
return
headers = headers or {}
if auto_email:
headers['X-Auto-Response-Suppress'] = 'OOF, NRN, AutoReply, RN'
headers['Auto-Submitted'] = 'auto-generated'
with language(locale):
if isinstance(context, dict) and event:
@@ -202,7 +215,8 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
invoices=[i.pk for i in invoices] if invoices and not position else [],
order=order.pk if order else None,
position=position.pk if position else None,
attach_tickets=attach_tickets
attach_tickets=attach_tickets,
user=user.pk if user else None
)
if invoices:
@@ -217,10 +231,15 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
@app.task(base=TransactionAwareTask, bind=True)
def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: str, sender: str,
event: int=None, position: int=None, headers: dict=None, bcc: List[str]=None,
invoices: List[int]=None, order: int=None, attach_tickets=False) -> bool:
invoices: List[int]=None, order: int=None, attach_tickets=False, user=None) -> bool:
email = EmailMultiAlternatives(subject, body, sender, to=to, bcc=bcc, headers=headers)
if html is not None:
email.attach_alternative(html, "text/html")
html_with_cid, cid_images = replace_images_with_cid_paths(html)
email = attach_cid_images(email, cid_images, verify_ssl=True)
email.attach_alternative(html_with_cid, "text/html")
if user:
user = User.objects.get(pk=user)
if event:
with scopes_disabled():
@@ -283,7 +302,9 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
}
)
email = email_filter.send_chained(event, 'message', message=email, order=order)
email = email_filter.send_chained(event, 'message', message=email, order=order, user=user)
email = global_email_filter.send_chained(event, 'message', message=email, user=user, order=order)
try:
backend.send_messages([email])
@@ -332,3 +353,107 @@ def render_mail(template, context):
tpl = get_template(template)
body = tpl.render(context)
return body
def replace_images_with_cid_paths(body_html):
if body_html:
email = BeautifulSoup(body_html, "lxml")
cid_images = []
for image in email.findAll('img'):
original_image_src = image['src']
try:
cid_id = "image_%s" % cid_images.index(original_image_src)
except ValueError:
cid_images.append(original_image_src)
cid_id = "image_%s" % (len(cid_images) - 1)
image['src'] = "cid:%s" % cid_id
return email.prettify(), cid_images
else:
return body_html, []
def attach_cid_images(msg, cid_images, verify_ssl=True):
if cid_images and len(cid_images) > 0:
msg.mixed_subtype = 'mixed'
for key, image in enumerate(cid_images):
cid = 'image_%s' % key
try:
mime_image = convert_image_to_cid(
image, cid, verify_ssl)
if mime_image:
msg.attach(mime_image)
except:
logger.exception("ERROR attaching CID image %s[%s]" % (cid, image))
return msg
def encoder_linelength(msg):
"""
RFC1341 mandates that base64 encoded data may not be longer than 76 characters per line
https://www.w3.org/Protocols/rfc1341/5_Content-Transfer-Encoding.html section 5.2
"""
orig = msg.get_payload(decode=True).replace(b"\n", b"").replace(b"\r", b"")
max_length = 76
pieces = []
for i in range(0, len(orig), max_length):
chunk = orig[i:i + max_length]
pieces.append(chunk)
msg.set_payload(b"\r\n".join(pieces))
def convert_image_to_cid(image_src, cid_id, verify_ssl=True):
try:
if image_src.startswith('data:image/'):
image_type, image_content = image_src.split(',', 1)
image_type = re.findall(r'data:image/(\w+);base64', image_type)[0]
mime_image = MIMEImage(image_content, _subtype=image_type, _encoder=encoder_linelength)
mime_image.add_header('Content-Transfer-Encoding', 'base64')
elif image_src.startswith('data:'):
logger.exception("ERROR creating MIME element %s[%s]" % (cid_id, image_src))
return None
else:
image_src = normalize_image_url(image_src)
path = urlparse(image_src).path
guess_subtype = os.path.splitext(path)[1][1:]
response = requests.get(image_src, verify=verify_ssl)
mime_image = MIMEImage(
response.content, _subtype=guess_subtype)
mime_image.add_header('Content-ID', '<%s>' % cid_id)
return mime_image
except:
logger.exception("ERROR creating mime_image %s[%s]" % (cid_id, image_src))
return None
def normalize_image_url(url):
if '://' not in url:
"""
If we see a relative URL in an email, we can't know if it is meant to be a media file
or a static file, so we need to guess. If it is a static file included with the
``{% static %}`` template tag (as it should be), then ``STATIC_URL`` is already prepended.
If ``STATIC_URL`` is absolute, then ``url`` should already be absolute and this
function should not be triggered. Thus, if we see a relative URL and ``STATIC_URL``
is absolute *or* ``url`` does not start with ``STATIC_URL``, we can be sure this
is a media file (or a programmer error …).
Constructing the URL of either a static file or a media file from settings is still
not clean, since custom storage backends might very well use more complex approaches
to build those URLs. However, this is good enough as a best-effort approach. Complex
storage backends (such as cloud storages) will return absolute URLs anyways so this
function is not needed in that case.
"""
if '://' not in settings.STATIC_URL and url.startswith(settings.STATIC_URL):
url = urljoin(settings.SITE_URL, url)
else:
url = urljoin(settings.MEDIA_URL, url)
return url

View File

@@ -120,4 +120,5 @@ def send_notification_mail(notification: Notification, user: User):
'html': body_html,
'sender': settings.MAIL_FROM,
'headers': {},
'user': user.pk
})

View File

@@ -1,4 +1,3 @@
import inspect
import json
import logging
from collections import Counter, namedtuple
@@ -6,23 +5,20 @@ from datetime import datetime, time, timedelta
from decimal import Decimal
from typing import List, Optional
import pytz
from celery.exceptions import MaxRetriesExceededError
from django.conf import settings
from django.db import transaction
from django.db.models import Exists, F, Max, OuterRef, Q, Sum
from django.db.models.functions import Greatest
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now
from django.utils.translation import ugettext as _
from django_scopes import scopes_disabled
from pretix.api.models import OAuthApplication
from pretix.base.i18n import (
LazyCurrencyNumber, LazyDate, LazyLocaleException, LazyNumber, language,
)
from pretix.base.email import get_email_context
from pretix.base.i18n import LazyLocaleException, language
from pretix.base.models import (
CartPosition, Device, Event, Item, ItemVariation, Order, OrderPayment,
OrderPosition, Quota, Seat, SeatCategoryMapping, User, Voucher,
@@ -45,7 +41,6 @@ from pretix.base.services.locking import LockTimeoutException, NoLockManager
from pretix.base.services.mail import SendMailException
from pretix.base.services.pricing import get_price
from pretix.base.services.tasks import ProfiledEventTask, ProfiledTask
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import (
allow_ticket_download, order_approved, order_canceled, order_changed,
order_denied, order_expired, order_fee_calculation, order_placed,
@@ -53,7 +48,6 @@ from pretix.base.signals import (
)
from pretix.celery_app import app
from pretix.helpers.models import modelcopy
from pretix.multidomain.urlreverse import build_absolute_uri
error_messages = {
'unavailable': _('Some of the products you selected were no longer available. '
@@ -206,13 +200,6 @@ def approve_order(order, user=None, send_mail: bool=True, auth=None, force=False
# send_mail will trigger PDF generation later
if send_mail:
try:
invoice_name = order.invoice_address.name
invoice_company = order.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
with language(order.locale):
if order.total == Decimal('0.00'):
email_template = order.event.settings.mail_text_order_free
@@ -221,20 +208,7 @@ def approve_order(order, user=None, send_mail: bool=True, auth=None, force=False
email_template = order.event.settings.mail_text_order_approved
email_subject = _('Order approved and awaiting payment: %(code)s') % {'code': order.code}
email_context = {
'total': LazyNumber(order.total),
'currency': order.event.currency,
'total_with_currency': LazyCurrencyNumber(order.total, order.event.currency),
'date': LazyDate(order.expires),
'event': order.event.name,
'url': build_absolute_uri(order.event, 'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash()
}),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
}
email_context = get_email_context(event=order.event, order=order)
try:
order.send_mail(
email_subject, email_template, email_context,
@@ -275,28 +249,8 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
order_denied.send(order.event, order=order)
if send_mail:
try:
invoice_name = order.invoice_address.name
invoice_company = order.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
email_template = order.event.settings.mail_text_order_denied
email_context = {
'total': LazyNumber(order.total),
'currency': order.event.currency,
'total_with_currency': LazyCurrencyNumber(order.total, order.event.currency),
'date': LazyDate(order.expires),
'event': order.event.name,
'url': build_absolute_uri(order.event, 'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash()
}),
'comment': comment,
'invoice_name': invoice_name,
'invoice_company': invoice_company,
}
email_context = get_email_context(event=order.event, order=order, comment=comment)
with language(order.locale):
email_subject = _('Order denied: %(code)s') % {'code': order.code}
try:
@@ -379,16 +333,8 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
if send_mail:
email_template = order.event.settings.mail_text_order_canceled
email_context = {
'event': order.event.name,
'code': order.code,
'url': build_absolute_uri(order.event, 'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash()
})
}
with language(order.locale):
email_context = get_email_context(event=order.event, order=order)
email_subject = _('Order canceled: %(code)s') % {'code': order.code}
try:
order.send_mail(
@@ -403,7 +349,15 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
class OrderError(LazyLocaleException):
pass
def __init__(self, *args):
msg = args[0]
msgargs = args[1] if len(args) > 1 else None
self.args = args
if msgargs:
msg = _(msg) % msgargs
else:
msg = _(msg)
super().__init__(msg)
def _check_date(event: Event, now_dt: datetime):
@@ -501,12 +455,14 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
if cp.seat:
seats_seen.add(cp.seat)
if cp.item.require_voucher and cp.voucher is None:
if cp.item.require_voucher and cp.voucher is None and not cp.is_bundled:
delete(cp)
err = err or error_messages['voucher_required']
break
if cp.item.hide_without_voucher and (cp.voucher is None or not cp.voucher.show_hidden_items or not cp.voucher.applies_to(cp.item, cp.variation)):
if cp.item.hide_without_voucher and (
cp.voucher is None or not cp.voucher.show_hidden_items or not cp.voucher.applies_to(cp.item, cp.variation)
) and not cp.is_bundled:
delete(cp)
cp.delete()
err = error_messages['voucher_required']
@@ -672,36 +628,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
def _order_placed_email(event: Event, order: Order, pprov: BasePaymentProvider, email_template, log_entry: str,
invoice, payment: OrderPayment):
try:
invoice_name = order.invoice_address.name
invoice_company = order.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
if pprov:
if 'payment' in inspect.signature(pprov.order_pending_mail_render).parameters:
payment_info = str(pprov.order_pending_mail_render(order, payment))
else:
payment_info = str(pprov.order_pending_mail_render(order))
else:
payment_info = None
email_context = {
'total': LazyNumber(order.total),
'currency': event.currency,
'total_with_currency': LazyCurrencyNumber(order.total, event.currency),
'date': LazyDate(order.expires),
'event': event.name,
'url': build_absolute_uri(event, 'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash()
}),
'payment_info': payment_info,
'invoice_name': invoice_name,
'invoice_company': invoice_company,
}
email_context = get_email_context(event=event, order=order, payment=payment if pprov else None)
email_subject = _('Your order: %(code)s') % {'code': order.code}
try:
order.send_mail(
@@ -715,19 +642,7 @@ def _order_placed_email(event: Event, order: Order, pprov: BasePaymentProvider,
def _order_placed_email_attendee(event: Event, order: Order, position: OrderPosition, email_template, log_entry: str):
name_scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
email_context = {
'event': event.name,
'url': build_absolute_uri(event, 'presale:event.order.position', kwargs={
'order': order.code,
'secret': position.web_secret,
'position': position.positionid
}),
'attendee_name': position.attendee_name,
}
for f, l, w in name_scheme['fields']:
email_context['attendee_name_%s' % f] = position.attendee_name_parts.get(f, '')
email_context = get_email_context(event=event, order=order, position=position)
email_subject = _('Your event registration: %(code)s') % {'code': order.code}
try:
@@ -874,29 +789,12 @@ def send_expiry_warnings(sender, **kwargs):
eventcache[o.event.pk] = eventsettings
days = eventsettings.get('mail_days_order_expire_warning', as_type=int)
tz = pytz.timezone(eventsettings.get('timezone', settings.TIME_ZONE))
if days and (o.expires - today).days <= days:
with language(o.locale):
o.expiry_reminder_sent = True
o.save(update_fields=['expiry_reminder_sent'])
try:
invoice_name = o.invoice_address.name
invoice_company = o.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
email_template = eventsettings.mail_text_order_expire_warning
email_context = {
'event': o.event.name,
'url': build_absolute_uri(o.event, 'presale:event.order.open', kwargs={
'order': o.code,
'secret': o.secret,
'hash': o.email_confirm_hash()
}),
'expire_date': date_format(o.expires.astimezone(tz), 'SHORT_DATE_FORMAT'),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
}
email_context = get_email_context(event=o.event, order=o)
if eventsettings.payment_term_expire_automatically:
email_subject = _('Your order is about to expire: %(code)s') % {'code': o.code}
else:
@@ -939,14 +837,7 @@ def send_download_reminders(sender, **kwargs):
o.download_reminder_sent = True
o.save(update_fields=['download_reminder_sent'])
email_template = e.settings.mail_text_download_reminder
email_context = {
'event': o.event.name,
'url': build_absolute_uri(o.event, 'presale:event.order.open', kwargs={
'order': o.code,
'secret': o.secret,
'hash': o.email_confirm_hash()
}),
}
email_context = get_email_context(event=e, order=o)
email_subject = _('Your ticket is ready for download: %(code)s') % {'code': o.code}
try:
o.send_mail(
@@ -958,21 +849,10 @@ def send_download_reminders(sender, **kwargs):
logger.exception('Reminder email could not be sent')
if e.settings.mail_send_download_reminder_attendee:
name_scheme = PERSON_NAME_SCHEMES[e.settings.name_scheme]
for p in o.positions.all():
if p.addon_to_id is None and p.attendee_email and p.attendee_email != o.email:
email_template = e.settings.mail_text_download_reminder_attendee
email_context = {
'event': e.name,
'url': build_absolute_uri(e, 'presale:event.order.position', kwargs={
'order': o.code,
'secret': p.web_secret,
'position': p.positionid
}),
'attendee_name': p.attendee_name,
}
for f, l, w in name_scheme['fields']:
email_context['attendee_name_%s' % f] = p.attendee_name_parts.get(f, '')
email_context = get_email_context(event=e, order=o, position=p)
try:
o.send_mail(
email_subject, email_template, email_context,
@@ -985,23 +865,8 @@ def send_download_reminders(sender, **kwargs):
def notify_user_changed_order(order, user=None, auth=None):
with language(order.locale):
try:
invoice_name = order.invoice_address.name
invoice_company = order.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
email_template = order.event.settings.mail_text_order_changed
email_context = {
'event': order.event.name,
'url': build_absolute_uri(order.event, 'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash()
}),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
}
email_context = get_email_context(event=order.event, order=order)
email_subject = _('Your order has been changed: %(code)s') % {'code': order.code}
try:
order.send_mail(
@@ -1039,12 +904,13 @@ class OrderChangeManager:
SplitOperation = namedtuple('SplitOperation', ('position',))
RegenerateSecretOperation = namedtuple('RegenerateSecretOperation', ('position',))
def __init__(self, order: Order, user=None, auth=None, notify=True):
def __init__(self, order: Order, user=None, auth=None, notify=True, reissue_invoice=True):
self.order = order
self.user = user
self.auth = auth
self.event = order.event
self.split_order = None
self.reissue_invoice = reissue_invoice
self._committed = False
self._totaldiff = 0
self._quotadiff = Counter()
@@ -1527,7 +1393,7 @@ class OrderChangeManager:
def _reissue_invoice(self):
i = self.order.invoices.filter(is_cancellation=False).last()
if i and self._invoice_dirty:
if self.reissue_invoice and i and self._invoice_dirty:
generate_cancellation(i)
generate_invoice(self.order)
@@ -1674,7 +1540,9 @@ def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_tok
raise OrderError(error_messages['busy'])
def change_payment_provider(order: Order, payment_provider, amount=None, new_payment=None):
def change_payment_provider(order: Order, payment_provider, amount=None, new_payment=None, create_log=True,
recreate_invoices=True):
oldtotal = order.total
e = OrderPayment.objects.filter(fee=OuterRef('pk'), state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED,
OrderPayment.PAYMENT_STATE_REFUNDED))
open_fees = list(
@@ -1720,4 +1588,30 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
order.total = (order.positions.aggregate(sum=Sum('price'))['sum'] or 0) + (order.fees.aggregate(sum=Sum('value'))['sum'] or 0)
order.save(update_fields=['total'])
return old_fee, new_fee, fee
if not new_payment:
new_payment = order.payments.create(
state=OrderPayment.PAYMENT_STATE_CREATED,
provider=payment_provider.identifier,
amount=order.pending_sum,
fee=fee
)
if create_log and new_payment:
order.log_action(
'pretix.event.order.payment.changed' if open_payment else 'pretix.event.order.payment.started',
{
'fee': new_fee,
'old_fee': old_fee,
'provider': payment_provider.identifier,
'payment': new_payment.pk,
'local_id': new_payment.local_id,
}
)
if recreate_invoices:
i = order.invoices.filter(is_cancellation=False).last()
if i and order.total != oldtotal:
generate_cancellation(i)
generate_invoice(order)
return old_fee, new_fee, fee, new_payment

View File

@@ -25,18 +25,33 @@ def validate_plan_change(event, subevent, plan):
def generate_seats(event, subevent, plan, mapping):
current_seats = {
s.seat_guid: s for s in
event.seats.select_related('product').annotate(has_op=Count('orderposition')).filter(subevent=subevent)
}
current_seats = {}
for s in event.seats.select_related('product').annotate(has_op=Count('orderposition')).filter(subevent=subevent):
if s.seat_guid in current_seats:
s.delete() # Duplicates should not exist
else:
current_seats[s.seat_guid] = s
def update(o, a, v):
if getattr(o, a) != v:
setattr(o, a, v)
return True
return False
create_seats = []
if plan:
for ss in plan.iter_all_seats():
p = mapping.get(ss.category)
if ss.guid in current_seats:
seat = current_seats.pop(ss.guid)
if seat.product != p:
seat.product = p
updated = any([
update(seat, 'product', p),
update(seat, 'name', ss.name),
update(seat, 'row_name', ss.row),
update(seat, 'seat_number', ss.number),
update(seat, 'zone_name', ss.zone),
])
if updated:
seat.save()
else:
create_seats.append(Seat(
@@ -44,6 +59,9 @@ def generate_seats(event, subevent, plan, mapping):
subevent=subevent,
seat_guid=ss.guid,
name=ss.name,
row_name=ss.row,
seat_number=ss.number,
zone_name=ss.zone,
product=p,
))

View File

@@ -76,7 +76,8 @@ def dictsum(*dicts) -> dict:
def order_overview(
event: Event, subevent: SubEvent=None, date_filter='', date_from=None, date_until=None
event: Event, subevent: SubEvent=None, date_filter='', date_from=None, date_until=None, fees=False,
admission_only=False
) -> Tuple[List[Tuple[ItemCategory, List[Item]]], Dict[str, Tuple[Decimal, Decimal]]]:
items = event.items.all().select_related(
'category', # for re-grouping
@@ -87,6 +88,9 @@ def order_overview(
qs = OrderPosition.all
if subevent:
qs = qs.filter(subevent=subevent)
if admission_only:
qs = qs.filter(item__admission=True)
items = items.filter(admission=True)
if date_from and isinstance(date_from, date):
date_from = make_aware(datetime.combine(
@@ -189,7 +193,7 @@ def order_overview(
payment_cat_obj.name = _('Fees')
payment_items = []
if not subevent:
if not subevent and fees:
qs = OrderFee.all.filter(
order__event=event
).annotate(

View File

@@ -106,3 +106,15 @@ class TransactionAwareTask(ProfiledTask):
transaction.on_commit(
lambda: super(TransactionAwareTask, self).apply_async(*args, **kwargs)
)
class TransactionAwareProfiledEventTask(ProfiledEventTask):
def apply_async(self, *args, **kwargs):
"""
Unlike the default task in celery, this task does not return an async
result
"""
transaction.on_commit(
lambda: super(TransactionAwareProfiledEventTask, self).apply_async(*args, **kwargs)
)

View File

@@ -22,7 +22,7 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
qs = WaitingListEntry.objects.filter(
event=event, voucher__isnull=True
).select_related('item', 'variation').prefetch_related(
).select_related('item', 'variation', 'subevent').prefetch_related(
'item__quotas', 'variation__quotas'
).order_by('-priority', 'created')
@@ -34,12 +34,14 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
with event.lock():
for wle in qs:
if (wle.item, wle.variation) in gone:
if (wle.item, wle.variation, wle.subevent) in gone:
continue
ev = (wle.subevent or event)
if not ev.presale_is_running or (wle.subevent and not wle.subevent.active):
continue
if wle.subevent and not wle.subevent.presale_is_running:
continue
quotas = (wle.variation.quotas.filter(subevent=wle.subevent)
if wle.variation
@@ -63,7 +65,7 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
quota_cache[q.pk][1] - 1 if quota_cache[q.pk][1] is not None else sys.maxsize
)
else:
gone.add((wle.item, wle.variation))
gone.add((wle.item, wle.variation, wle.subevent))
return sent
@@ -75,5 +77,5 @@ def process_waitinglist(sender, **kwargs):
live=True
).prefetch_related('_settings_objects', 'organizer___settings_objects').select_related('organizer')
for e in qs:
if e.settings.waiting_list_auto and e.presale_is_running:
if e.settings.waiting_list_auto and (e.presale_is_running or e.has_subevents):
assign_automatically.apply_async(args=(e.pk,))

View File

@@ -93,6 +93,10 @@ DEFAULTS = {
'default': '',
'type': str,
},
'invoice_numbers_prefix_cancellations': {
'default': '',
'type': str,
},
'invoice_renderer': {
'default': 'classic',
'type': str,
@@ -382,7 +386,7 @@ Your {event} team"""))
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
we successfully received your order for {event} with a total value
of {total_with_currency}. Please complete your payment before {date}.
of {total_with_currency}. Please complete your payment before {expire_date}.
{payment_info}
@@ -510,7 +514,7 @@ Your {event} team"""))
we approved your order for {event} and will be happy to welcome you
at our event.
Please continue by paying for your order before {date}.
Please continue by paying for your order before {expire_date}.
You can select a payment method and perform the payment here:
@@ -876,6 +880,19 @@ PERSON_NAME_SCHEMES = OrderedDict([
},
}),
])
COUNTRIES_WITH_STATE_IN_ADDRESS = {
# Source: http://www.bitboost.com/ref/international-address-formats.html
# This is not a list of countries that *have* states, this is a list of countries where states
# are actually *used* in postal addresses. This is obviously not complete and opinionated.
# Country: [(List of subdivision types as defined by pycountry), (short or long form to be used)]
'AU': (['State', 'Territory'], 'short'),
'BR': (['State'], 'short'),
'CA': (['Province', 'Territory'], 'short'),
'CN': (['Province', 'Autonomous region', 'Munincipality'], 'long'),
'MY': (['State'], 'long'),
'MX': (['State', 'Federal District'], 'short'),
'US': (['State', 'Outlying area', 'District'], 'short'),
}
settings_hierarkey = Hierarkey(attribute_name='settings')

View File

@@ -139,6 +139,24 @@ class EventPluginSignal(django.dispatch.Signal):
return sorted_list
class GlobalSignal(django.dispatch.Signal):
def send_chained(self, sender: Event, chain_kwarg_name, **named) -> List[Tuple[Callable, Any]]:
"""
Send signal from sender to all connected receivers. The return value of the first receiver
will be used as the keyword argument specified by ``chain_kwarg_name`` in the input to the
second receiver and so on. The return value of the last receiver is returned by this method.
"""
response = named.get(chain_kwarg_name)
if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS:
return response
for receiver in self._live_receivers(sender):
named[chain_kwarg_name] = response
response = receiver(signal=self, sender=sender, **named)
return response
class DeprecatedSignal(django.dispatch.Signal):
def connect(self, receiver, sender=None, weak=True, dispatch_uid=None):
@@ -168,6 +186,16 @@ subclass of pretix.base.payment.BasePaymentProvider or a list of these
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
register_mail_placeholders = EventPluginSignal(
providing_args=[]
)
"""
This signal is sent out to get all known email text placeholders. Receivers should return
an instance of a subclass of pretix.base.email.BaseMailTextPlaceholder or a list of these.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
register_html_mail_renderers = EventPluginSignal(
providing_args=[]
)
@@ -500,7 +528,7 @@ As with all event-plugin signals, the ``sender`` keyword argument will contain t
"""
email_filter = EventPluginSignal(
providing_args=['message', 'order']
providing_args=['message', 'order', 'user']
)
"""
This signal allows you to implement a middleware-style filter on all outgoing emails. You are expected to
@@ -510,8 +538,24 @@ As with all event-plugin signals, the ``sender`` keyword argument will contain t
The ``message`` argument will contain an ``EmailMultiAlternatives`` object.
If the email is associated with a specific order, the ``order`` argument will be passed as well, otherwise
it will be ``None``.
If the email is associated with a specific user, e.g. a notification email, the ``user`` argument will be passed as
well, otherwise it will be ``None``.
"""
global_email_filter = GlobalSignal(
providing_args=['message', 'order', 'user']
)
"""
This signal allows you to implement a middleware-style filter on all outgoing emails. You are expected to
return a (possibly modified) copy of the message object passed to you.
This signal is called on all events and even if there is no known event. ``sender`` is an event or None.
The ``message`` argument will contain an ``EmailMultiAlternatives`` object.
If the email is associated with a specific order, the ``order`` argument will be passed as well, otherwise
it will be ``None``.
If the email is associated with a specific user, e.g. a notification email, the ``user`` argument will be passed as
well, otherwise it will be ``None``.
"""
layout_text_variables = EventPluginSignal()
"""

View File

@@ -159,8 +159,7 @@
<!--[if !mso]><!-- -->
<tr>
<td>
<img class="wide" src="data:image/png;base64,
iVBORw0KGgoAAAANSUhEUgAAAlgAAAA8CAAAAACf95tlAAAAAXNCSVQI5gpbmQAAAAlwSFlzAAAOxAAADsQBlSsOGwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAG/SURBVHja7dvRboMwDIXhvf/DLiQQAwkku9+qDgq2hPyfN6j1qTlx06/uMunbLMnnhL98fuzRDtYILEeZ7GBNwAIWsIB1LdkOVgaWo4gdLAGWo6x2sFZgOUq1g1WB5SjNDlYDlqcEK1dDB5anmK3eE7C4FnIpBNbVFLo7sB7d3huwKFlULGA9pWQJsJxls4G1ActbooWr2IHlLbMFrBlY7rJbwNqBxb2QZ8nAuiUGO9ICLOo71R1YN0X9td8KLJ8ZeDEDrAd+Za3A4mLIz4TAujGqv+tUYPmN4v8LcweW3zS1t++hActzCrtRYD3pMJQOLOeJ7NyBpZFdoWaFDVjuJ6BRswpTBZbCAn5hpsDq/fbHpDMTBZbC1TAzT2ApyMIVsDROQ2GWwFJo8PR2YP3eOtywzwrsGYD1J9vlHXzcmSKw7q/wU2OEwHpdtALHILA00jJfV8DSaVofvYOPlckB658sp/8VNrBkANahqnXqfhhXJgasgymHD8REZwfWmezzga+tQdhcAet0qry1FYV3osD6dP1QJL3YbYUkhfUCsK6einWRPI0pxjROWZbK+QcsAiwCLEKARYBFgEXIu/wAYbjtwujw8KwAAAAASUVORK5CYII="
<img class="wide" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAlgAAAA8CAAAAACf95tlAAAAAXNCSVQI5gpbmQAAAAlwSFlzAAAOxAAADsQBlSsOGwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAG/SURBVHja7dvRboMwDIXhvf/DLiQQAwkku9+qDgq2hPyfN6j1qTlx06/uMunbLMnnhL98fuzRDtYILEeZ7GBNwAIWsIB1LdkOVgaWo4gdLAGWo6x2sFZgOUq1g1WB5SjNDlYDlqcEK1dDB5anmK3eE7C4FnIpBNbVFLo7sB7d3huwKFlULGA9pWQJsJxls4G1ActbooWr2IHlLbMFrBlY7rJbwNqBxb2QZ8nAuiUGO9ICLOo71R1YN0X9td8KLJ8ZeDEDrAd+Za3A4mLIz4TAujGqv+tUYPmN4v8LcweW3zS1t++hActzCrtRYD3pMJQOLOeJ7NyBpZFdoWaFDVjuJ6BRswpTBZbCAn5hpsDq/fbHpDMTBZbC1TAzT2ApyMIVsDROQ2GWwFJo8PR2YP3eOtywzwrsGYD1J9vlHXzcmSKw7q/wU2OEwHpdtALHILA00jJfV8DSaVofvYOPlckB658sp/8VNrBkANahqnXqfhhXJgasgymHD8REZwfWmezzga+tQdhcAet0qry1FYV3osD6dP1QJL3YbYUkhfUCsK6einWRPI0pxjROWZbK+QcsAiwCLEKARYBFgEXIu/wAYbjtwujw8KwAAAAASUVORK5CYII="
style="max-height: 60px;">
</td>
</tr>

View File

@@ -14,6 +14,8 @@ def money_filter(value: Decimal, arg='', hide_currency=False):
if isinstance(value, float) or isinstance(value, int):
value = Decimal(value)
if not isinstance(value, Decimal):
if value == '':
return value
raise TypeError("Invalid data type passed to money filter: %r" % type(value))
if not arg:
raise ValueError("No currency passed.")

View File

@@ -54,7 +54,7 @@ ALLOWED_ATTRIBUTES = {
'td': ['width', 'align'],
'div': ['class'],
'p': ['class'],
'span': ['class'],
'span': ['class', 'title'],
# Update doc/user/markdown.rst if you change this!
}

View File

@@ -0,0 +1,16 @@
import pycountry
from django.http import JsonResponse
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
def states(request):
cc = request.GET.get("country", "DE")
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
return JsonResponse({'data': []})
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc]
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
return JsonResponse({'data': [
{'name': s.name, 'code': s.code[3:]}
for s in sorted(statelist, key=lambda s: s.name)
]})

View File

@@ -160,7 +160,7 @@ class OrderQuestionsViewMixin(BaseQuestionsViewMixin):
def positions(self):
qqs = self.request.event.questions.all()
if self.only_user_visible:
qqs = qqs.filter(ask_during_checkin=False)
qqs = qqs.filter(ask_during_checkin=False, hidden=False)
return list(self.order.positions.select_related(
'item', 'variation'
).prefetch_related(

View File

@@ -99,11 +99,6 @@ def contextprocessor(request):
ctx['js_locale'] = get_moment_locale()
ctx['select2locale'] = get_language()[:2]
if settings.DEBUG and 'runserver' not in sys.argv:
ctx['debug_warning'] = True
elif 'runserver' in sys.argv:
ctx['development_warning'] = True
ctx['warning_update_available'] = False
ctx['warning_update_check_active'] = False
gs = GlobalSettingsObject()

View File

@@ -5,6 +5,7 @@ from django_scopes.forms import (
SafeModelChoiceField, SafeModelMultipleChoiceField,
)
from pretix.base.channels import get_all_sales_channels
from pretix.base.models.checkin import CheckinList
from pretix.control.forms.widgets import Select2
@@ -15,6 +16,16 @@ class CheckinListForm(forms.ModelForm):
kwargs.pop('locales', None)
super().__init__(**kwargs)
self.fields['limit_products'].queryset = self.event.items.all()
self.fields['auto_checkin_sales_channels'] = forms.MultipleChoiceField(
label=self.fields['auto_checkin_sales_channels'].label,
help_text=self.fields['auto_checkin_sales_channels'].help_text,
required=self.fields['auto_checkin_sales_channels'].required,
choices=(
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
),
widget=forms.CheckboxSelectMultiple
)
if self.event.has_subevents:
self.fields['subevent'].queryset = self.event.subevents.all()
self.fields['subevent'].widget = Select2(
@@ -40,12 +51,14 @@ class CheckinListForm(forms.ModelForm):
'all_products',
'limit_products',
'subevent',
'include_pending'
'include_pending',
'auto_checkin_sales_channels'
]
widgets = {
'limit_products': forms.CheckboxSelectMultiple(attrs={
'data-inverse-dependency': '<[name$=all_products]'
}),
'auto_checkin_sales_channels': forms.CheckboxSelectMultiple()
}
field_classes = {
'limit_products': SafeModelMultipleChoiceField,

View File

@@ -20,6 +20,7 @@ from i18nfield.forms import (
from pytz import common_timezones, timezone
from pretix.base.channels import get_all_sales_channels
from pretix.base.email import get_available_placeholders
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
from pretix.base.models import Event, Organizer, TaxRule
from pretix.base.models.event import EventMetaValue, SubEvent
@@ -177,7 +178,7 @@ class EventWizardBasicsForm(I18nModelForm):
return slug
class EventChoiceField(forms.ModelChoiceField):
class EventChoiceMixin:
def label_from_instance(self, obj):
return mark_safe('{}<br /><span class="text-muted">{} · {}</span>'.format(
escape(str(obj)),
@@ -186,6 +187,16 @@ class EventChoiceField(forms.ModelChoiceField):
))
class EventChoiceField(forms.ModelChoiceField):
pass
class SafeEventMultipleChoiceField(EventChoiceMixin, forms.ModelMultipleChoiceField):
def __init__(self, queryset, *args, **kwargs):
queryset = queryset.model.objects.none()
super().__init__(queryset, *args, **kwargs)
class EventWizardCopyForm(forms.Form):
@staticmethod
@@ -325,7 +336,7 @@ class EventSettingsForm(SettingsForm):
)
timezone = forms.ChoiceField(
choices=((a, a) for a in common_timezones),
label=_("Default timezone"),
label=_("Event timezone"),
)
locales = forms.MultipleChoiceField(
choices=settings.LANGUAGES,
@@ -395,8 +406,8 @@ class EventSettingsForm(SettingsForm):
"only to that email address. If you enable this option, the system will additionally ask for "
"individual email addresses for every admission ticket. This might be useful if you want to "
"obtain individual addresses for every attendee even in case of group orders. However, "
"pretix will send the order confirmation only to the one primary email address, not to the "
"per-attendee addresses."),
"pretix will send the order confirmation by default only to the one primary email address, not to "
"the per-attendee addresses. You can however enable this in the E-mail settings."),
required=False
)
attendee_emails_required = forms.BooleanField(
@@ -441,10 +452,100 @@ class EventSettingsForm(SettingsForm):
required=False,
help_text=_("We'll show this publicly to allow attendees to contact you.")
)
show_variations_expanded = forms.BooleanField(
label=_("Show variations of a product expanded by default"),
required=False
)
hide_sold_out = forms.BooleanField(
label=_("Hide all products that are sold out"),
required=False
)
meta_noindex = forms.BooleanField(
label=_('Ask search engines not to index the ticket shop'),
required=False
)
redirect_to_checkout_directly = forms.BooleanField(
label=_('Directly redirect to check-out after a product has been added to the cart.'),
required=False
)
frontpage_subevent_ordering = forms.ChoiceField(
label=pgettext('subevent', 'Date ordering'),
choices=[
('date_ascending', _('Event start time')),
('date_descending', _('Event start time (descending)')),
('name_ascending', _('Name')),
('name_descending', _('Name (descending)')),
], # When adding a new ordering, remember to also define it in the event model
)
logo_image = ExtFileField(
label=_('Logo image'),
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
required=False,
help_text=_('If you provide a logo image, we will by default not show your events name and date '
'in the page header. We will show your logo with a maximal height of 120 pixels.')
)
frontpage_text = I18nFormField(
label=_("Frontpage text"),
required=False,
widget=I18nTextarea
)
presale_has_ended_text = I18nFormField(
label=_("End of presale text"),
required=False,
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown above the ticket shop once the designated sales timeframe for this event "
"is over. You can use it to describe other options to get a ticket, such as a box office.")
)
voucher_explanation_text = I18nFormField(
label=_("Voucher explanation"),
required=False,
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown next to the input for a voucher code. You can use it e.g. to explain "
"how to obtain a voucher code.")
)
primary_color = forms.CharField(
label=_("Primary color"),
required=False,
validators=[
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
message=_('Please enter the hexadecimal code of a color, e.g. #990000.')),
],
widget=forms.TextInput(attrs={'class': 'colorpickerfield'})
)
theme_color_success = forms.CharField(
label=_("Accent color for success"),
help_text=_("We strongly suggest to use a shade of green."),
required=False,
validators=[
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
message=_('Please enter the hexadecimal code of a color, e.g. #990000.')),
],
widget=forms.TextInput(attrs={'class': 'colorpickerfield'})
)
theme_color_danger = forms.CharField(
label=_("Accent color for errors"),
help_text=_("We strongly suggest to use a dark shade of red."),
required=False,
validators=[
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
message=_('Please enter the hexadecimal code of a color, e.g. #990000.')),
],
widget=forms.TextInput(attrs={'class': 'colorpickerfield'})
)
primary_font = forms.ChoiceField(
label=_('Font'),
choices=[
('Open Sans', 'Open Sans')
],
widget=FontSelect,
help_text=_('Only respected by modern browsers.')
)
def clean(self):
data = super().clean()
if data['locale'] not in data['locales']:
if 'locales' in data and data['locale'] not in data['locales']:
raise ValidationError({
'locale': _('Your default locale must also be enabled for your event (see box above).')
})
@@ -459,6 +560,7 @@ class EventSettingsForm(SettingsForm):
return data
def __init__(self, *args, **kwargs):
event = kwargs['obj']
super().__init__(*args, **kwargs)
self.fields['confirm_text'].widget.attrs['rows'] = '3'
self.fields['confirm_text'].widget.attrs['placeholder'] = _(
@@ -479,6 +581,11 @@ class EventSettingsForm(SettingsForm):
))
for k, v in PERSON_NAME_TITLE_GROUPS.items()
]
if not event.has_subevents:
del self.fields['frontpage_subevent_ordering']
self.fields['primary_font'].choices += [
(a, a) for a in get_fonts()
]
class CancelSettingsForm(SettingsForm):
@@ -691,6 +798,12 @@ class InvoiceSettingsForm(SettingsForm):
"used at most once over all of your events. This setting only affects future invoices."),
required=False,
)
invoice_numbers_prefix_cancellations = forms.CharField(
label=_("Invoice number prefix for cancellations"),
help_text=_("This will be prepended to invoice numbers of cancellations. If you leave this field empty, "
"the same numbering scheme will be used that you configured for regular invoices."),
required=False,
)
invoice_generate = forms.ChoiceField(
label=_("Generate invoices"),
required=False,
@@ -824,6 +937,10 @@ class InvoiceSettingsForm(SettingsForm):
(r.identifier, r.verbose_name) for r in event.get_invoice_renderers().values()
]
self.fields['invoice_numbers_prefix'].widget.attrs['placeholder'] = event.slug.upper() + '-'
if event.settings.invoice_numbers_prefix:
self.fields['invoice_numbers_prefix_cancellations'].widget.attrs['placeholder'] = event.settings.invoice_numbers_prefix
else:
self.fields['invoice_numbers_prefix_cancellations'].widget.attrs['placeholder'] = event.slug.upper() + '-'
locale_names = dict(settings.LANGUAGES)
self.fields['invoice_language'].choices = [('__user__', _('The user\'s language'))] + [(a, locale_names[a]) for a in event.settings.locales]
self.fields['invoice_generate_sales_channels'].choices = (
@@ -886,10 +1003,6 @@ class MailSettingsForm(SettingsForm):
label=_("Text sent to order contact address"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {total_with_currency}, {total}, {currency}, {date}, "
"{payment_info}, {url}, {invoice_name}, {invoice_company}"),
validators=[PlaceholderValidator(['{event}', '{total_with_currency}', '{total}', '{currency}', '{date}',
'{payment_info}', '{url}', '{invoice_name}', '{invoice_company}'])]
)
mail_send_order_placed_attendee = forms.BooleanField(
label=_("Send an email to attendees"),
@@ -901,16 +1014,12 @@ class MailSettingsForm(SettingsForm):
label=_("Text sent to attendees"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {attendee_name}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{attendee_name}'])],
)
mail_text_order_paid = I18nFormField(
label=_("Text sent to order contact address"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}, {payment_info}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{invoice_name}', '{invoice_company}', '{payment_info}'])]
)
mail_send_order_paid_attendee = forms.BooleanField(
label=_("Send an email to attendees"),
@@ -922,16 +1031,12 @@ class MailSettingsForm(SettingsForm):
label=_("Text sent to attendees"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {attendee_name}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{attendee_name}'])],
)
mail_text_order_free = I18nFormField(
label=_("Text sent to order contact address"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{invoice_name}', '{invoice_company}'])]
)
mail_send_order_free_attendee = forms.BooleanField(
label=_("Send an email to attendees"),
@@ -943,34 +1048,26 @@ class MailSettingsForm(SettingsForm):
label=_("Text sent to attendees"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {attendee_name}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{attendee_name}'])],
)
mail_text_order_changed = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{invoice_name}', '{invoice_company}'])]
)
mail_text_resend_link = I18nFormField(
label=_("Text (sent by admin)"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{invoice_name}', '{invoice_company}'])]
)
mail_text_resend_all_links = I18nFormField(
label=_("Text (requested by user)"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {orders}"),
validators=[PlaceholderValidator(['{event}', '{orders}'])]
)
mail_days_order_expire_warning = forms.IntegerField(
label=_("Number of days"),
required=False,
required=True,
min_value=0,
help_text=_("This email will be sent out this many days before the order expires. If the "
"value is 0, the mail will never be sent.")
@@ -979,38 +1076,26 @@ class MailSettingsForm(SettingsForm):
label=_("Text"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {expire_date}, {invoice_name}, {invoice_company}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{expire_date}', '{invoice_name}', '{invoice_company}'])]
)
mail_text_waiting_list = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {product}, {hours}, {code}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{product}', '{hours}', '{code}'])]
)
mail_text_order_canceled = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {code}, {url}"),
validators=[PlaceholderValidator(['{event}', '{code}', '{url}'])]
)
mail_text_order_custom_mail = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {expire_date}, {event}, {code}, {date}, {url}, "
"{invoice_name}, {invoice_company}"),
validators=[PlaceholderValidator(['{expire_date}', '{event}', '{code}', '{date}', '{url}',
'{invoice_name}', '{invoice_company}'])]
)
mail_text_download_reminder = I18nFormField(
label=_("Text sent to order contact address"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}"),
validators=[PlaceholderValidator(['{event}', '{url}'])]
)
mail_send_download_reminder_attendee = forms.BooleanField(
label=_("Send an email to attendees"),
@@ -1022,8 +1107,6 @@ class MailSettingsForm(SettingsForm):
label=_("Text sent to attendees"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {attendee_name}, {event}, {url}"),
validators=[PlaceholderValidator(['{attendee_name}', '{event}', '{url}'])]
)
mail_days_download_reminder = forms.IntegerField(
label=_("Number of days"),
@@ -1036,29 +1119,18 @@ class MailSettingsForm(SettingsForm):
label=_("Received order"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {total_with_currency}, {total}, {currency}, {date}, "
"{url}, {invoice_name}, {invoice_company}"),
validators=[PlaceholderValidator(['{event}', '{total_with_currency}', '{total}', '{currency}', '{date}',
'{url}', '{invoice_name}', '{invoice_company}'])]
)
mail_text_order_approved = I18nFormField(
label=_("Approved order"),
required=False,
widget=I18nTextarea,
help_text=_("This will only be sent out for non-free orders. Free orders will receive the free order "
"template from above instead. Available placeholders: {event}, {total_with_currency}, {total}, "
"{currency}, {date}, {payment_info}, {url}, {invoice_name}, {invoice_company}"),
validators=[PlaceholderValidator(['{event}', '{total_with_currency}', '{total}', '{currency}', '{date}',
'{url}', '{invoice_name}', '{invoice_company}'])]
"template from above instead."),
)
mail_text_order_denied = I18nFormField(
label=_("Denied order"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {total_with_currency}, {total}, {currency}, {date}, "
"{comment}, {url}, {invoice_name}, {invoice_company}"),
validators=[PlaceholderValidator(['{event}', '{total_with_currency}', '{total}', '{currency}', '{date}',
'{comment}', '{url}', '{invoice_name}', '{invoice_company}'])]
)
smtp_use_custom = forms.BooleanField(
label=_("Use custom SMTP server"),
@@ -1097,29 +1169,53 @@ class MailSettingsForm(SettingsForm):
help_text=_("Commonly enabled on port 465."),
required=False
)
base_context = {
'mail_text_order_placed': ['event', 'order', 'payment'],
'mail_text_order_placed_attendee': ['event', 'order', 'position'],
'mail_text_order_placed_require_approval': ['event', 'order'],
'mail_text_order_approved': ['event', 'order'],
'mail_text_order_denied': ['event', 'order', 'comment'],
'mail_text_order_paid': ['event', 'order', 'payment_info'],
'mail_text_order_paid_attendee': ['event', 'order', 'position'],
'mail_text_order_free': ['event', 'order'],
'mail_text_order_free_attendee': ['event', 'order', 'position'],
'mail_text_order_changed': ['event', 'order'],
'mail_text_order_canceled': ['event', 'order'],
'mail_text_order_expire_warning': ['event', 'order'],
'mail_text_order_custom_mail': ['event', 'order'],
'mail_text_download_reminder': ['event', 'order'],
'mail_text_download_reminder_attendee': ['event', 'order', 'position'],
'mail_text_resend_link': ['event', 'order'],
'mail_text_waiting_list': ['event', 'waiting_list_entry'],
'mail_text_resend_all_links': ['event', 'orders']
}
def _set_field_placeholders(self, fn, base_parameters):
phs = [
'{%s}' % p
for p in sorted(get_available_placeholders(self.event, base_parameters).keys())
]
ht = _('Available placeholders: {list}').format(
list=', '.join(phs)
)
if self.fields[fn].help_text:
self.fields[fn].help_text += ' ' + str(ht)
else:
self.fields[fn].help_text = ht
self.fields[fn].validators.append(
PlaceholderValidator(phs)
)
def __init__(self, *args, **kwargs):
event = kwargs.get('obj')
self.event = event = kwargs.get('obj')
super().__init__(*args, **kwargs)
self.fields['mail_html_renderer'].choices = [
(r.identifier, r.verbose_name) for r in event.get_html_mail_renderers().values()
]
keys = list(event.meta_data.keys())
name_scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
for k, v in self.base_context.items():
self._set_field_placeholders(k, v)
for k, v in list(self.fields.items()):
if k.startswith('mail_text_'):
v.help_text = str(v.help_text) + ', ' + ', '.join({
'{meta_' + p + '}' for p in keys
})
v.validators[0].limit_value += ['{meta_' + p + '}' for p in keys]
if '{attendee_name}' in v.validators[0].limit_value:
for f, l, w in name_scheme['fields']:
if f == 'full_name':
continue
v.help_text = str(v.help_text) + ', ' + '{attendee_name_%s}' % f
v.validators[0].limit_value += ['{attendee_name_' + f + '}']
if k.endswith('_attendee') and not event.settings.attendee_emails_asked:
# If we don't ask for attendee emails, we can't send them anything and we don't need to clutter
# the user interface with it
@@ -1137,108 +1233,6 @@ class MailSettingsForm(SettingsForm):
raise ValidationError(_('You can activate either SSL or STARTTLS security, but not both at the same time.'))
class DisplaySettingsForm(SettingsForm):
primary_color = forms.CharField(
label=_("Primary color"),
required=False,
validators=[
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
message=_('Please enter the hexadecimal code of a color, e.g. #990000.')),
],
widget=forms.TextInput(attrs={'class': 'colorpickerfield'})
)
theme_color_success = forms.CharField(
label=_("Accent color for success"),
help_text=_("We strongly suggest to use a shade of green."),
required=False,
validators=[
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
message=_('Please enter the hexadecimal code of a color, e.g. #990000.')),
],
widget=forms.TextInput(attrs={'class': 'colorpickerfield'})
)
theme_color_danger = forms.CharField(
label=_("Accent color for errors"),
help_text=_("We strongly suggest to use a dark shade of red."),
required=False,
validators=[
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
message=_('Please enter the hexadecimal code of a color, e.g. #990000.')),
],
widget=forms.TextInput(attrs={'class': 'colorpickerfield'})
)
logo_image = ExtFileField(
label=_('Logo image'),
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
required=False,
help_text=_('If you provide a logo image, we will by default not show your events name and date '
'in the page header. We will show your logo with a maximal height of 120 pixels.')
)
primary_font = forms.ChoiceField(
label=_('Font'),
choices=[
('Open Sans', 'Open Sans')
],
widget=FontSelect,
help_text=_('Only respected by modern browsers.')
)
frontpage_text = I18nFormField(
label=_("Frontpage text"),
required=False,
widget=I18nTextarea
)
presale_has_ended_text = I18nFormField(
label=_("End of presale text"),
required=False,
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown above the ticket shop once the designated sales timeframe for this event "
"is over. You can use it to describe other options to get a ticket, such as a box office.")
)
voucher_explanation_text = I18nFormField(
label=_("Voucher explanation"),
required=False,
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown next to the input for a voucher code. You can use it e.g. to explain "
"how to obtain a voucher code.")
)
show_variations_expanded = forms.BooleanField(
label=_("Show variations of a product expanded by default"),
required=False
)
hide_sold_out = forms.BooleanField(
label=_("Hide all products that are sold out"),
required=False
)
frontpage_subevent_ordering = forms.ChoiceField(
label=pgettext('subevent', 'Date ordering'),
choices=[
('date_ascending', _('Event start time')),
('date_descending', _('Event start time (descending)')),
('name_ascending', _('Name')),
('name_descending', _('Name (descending)')),
], # When adding a new ordering, remember to also define it in the event model
)
meta_noindex = forms.BooleanField(
label=_('Ask search engines not to index the ticket shop'),
required=False
)
redirect_to_checkout_directly = forms.BooleanField(
label=_('Directly redirect to check-out after a product has been added to the cart.'),
required=False
)
def __init__(self, *args, **kwargs):
event = kwargs['obj']
super().__init__(*args, **kwargs)
self.fields['primary_font'].choices += [
(a, a) for a in get_fonts()
]
if not event.has_subevents:
del self.fields['frontpage_subevent_ordering']
class TicketSettingsForm(SettingsForm):
ticket_download = forms.BooleanField(
label=_("Use feature"),

View File

@@ -902,6 +902,43 @@ class VoucherFilterForm(FilterForm):
return qs
class VoucherTagFilterForm(FilterForm):
subevent = forms.ModelChoiceField(
label=pgettext_lazy('subevent', 'Date'),
queryset=SubEvent.objects.none(),
required=False,
empty_label=pgettext_lazy('subevent', 'All dates')
)
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event')
super().__init__(*args, **kwargs)
if self.event.has_subevents:
self.fields['subevent'].queryset = self.event.subevents.all()
self.fields['subevent'].widget = Select2(
attrs={
'data-model-select2': 'event',
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
}),
'data-placeholder': pgettext_lazy('subevent', 'All dates')
}
)
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
elif 'subevent':
del self.fields['subevent']
def filter_qs(self, qs):
fdata = self.cleaned_data
if fdata.get('subevent'):
qs = qs.filter(subevent_id=fdata.get('subevent').pk)
return qs
class RefundFilterForm(FilterForm):
provider = forms.ChoiceField(
label=_('Payment provider'),

View File

@@ -94,7 +94,8 @@ class QuestionForm(I18nModelForm):
'identifier',
'items',
'dependency_question',
'dependency_values'
'dependency_values',
'print_on_invoice',
]
widgets = {
'items': forms.CheckboxSelectMultiple(
@@ -124,7 +125,7 @@ class QuotaForm(I18nModelForm):
items = kwargs.pop('items', None) or self.event.items.prefetch_related('variations')
self.original_instance = modelcopy(self.instance) if self.instance else None
initial = kwargs.get('initial', {})
if self.instance and self.instance.pk:
if self.instance and self.instance.pk and 'itemvars' not in initial:
initial['itemvars'] = [str(i.pk) for i in self.instance.items.all()] + [
'{}-{}'.format(v.item_id, v.pk) for v in self.instance.variations.all()
]
@@ -358,7 +359,6 @@ class ItemCreateForm(I18nModelForm):
'admission',
'default_price',
'tax_rule',
'allow_cancel'
]
@@ -403,6 +403,19 @@ class ItemUpdateForm(I18nModelForm):
widget=forms.CheckboxSelectMultiple
)
change_decimal_field(self.fields['default_price'], self.event.currency)
self.fields['hidden_if_available'].queryset = self.event.quotas.all()
self.fields['hidden_if_available'].widget = Select2(
attrs={
'data-model-select2': 'generic',
'data-select2-url': reverse('control:event.items.quotas.select2', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
}),
'data-placeholder': _('Quota')
}
)
self.fields['hidden_if_available'].widget.choices = self.fields['hidden_if_available'].choices
self.fields['hidden_if_available'].required = False
class Meta:
model = Item
@@ -425,17 +438,20 @@ class ItemUpdateForm(I18nModelForm):
'require_approval',
'hide_without_voucher',
'allow_cancel',
'allow_waitinglist',
'max_per_order',
'min_per_order',
'checkin_attention',
'generate_tickets',
'original_price',
'require_bundling',
'show_quota_left'
'show_quota_left',
'hidden_if_available',
]
field_classes = {
'available_from': SplitDateTimeField,
'available_until': SplitDateTimeField,
'hidden_if_available': SafeModelChoiceField,
}
widgets = {
'available_from': SplitDateTimePickerWidget(),
@@ -446,6 +462,9 @@ class ItemUpdateForm(I18nModelForm):
class ItemVariationsFormSet(I18nFormSet):
template = "pretixcontrol/item/include_variations.html"
title = _('Variations')
def clean(self):
super().clean()
for f in self.forms:
@@ -502,6 +521,9 @@ class ItemVariationForm(I18nModelForm):
class ItemAddOnsFormSet(I18nFormSet):
title = _('Add-ons')
template = "pretixcontrol/item/include_addons.html"
def __init__(self, *args, **kwargs):
self.event = kwargs.get('event')
super().__init__(*args, **kwargs)
@@ -567,6 +589,9 @@ class ItemAddOnForm(I18nModelForm):
class ItemBundleFormSet(I18nFormSet):
template = "pretixcontrol/item/include_bundles.html"
title = _('Bundled products')
def __init__(self, *args, **kwargs):
self.event = kwargs.get('event')
self.item = kwargs.pop('item')

View File

@@ -8,6 +8,7 @@ from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from pretix.base.email import get_available_placeholders
from pretix.base.forms import I18nModelForm, PlaceholderValidator
from pretix.base.forms.widgets import DatePickerWidget
from pretix.base.models import (
@@ -167,6 +168,15 @@ class OtherOperationsForm(forms.Form):
'Use with care and only if you need to. Note that rounding differences might occur in this procedure.'
)
)
reissue_invoice = forms.BooleanField(
label=_('Issue a new invoice if required'),
required=False,
initial=True,
help_text=_(
'If an invoice exists for this order and this operation would change its contents, the old invoice will '
'be cancelled and a new invoice will be issued.'
)
)
notify = forms.BooleanField(
label=_('Notify user'),
required=False,
@@ -186,10 +196,6 @@ class OtherOperationsForm(forms.Form):
class OrderPositionAddForm(forms.Form):
do = forms.BooleanField(
label=_('Add a new product to the order'),
required=False
)
itemvar = forms.ChoiceField(
label=_('Product')
)
@@ -281,6 +287,28 @@ class OrderPositionAddForm(forms.Form):
change_decimal_field(self.fields['price'], order.event.currency)
class OrderPositionAddFormset(forms.BaseFormSet):
def __init__(self, *args, **kwargs):
self.order = kwargs.pop('order', None)
super().__init__(*args, **kwargs)
def _construct_form(self, i, **kwargs):
kwargs['order'] = self.order
return super()._construct_form(i, **kwargs)
@property
def empty_form(self):
form = self.form(
auto_id=self.auto_id,
prefix=self.add_prefix('__prefix__'),
empty_permitted=True,
use_required_attribute=False,
order=self.order,
)
self.add_fields(form, None)
return form
class OrderPositionChangeForm(forms.Form):
itemvar = forms.ChoiceField(
required=False,
@@ -408,8 +436,24 @@ class OrderMailForm(forms.Form):
required=True
)
def _set_field_placeholders(self, fn, base_parameters):
phs = [
'{%s}' % p
for p in sorted(get_available_placeholders(self.order.event, base_parameters).keys())
]
ht = _('Available placeholders: {list}').format(
list=', '.join(phs)
)
if self.fields[fn].help_text:
self.fields[fn].help_text += ' ' + str(ht)
else:
self.fields[fn].help_text = ht
self.fields[fn].validators.append(
PlaceholderValidator(phs)
)
def __init__(self, *args, **kwargs):
order = kwargs.pop('order')
order = self.order = kwargs.pop('order')
super().__init__(*args, **kwargs)
self.fields['sendto'] = forms.EmailField(
label=_("Recipient"),
@@ -422,11 +466,8 @@ class OrderMailForm(forms.Form):
required=True,
widget=forms.Textarea,
initial=order.event.settings.mail_text_order_custom_mail.localize(order.locale),
help_text=_("Available placeholders: {expire_date}, {event}, {code}, {date}, {url}, "
"{invoice_name}, {invoice_company}"),
validators=[PlaceholderValidator(['{expire_date}', '{event}', '{code}', '{date}', '{url}',
'{invoice_name}', '{invoice_company}'])]
)
self._set_field_placeholders('message', ['event', 'order'])
class OrderRefundForm(forms.Form):

View File

@@ -16,6 +16,7 @@ from pretix.base.models import Device, Organizer, Team
from pretix.control.forms import (
ExtFileField, FontSelect, MultipleLanguagesWidget,
)
from pretix.control.forms.event import SafeEventMultipleChoiceField
from pretix.multidomain.models import KnownDomain
from pretix.presale.style import get_fonts
@@ -136,7 +137,9 @@ class TeamForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
organizer = kwargs.pop('organizer')
super().__init__(*args, **kwargs)
self.fields['limit_events'].queryset = organizer.events.all()
self.fields['limit_events'].queryset = organizer.events.all().order_by(
'-has_subevents', '-date_from'
)
class Meta:
model = Team
@@ -147,11 +150,12 @@ class TeamForm(forms.ModelForm):
'can_view_vouchers', 'can_change_vouchers']
widgets = {
'limit_events': forms.CheckboxSelectMultiple(attrs={
'data-inverse-dependency': '#id_all_events'
'data-inverse-dependency': '#id_all_events',
'class': 'scrolling-multiple-choice scrolling-multiple-choice-large',
}),
}
field_classes = {
'limit_events': SafeModelMultipleChoiceField
'limit_events': SafeEventMultipleChoiceField
}
def clean(self):
@@ -171,7 +175,9 @@ class DeviceForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
organizer = kwargs.pop('organizer')
super().__init__(*args, **kwargs)
self.fields['limit_events'].queryset = organizer.events.all()
self.fields['limit_events'].queryset = organizer.events.all().order_by(
'-has_subevents', '-date_from'
)
def clean(self):
d = super().clean()
@@ -185,11 +191,12 @@ class DeviceForm(forms.ModelForm):
fields = ['name', 'all_events', 'limit_events']
widgets = {
'limit_events': forms.CheckboxSelectMultiple(attrs={
'data-inverse-dependency': '#id_all_events'
'data-inverse-dependency': '#id_all_events',
'class': 'scrolling-multiple-choice scrolling-multiple-choice-large',
}),
}
field_classes = {
'limit_events': SafeModelMultipleChoiceField
'limit_events': SafeEventMultipleChoiceField
}
@@ -201,9 +208,6 @@ class OrganizerSettingsForm(SettingsForm):
widget=I18nTextarea,
help_text=_('Not displayed anywhere by default, but if you want to, you can use this e.g. in ticket templates.')
)
class OrganizerDisplaySettingsForm(SettingsForm):
primary_color = forms.CharField(
label=_("Primary color"),
required=False,

View File

@@ -179,9 +179,7 @@ class VoucherForm(I18nModelForm):
return data
def save(self, commit=True):
super().save(commit)
return ['item']
return super().save(commit)
class VoucherBulkForm(VoucherForm):

View File

@@ -48,14 +48,6 @@ def get_event_navigation(request: HttpRequest):
}),
'active': url.url_name == 'event.settings.plugins',
},
{
'label': _('Display'),
'url': reverse('control:event.settings.display', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': url.url_name == 'event.settings.display',
},
{
'label': _('Tickets'),
'url': reverse('control:event.settings.tickets', kwargs={
@@ -78,7 +70,7 @@ def get_event_navigation(request: HttpRequest):
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': url.url_name == 'event.settings.tax',
'active': url.url_name.startswith('event.settings.tax'),
},
{
'label': _('Invoicing'),
@@ -425,13 +417,6 @@ def get_organizer_navigation(request):
}),
'active': url.url_name == 'organizer.edit',
},
{
'label': _('Display'),
'url': reverse('control:organizer.display', kwargs={
'organizer': request.organizer.slug
}),
'active': url.url_name == 'organizer.display',
},
]
})
if 'can_change_teams' in request.orgapermset:

View File

@@ -237,24 +237,6 @@ As with all plugin signals, the ``sender`` keyword argument will contain the eve
A second keyword argument ``request`` will contain the request object.
"""
nav_item = EventPluginSignal(
providing_args=['request', 'item']
)
"""
This signal is sent out to include tab links on the settings page of an item.
Receivers are expected to return a list of dictionaries. The dictionaries
should contain at least the keys ``label`` and ``url``. You should also return
an ``active`` key with a boolean set to ``True``, when this item should be marked
as active.
If your linked view should stay in the tab-like context of this page, we recommend
that you use ``pretix.control.views.item.ItemDetailMixin`` for your view
and your template inherits from ``pretixcontrol/item/base.html``.
As with all plugin signals, the ``sender`` keyword argument will contain the event.
A second keyword argument ``request`` will contain the request object.
"""
event_settings_widget = EventPluginSignal(
providing_args=['request']
)
@@ -279,6 +261,24 @@ styles. It is advisable to set a prefix for your form to avoid clashes with othe
As with all plugin signals, the ``sender`` keyword argument will contain the event.
"""
item_formsets = EventPluginSignal(
providing_args=['request', 'item']
)
"""
This signal allows you to return additional formsets that should be rendered on the product
modification page. You are passed ``request`` and ``item`` arguments and are expected to return
an instance of a formset class that you bind yourself when appropriate. Your formset will be
executed as part of the standard validation and rendering cycle and rendered using default
bootstrap styles. It is advisable to set a prefix for your formset to avoid clashes with other
plugins.
Your formset needs to have two special properties: ``template`` with a template that will be
included to render the formset and ``title`` that will be used as a headline. Your template
will be passed a ``formset`` variable with your formset.
As with all plugin signals, the ``sender`` keyword argument will contain the event.
"""
subevent_forms = EventPluginSignal(
providing_args=['request', 'subevent']
)

View File

@@ -10,12 +10,16 @@
{% endcompress %}
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="{% static "pretixbase/img/favicon.ico" %}">
{% if development_warning or debug_warning %}
<link rel="shortcut icon" href="{% static "pretixbase/img/favicon-debug.ico" %}">
{% else %}
<link rel="shortcut icon" href="{% static "pretixbase/img/favicon.ico" %}">
<link rel="icon" type="image/png" sizes="32x32" href="{% static "pretixbase/img/icons/favicon-32x32.png" %}">
<link rel="icon" type="image/png" sizes="194x194" href="{% static "pretixbase/img/icons/favicon-194x194.png" %}">
<link rel="icon" type="image/png" sizes="16x16" href="{% static "pretixbase/img/icons/favicon-16x16.png" %}">
{% endif %}
<link rel="apple-touch-icon" sizes="180x180" href="{% static "pretixbase/img/icons/apple-touch-icon.png" %}">
<link rel="icon" type="image/png" sizes="32x32" href="{% static "pretixbase/img/icons/favicon-32x32.png" %}">
<link rel="icon" type="image/png" sizes="194x194" href="{% static "pretixbase/img/icons/favicon-194x194.png" %}">
<link rel="icon" type="image/png" sizes="192x192" href="{% static "pretixbase/img/icons/android-chrome-192x192.png" %}">
<link rel="icon" type="image/png" sizes="16x16" href="{% static "pretixbase/img/icons/favicon-16x16.png" %}">
<link rel="manifest" href="{% url "presale:site.webmanifest" %}">
<link rel="mask-icon" href="{% static "pretixbase/img/icons/safari-pinned-tab.svg" %}" color="#3b1c4a">
<meta name="msapplication-TileColor" content="#3b1c4a">

View File

@@ -4,7 +4,7 @@
{% load static %}
{% load compress %}
{% block content %}
<form class="form-signin" action="" method="post" id="u2f-form">
<form class="form-signin" action="" method="post" id="webauthn-form">
{% csrf_token %}
<h3>{% trans "Welcome back!" %}</h3>
<p>
@@ -12,14 +12,14 @@
</p>
<div class="form-group">
<input class="form-control" name="token" placeholder="{% trans "Token" %}"
type="text" required="required" autofocus="autofocus" id="u2f-response">
type="text" required="required" autofocus="autofocus" id="webauthn-response">
</div>
<div class="sr-only alert alert-danger" id="u2f-error">
{% trans "U2F failed. Check that the correct authentication device is correctly plugged in." %}
<div class="sr-only alert alert-danger" id="webauthn-error">
{% trans "WebAuthn failed. Check that the correct authentication device is correctly plugged in." %}
</div>
{% if jsondata %}
<p><small>
{% trans "Alternatively, connect your U2F device. If it has a button, touch it now. You might have to unplug the device and plug it back in again." %}
{% trans "Alternatively, connect your WebAuthn device. If it has a button, touch it now. You might have to unplug the device and plug it back in again." %}
</small></p>
{% endif %}
<div class="form-group buttons">
@@ -29,14 +29,14 @@
</div>
</form>
{% if jsondata %}
<script type="text/json" id="u2f-login">
<script type="text/json" id="webauthn-login">
{{ jsondata|safe }}
</script>
{% endif %}
{% compress js %}
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/u2f-api.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/u2f.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/base64js.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/webauthn.js" %}"></script>
{% endcompress %}
{% endblock %}

View File

@@ -47,6 +47,8 @@
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/orderchange.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/typeahead.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/quicksetup.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/dashboard.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/tabs.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/details.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/asynctask.js" %}"></script>
<script type="text/javascript" src="{% static "colorpicker/bootstrap-colorpicker.js" %}"></script>
@@ -57,12 +59,16 @@
{{ html_head|safe }}
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="{% static "pretixbase/img/favicon.ico" %}">
{% if development_warning or debug_warning %}
<link rel="shortcut icon" href="{% static "pretixbase/img/favicon-debug.ico" %}">
{% else %}
<link rel="shortcut icon" href="{% static "pretixbase/img/favicon.ico" %}">
<link rel="icon" type="image/png" sizes="32x32" href="{% static "pretixbase/img/icons/favicon-32x32.png" %}">
<link rel="icon" type="image/png" sizes="194x194" href="{% static "pretixbase/img/icons/favicon-194x194.png" %}">
<link rel="icon" type="image/png" sizes="16x16" href="{% static "pretixbase/img/icons/favicon-16x16.png" %}">
{% endif %}
<link rel="apple-touch-icon" sizes="180x180" href="{% static "pretixbase/img/icons/apple-touch-icon.png" %}">
<link rel="icon" type="image/png" sizes="32x32" href="{% static "pretixbase/img/icons/favicon-32x32.png" %}">
<link rel="icon" type="image/png" sizes="194x194" href="{% static "pretixbase/img/icons/favicon-194x194.png" %}">
<link rel="icon" type="image/png" sizes="192x192" href="{% static "pretixbase/img/icons/android-chrome-192x192.png" %}">
<link rel="icon" type="image/png" sizes="16x16" href="{% static "pretixbase/img/icons/favicon-16x16.png" %}">
<link rel="manifest" href="{% url "presale:site.webmanifest" %}">
<link rel="mask-icon" href="{% static "pretixbase/img/icons/safari-pinned-tab.svg" %}" color="#3b1c4a">
<meta name="msapplication-TileColor" content="#3b1c4a">
@@ -159,13 +165,13 @@
<ul class="dropdown-menu" role="menu">
{% for item in nav.children %}
<li>
<a href="{{ item.url }}"
<a {% if item.url %}href="{{ item.url }}"{% endif %}
{% if item.external %}target="_blank"{% endif %}
{% if item.active %}class="active"{% endif %}>
{% if item.icon %}
<span class="fa fa-{{ item.icon }}"></span>
<span class="fa fa-fw fa-{{ item.icon }}"></span>
{% endif %}
{{ item.label|safe }}
</a>
{{ item.label|safe }}</a>
</li>
{% endfor %}
</ul>
@@ -254,7 +260,7 @@
<div class="form-box">
<input type="text" class="form-control" id="event-dropdown-field"
placeholder="{% trans "Search for events" %}"
data-typeahead-query>
data-typeahead-query autocomplete="off">
</div>
</li>
</ul>

View File

@@ -105,7 +105,7 @@
{% endif %}
</td>
<td>
{% if e.addon_to %}
{% if e.addon_to and not e.attendee_name %}
{{ e.addon_to.attendee_name }}
{% elif e.attendee_name %}
{{ e.attendee_name }}
@@ -119,6 +119,10 @@
<span class="label label-danger">{% trans "Not checked in" %}</span>
{% else %}
<span class="label label-success">{% trans "Checked in" %}</span>
{% if e.auto_checked_in %}
<span class="fa fa-magic text-muted"
data-toggle="tooltip" title="{% trans "Checked in automatically" %}"></span>
{% endif %}
{% endif %}
</td>
<td>

View File

@@ -24,6 +24,9 @@
{% bootstrap_field form.subevent layout="control" %}
{% endif %}
{% bootstrap_field form.include_pending layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Products" %}</legend>
<p>
{% blocktrans trimmed %}
@@ -33,6 +36,10 @@
{% bootstrap_field form.all_products layout="control" %}
{% bootstrap_field form.limit_products layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Advanced" %}</legend>
{% bootstrap_field form.auto_checkin_sales_channels layout="control" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}

View File

@@ -60,6 +60,7 @@
{% if request.event.has_subevents %}
<th>{% trans "Date" context "subevent" %}</th>
{% endif %}
<th class="iconcol">{% trans "Automated check-in" %}</th>
<th>{% trans "Products" %}</th>
<th class="action-col-2"></th>
</tr>
@@ -84,6 +85,12 @@
{% if request.event.has_subevents %}
<td>{{ cl.subevent.name }} {{ cl.subevent.get_date_range_display }}</td>
{% endif %}
<td>
{% for channel in cl.auto_checkin_sales_channels %}
<span class="fa fa-{{ channel.icon }} text-muted"
data-toggle="tooltip" title="{% trans channel.verbose_name %}"></span>
{% endfor %}
</td>
<td>
{% if cl.all_products %}
<em>{% trans "All" %}</em>

View File

@@ -20,9 +20,13 @@
</a>
</div>
{% for w in upcoming %}
<div class="widget-{{ w.display_size|default:"small" }} {{ w.container_class|default:"widget-container" }}">
<div class="widget-{{ w.display_size|default:"small" }} {{ w.container_class|default:"widget-container" }} {% if w.lazy %}widget-lazy-loading{% endif %}" data-lazy-id="{{ w.lazy }}">
<div class="widget">
{{ w.content|safe }}
{% if w.lazy %}
<span class="fa fa-cog fa-4x"></span>
{% else %}
{{ w.content|safe }}
{% endif %}
</div>
</div>
{% endfor %}
@@ -38,9 +42,13 @@
<h2>{% trans "Your most recent events" %}</h2>
<div class="dashboard">
{% for w in past %}
<div class="widget-{{ w.display_size|default:"small" }} {{ w.container_class|default:"widget-container" }}">
<div class="widget-{{ w.display_size|default:"small" }} {{ w.container_class|default:"widget-container" }} {% if w.lazy %}widget-lazy-loading{% endif %}" data-lazy-id="{{ w.lazy }}">
<div class="widget">
{{ w.content|safe }}
{% if w.lazy %}
<span class="fa fa-cog fa-4x"></span>
{% else %}
{{ w.content|safe }}
{% endif %}
</div>
</div>
{% endfor %}
@@ -55,9 +63,13 @@
<h2>{% trans "Your event series" %}</h2>
<div class="dashboard">
{% for w in series %}
<div class="widget-{{ w.display_size|default:"small" }} {{ w.container_class|default:"widget-container" }}">
<div class="widget-{{ w.display_size|default:"small" }} {{ w.container_class|default:"widget-container" }} {% if w.lazy %}widget-lazy-loading{% endif %}" data-lazy-id="{{ w.lazy }}">
<div class="widget">
{{ w.content|safe }}
{% if w.lazy %}
<span class="fa fa-cog fa-4x"></span>
{% else %}
{{ w.content|safe }}
{% endif %}
</div>
</div>
{% endfor %}
@@ -72,14 +84,22 @@
<h2>{% trans "Other features" %}</h2>
<div class="dashboard">
{% for w in widgets %}
<div class="widget-{{ w.display_size|default:"small" }} {{ w.container_class|default:"widget-container" }}">
<div class="widget-{{ w.display_size|default:"small" }} {{ w.container_class|default:"widget-container" }} {% if w.lazy %}widget-lazy-loading{% endif %}" data-lazy-id="{{ w.lazy }}">
{% if w.url %}
<a href="{{ w.url }}" class="widget">
{{ w.content|safe }}
{% if w.lazy %}
<span class="fa fa-cog fa-4x"></span>
{% else %}
{{ w.content|safe }}
{% endif %}
</a>
{% else %}
<div class="widget">
{{ w.content|safe }}
{% if w.lazy %}
<span class="fa fa-cog fa-4x"></span>
{% else %}
{{ w.content|safe }}
{% endif %}
</div>
{% endif %}
</div>

View File

@@ -6,31 +6,33 @@
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
{% csrf_token %}
{% bootstrap_form_errors form %}
<fieldset>
<legend>{% trans "Cancellation of unpaid or free orders" %}</legend>
{% bootstrap_field form.cancel_allow_user layout="control" %}
{% bootstrap_field form.cancel_allow_user_until layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Cancellation of paid orders" %}</legend>
{% bootstrap_field form.cancel_allow_user_paid layout="control" %}
{% bootstrap_field form.cancel_allow_user_paid_keep layout="control" %}
{% bootstrap_field form.cancel_allow_user_paid_keep_percentage layout="control" %}
{% bootstrap_field form.cancel_allow_user_paid_keep_fees layout="control" %}
{% bootstrap_field form.cancel_allow_user_paid_until layout="control" %}
{% if not gets_notification %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
If a user requests cancels a paid order and the money can not be refunded automatically, e.g.
due to the selected payment method, you will need to take manual action. However, you have
currently turned off notifications for this event.
{% endblocktrans %}
<a href="{% url "control:user.settings.notifications" %}" class="btn btn-default">
{% trans "Change notification settings" %}
</a>
</div>
{% endif %}
</fieldset>
<div class="tabbed-form">
<fieldset>
<legend>{% trans "Unpaid or free orders" %}</legend>
{% bootstrap_field form.cancel_allow_user layout="control" %}
{% bootstrap_field form.cancel_allow_user_until layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Paid orders" %}</legend>
{% bootstrap_field form.cancel_allow_user_paid layout="control" %}
{% bootstrap_field form.cancel_allow_user_paid_keep layout="control" %}
{% bootstrap_field form.cancel_allow_user_paid_keep_percentage layout="control" %}
{% bootstrap_field form.cancel_allow_user_paid_keep_fees layout="control" %}
{% bootstrap_field form.cancel_allow_user_paid_until layout="control" %}
{% if not gets_notification %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
If a user requests cancels a paid order and the money can not be refunded automatically, e.g.
due to the selected payment method, you will need to take manual action. However, you have
currently turned off notifications for this event.
{% endblocktrans %}
<a href="{% url "control:user.settings.notifications" %}" class="btn btn-default">
{% trans "Change notification settings" %}
</a>
</div>
{% endif %}
</fieldset>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}

View File

@@ -1,44 +0,0 @@
{% extends "pretixcontrol/event/settings_base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load hierarkey_form %}
{% block custom_header %}
{{ block.super }}
<link type="text/css" rel="stylesheet" href="{% url "control:pdf.css" %}">
{% endblock %}
{% block inside %}
<h1>{% trans "Display settings" %}</h1>
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
{% csrf_token %}
{% bootstrap_form_errors form %}
<fieldset>
<legend>{% trans "Event page" %}</legend>
{% bootstrap_field form.logo_image layout="control" %}
{% bootstrap_field form.frontpage_text layout="control" %}
{% bootstrap_field form.presale_has_ended_text layout="control" %}
{% bootstrap_field form.voucher_explanation_text layout="control" %}
{% bootstrap_field form.show_variations_expanded layout="control" %}
{% bootstrap_field form.hide_sold_out layout="control" %}
{% bootstrap_field form.meta_noindex layout="control" %}
{% if form.frontpage_subevent_ordering %}
{% bootstrap_field form.frontpage_subevent_ordering layout="control" %}
{% endif %}
{% bootstrap_field form.redirect_to_checkout_directly layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Shop design" %}</legend>
{% url "control:organizer.display" organizer=request.organizer.slug as org_url %}
{% propagated request.event org_url "primary_color" "primary_font" "theme_color_success" "theme_color_danger" %}
{% bootstrap_field form.primary_color layout="control" %}
{% bootstrap_field form.theme_color_success layout="control" %}
{% bootstrap_field form.theme_color_danger layout="control" %}
{% bootstrap_field form.primary_font layout="control" %}
{% endpropagated %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -95,18 +95,30 @@
{% endif %}
<div class="dashboard">
{% for w in widgets %}
<div class="widget-container widget-{{ w.display_size|default:"small" }}">
<div class="widget-container widget-{{ w.display_size|default:"small" }} {% if w.lazy %}widget-lazy-loading{% endif %}" data-lazy-id="{{ w.lazy }}">
{% if w.url %}{# backwards compatibility #}
<a href="{{ w.url }}" class="widget">
{{ w.content|safe }}
{% if w.lazy %}
<span class="fa fa-cog fa-4x"></span>
{% else %}
{{ w.content|safe }}
{% endif %}
</a>
{% elif w.link %}
<a href="{{ w.link }}" class="widget">
{{ w.content|safe }}
{% if w.lazy %}
<span class="fa fa-cog fa-4x´"></span>
{% else %}
{{ w.content|safe }}
{% endif %}
</a>
{% else %}
<div class="widget">
{{ w.content|safe }}
{% if w.lazy %}
<span class="fa fa-cog fa-4x"></span>
{% else %}
{{ w.content|safe }}
{% endif %}
</div>
{% endif %}
</div>

View File

@@ -4,48 +4,51 @@
{% block inside %}
<h1>{% trans "Invoice settings" %}</h1>
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
{% csrf_token %}
{% bootstrap_form_errors form %}
<fieldset>
<legend>{% trans "General settings" %}</legend>
{% bootstrap_field form.invoice_generate layout="control" %}
{% bootstrap_field form.invoice_generate_sales_channels layout="control" %}
{% bootstrap_field form.invoice_email_attachment layout="control" %}
{% bootstrap_field form.invoice_numbers_prefix layout="control" %}
{% bootstrap_field form.invoice_numbers_consecutive layout="control" %}
{% bootstrap_field form.invoice_language layout="control" %}
{% bootstrap_field form.invoice_include_free layout="control" %}
{% bootstrap_field form.invoice_attendee_name layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Invoice address form" %}</legend>
{% bootstrap_field form.invoice_address_asked layout="control" %}
{% bootstrap_field form.invoice_address_required layout="control" %}
{% bootstrap_field form.invoice_name_required layout="control" %}
{% bootstrap_field form.invoice_address_company_required layout="control" %}
{% bootstrap_field form.invoice_address_vatid layout="control" %}
{% bootstrap_field form.invoice_address_beneficiary layout="control" %}
{% bootstrap_field form.invoice_address_not_asked_free layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Your invoice details" %}</legend>
{% bootstrap_field form.invoice_address_from_name layout="control" %}
{% bootstrap_field form.invoice_address_from layout="control" %}
{% bootstrap_field form.invoice_address_from_zipcode layout="control" %}
{% bootstrap_field form.invoice_address_from_city layout="control" %}
{% bootstrap_field form.invoice_address_from_country layout="control" %}
{% bootstrap_field form.invoice_address_from_tax_id layout="control" %}
{% bootstrap_field form.invoice_address_from_vat_id layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Invoice customization" %}</legend>
{% bootstrap_field form.invoice_renderer layout="control" %}
{% bootstrap_field form.invoice_introductory_text layout="control" %}
{% bootstrap_field form.invoice_additional_text layout="control" %}
{% bootstrap_field form.invoice_footer_text layout="control" %}
{% bootstrap_field form.invoice_logo_image layout="control" %}
{% bootstrap_field form.invoice_address_explanation_text layout="control" %}
</fieldset>
<div class="tabbed-form">
{% csrf_token %}
{% bootstrap_form_errors form %}
<fieldset>
<legend>{% trans "Invoice generation" %}</legend>
{% bootstrap_field form.invoice_generate layout="control" %}
{% bootstrap_field form.invoice_generate_sales_channels layout="control" %}
{% bootstrap_field form.invoice_email_attachment layout="control" %}
{% bootstrap_field form.invoice_language layout="control" %}
{% bootstrap_field form.invoice_include_free layout="control" %}
{% bootstrap_field form.invoice_numbers_consecutive layout="control" %}
{% bootstrap_field form.invoice_numbers_prefix layout="control" %}
{% bootstrap_field form.invoice_numbers_prefix_cancellations layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Address form" %}</legend>
{% bootstrap_field form.invoice_address_asked layout="control" %}
{% bootstrap_field form.invoice_address_required layout="control" %}
{% bootstrap_field form.invoice_name_required layout="control" %}
{% bootstrap_field form.invoice_address_company_required layout="control" %}
{% bootstrap_field form.invoice_address_vatid layout="control" %}
{% bootstrap_field form.invoice_address_beneficiary layout="control" %}
{% bootstrap_field form.invoice_address_not_asked_free layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Issuer details" %}</legend>
{% bootstrap_field form.invoice_address_from_name layout="control" %}
{% bootstrap_field form.invoice_address_from layout="control" %}
{% bootstrap_field form.invoice_address_from_zipcode layout="control" %}
{% bootstrap_field form.invoice_address_from_city layout="control" %}
{% bootstrap_field form.invoice_address_from_country layout="control" %}
{% bootstrap_field form.invoice_address_from_tax_id layout="control" %}
{% bootstrap_field form.invoice_address_from_vat_id layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Invoice customization" %}</legend>
{% bootstrap_field form.invoice_renderer layout="control" %}
{% bootstrap_field form.invoice_attendee_name layout="control" %}
{% bootstrap_field form.invoice_introductory_text layout="control" %}
{% bootstrap_field form.invoice_additional_text layout="control" %}
{% bootstrap_field form.invoice_footer_text layout="control" %}
{% bootstrap_field form.invoice_logo_image layout="control" %}
{% bootstrap_field form.invoice_address_explanation_text layout="control" %}
</fieldset>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-default btn-lg" name="preview" value="preview" formtarget="_blank">
{% trans "Save and show preview" %}

View File

@@ -5,85 +5,87 @@
{% block inside %}
<h1>{% trans "E-mail settings" %}</h1>
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data"
mail-preview-url="{% url "control:event.settings.mail.preview" event=request.event.slug organizer=request.event.organizer.slug %}">
mail-preview-url="{% url "control:event.settings.mail.preview" event=request.event.slug organizer=request.event.organizer.slug %}">
{% csrf_token %}
{% bootstrap_form_errors form %}
<fieldset>
<legend>{% trans "General settings" %}</legend>
{% bootstrap_field form.mail_prefix layout="control" %}
{% bootstrap_field form.mail_from layout="control" %}
{% bootstrap_field form.mail_from_name layout="control" %}
{% bootstrap_field form.mail_text_signature layout="control" %}
{% bootstrap_field form.mail_bcc layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "E-mail design" %}</legend>
<div class="row">
{% for r in renderers.values %}
<div class="col-md-3">
<div class="well maildesignpreview text-center">
<label class="radio">
<input type="radio" name="mail_html_renderer" value="{{ r.identifier }}"
{% if request.event.settings.mail_html_renderer == r.identifier %}checked{% endif %}>
{{ r.verbose_name }}
</label>
<img src="{% static r.thumbnail_filename %}">
<a class="btn btn-default btn-sm" target="_blank"
href="{% url "control:event.settings.mail.preview.layout" event=request.event.slug organizer=request.event.organizer.slug %}?renderer={{ r.identifier }}">
{% trans "Preview" %}
</a>
<div class="tabbed-form">
<fieldset>
<legend>{% trans "General" %}</legend>
{% bootstrap_field form.mail_prefix layout="control" %}
{% bootstrap_field form.mail_from layout="control" %}
{% bootstrap_field form.mail_from_name layout="control" %}
{% bootstrap_field form.mail_text_signature layout="control" %}
{% bootstrap_field form.mail_bcc layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "E-mail design" %}</legend>
<div class="row">
{% for r in renderers.values %}
<div class="col-md-3">
<div class="well maildesignpreview text-center">
<label class="radio">
<input type="radio" name="mail_html_renderer" value="{{ r.identifier }}"
{% if request.event.settings.mail_html_renderer == r.identifier %}checked{% endif %}>
{{ r.verbose_name }}
</label>
<img src="{% static r.thumbnail_filename %}">
<a class="btn btn-default btn-sm" target="_blank"
href="{% url "control:event.settings.mail.preview.layout" event=request.event.slug organizer=request.event.organizer.slug %}?renderer={{ r.identifier }}">
{% trans "Preview" %}
</a>
</div>
</div>
</div>
{% endfor %}
</div>
</fieldset>
<fieldset>
<legend>{% trans "E-mail content" %}</legend>
<div class="panel-group" id="questions_group">
{% blocktrans asvar title_placed_order %}Placed order{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_placed" title=title_placed_order items="mail_text_order_placed,mail_send_order_placed_attendee,mail_text_order_placed_attendee" exclude="mail_send_order_placed_attendee" %}
{% endfor %}
</div>
</fieldset>
<fieldset>
<legend>{% trans "E-mail content" %}</legend>
<div class="panel-group" id="questions_group">
{% blocktrans asvar title_placed_order %}Placed order{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_placed" title=title_placed_order items="mail_text_order_placed,mail_send_order_placed_attendee,mail_text_order_placed_attendee" exclude="mail_send_order_placed_attendee" %}
{% blocktrans asvar title_paid_order %}Paid order{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_paid" title=title_paid_order items="mail_text_order_paid,mail_send_order_paid_attendee,mail_text_order_paid_attendee" exclude="mail_send_order_paid_attendee" %}
{% blocktrans asvar title_paid_order %}Paid order{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_paid" title=title_paid_order items="mail_text_order_paid,mail_send_order_paid_attendee,mail_text_order_paid_attendee" exclude="mail_send_order_paid_attendee" %}
{% blocktrans asvar title_free_order %}Free order{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_free" title=title_free_order items="mail_text_order_free,mail_send_order_free_attendee,mail_text_order_free_attendee" exclude="mail_send_order_free_attendee" %}
{% blocktrans asvar title_free_order %}Free order{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_free" title=title_free_order items="mail_text_order_free,mail_send_order_free_attendee,mail_text_order_free_attendee" exclude="mail_send_order_free_attendee" %}
{% blocktrans asvar title_resend_link %}Resend link{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="resend_link" title=title_resend_link items="mail_text_resend_link,mail_text_resend_all_links" %}
{% blocktrans asvar title_resend_link %}Resend link{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="resend_link" title=title_resend_link items="mail_text_resend_link,mail_text_resend_all_links" %}
{% blocktrans asvar title_order_changed %}Order changed{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_changed" title=title_order_changed items="mail_text_order_changed" %}
{% blocktrans asvar title_order_changed %}Order changed{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_changed" title=title_order_changed items="mail_text_order_changed" %}
{% blocktrans asvar title_payment_reminder %}Payment reminder{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_expirew" title=title_payment_reminder items="mail_days_order_expire_warning,mail_text_order_expire_warning" exclude="mail_days_order_expire_warning" %}
{% blocktrans asvar title_payment_reminder %}Payment reminder{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_expirew" title=title_payment_reminder items="mail_days_order_expire_warning,mail_text_order_expire_warning" exclude="mail_days_order_expire_warning" %}
{% blocktrans asvar title_waiting_list_notification %}Waiting list notification{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="waiting_list" title=title_waiting_list_notification items="mail_text_waiting_list" %}
{% blocktrans asvar title_waiting_list_notification %}Waiting list notification{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="waiting_list" title=title_waiting_list_notification items="mail_text_waiting_list" %}
{% blocktrans asvar title_order_canceled %}Order canceled{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_canceled" title=title_order_canceled items="mail_text_order_canceled" %}
{% blocktrans asvar title_order_canceled %}Order canceled{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_canceled" title=title_order_canceled items="mail_text_order_canceled" %}
{% blocktrans asvar title_order_custom_mail %}Order custom mail{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="custom_mail" title=title_order_custom_mail items="mail_text_order_custom_mail" %}
{% blocktrans asvar title_order_custom_mail %}Order custom mail{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="custom_mail" title=title_order_custom_mail items="mail_text_order_custom_mail" %}
{% blocktrans asvar title_download_tickets_reminder %}Reminder to download tickets{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_download_tickets_reminder items="mail_days_download_reminder,mail_text_download_reminder,mail_send_download_reminder_attendee,mail_text_download_reminder_attendee" exclude="mail_days_download_reminder,mail_send_download_reminder_attendee" %}
{% blocktrans asvar title_download_tickets_reminder %}Reminder to download tickets{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_download_tickets_reminder items="mail_days_download_reminder,mail_text_download_reminder,mail_send_download_reminder_attendee,mail_text_download_reminder_attendee" exclude="mail_days_download_reminder,mail_send_download_reminder_attendee" %}
{% blocktrans asvar title_require_approval %}Order approval process{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_require_approval items="mail_text_order_placed_require_approval,mail_text_order_approved,mail_text_order_denied" %}
</div>
</fieldset>
<fieldset>
<legend>{% trans "SMTP settings" %}</legend>
{% bootstrap_field form.smtp_use_custom layout="control" %}
{% bootstrap_field form.smtp_host layout="control" %}
{% bootstrap_field form.smtp_port layout="control" %}
{% bootstrap_field form.smtp_username layout="control" %}
{% bootstrap_field form.smtp_password layout="control" %}
{% bootstrap_field form.smtp_use_tls layout="control" %}
{% bootstrap_field form.smtp_use_ssl layout="control" %}
</fieldset>
{% blocktrans asvar title_require_approval %}Order approval process{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_require_approval items="mail_text_order_placed_require_approval,mail_text_order_approved,mail_text_order_denied" %}
</div>
</fieldset>
<fieldset>
<legend>{% trans "SMTP settings" %}</legend>
{% bootstrap_field form.smtp_use_custom layout="control" %}
{% bootstrap_field form.smtp_host layout="control" %}
{% bootstrap_field form.smtp_port layout="control" %}
{% bootstrap_field form.smtp_username layout="control" %}
{% bootstrap_field form.smtp_password layout="control" %}
{% bootstrap_field form.smtp_use_tls layout="control" %}
{% bootstrap_field form.smtp_use_ssl layout="control" %}
</fieldset>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}

View File

@@ -5,60 +5,67 @@
<h1>{% trans "Payment settings" %}</h1>
<form action="" method="post" class="form-horizontal form-plugins">
{% csrf_token %}
<fieldset>
<legend>{% trans "Payment providers" %}</legend>
<table class="table table-payment-providers">
<tbody>
{% for provider in providers %}
<tr>
<td>
<strong>{{ provider.verbose_name }}</strong>
</td>
<td>
{% if provider.show_enabled %}
<span class="text-success">
<div class="tabbed-form">
<fieldset>
<legend>{% trans "Payment providers" %}</legend>
<table class="table table-payment-providers">
<tbody>
{% for provider in providers %}
<tr>
<td>
<strong>{{ provider.verbose_name }}</strong>
</td>
<td>
{% if provider.show_enabled %}
<span class="text-success">
<span class="fa fa-check"></span>
{% trans "Enabled" %}
</span>
{% else %}
<span class="text-danger">
{% else %}
<span class="text-danger">
<span class="fa fa-times"></span>
{% trans "Disabled" %}
</span>
{% endif %}
</td>
<td class="text-right">
<a href="{% url 'control:event.settings.payment.provider' event=request.event.slug organizer=request.organizer.slug provider=provider.identifier %}"
class="btn btn-default">
<span class="fa fa-cog"></span>
{% trans "Settings" %}
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="3">
{% url "control:event.settings.plugins" event=request.event.slug organizer=request.organizer.slug as plugin_settings_url %}
{% blocktrans trimmed with plugin_settings_href='href="'|add:plugin_settings_url|add:'"'|safe %}
There are no payment providers available. Please go to the <a {{ plugin_settings_href }}>plugin settings</a> and activate one or more payment plugins.
{% endblocktrans %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</fieldset>
<fieldset>
<legend>{% trans "General payment settings" %}</legend>
{% bootstrap_form_errors form layout="control" %}
{% bootstrap_field form.payment_term_days layout="control" %}
{% bootstrap_field form.payment_term_last layout="control" %}
{% bootstrap_field form.payment_term_weekdays layout="control" %}
{% bootstrap_field form.payment_term_expire_automatically layout="control" %}
{% bootstrap_field form.payment_term_accept_late layout="control" %}
{% bootstrap_field form.tax_rate_default layout="control" %}
{% bootstrap_field form.payment_explanation layout="control" %}
</fieldset>
{% endif %}
</td>
<td class="text-right">
<a href="{% url 'control:event.settings.payment.provider' event=request.event.slug organizer=request.organizer.slug provider=provider.identifier %}"
class="btn btn-default">
<span class="fa fa-cog"></span>
{% trans "Settings" %}
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="3">
{% url "control:event.settings.plugins" event=request.event.slug organizer=request.organizer.slug as plugin_settings_url %}
{% blocktrans trimmed with plugin_settings_href='href="'|add:plugin_settings_url|add:'"'|safe %}
There are no payment providers available. Please go to the
<a {{ plugin_settings_href }}>plugin settings</a> and activate one or more payment plugins.
{% endblocktrans %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</fieldset>
<fieldset>
<legend>{% trans "Deadlines" %}</legend>
{% bootstrap_form_errors form layout="control" %}
{% bootstrap_field form.payment_term_days layout="control" %}
{% bootstrap_field form.payment_term_last layout="control" %}
{% bootstrap_field form.payment_term_weekdays layout="control" %}
{% bootstrap_field form.payment_term_expire_automatically layout="control" %}
{% bootstrap_field form.payment_term_accept_late layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Advanced" %}</legend>
{% bootstrap_form_errors form layout="control" %}
{% bootstrap_field form.tax_rate_default layout="control" %}
{% bootstrap_field form.payment_explanation layout="control" %}
</fieldset>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}

View File

@@ -3,77 +3,69 @@
{% load bootstrap3 %}
{% block inside %}
<h1>{% trans "Installed plugins" %}</h1>
<form action="" method="post" class="form-horizontal form-plugins">
{% csrf_token %}
<fieldset>
{% if "success" in request.GET %}
<div class="alert alert-success">
{% trans "Your changes have been saved." %}
</div>
{% endif %}
<div class="row row-plugins">
{% for plugin in plugins %}
<div class="col-md-6 col-sm-12">
<div class="panel panel-{% if plugin.app.compatibility_errors %}warning{% elif plugin.module in plugins_active %}success{% else %}default{% endif %}">
<div class="panel-heading">
<div class="row">
<div class="col-sm-8">
<h3 class="panel-title">{{ plugin.name }}</h3>
</div>
<div class="col-sm-4">
{% if plugin.app.compatibility_errors %}
<button class="btn disabled btn-block btn-default" disabled="disabled">{% trans "Incompatible" %}</button>
{% elif plugin.restricted and not staff_session %}
<button class="btn disabled btn-block btn-default" disabled="disabled">{% trans "Not available" %}</button>
{% elif plugin.module in plugins_active %}
<button class="btn btn-default btn-block" name="plugin:{{ plugin.module }}" value="disable">{% trans "Disable" %}</button>
{% else %}
<button class="btn btn-default btn-block" name="plugin:{{ plugin.module }}" value="enable">{% trans "Enable" %}</button>
{% endif %}
</div>
</div>
</div>
<div class="panel-body">
{% if plugin.author %}
<p class="meta">{% blocktrans trimmed with v=plugin.version a=plugin.author %}
Version {{ v }} by <em>{{ a }}</em>
{% endblocktrans %}</p>
{% else %}
<p class="meta">{% blocktrans trimmed with v=plugin.version a=plugin.author %}
Version {{ v }}
{% endblocktrans %}</p>
{% endif %}
<p>{{ plugin.description }}</p>
{% if plugin.restricted and not request.user.is_staff %}
<div class="alert alert-warning">
{% trans "This plugin needs to be enabled by a system administrator for your event." %}
</div>
{% endif %}
{% if plugin.app.compatibility_errors %}
<div class="alert alert-warning">
{% trans "This plugin cannot be enabled for the following reasons:" %}
<ul>
{% for e in plugin.app.compatibility_errors %}
<li>{{ e }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if plugin.app.compatibility_warnings %}
<div class="alert alert-warning">
{% trans "This plugin reports the following problems:" %}
<ul>
{% for e in plugin.app.compatibility_warnings %}
<li>{{ e }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</div>
</div>
{% endfor %}
<form action="" method="post" class="form-horizontal form-plugins">
{% csrf_token %}
{% if "success" in request.GET %}
<div class="alert alert-success">
{% trans "Your changes have been saved." %}
</div>
</fieldset>
</form>
{% endif %}
<div class="table-responsive">
<table class="table">
{% for plugin in plugins %}
<tr class="{% if plugin.app.compatibility_errors %}warning{% elif plugin.module in plugins_active %}success{% else %}default{% endif %}">
<td>
<strong>{{ plugin.name }}</strong>
{% if plugin.author %}
<p class="meta text-muted">{% blocktrans trimmed with v=plugin.version a=plugin.author %}
Version {{ v }} by <em>{{ a }}</em>
{% endblocktrans %}</p>
{% else %}
<p class="meta text-muted">{% blocktrans trimmed with v=plugin.version a=plugin.author %}
Version {{ v }}
{% endblocktrans %}</p>
{% endif %}
<p>{{ plugin.description }}</p>
{% if plugin.restricted and not request.user.is_staff %}
<span class="text-muted">
{% trans "This plugin needs to be enabled by a system administrator for your event." %}
</span>
{% endif %}
{% if plugin.app.compatibility_errors %}
<div class="alert alert-warning">
{% trans "This plugin cannot be enabled for the following reasons:" %}
<ul>
{% for e in plugin.app.compatibility_errors %}
<li>{{ e }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if plugin.app.compatibility_warnings %}
<div class="alert alert-warning">
{% trans "This plugin reports the following problems:" %}
<ul>
{% for e in plugin.app.compatibility_warnings %}
<li>{{ e }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</td>
<td class="text-right">
{% if plugin.app.compatibility_errors %}
<button class="btn disabled btn-block btn-default" disabled="disabled">{% trans "Incompatible" %}</button>
{% elif plugin.restricted and not staff_session %}
<button class="btn disabled btn-block btn-default" disabled="disabled">{% trans "Not available" %}</button>
{% elif plugin.module in plugins_active %}
<button class="btn btn-default btn-block" name="plugin:{{ plugin.module }}" value="disable">{% trans "Disable" %}</button>
{% else %}
<button class="btn btn-default btn-block" name="plugin:{{ plugin.module }}" value="enable">{% trans "Enable" %}</button>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
</div>
</form>
{% endblock %}

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