Compare commits

..

210 Commits

Author SHA1 Message Date
Richard Schreiber e54837c532 remove print statement 2023-08-23 09:51:12 +02:00
Raphael Michel bc49f0f7f1 Fix cache invalidation 2023-08-23 09:47:05 +02:00
Raphael Michel 3e122e0270 Fix quota cache mixup 2023-08-22 13:00:16 +02:00
Raphael Michel e8ea6e0f5c Item creation: Fix failing test 2023-08-22 12:59:57 +02:00
Raphael Michel e94e5be878 Item creation: Fix bug in copying meta data 2023-08-22 11:32:43 +02:00
Richard Schreiber 1073ea626e Banktransfer: make row-headers sticky (Z#23127000) (#3537) 2023-08-22 10:53:26 +02:00
Raphael Michel 23ab8df443 Translations: Add Welsh 2023-08-22 10:53:15 +02:00
Kian Cross d6caf01a38 Add warning about configuration of Celery in development mode to docs (#3525) 2023-08-22 10:44:11 +02:00
Raphael Michel 1424ae78e9 Revert accidental change 2023-08-22 10:20:19 +02:00
Raphael Michel 827382edc3 Bump redis to 4.6.* 2023-08-22 09:43:21 +02:00
Maurice Kaag 85482bc939 Translations: Update French
Currently translated at 100.0% (5400 of 5400 strings)

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

powered by weblate
2023-08-22 09:20:21 +02:00
Felix Hartnagel 42ce545f2f Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5400 of 5400 strings)

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

powered by weblate
2023-08-22 09:20:21 +02:00
Raphael Michel e49bc5d78d Item creation: Fix crash (PRETIXEU-8VE) 2023-08-22 09:14:23 +02:00
Richard Schreiber 6e7a32ef2a Vouchers: improve batch-select UI 2023-08-22 09:11:14 +02:00
Raphael Michel 37df7a6313 Allow PDF variables to provide a bulk evaluation method (second try at #3517) (#3535) 2023-08-21 17:59:55 +02:00
Raphael Michel d5951415a4 Item creation: Fix saving meta data (#3534) 2023-08-21 16:21:17 +02:00
Raphael Michel 691159ed83 Check-in list: Fix ordering by seat 2023-08-21 15:41:50 +02:00
Raphael Michel 18f517af44 Waiting list: Extend compatibility note 2023-08-21 14:52:39 +02:00
Raphael Michel 89ba2da7e7 QR code generator for voucher URLs and general URLs (#3518)
* QR code generator: Allow other URLs to be used (e.g. for plugins)

* Add QR code to voucher URL view

* Fix allowed_hosts

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-08-17 10:10:27 +02:00
Raphael Michel c1c47e50c3 Voucher redemption: Display event title in some cases (#3519)
* Voucher redemption: Display event title in some cases (Z#23127871)

* Remove unnecessary "with" statement

* fix indentation

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-08-17 09:17:47 +02:00
Richard Schreiber f262cd632c Chekout: make disabling sneak-peek more robust (#3527) 2023-08-16 15:02:02 +02:00
Richard Schreiber 8d58294af1 Fix typeahead item variations order_by 2023-08-16 10:01:27 +02:00
Richard Schreiber ddc94a8a16 Revert "Allow PDF variables to provide a bulk evaluation method (#3517)"
This reverts commit 6ada83df9a.
2023-08-14 15:11:13 +02:00
Raphael Michel 83811c0343 Fix minor CSS issue in button groups 2023-08-10 14:12:19 +02:00
Raphael Michel b2c05a72e5 Voucher list: Fix ordering by product 2023-08-10 11:29:10 +02:00
Martin Gross 8c56a23562 Add logentry plain for pretix.giftcards.acceptance.acceptor.removed 2023-08-10 11:21:54 +02:00
Raphael Michel 53e1d9c6c4 Tests: Fix improper cleanup of SITE_URL 2023-08-10 11:20:26 +02:00
Mira 6250ab2165 Bank transfer: Allow customer to send latest invoice via email (Z#207218) (#3511)
Co-authored-by: Raphael Michel <michel@rami.io>
2023-08-09 18:23:45 +02:00
Raphael Michel 6ada83df9a Allow PDF variables to provide a bulk evaluation method (#3517) 2023-08-09 18:22:56 +02:00
Raphael Michel cfd6376936 Fix transaction view after Django upgrade 2023-08-09 17:11:20 +02:00
Raphael Michel edb0cd0941 Update STORAGES in docker settings 2023-08-09 15:01:21 +02:00
Raphael Michel 88ac407cf3 Cart: Disable sneak peek on very small carts (#3512) 2023-08-09 14:53:50 +02:00
dependabot[bot] 5ba56fb5ac Bump @babel/core from 7.22.5 to 7.22.9 in /src/pretix/static/npm_dir (#3501)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-09 14:53:34 +02:00
Raphael Michel b51c9f7552 Upgrade to Django 4.2 (#3497) 2023-08-09 14:47:41 +02:00
Ronan LE MEILLAT 0853296663 Translations: Update French
Currently translated at 99.9% (5398 of 5400 strings)

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

powered by weblate
2023-08-09 14:47:26 +02:00
Raphael Michel 721e7549bc Remove forgotten debug statement 2023-08-09 10:34:24 +02:00
Martin Gross aee86de330 Import: Allow to import "False"-value (Z#23127414) (#3505) 2023-08-08 15:36:51 +02:00
Raphael Michel 756a4355d1 Use newer postgres version for test 2023-08-08 15:32:02 +02:00
Mira 5119bbd0b1 Docs: Update i18n.rst (fix dead link) (#3513) 2023-08-08 15:04:51 +02:00
Raphael Michel 728bd74e28 Organizer settings: Move save button to the left 2023-08-07 17:44:52 +02:00
Mira 015ffeecbf Main menu: Add load indicator to event selector (#3508) 2023-08-07 14:25:50 +02:00
Raphael Michel 0365f6d9fc Order change manager: Set new expiry date if splitted order is pending (#3509) 2023-08-07 14:13:44 +02:00
Raphael Michel e208a79c32 Docs: Update implementation docs for URL routing (#3510) 2023-08-07 14:13:19 +02:00
ticketflock 0037d37960 Translations: Add English (Old) 2023-08-07 14:04:34 +02:00
ticketflock 50d9b1e4a3 Translations: Add English (Middle) 2023-08-07 14:04:34 +02:00
Patrizia Cotza 7919d012e6 Translations: Update Spanish
Currently translated at 58.5% (3159 of 5400 strings)

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

powered by weblate
2023-08-07 14:04:34 +02:00
Ronan LE MEILLAT 327f95a9cc Translations: Update French
Currently translated at 100.0% (212 of 212 strings)

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

powered by weblate
2023-08-07 14:04:34 +02:00
Ronan LE MEILLAT 98946ded4b Translations: Update French
Currently translated at 99.9% (5398 of 5400 strings)

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

powered by weblate
2023-08-07 14:04:34 +02:00
Ronan LE MEILLAT cf47b69bd3 Translations: Update French
Currently translated at 99.3% (5366 of 5400 strings)

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

powered by weblate
2023-08-07 14:04:34 +02:00
Raphael Michel fa5c69ce0a Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5400 of 5400 strings)

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

powered by weblate
2023-08-07 14:04:34 +02:00
Raphael Michel 39d85fc112 Event creation: Fix rare crash (PRETIXEU-8RD) 2023-08-07 09:47:14 +02:00
Mira 23e222bf13 Sidebar dropdown: remove menu load delay 2023-08-03 14:28:59 +02:00
Raphael Michel cb068b029f Wallet detection: Fix race condition 2023-07-28 17:31:47 +02:00
Raphael Michel 9e95f3be1b Wallet detection: Extend CSP header for google pay 2023-07-28 16:49:11 +02:00
Raphael Michel 401c02865b Voucher form: Sort quotas by date 2023-07-28 16:29:03 +02:00
Raphael Michel 062450002d Bump to 2023.8.0.dev0 2023-07-28 09:30:04 +02:00
Raphael Michel 6d834762c4 Bump to 2023.7.0 2023-07-28 09:29:07 +02:00
Raphael Michel 4f1e9a31c6 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5400 of 5400 strings)

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

powered by weblate
2023-07-27 14:17:58 +02:00
Raphael Michel 8ed3911dfb Translations: Update German
Currently translated at 100.0% (5400 of 5400 strings)

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

powered by weblate
2023-07-27 14:17:58 +02:00
Raphael Michel 4562879cb2 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2023-07-27 13:50:15 +02:00
Raphael Michel ef0024b2ef Payment deadline delay: Respect week days 2023-07-27 13:49:31 +02:00
Raphael Michel 8e603410fa Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2023-07-27 10:38:23 +02:00
Raphael Michel 16691ca2f6 Prevent 65ecdc184 clashing with forms that have a field called template 2023-07-26 19:18:53 +02:00
Raphael Michel d7e70fd0b9 Order change: Do not expose internal name 2023-07-26 15:41:15 +02:00
Raphael Michel 071a3e2c9b PDF layouts: Allow negative numbers in JSON schema 2023-07-26 15:41:15 +02:00
Raphael Michel 1733c383b3 Docs: Add description of NFC support (#3494)
* Add documentation on NFC support

* Add a .

* Update doc/development/nfc/uid.rst

Co-authored-by: robbi5 <richt@rami.io>

---------

Co-authored-by: robbi5 <richt@rami.io>
2023-07-26 13:26:00 +02:00
Kian Cross 65ecdc184e Recognise title and template attributes on item_forms signal (#3492) 2023-07-24 17:35:39 +02:00
Raphael Michel 63ae0724cf Accounting report: Refactor for easier extensibility 2023-07-24 15:42:16 +02:00
Ronan LE MEILLAT 370d1bf06b Translations: Update French
Currently translated at 99.2% (5359 of 5399 strings)

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

powered by weblate
2023-07-24 13:50:57 +02:00
Raphael Michel 06f361cece PDF: Deduplicate list of add-ons (#3490) 2023-07-24 09:27:38 +02:00
Phin Wolkwitz 4b706339ed Sendmail rules: Extend filter by order status (#3402)
Add new order status filter settings instead of in form and API, while keeping backwards-compatibility
2023-07-21 17:43:19 +02:00
Raphael Michel 26213f2ba9 Docs: Adjust docs for installing enterprise plugins with docker 2023-07-21 15:32:58 +02:00
Raphael Michel c183351d50 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5399 of 5399 strings)

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

powered by weblate
2023-07-21 15:14:24 +02:00
Raphael Michel 14131a7cec Translations: Update German
Currently translated at 100.0% (5399 of 5399 strings)

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

powered by weblate
2023-07-21 15:14:24 +02:00
Raphael Michel dfde308010 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2023-07-21 15:02:56 +02:00
Raphael Michel 96b8631e09 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5399 of 5399 strings)

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

powered by weblate
2023-07-21 15:02:24 +02:00
Raphael Michel 84f464885d Translations: Update German
Currently translated at 99.5% (211 of 212 strings)

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

powered by weblate
2023-07-21 15:02:24 +02:00
Raphael Michel 098147ce70 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5399 of 5399 strings)

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

powered by weblate
2023-07-21 15:02:24 +02:00
Raphael Michel 08b6186d77 Translations: Update German
Currently translated at 100.0% (5399 of 5399 strings)

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

powered by weblate
2023-07-21 15:02:24 +02:00
Raphael Michel e9e98a7821 Fix typos 2023-07-21 14:54:35 +02:00
Raphael Michel 3150c6a3ea Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2023-07-21 14:19:29 +02:00
Raphael Michel 898d1ab6ed Fix missing pluralization of error message 2023-07-21 14:18:56 +02:00
Phin Wolkwitz 52ae7626b0 Send mail on payment failure [Z#23122835] (#3473)
Co-authored-by: Raphael Michel <michel@rami.io>
2023-07-21 14:17:51 +02:00
Raphael Michel c652911bfb Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2023-07-21 13:46:43 +02:00
Raphael Michel 52023cde09 Reusable Media: Mifare Ultralight AES support (#3335) 2023-07-21 13:45:42 +02:00
Martin Gross b134f29cf6 Fix #1749 -- Stripe: Rewrite for Payment Methods and Payment Intents (#2494)
Co-authored-by: Raphael Michel <michel@rami.io>
Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
2023-07-21 13:19:24 +02:00
Raphael Michel 19e1d132c2 Fix image being used twice on badge (#3486) 2023-07-21 12:17:36 +02:00
Ronan LE MEILLAT 393a218df5 Translations: Update French
Currently translated at 100.0% (211 of 211 strings)

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

powered by weblate
2023-07-20 20:50:57 +02:00
Ronan LE MEILLAT f247eb0568 Translations: Update French
Currently translated at 99.9% (5361 of 5362 strings)

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

powered by weblate
2023-07-20 20:50:57 +02:00
Pascal Zimmermann b35a388685 Add PostgreSQL & Redis TLS/mTLS support (#3435) 2023-07-20 20:50:41 +02:00
Raphael Michel 6dbbfe3b04 Fix test failures caused by b2c49461b 2023-07-20 15:47:10 +02:00
Raphael Michel b2c49461bc API: Fix validation issue in sendmail rules 2023-07-20 14:29:48 +02:00
Raphael Michel 23dcdf1fd1 Export tasks: Request new database connection after completing output 2023-07-20 11:41:54 +02:00
dependabot[bot] 1f80e9ef82 Bump @babel/preset-env from 7.22.4 to 7.22.9 in /src/pretix/static/npm_dir (#3474)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-17 12:51:47 +02:00
Richard Schreiber 0969abb460 Badges: reduce memory usage when placing multiple per page (Z#23125583) (#3472)
Co-authored-by: Raphael Michel <michel@rami.io>
2023-07-17 12:50:48 +02:00
Freek Engelbarts 7b5789b110 Translations: Update Dutch (informal) (nl_Informal)
Currently translated at 74.2% (3983 of 5362 strings)

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

powered by weblate
2023-07-17 12:16:24 +02:00
Freek Engelbarts f3b5996b82 Translations: Update Dutch
Currently translated at 84.6% (4537 of 5362 strings)

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

powered by weblate
2023-07-17 12:16:24 +02:00
umarbgs 5dcab59174 Translations: Add Indonesian 2023-07-17 12:16:24 +02:00
Martin Gross a2e38bb415 Translations: Update Spanish
Currently translated at 57.6% (3093 of 5362 strings)

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

powered by weblate
2023-07-17 12:16:24 +02:00
Felipe 0510814aae Translations: Update Spanish
Currently translated at 57.6% (3093 of 5362 strings)

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

powered by weblate
2023-07-17 12:16:24 +02:00
Felipe dee2818f5d Translations: Update Spanish
Currently translated at 56.2% (3014 of 5362 strings)

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

powered by weblate
2023-07-17 12:16:24 +02:00
Iria Costas 0d7809c36b Translations: Update Spanish
Currently translated at 56.2% (3014 of 5362 strings)

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

powered by weblate
2023-07-17 12:16:24 +02:00
Iria Costas 4c494b5265 Translations: Update Spanish
Currently translated at 55.5% (2977 of 5362 strings)

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

powered by weblate
2023-07-17 12:16:24 +02:00
Felipe 9e85e8c60a Translations: Update Spanish
Currently translated at 55.5% (2977 of 5362 strings)

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

powered by weblate
2023-07-17 12:16:24 +02:00
hara metaxa ab8c71fab8 Translations: Update Greek
Currently translated at 52.6% (2821 of 5362 strings)

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

powered by weblate
2023-07-17 12:16:24 +02:00
alemao8 1fa8ea3a12 Translations: Update Greek
Currently translated at 52.5% (2820 of 5362 strings)

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

powered by weblate
2023-07-17 12:16:24 +02:00
Ronan LE MEILLAT f584d3d5af Translations: Update French
Currently translated at 99.9% (5361 of 5362 strings)

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

powered by weblate
2023-07-17 12:16:24 +02:00
Maciej Szymczak 46ae911ade Translations: Update Polish
Currently translated at 14.9% (801 of 5362 strings)

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

powered by weblate
2023-07-17 12:16:24 +02:00
Thomas Vranken 85db5698a6 Translations: Update Dutch (informal) (nl_Informal)
Currently translated at 74.1% (3978 of 5362 strings)

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

powered by weblate
2023-07-17 12:16:24 +02:00
Thomas Vranken 09a17b57ce Translations: Update Dutch (Belgium)
Currently translated at 0.1% (1 of 5362 strings)

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

powered by weblate
2023-07-17 12:16:24 +02:00
Thomas Vranken 826962d6e2 Translations: Update Dutch
Currently translated at 84.4% (4530 of 5362 strings)

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

powered by weblate
2023-07-17 12:16:24 +02:00
Ronan LE MEILLAT f77e79bb38 Translations: Update French
Currently translated at 99.9% (5361 of 5362 strings)

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

powered by weblate
2023-07-17 12:16:24 +02:00
Maurice Kaag d21e832204 Translations: Update French
Currently translated at 99.9% (5360 of 5362 strings)

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

powered by weblate
2023-07-17 12:16:24 +02:00
Ronan LE MEILLAT 119d4f0e04 Translations: Update French
Currently translated at 100.0% (211 of 211 strings)

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

powered by weblate
2023-07-17 12:16:24 +02:00
Mossroy feab6acfbd Translations: Update French
Currently translated at 99.7% (5351 of 5362 strings)

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

powered by weblate
2023-07-17 12:16:24 +02:00
Ronan LE MEILLAT d85a6074ec Translations: Update French
Currently translated at 99.7% (5351 of 5362 strings)

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

powered by weblate
2023-07-17 12:16:24 +02:00
Raphael Michel 6c813ea299 Waiting list: Make it harder to accidentally delete full list 2023-07-17 11:54:37 +02:00
Martin Gross 8a903f21ae Stripe/Middleware: Move CSP to signal (#3465) 2023-07-17 11:15:12 +02:00
Kian Cross a7f7c64cce Add signals for customer account creation and sign in (#3470) 2023-07-17 11:09:05 +02:00
dependabot[bot] 82969daf37 Bump semver from 5.7.1 to 5.7.2 in /src/pretix/static/npm_dir (#3467)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-17 11:08:55 +02:00
Raphael Michel 8e9d0fb723 API: Order position search, add invoice company 2023-07-17 09:37:20 +02:00
Raphael Michel ef3d44e581 Stripe: Fix crash in rendering of bancontact payments 2023-07-14 16:49:33 +02:00
Raphael Michel f9055fce9f Disable slow safety mode of reportlab in prod 2023-07-14 16:12:19 +02:00
Raphael Michel cff0e86fd9 Email settings: Block with invalid SPF setup (#3471) 2023-07-12 12:36:41 +02:00
Raphael Michel f0913fc720 Fix #3452 -- Encode UUIDs to string before passing through celery (#3463) 2023-07-11 15:36:29 +02:00
dependabot[bot] 23a9f60171 Bump @babel/core from 7.22.1 to 7.22.5 in /src/pretix/static/npm_dir (#3445)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-11 15:36:18 +02:00
Raphael Michel faf41c805c Waiting list: Fix display on unlimited quota 2023-07-11 13:38:17 +02:00
Martin Gross 41cded095c PProv: Implement detection of wallets such as Google Pay and Apple Pay (#3444)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-07-11 11:51:43 +02:00
Raphael Michel 90fb034897 Check-in simulator: Fix usage of simulated time in rules 2023-07-11 09:17:02 +02:00
Raphael Michel f4203b7408 Vouchers: Don't allow to generate more than 100k random codes at once 2023-07-10 15:11:49 +02:00
Richard Schreiber 8a9f14db03 Fix cart sneak-peek on async error 2023-07-07 09:02:53 +02:00
Richard Schreiber a2adf2825a PDF: fix page-size when mediabox of background-pdf uses offsets 2023-07-04 13:10:27 +02:00
Martin Gross 8f7220b574 isort plugins/badges/exporters.py 2023-06-30 16:22:19 +02:00
Martin Gross 5adbdb80a8 Badge-Export: Explicitly convert dt/df to deal with celery (Fixes PRETIXEU-8NW) 2023-06-30 15:50:32 +02:00
Moritz Lerch 3717c4b553 Translations: Update German (informal) (de_Informal)
Currently translated at 99.9% (5361 of 5362 strings)

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

powered by weblate
2023-06-29 13:29:28 +02:00
Moritz Lerch 609f45d818 Translations: Update German
Currently translated at 100.0% (5362 of 5362 strings)

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

powered by weblate
2023-06-29 13:29:28 +02:00
Richard Schreiber 1d49c98cf2 Widget: add lightbox for product images (Z#23123811) (#3439) 2023-06-29 12:23:00 +02:00
Richard Schreiber 586f42557f Event URLs: Add access-control-allow-origin header for redirects (#3441) 2023-06-29 11:36:50 +02:00
Raphael Michel e3f219366d Fix crash when removing the phone number (PRETIXEU-8P0) 2023-06-29 09:58:35 +02:00
Yucheng Lin c571b269ff Translations: Update Chinese (Traditional)
Currently translated at 100.0% (5363 of 5363 strings)

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

powered by weblate
2023-06-28 14:08:41 +02:00
Moritz Lerch 6d57501c5c Translations: Update German (informal) (de_Informal)
Currently translated at 99.9% (5361 of 5362 strings)

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

powered by weblate
2023-06-28 14:08:41 +02:00
Moritz Lerch 5f3e039b2e Translations: Update German
Currently translated at 100.0% (5362 of 5362 strings)

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

powered by weblate
2023-06-28 14:08:41 +02:00
Raphael Michel 8fa7aeef78 Markdown: Allow to escape domain name (#3430)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-06-28 14:03:53 +02:00
Raphael Michel 3b5baa7701 Order import: Fix customer column being a required column 2023-06-28 14:00:16 +02:00
Raphael Michel c6bb3e71bf Order expiration: Allow to configure a delay in days (#3425)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-06-28 13:30:36 +02:00
Richard Schreiber 104607d34e PDF: fix normalization of unicode combination characters 2023-06-28 10:34:17 +02:00
Raphael Michel 714ef0d3b6 Order import: User lowercase email addresses 2023-06-28 09:13:36 +02:00
robbi5 db7c52ca93 Add OS name and version to stored device information (#3434)
Co-authored-by: Raphael Michel <michel@rami.io>
2023-06-28 09:02:07 +02:00
Raphael Michel fc94fbd9c8 Dockerfile: Remove broken npm installation line 2023-06-27 23:20:06 +02:00
Raphael Michel 61b3207ea2 Bump to 2023.7.0.dev0 2023-06-27 22:53:14 +02:00
Raphael Michel ccf17db972 Bump to 2023.6.0 2023-06-27 22:48:28 +02:00
Raphael Michel 456bee7efa Order import: Allow to assign a customer 2023-06-27 17:09:09 +02:00
Raphael Michel ccfdd364a3 Translations: Update German
Currently translated at 100.0% (211 of 211 strings)

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

powered by weblate
2023-06-27 17:00:48 +02:00
Raphael Michel cf92988eae Translations: Update German (informal) (de_Informal)
Currently translated at 99.9% (5361 of 5362 strings)

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

powered by weblate
2023-06-27 17:00:48 +02:00
Raphael Michel 6c561b1908 Translations: Update German
Currently translated at 100.0% (5362 of 5362 strings)

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

powered by weblate
2023-06-27 17:00:48 +02:00
Raphael Michel 5634a16a85 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (211 of 211 strings)

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

powered by weblate
2023-06-27 17:00:48 +02:00
Raphael Michel 6883ae268f Translations: Update German (informal) (de_Informal)
Currently translated at 99.9% (5361 of 5362 strings)

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

powered by weblate
2023-06-27 17:00:48 +02:00
Raphael Michel f75f8dead6 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (211 of 211 strings)

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

powered by weblate
2023-06-27 17:00:48 +02:00
Raphael Michel 0b28df8b83 Translations: Update German
Currently translated at 100.0% (211 of 211 strings)

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

powered by weblate
2023-06-27 17:00:48 +02:00
Yucheng Lin 0ffffc6a51 Translations: Update Chinese (Traditional)
Currently translated at 100.0% (5353 of 5353 strings)

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

powered by weblate
2023-06-27 17:00:48 +02:00
M C 3f95f06845 Translations: Update Italian
Currently translated at 83.8% (177 of 211 strings)

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

powered by weblate
2023-06-27 17:00:48 +02:00
M C 22bb4a9ac4 Translations: Update Italian
Currently translated at 19.0% (1020 of 5353 strings)

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

powered by weblate
2023-06-27 17:00:48 +02:00
Raphael Michel ee50ee8e99 Translations: Extend wordlist 2023-06-27 16:53:21 +02:00
Raphael Michel 63a6b17229 Loosen version constraint on importlib_metadata 2023-06-27 15:09:07 +02:00
Raphael Michel f33153ef01 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2023-06-27 14:51:54 +02:00
Raphael Michel 09517837ba IdempotencyMiddleware: Require a durable transaction 2023-06-27 13:16:04 +02:00
Raphael Michel 0f9ec8beca API: Expose TaxRule.custom_rules (#3426) 2023-06-27 13:05:54 +02:00
Raphael Michel 6d604889f2 Translations: Update Chinese (Traditional)
Currently translated at 99.9% (5351 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
Raphael Michel f9da500c06 Translations: Update Chinese (Traditional)
Currently translated at 99.9% (5351 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
Jonathan Berger 8f3b92a5b4 Translations: Update French
Currently translated at 99.9% (5348 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
Yucheng Lin c82aa891e6 Translations: Update Chinese (Traditional)
Currently translated at 100.0% (211 of 211 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/zh_Hant/

powered by weblate
2023-06-27 10:30:50 +02:00
Yucheng Lin 591ff61d1b Translations: Update Chinese (Traditional)
Currently translated at 100.0% (5353 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
M C af3ba16631 Translations: Update Italian
Currently translated at 18.7% (1006 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
Raphael Michel dce0bba707 Translations: Update Chinese (Traditional)
Currently translated at 88.8% (4754 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
Yucheng Lin 0a942a670f Translations: Update Chinese (Traditional)
Currently translated at 88.8% (4754 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
Yucheng Lin 310b1f50bc Translations: Update Chinese (Traditional)
Currently translated at 87.8% (4703 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
Yucheng Lin 0cef7029e1 Translations: Update Chinese (Traditional)
Currently translated at 87.2% (4672 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
Yucheng Lin fbc2a4cdc2 Translations: Update Chinese (Traditional)
Currently translated at 84.4% (4519 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
Ronan LE MEILLAT 2daf6f6d97 Translations: Update French
Currently translated at 99.9% (5348 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
cpoisnel 1fe80fa8c5 Translations: Update French
Currently translated at 99.4% (5325 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
Maurice Kaag fa0b31b19f Translations: Update French
Currently translated at 99.4% (5325 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
Ronan LE MEILLAT 3a77eeaa91 Translations: Update French
Currently translated at 99.4% (5325 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
Jonathan Berger a1faa66ecd Translations: Update French
Currently translated at 99.4% (5325 of 5353 strings)

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

powered by weblate
2023-06-27 10:30:50 +02:00
Raphael Michel 1e458d21f9 Data shredder: Add log entries 2023-06-27 09:34:39 +02:00
Raphael Michel d1a051544f Bump celery to 5.3 (#3433)
also fixes #3070
2023-06-26 12:47:07 +02:00
Raphael Michel 8bd4ddcd0d Add timeout for SMTP connections 2023-06-26 12:36:08 +02:00
Raphael Michel 59a16789ea CartManager: Fix crash PRETIXEU-8NF 2023-06-26 11:12:13 +02:00
Raphael Michel f4ce3654bb Data shredder: Add missing data-asynctask-long 2023-06-26 09:37:59 +02:00
Raphael Michel 3ad99d8239 Event deletion: Delete failed checkins 2023-06-26 09:37:51 +02:00
Raphael Michel b415393ccf Data shredder optimizations (#3429)
Co-authored-by: Martin Gross <gross@rami.io>
2023-06-23 16:56:19 +02:00
Raphael Michel 84dbd93d9e Translations: Update Chinese (Traditional)
Currently translated at 84.3% (4515 of 5353 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Raphael Michel 5a4f990ab9 Translations: Update Chinese (Traditional)
Currently translated at 84.3% (4516 of 5353 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Yucheng Lin 35f3d95a46 Translations: Update Chinese (Traditional)
Currently translated at 84.4% (4518 of 5353 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Ronan LE MEILLAT c729b71320 Translations: Update French
Currently translated at 99.4% (5325 of 5353 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Ronan LE MEILLAT 8eb7c8db9e Translations: Update French
Currently translated at 99.0% (5300 of 5353 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Yucheng Lin d5609f6ab0 Translations: Update Chinese (Traditional)
Currently translated at 81.7% (4376 of 5353 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Yucheng Lin 5d8fa31bdf Translations: Update Chinese (Traditional)
Currently translated at 81.7% (4374 of 5353 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Raphael Michel 9360b1fd90 Translations: Update Chinese (Traditional)
Currently translated at 81.6% (4372 of 5353 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Ronan LE MEILLAT 51da6570bf Translations: Update French
Currently translated at 97.3% (5212 of 5353 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Yucheng Lin fbdbddd555 Translations: Update Chinese (Traditional)
Currently translated at 81.7% (4374 of 5353 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Ronan LE MEILLAT eb3edd83b8 Translations: Update French
Currently translated at 94.7% (5071 of 5353 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Ronan LE MEILLAT 25f5fe54a9 Translations: Update French
Currently translated at 93.8% (5023 of 5353 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Ronan LE MEILLAT 7bf153bb3b Translations: Update French
Currently translated at 92.7% (4963 of 5353 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Yucheng Lin 48e64071a1 Translations: Update Chinese (Traditional)
Currently translated at 100.0% (211 of 211 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/zh_Hant/

powered by weblate
2023-06-23 16:05:46 +02:00
Yucheng Lin 95ea4fd4c9 Translations: Update Chinese (Traditional)
Currently translated at 81.6% (4371 of 5353 strings)

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

powered by weblate
2023-06-23 16:05:46 +02:00
Raphael Michel 206b57adfd Revert "Markdown: Allow to escape domain name"
This reverts commit b7f3f7a7a1.
2023-06-23 15:32:16 +02:00
Raphael Michel b7f3f7a7a1 Markdown: Allow to escape domain name 2023-06-23 15:32:00 +02:00
Raphael Michel 34e7a0fc31 PDF renderer: Fix crash while embedding iamge (PRETIXEU-8MY) 2023-06-23 11:51:23 +02:00
Raphael Michel cc7f249cb8 Fix crash if a tax rule on a fee prevents sale (PRETIXEU-8MZ) 2023-06-23 11:49:09 +02:00
Raphael Michel 147061eaa4 Fix issue in middleware after organizer deletion (PRETIXEU-8N3) 2023-06-23 11:25:55 +02:00
267 changed files with 219236 additions and 86562 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
- uses: actions/checkout@v2
- uses: harmon758/postgresql-action@v1
with:
postgresql version: '11'
postgresql version: '15'
postgresql db: 'pretix'
postgresql user: 'postgres'
postgresql password: 'postgres'
+1 -2
View File
@@ -33,8 +33,7 @@ RUN apt-get update && \
mkdir /static && \
mkdir /etc/supervisord && \
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash - && \
apt-get install -y nodejs && \
curl -qL https://www.npmjs.com/install.sh | sh
apt-get install -y nodejs
ENV LC_ALL=C.UTF-8 \
+1 -1
View File
@@ -1,4 +1,4 @@
from pretix.settings import *
LOGGING['handlers']['mail_admins']['include_html'] = True
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
STORAGES["staticfiles"]["BACKEND"] = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
+31
View File
@@ -152,6 +152,10 @@ Example::
password=abcd
host=localhost
port=3306
sslmode=require
sslrootcert=/etc/pretix/postgresql-ca.crt
sslcert=/etc/pretix/postgresql-client-crt.crt
sslkey=/etc/pretix/postgresql-client-key.key
``backend``
One of ``sqlite3`` and ``postgresql``.
@@ -163,6 +167,11 @@ Example::
``user``, ``password``, ``host``, ``port``
Connection details for the database connection. Empty by default.
``sslmode``, ``sslrootcert``
Connection TLS details for the PostgreSQL database connection. Possible values of ``sslmode`` are ``disable``, ``allow``, ``prefer``, ``require``, ``verify-ca``, and ``verify-full``. ``sslrootcert`` should be the accessible path of the ca certificate. Both values are empty by default.
``sslcert``, ``sslkey``
Connection mTLS details for the PostgreSQL database connection. It's also necessary to specify ``sslmode`` and ``sslrootcert`` parameters, please check the correct values from the TLS part. ``sslcert`` should be the accessible path of the client certificate. ``sslkey`` should be the accessible path of the client key. All values are empty by default.
.. _`config-replica`:
Database replica settings
@@ -324,6 +333,10 @@ to speed up various operations::
["sentinel_host_3", 26379]
]
password=password
ssl_cert_reqs=required
ssl_ca_certs=/etc/pretix/redis-ca.pem
ssl_keyfile=/etc/pretix/redis-client-crt.pem
ssl_certfile=/etc/pretix/redis-client-key.key
``location``
The location of redis, as a URL of the form ``redis://[:password]@localhost:6379/0``
@@ -347,6 +360,22 @@ to speed up various operations::
If your redis setup doesn't require a password or you already specified it in the location you can omit this option.
If this is set it will be passed to redis as the connection option PASSWORD.
``ssl_cert_reqs``
If this is set it will be passed to redis as the connection option ``SSL_CERT_REQS``.
Possible values are ``none``, ``optional``, and ``required``.
``ssl_ca_certs``
If your redis setup doesn't require TLS you can omit this option.
If this is set it will be passed to redis as the connection option ``SSL_CA_CERTS``. Possible value is the ca path.
``ssl_keyfile``
If your redis setup doesn't require mTLS you can omit this option.
If this is set it will be passed to redis as the connection option ``SSL_KEYFILE``. Possible value is the keyfile path.
``ssl_certfile``
If your redis setup doesn't require mTLS you can omit this option.
If this is set it will be passed to redis as the connection option ``SSL_CERTFILE``. Possible value is the certfile path.
If redis is not configured, pretix will store sessions and locks in the database. If memcached
is configured, memcached will be used for caching instead of redis.
@@ -396,6 +425,8 @@ The two ``transport_options`` entries can be omitted in most cases.
If they are present they need to be a valid JSON dictionary.
For possible entries in that dictionary see the `Celery documentation`_.
It is possible the use Redis with TLS/mTLS for the broker or the backend. To do so, it is necessary to specify the TLS identifier ``rediss``, the ssl mode ``ssl_cert_reqs`` and optionally specify the CA (TLS) ``ssl_ca_certs``, cert ``ssl_certfile`` and key ``ssl_keyfile`` (mTLS) path as encoded string. the following uri describes the format and possible parameters ``rediss://0.0.0.0:6379/1?ssl_cert_reqs=required&ssl_ca_certs=%2Fetc%2Fpretix%2Fredis-ca.pem&ssl_certfile=%2Fetc%2Fpretix%2Fredis-client-crt.pem&ssl_keyfile=%2Fetc%2Fpretix%2Fredis-client-key.key``
To use redis with sentinels set the broker or backend to ``sentinel://sentinel_host_1:26379;sentinel_host_2:26379/0``
and the respective transport_options to ``{"master_name":"mymaster"}``.
If your redis instances behind the sentinel have a password use ``sentinel://:my_password@sentinel_host_1:26379;sentinel_host_2:26379/0``.
+1 -1
View File
@@ -68,7 +68,7 @@ generated key and installs the plugin from the URL we told you::
mkdir -p /etc/ssh && \
ssh-keyscan -t rsa -p 10022 code.rami.io >> /root/.ssh/known_hosts && \
echo StrictHostKeyChecking=no >> /root/.ssh/config && \
DJANGO_SETTINGS_MODULE=pretix.settings pip3 install -U "git+ssh://git@code.rami.io:10022/pretix/pretix-slack.git@stable#egg=pretix-slack" && \
DJANGO_SETTINGS_MODULE= pip3 install -U "git+ssh://git@code.rami.io:10022/pretix/pretix-slack.git@stable#egg=pretix-slack" && \
cd /pretix/src && \
sudo -u pretixuser make production
USER pretixuser
+30 -2
View File
@@ -32,10 +32,16 @@ as well as the type of underlying hardware. Example:
"token": "kpp4jn8g2ynzonp6",
"hardware_brand": "Samsung",
"hardware_model": "Galaxy S",
"os_name": "Android",
"os_version": "2.3.6",
"software_brand": "pretixdroid",
"software_version": "4.0.0"
"software_version": "4.0.0",
"rsa_pubkey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqh…nswIDAQAB\n-----END PUBLIC KEY-----\n"
}
The ``rsa_pubkey`` is optional any only required for certain fatures such as working with reusable
media and NFC cryptography.
Every initialization token can only be used once. On success, you will receive a response containing
information on your device as well as your API token:
@@ -98,6 +104,8 @@ following endpoint:
{
"hardware_brand": "Samsung",
"hardware_model": "Galaxy S",
"os_name": "Android",
"os_version": "2.3.6",
"software_brand": "pretixdroid",
"software_version": "4.1.0",
"info": {"arbitrary": "data"}
@@ -133,9 +141,29 @@ The response will look like this:
"id": 3,
"name": "South entrance"
}
}
},
"server": {
"version": {
"pretix": "3.6.0.dev0",
"pretix_numeric": 30060001000
}
},
"medium_key_sets": [
{
"public_id": 3456349,
"organizer": "foo",
"active": true,
"media_type": "nfc_mf0aes",
"uid_key": "base64-encoded-encrypted-key",
"diversification_key": "base64-encoded-encrypted-key",
}
]
}
``"medium_key_sets`` will always be empty if you did not set an ``rsa_pubkey``.
The individual keys in the key sets are encrypted with the device's ``rsa_pubkey``
using ``RSA/ECB/PKCS1Padding``.
Creating a new API key
----------------------
+8
View File
@@ -24,6 +24,8 @@ all_events boolean Whether this de
limit_events list List of event slugs this device has access to
hardware_brand string Device hardware manufacturer (read-only)
hardware_model string Device hardware model (read-only)
os_name string Device operating system name (read-only)
os_version string Device operating system version (read-only)
software_brand string Device software product (read-only)
software_version string Device software version (read-only)
created datetime Creation time
@@ -76,6 +78,8 @@ Device endpoints
"security_profile": "full",
"hardware_brand": "Zebra",
"hardware_model": "TC25",
"os_name": "Android",
"os_version": "8.1.0",
"software_brand": "pretixSCAN",
"software_version": "1.5.1"
}
@@ -123,6 +127,8 @@ Device endpoints
"security_profile": "full",
"hardware_brand": "Zebra",
"hardware_model": "TC25",
"os_name": "Android",
"os_version": "8.1.0",
"software_brand": "pretixSCAN",
"software_version": "1.5.1"
}
@@ -173,6 +179,8 @@ Device endpoints
"initialized": null
"hardware_brand": null,
"hardware_model": null,
"os_name": null,
"os_version": null,
"software_brand": null,
"software_version": null
}
+2 -1
View File
@@ -18,7 +18,7 @@ The reusable medium resource contains the following public fields:
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the medium
type string Type of medium, e.g. ``"barcode"`` or ``"nfc_uid"``.
type string Type of medium, e.g. ``"barcode"``, ``"nfc_uid"`` or ``"nfc_mf0aes"``.
organizer string Organizer slug of the organizer who "owns" this medium.
identifier string Unique identifier of the medium. The format depends on the ``type``.
active boolean Whether this medium may be used.
@@ -37,6 +37,7 @@ Existing media types are:
- ``barcode``
- ``nfc_uid``
- ``nfc_mf0aes``
Endpoints
---------
+36 -6
View File
@@ -18,8 +18,15 @@ subject multi-lingual string The subject of
template multi-lingual string The body of the email
all_products boolean If ``true``, the email is sent to buyers of all products
limit_products list of integers List of product IDs, if ``all_products`` is not set
include_pending boolean If ``true``, the email is sent to pending orders. If ``false``,
[**DEPRECATED**] include_pending boolean If ``true``, the email is sent to pending orders. If ``false``,
only paid orders are considered.
restrict_to_status list List of order states to restrict recipients to. Valid
entries are ``p`` for paid, ``e`` for expired, ``c`` for canceled,
``n__pending_approval`` for pending approval,
``n__not_pending_approval_and_not_valid_if_pending`` for payment pending,
``n__valid_if_pending`` for payment pending but already confirmed,
and ``n__pending_overdue`` for pending with payment overdue.
The default is ``["p", "n__valid_if_pending"]``.
date_is_absolute boolean If ``true``, the email is set at a specific point in time.
send_date datetime If ``date_is_absolute`` is set: Date and time to send the email.
send_offset_days integer If ``date_is_absolute`` is not set, this is the number of days
@@ -37,7 +44,10 @@ send_to string Can be ``"order
or ``"both"``.
date. Otherwise it is relative to the event start date.
===================================== ========================== =======================================================
.. versionchanged:: 2023.7
The ``include_pending`` field has been deprecated.
The ``restrict_to_status`` field has been added.
Endpoints
---------
@@ -74,7 +84,11 @@ Endpoints
"template": {"en": "Don't forget your tickets, download them at {url}"},
"all_products": true,
"limit_products": [],
"include_pending": false,
"restrict_to_status": [
"p",
"n__not_pending_approval_and_not_valid_if_pending",
"n__valid_if_pending"
],
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
@@ -120,7 +134,11 @@ Endpoints
"template": {"en": "Don't forget your tickets, download them at {url}"},
"all_products": true,
"limit_products": [],
"include_pending": false,
"restrict_to_status": [
"p",
"n__not_pending_approval_and_not_valid_if_pending",
"n__valid_if_pending"
],
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
@@ -157,7 +175,11 @@ Endpoints
"template": {"en": "Don't forget your tickets, download them at {url}"},
"all_products": true,
"limit_products": [],
"include_pending": false,
"restrict_to_status": [
"p",
"n__not_pending_approval_and_not_valid_if_pending",
"n__valid_if_pending"
],
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
@@ -182,7 +204,11 @@ Endpoints
"template": {"en": "Don't forget your tickets, download them at {url}"},
"all_products": true,
"limit_products": [],
"include_pending": false,
"restrict_to_status": [
"p",
"n__not_pending_approval_and_not_valid_if_pending",
"n__valid_if_pending"
],
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
@@ -235,7 +261,11 @@ Endpoints
"template": {"en": "Don't forget your tickets, download them at {url}"},
"all_products": true,
"limit_products": [],
"include_pending": false,
"restrict_to_status": [
"p",
"n__not_pending_approval_and_not_valid_if_pending",
"n__valid_if_pending"
],
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
+1 -1
View File
@@ -61,7 +61,7 @@ Backend
item_formsets, order_search_filter_q, order_search_forms
.. automodule:: pretix.base.signals
:members: logentry_display, logentry_object_link, requiredaction_display, timeline_events, orderposition_blocked_display
:members: logentry_display, logentry_object_link, requiredaction_display, timeline_events, orderposition_blocked_display, customer_created, customer_signed_in
Vouchers
""""""""
+2
View File
@@ -70,6 +70,8 @@ The provider class
.. autoattribute:: settings_form_fields
.. autoattribute:: walletqueries
.. automethod:: settings_form_clean
.. automethod:: settings_content_render
+1 -1
View File
@@ -37,7 +37,7 @@ you to execute a piece of code with a different locale:
This is very useful e.g. when sending an email to a user that has a different language than the user performing the
action that causes the mail to be sent.
.. _translation features: https://docs.djangoproject.com/en/1.9/topics/i18n/translation/
.. _translation features: https://docs.djangoproject.com/en/4.2/topics/i18n/translation/
.. _GNU gettext: https://www.gnu.org/software/gettext/
.. _strings: https://django-i18nfield.readthedocs.io/en/latest/strings.html
.. _database fields: https://django-i18nfield.readthedocs.io/en/latest/quickstart.html
+23 -10
View File
@@ -15,25 +15,33 @@ and the admin panel is available at ``https://pretix.eu/control/event/bigorg/awe
If the organizer now configures a custom domain like ``tickets.bigorg.com``, his event will
from now on be available on ``https://tickets.bigorg.com/awesomecon/``. The former URL at
``pretix.eu`` will redirect there. However, the admin panel will still only be available
on ``pretix.eu`` for convenience and security reasons.
``pretix.eu`` will redirect there. It's also possible to do this for just an event, in which
case the event will be available on ``https://tickets.awesomecon.org/``.
However, the admin panel will still only be available on ``pretix.eu`` for convenience and security reasons.
URL routing
-----------
The hard part about implementing this URL routing in Django is that
``https://pretix.eu/bigorg/awesomecon/`` contains two parameters of nearly arbitrary content
and ``https://tickets.bigorg.com/awesomecon/`` contains only one. The only robust way to do
this is by having *separate* URL configuration for those two cases. In pretix, we call the
former our ``maindomain`` config and the latter our ``subdomain`` config. For pretix's core
modules we do some magic to avoid duplicate configuration, but for a fairly simple plugin with
only a handful of routes, we recommend just configuring the two URL sets separately.
and ``https://tickets.bigorg.com/awesomecon/`` contains only one and ``https://tickets.awesomecon.org/`` does not contain any.
The only robust way to do this is by having *separate* URL configuration for those three cases.
In pretix, we therefore do not have a global URL configuration, but three, living in the following modules:
- ``pretix.multidomain.maindomain_urlconf``
- ``pretix.multidomain.organizer_domain_urlconf``
- ``pretix.multidomain.event_domain_urlconf``
We provide some helper utilities to work with these to avoid duplicate configuration of the individual URLs.
The file ``urls.py`` inside your plugin package will be loaded and scanned for URL configuration
automatically and should be provided by any plugin that provides any view.
However, unlike plain Django, we look not only for a ``urlpatterns`` attribute on the module but support other
attributes like ``event_patterns`` and ``organizer_patterns`` as well.
A very basic example that provides one view in the admin panel and one view in the frontend
could look like this::
For example, for a simple plugin that adds one URL to the backend and one event-level URL to the frontend, you can
create the following configuration in your ``urls.py``::
from django.urls import re_path
@@ -52,7 +60,7 @@ could look like this::
As you can see, the view in the frontend is not included in the standard Django ``urlpatterns``
setting but in a separate list with the name ``event_patterns``. This will automatically prepend
the appropriate parameters to the regex (e.g. the event or the event and the organizer, depending
on the called domain).
on the called domain). For organizer-level views, ``organizer_patterns`` works the same way.
If you only provide URLs in the admin area, you do not need to provide a ``event_patterns`` attribute.
@@ -71,11 +79,16 @@ is a python method that emulates a behavior similar to ``reverse``:
.. autofunction:: pretix.multidomain.urlreverse.eventreverse
If you need to communicate the URL externally, you can use a different method to ensure that it is always an absolute URL:
.. autofunction:: pretix.multidomain.urlreverse.build_absolute_uri
In addition, there is a template tag that works similar to ``url`` but takes an event or organizer object
as its first argument and can be used like this::
{% load eventurl %}
<a href="{% eventurl request.event "presale:event.checkout" step="payment" %}">Pay</a>
<a href="{% abseventurl request.event "presale:event.checkout" step="payment" %}">Pay</a>
Implementation details
+1
View File
@@ -12,3 +12,4 @@ Developer documentation
api/index
structure
translation/index
nfc/index
+15
View File
@@ -0,0 +1,15 @@
NFC media
=========
pretix supports using NFC chips as "reusable media", for example to store gift cards or tickets.
Most of this implementation currently lives in our proprietary app pretixPOS, but in the future might also become part of our open-source pretixSCAN solution.
Either way, we want this to be an open ecosystem and therefore document the exact mechanisms in use on the following pages.
We support multiple implementations of NFC media, each documented on its own page:
.. toctree::
:maxdepth: 2
uid
mf0aes
+113
View File
@@ -0,0 +1,113 @@
Mifare Ultralight AES
=====================
We offer an implementation that provides a higher security level than the UID-based approach and uses the `Mifare Ultralight AES`_ chip sold by NXP.
We believe the security model of this approach is adequate to the situation where this will usually be used and we'll outline known risks below.
If you want to dive deeper into the properties of the Mifare Ultralight AES chip, we recommend reading the `data sheet`_.
Random UIDs
-----------
Mifare Ultralight AES supports a feature that returns a randomized UID every time a non-authenticated user tries to
read the UID. This has a strong privacy benefit, since no unauthorized entity can use the NFC chips to track users.
On the other hand, this reduces interoperability of the system. For example, this prevents you from using the same NFC
chips for a different purpose where you only need the UID. This will also prevent your guests from reading their UID
themselves with their phones, which might be useful e.g. in debugging situations.
Since there's no one-size-fits-all choice here, you can enable or disable this feature in the pretix organizer
settings. If you change it, the change will apply to all newly encoded chips after the change.
Key management
--------------
For every organizer, the server will generate create a "key set", which consists of a publicly known ID (random 32-bit integer) and two 16-byte keys ("diversification key" and "UID key").
Using our :ref:`Device authentication mechanism <rest-deviceauth>`, an authorized device can submit a locally generated RSA public key to the server.
This key can no longer changed on the server once it is set, thus protecting against the attack scenario of a leaked device API token.
The server will then include key sets in the response to ``/api/v1/device/info``, encrypted with the device's RSA key.
This includes all key sets generated for the organizer the device belongs to, as well as all keys of organizers that have granted sufficient access to this organizer.
The device will decrypt the key sets using its RSA key and store the key sets locally.
.. warning:: The device **will** have access to the raw key sets. Therefore, there is a risk of leaked master keys if an
authorized device is stolen or abused. Our implementation in pretixPOS attempts to make this very hard on
modern, non-rooted Android devices by keeping them encrypted with the RSA key and only storing the RSA key
in the hardware-backed keystore of the device. A sufficiently motivated attacker, however, will likely still
be able to extract the keys from a stolen device.
Encoding a chip
---------------
When a new chip is encoded, the following steps will be taken:
- The UID of the chip is retrieved.
- A chip-specific key is generated using the mechanism documented in `AN10922`_ using the "diversification key" from the
organizer's key set as the CMAC key and the diversification input concatenated in the from of ``0x01 + UID + APPID + SYSTEMID``
with the following values:
- The UID of the chip as ``UID``
- ``"eu.pretix"`` (``0x65 0x75 0x2e 0x70 0x72 0x65 0x74 0x69 0x78``) as ``APPID``
- The ``public_id`` from the organizer's key set as a 4-byte big-endian value as ``SYSTEMID``
- The chip-specific key is written to the chip as the "data protection key" (config pages 0x30 to 0x33)
- The UID key from the organizer's key set is written to the chip as the "UID retrieval key" (config pages 0x34 to 0x37)
- The config page 0x29 is set like this:
- ``RID_ACT`` (random UID) to ``1`` or ``0`` based on the organizer's configuration
- ``SEC_MSG_ACT`` (secure messaging) to ``1``
- ``AUTH0`` (first page that needs authentication) to 0x04 (first non-UID page)
- The config page 0x2A is set like this:
- ``PROT`` to ``0`` (only write access restricted, not read access)
- ``AUTHLIM`` to ``256`` (maximum number of wrong authentications before "self-desctruction")
- Everything else to its default value (no lock bits are set)
- The ``public_id`` of the key set will be written to page 0x04 as a big-endian value
- The UID of the chip will be registered as a reusable medium on the server.
.. warning:: During encoding, the chip-specific key and the UID key are transmitted in plain text over the air. The
security model therefore relies on the encoding of chips being performed in a trusted physical environment
to prevent a nearby attacker from sniffing the keys with a strong antenna.
.. note:: If an attacker tries to authenticate with the chip 256 times using the wrong key, the chip will become
unusable. A chip may also become unusable if it is detached from the reader in the middle of the encoding
process (even though we've tried to implement it in a way that makes this unlikely).
Usage
-----
When a chip is presented to the NFC reader, the following steps will be taken:
- Command ``GET_VERSION`` is used to determine if it is a Mifare Ultralight AES chip (if not, abort).
- Page 0x04 is read. If it is all zeroes, the chip is considered un-encoded (abort). If it contains a value that
corresponds to the ``public_id`` of a known key set, this key set is used for all further operations. If it contains
a different value, we consider this chip to belong to a different organizer or not to a pretix system at all (abort).
- An authentication with the chip using the UID key is performed.
- The UID of the chip will be read.
- The chip-specific key will be derived using the mechanism described above in the encoding step.
- An authentication with the chip using the chip-specific key is performed. If this is fully successful, this step
proves that the chip knows the same chip-specific key as we do and is therefore an authentic chip encoded by us and
we can trust its UID value.
- The UID is transmitted to the server to fetch the correct medium.
During these steps, the keys are never transmitted in plain text and can thus not be sniffed by a nearby attacker
with a strong antenna.
.. _Mifare Ultralight AES: https://www.nxp.com/products/rfid-nfc/mifare-hf/mifare-ultralight/mifare-ultralight-aes-enhanced-security-for-limited-use-contactless-applications:MF0AESx20
.. _data sheet: https://www.nxp.com/docs/en/data-sheet/MF0AES(H)20.pdf
.. _AN10922: https://www.nxp.com/docs/en/application-note/AN10922.pdf
+10
View File
@@ -0,0 +1,10 @@
UID-based
=========
With UID-based NFC, only the unique ID (UID) of the NFC chip is used for identification purposes.
This can be used with virtually all NFC chips that provide compatibility with the NFC reader in use, typically at least all chips that comply with ISO/IEC 14443-3A.
We make only one restriction: The UID may not start with ``08``, since that usually signifies a randomized UID that changes on every read (which would not be very useful).
.. warning:: The UID-based approach provides only a very low level of security. It is easy to clone a chip with the same
UID and impersonate someone else.
+14
View File
@@ -96,6 +96,20 @@ http://localhost:8000/control/ for the admin view.
port (for example because you develop on `pretixdroid`_), you can check
`Django's documentation`_ for more options.
When running the local development webserver, ensure Celery is not configured
in ``pretix.cfg``. i.e., you should remove anything such as::
[celery]
backend=redis://redis:6379/2
broker=redis://redis:6379/2
If you choose to use Celery for development, you must also start a Celery worker
process::
celery -A pretix.celery_app worker -l info
However, beware that code changes will not auto-reload within Celery.
.. _`checksandtests`:
Code checks and unit tests
+6 -5
View File
@@ -30,13 +30,13 @@ dependencies = [
"babel",
"BeautifulSoup4==4.12.*",
"bleach==5.0.*",
"celery==5.2.*",
"celery==5.3.*",
"chardet==5.1.*",
"cryptography>=3.4.2",
"css-inline==0.8.*",
"defusedcsv>=1.1.0",
"dj-static",
"Django==4.1.*",
"Django==4.2.*",
"django-bootstrap3==23.1.*",
"django-compressor==4.3.*",
"django-countries==7.5.*",
@@ -59,10 +59,10 @@ dependencies = [
"dnspython==2.3.*",
"drf_ujson2==1.7.*",
"geoip2==4.*",
"importlib_metadata==6.6.*", # Polyfill, we can probably drop this once we require Python 3.10+
"importlib_metadata==6.*", # Polyfill, we can probably drop this once we require Python 3.10+
"isoweek",
"jsonschema",
"kombu==5.2.*",
"kombu==5.3.*",
"libsass==0.22.*",
"lxml",
"markdown==3.4.3", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
@@ -90,7 +90,7 @@ dependencies = [
"pytz-deprecation-shim==0.1.*",
"pyuca",
"qrcode==7.4.*",
"redis==4.5.*,>=4.5.4",
"redis==4.6.*",
"reportlab==4.0.*",
"requests==2.31.*",
"sentry-sdk==1.15.*",
@@ -112,6 +112,7 @@ memcached = ["pylibmc"]
dev = [
"coverage",
"coveralls",
"fakeredis==2.18.*",
"flake8==6.0.*",
"freezegun",
"isort==5.12.*",
+1 -1
View File
@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "4.21.0.dev0"
__version__ = "2023.8.0.dev0"
+8 -1
View File
@@ -196,7 +196,14 @@ STATICFILES_DIRS = [
STATICI18N_ROOT = os.path.join(BASE_DIR, "pretix/static")
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage",
},
}
# if os.path.exists(os.path.join(DATA_DIR, 'static')):
# STATICFILES_DIRS.insert(0, os.path.join(DATA_DIR, 'static'))
+1
View File
@@ -223,6 +223,7 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
('POST', 'api-v1:checkinrpc.redeem'),
('GET', 'api-v1:checkinrpc.search'),
('POST', 'api-v1:reusablemedium-lookup'),
('POST', 'api-v1:reusablemedium-list'),
)
+2 -2
View File
@@ -59,7 +59,7 @@ class IdempotencyMiddleware:
auth_hash = sha1(auth_hash_parts.encode()).hexdigest()
idempotency_key = request.headers.get('X-Idempotency-Key', '')
with transaction.atomic():
with transaction.atomic(durable=True):
call, created = ApiCall.objects.select_for_update(of=OF_SELF).get_or_create(
auth_hash=auth_hash,
idempotency_key=idempotency_key,
@@ -75,7 +75,7 @@ class IdempotencyMiddleware:
if created:
resp = self.get_response(request)
with transaction.atomic():
with transaction.atomic(durable=True):
if resp.status_code in (409, 429, 500, 503):
# This is the exception: These calls are *meant* to be retried!
call.delete()
+11
View File
@@ -728,6 +728,7 @@ class EventSettingsSerializer(SettingsSerializer):
'payment_term_minutes',
'payment_term_last',
'payment_term_expire_automatically',
'payment_term_expire_delay_days',
'payment_term_accept_late',
'payment_explanation',
'payment_pending_hidden',
@@ -816,6 +817,10 @@ class EventSettingsSerializer(SettingsSerializer):
'reusable_media_type_nfc_uid',
'reusable_media_type_nfc_uid_autocreate_giftcard',
'reusable_media_type_nfc_uid_autocreate_giftcard_currency',
'reusable_media_type_nfc_mf0aes',
'reusable_media_type_nfc_mf0aes_autocreate_giftcard',
'reusable_media_type_nfc_mf0aes_autocreate_giftcard_currency',
'reusable_media_type_nfc_mf0aes_random_uid',
]
readonly_fields = [
# These are read-only since they are currently only settable on organizers, not events
@@ -825,6 +830,10 @@ class EventSettingsSerializer(SettingsSerializer):
'reusable_media_type_nfc_uid',
'reusable_media_type_nfc_uid_autocreate_giftcard',
'reusable_media_type_nfc_uid_autocreate_giftcard_currency',
'reusable_media_type_nfc_mf0aes',
'reusable_media_type_nfc_mf0aes_autocreate_giftcard',
'reusable_media_type_nfc_mf0aes_autocreate_giftcard_currency',
'reusable_media_type_nfc_mf0aes_random_uid',
]
def __init__(self, *args, **kwargs):
@@ -893,6 +902,8 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
'name_scheme',
'reusable_media_type_barcode',
'reusable_media_type_nfc_uid',
'reusable_media_type_nfc_mf0aes',
'reusable_media_type_nfc_mf0aes_random_uid',
'system_question_order',
]
+86 -5
View File
@@ -27,6 +27,7 @@ from decimal import Decimal
import pycountry
from django.conf import settings
from django.core.files import File
from django.db import models
from django.db.models import F, Q
from django.utils.encoding import force_str
from django.utils.timezone import now
@@ -372,11 +373,15 @@ class PdfDataSerializer(serializers.Field):
self.context['vars_images'] = get_images(self.context['event'])
for k, f in self.context['vars'].items():
try:
res[k] = f['evaluate'](instance, instance.order, ev)
except:
logger.exception('Evaluating PDF variable failed')
res[k] = '(error)'
if 'evaluate_bulk' in f:
# Will be evaluated later by our list serializers
res[k] = (f['evaluate_bulk'], instance)
else:
try:
res[k] = f['evaluate'](instance, instance.order, ev)
except:
logger.exception('Evaluating PDF variable failed')
res[k] = '(error)'
if not hasattr(ev, '_cached_meta_data'):
ev._cached_meta_data = ev.meta_data
@@ -429,6 +434,38 @@ class PdfDataSerializer(serializers.Field):
return res
class OrderPositionListSerializer(serializers.ListSerializer):
def to_representation(self, data):
# We have a custom implementation of this method because PdfDataSerializer() might keep some elements unevaluated
# with a (callable, input) tuple. We'll loop over these entries and evaluate them bulk-wise to save on SQL queries.
if isinstance(self.parent, OrderSerializer) and isinstance(self.parent.parent, OrderListSerializer):
# Do not execute our custom code because it will be executed by OrderListSerializer later for the
# full result set.
return super().to_representation(data)
iterable = data.all() if isinstance(data, models.Manager) else data
data = []
evaluate_queue = defaultdict(list)
for item in iterable:
entry = self.child.to_representation(item)
if "pdf_data" in entry:
for k, v in entry["pdf_data"].items():
if isinstance(v, tuple) and callable(v[0]):
evaluate_queue[v[0]].append((v[1], entry, k))
data.append(entry)
for func, entries in evaluate_queue.items():
results = func([item for (item, entry, k) in entries])
for (item, entry, k), result in zip(entries, results):
entry["pdf_data"][k] = result
return data
class OrderPositionSerializer(I18nAwareModelSerializer):
checkins = CheckinSerializer(many=True, read_only=True)
answers = AnswerSerializer(many=True)
@@ -440,6 +477,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
attendee_name = serializers.CharField(required=False)
class Meta:
list_serializer_class = OrderPositionListSerializer
model = OrderPosition
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
'company', 'street', 'zipcode', 'city', 'country', 'state', 'discount',
@@ -468,6 +506,20 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
def validate(self, data):
raise TypeError("this serializer is readonly")
def to_representation(self, data):
if isinstance(self.parent, (OrderListSerializer, OrderPositionListSerializer)):
# Do not execute our custom code because it will be executed by OrderListSerializer later for the
# full result set.
return super().to_representation(data)
entry = super().to_representation(data)
if "pdf_data" in entry:
for k, v in entry["pdf_data"].items():
if isinstance(v, tuple) and callable(v[0]):
entry["pdf_data"][k] = v[0]([v[1]])[0]
return entry
class RequireAttentionField(serializers.Field):
def to_representation(self, instance: OrderPosition):
@@ -613,6 +665,34 @@ class OrderURLField(serializers.URLField):
})
class OrderListSerializer(serializers.ListSerializer):
def to_representation(self, data):
# We have a custom implementation of this method because PdfDataSerializer() might keep some elements
# unevaluated with a (callable, input) tuple. We'll loop over these entries and evaluate them bulk-wise to
# save on SQL queries.
iterable = data.all() if isinstance(data, models.Manager) else data
data = []
evaluate_queue = defaultdict(list)
for item in iterable:
entry = self.child.to_representation(item)
for p in entry.get("positions", []):
if "pdf_data" in p:
for k, v in p["pdf_data"].items():
if isinstance(v, tuple) and callable(v[0]):
evaluate_queue[v[0]].append((v[1], p, k))
data.append(entry)
for func, entries in evaluate_queue.items():
results = func([item for (item, entry, k) in entries])
for (item, entry, k), result in zip(entries, results):
entry["pdf_data"][k] = result
return data
class OrderSerializer(I18nAwareModelSerializer):
invoice_address = InvoiceAddressSerializer(allow_null=True)
positions = OrderPositionSerializer(many=True, read_only=True)
@@ -627,6 +707,7 @@ class OrderSerializer(I18nAwareModelSerializer):
class Meta:
model = Order
list_serializer_class = OrderListSerializer
fields = (
'code', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
+6 -1
View File
@@ -251,6 +251,8 @@ class DeviceSerializer(serializers.ModelSerializer):
unique_serial = serializers.CharField(read_only=True)
hardware_brand = serializers.CharField(read_only=True)
hardware_model = serializers.CharField(read_only=True)
os_name = serializers.CharField(read_only=True)
os_version = serializers.CharField(read_only=True)
software_brand = serializers.CharField(read_only=True)
software_version = serializers.CharField(read_only=True)
created = serializers.DateTimeField(read_only=True)
@@ -263,7 +265,7 @@ class DeviceSerializer(serializers.ModelSerializer):
fields = (
'device_id', 'unique_serial', 'initialization_token', 'all_events', 'limit_events',
'revoked', 'name', 'created', 'initialized', 'hardware_brand', 'hardware_model',
'software_brand', 'software_version', 'security_profile'
'os_name', 'os_version', 'software_brand', 'software_version', 'security_profile'
)
@@ -390,6 +392,9 @@ class OrganizerSettingsSerializer(SettingsSerializer):
'reusable_media_type_nfc_uid',
'reusable_media_type_nfc_uid_autocreate_giftcard',
'reusable_media_type_nfc_uid_autocreate_giftcard_currency',
'reusable_media_type_nfc_mf0aes',
'reusable_media_type_nfc_mf0aes_autocreate_giftcard',
'reusable_media_type_nfc_mf0aes_autocreate_giftcard_currency',
]
def __init__(self, *args, **kwargs):
+78 -2
View File
@@ -19,8 +19,12 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import base64
import logging
from cryptography.hazmat.backends.openssl.backend import Backend
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import load_pem_public_key
from django.db.models import Exists, OuterRef, Q
from django.db.models.functions import Coalesce
from django.utils.timezone import now
@@ -34,6 +38,8 @@ from pretix.api.auth.device import DeviceTokenAuthentication
from pretix.api.views.version import numeric_version
from pretix.base.models import CheckinList, Device, SubEvent
from pretix.base.models.devices import Gate, generate_api_token
from pretix.base.models.media import MediumKeySet
from pretix.base.services.media import get_keysets_for_organizer
logger = logging.getLogger(__name__)
@@ -42,17 +48,73 @@ class InitializationRequestSerializer(serializers.Serializer):
token = serializers.CharField(max_length=190)
hardware_brand = serializers.CharField(max_length=190)
hardware_model = serializers.CharField(max_length=190)
os_name = serializers.CharField(max_length=190, required=False, allow_null=True)
os_version = serializers.CharField(max_length=190, required=False, allow_null=True)
software_brand = serializers.CharField(max_length=190)
software_version = serializers.CharField(max_length=190)
info = serializers.JSONField(required=False, allow_null=True)
rsa_pubkey = serializers.CharField(required=False, allow_null=True)
def validate(self, attrs):
if attrs.get('rsa_pubkey'):
try:
load_pem_public_key(
attrs['rsa_pubkey'].encode(), Backend()
)
except:
raise ValidationError({'rsa_pubkey': ['Not a valid public key.']})
return attrs
class UpdateRequestSerializer(serializers.Serializer):
hardware_brand = serializers.CharField(max_length=190)
hardware_model = serializers.CharField(max_length=190)
os_name = serializers.CharField(max_length=190, required=False, allow_null=True)
os_version = serializers.CharField(max_length=190, required=False, allow_null=True)
software_brand = serializers.CharField(max_length=190)
software_version = serializers.CharField(max_length=190)
info = serializers.JSONField(required=False, allow_null=True)
rsa_pubkey = serializers.CharField(required=False, allow_null=True)
def validate(self, attrs):
if attrs.get('rsa_pubkey'):
try:
load_pem_public_key(
attrs['rsa_pubkey'].encode(), Backend()
)
except:
raise ValidationError({'rsa_pubkey': ['Not a valid public key.']})
return attrs
class RSAEncryptedField(serializers.Field):
def to_representation(self, value):
public_key = load_pem_public_key(
self.context['device'].rsa_pubkey.encode(), Backend()
)
cipher_text = public_key.encrypt(
# RSA/ECB/PKCS1Padding
value,
padding.PKCS1v15()
)
return base64.b64encode(cipher_text).decode()
class MediumKeySetSerializer(serializers.ModelSerializer):
uid_key = RSAEncryptedField(read_only=True)
diversification_key = RSAEncryptedField(read_only=True)
organizer = serializers.SlugRelatedField(slug_field='slug', read_only=True)
class Meta:
model = MediumKeySet
fields = [
'public_id',
'organizer',
'active',
'media_type',
'uid_key',
'diversification_key',
]
class GateSerializer(serializers.ModelSerializer):
@@ -99,9 +161,12 @@ class InitializeView(APIView):
device.initialized = now()
device.hardware_brand = serializer.validated_data.get('hardware_brand')
device.hardware_model = serializer.validated_data.get('hardware_model')
device.os_name = serializer.validated_data.get('os_name')
device.os_version = serializer.validated_data.get('os_version')
device.software_brand = serializer.validated_data.get('software_brand')
device.software_version = serializer.validated_data.get('software_version')
device.info = serializer.validated_data.get('info')
device.rsa_pubkey = serializer.validated_data.get('rsa_pubkey')
device.api_token = generate_api_token()
device.save()
@@ -120,8 +185,15 @@ class UpdateView(APIView):
device = request.auth
device.hardware_brand = serializer.validated_data.get('hardware_brand')
device.hardware_model = serializer.validated_data.get('hardware_model')
device.os_name = serializer.validated_data.get('os_name')
device.os_version = serializer.validated_data.get('os_version')
device.software_brand = serializer.validated_data.get('software_brand')
device.software_version = serializer.validated_data.get('software_version')
if serializer.validated_data.get('rsa_pubkey') and serializer.validated_data.get('rsa_pubkey') != device.rsa_pubkey:
if device.rsa_pubkey:
raise ValidationError({'rsa_pubkey': ['You cannot change the rsa_pubkey of the device once it is set.']})
else:
device.rsa_pubkey = serializer.validated_data.get('rsa_pubkey')
device.info = serializer.validated_data.get('info')
device.save()
device.log_action('pretix.device.updated', data=serializer.validated_data, auth=device)
@@ -169,8 +241,12 @@ class InfoView(APIView):
'pretix': __version__,
'pretix_numeric': numeric_version(__version__),
}
}
},
'medium_key_sets': MediumKeySetSerializer(
get_keysets_for_organizer(device.organizer),
many=True,
context={'device': request.auth}
).data if device.rsa_pubkey else []
})
+6
View File
@@ -104,6 +104,12 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
auth=self.request.auth,
data=merge_dicts(self.request.data, {'id': inst.pk})
)
mt = MEDIA_TYPES.get(serializer.validated_data["type"])
if mt:
m = mt.handle_new(self.request.organizer, inst, self.request.user, self.request.auth)
if m:
s = self.get_serializer(m)
return Response({"result": s.data})
@transaction.atomic()
def perform_update(self, serializer):
+1
View File
@@ -945,6 +945,7 @@ with scopes_disabled():
| Q(addon_to__attendee_email__icontains=value)
| Q(order__code__istartswith=value)
| Q(order__invoice_address__name_cached__icontains=value)
| Q(order__invoice_address__company__icontains=value)
| Q(order__email__icontains=value)
| Q(pk__in=matching_media)
)
+1 -1
View File
@@ -140,7 +140,7 @@ class BaseExporter:
"""
return {}
def render(self, form_data: dict) -> Tuple[str, str, bytes]:
def render(self, form_data: dict) -> Tuple[str, str, Optional[bytes]]:
"""
Render the exported file and return a tuple consisting of a filename, a file type
and file content.
+37
View File
@@ -49,6 +49,9 @@ class BaseMediaType:
def handle_unknown(self, organizer, identifier, user, auth):
pass
def handle_new(self, organizer, medium, user, auth):
pass
def __str__(self):
return str(self.verbose_name)
@@ -108,9 +111,43 @@ class NfcUidMediaType(BaseMediaType):
return m
class NfcMf0aesMediaType(BaseMediaType):
identifier = 'nfc_mf0aes'
verbose_name = 'NFC Mifare Ultralight AES'
medium_created_by_server = False
supports_giftcard = True
supports_orderposition = False
def handle_new(self, organizer, medium, user, auth):
from pretix.base.models import GiftCard
if organizer.settings.get(f'reusable_media_type_{self.identifier}_autocreate_giftcard', as_type=bool):
with transaction.atomic():
gc = GiftCard.objects.create(
issuer=organizer,
expires=organizer.default_gift_card_expiry,
currency=organizer.settings.get(f'reusable_media_type_{self.identifier}_autocreate_giftcard_currency'),
)
medium.linked_giftcard = gc
medium.save()
medium.log_action(
'pretix.reusable_medium.linked_giftcard.changed',
user=user, auth=auth,
data={
'linked_giftcard': gc.pk
}
)
gc.log_action(
'pretix.giftcards.created',
user=user, auth=auth,
)
return medium
MEDIA_TYPES = {
m.identifier: m for m in [
BarcodePlainMediaType(),
NfcUidMediaType(),
NfcMf0aesMediaType(),
]
}
+16 -6
View File
@@ -26,7 +26,7 @@ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from django.conf import settings
from django.http import Http404, HttpRequest, HttpResponse
from django.middleware.common import CommonMiddleware
from django.urls import get_script_prefix
from django.urls import get_script_prefix, resolve
from django.utils import timezone, translation
from django.utils.cache import patch_vary_headers
from django.utils.deprecation import MiddlewareMixin
@@ -230,6 +230,8 @@ class SecurityMiddleware(MiddlewareMixin):
)
def process_response(self, request, resp):
url = resolve(request.path_info)
if settings.DEBUG and resp.status_code >= 400:
# Don't use CSP on debug error page as it breaks of Django's fancy error
# pages
@@ -249,20 +251,28 @@ class SecurityMiddleware(MiddlewareMixin):
h = {
'default-src': ["{static}"],
'script-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
'script-src': ['{static}'],
'object-src': ["'none'"],
'frame-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
'frame-src': ['{static}'],
'style-src': ["{static}", "{media}"],
'connect-src': ["{dynamic}", "{media}", "https://checkout.stripe.com"],
'img-src': ["{static}", "{media}", "data:", "https://*.stripe.com"] + img_src,
'connect-src': ["{dynamic}", "{media}"],
'img-src': ["{static}", "{media}", "data:"] + img_src,
'font-src': ["{static}"],
'media-src': ["{static}", "data:"],
# form-action is not only used to match on form actions, but also on URLs
# form-actions redirect to. In the context of e.g. payment providers or
# single-sign-on this can be nearly anything so we cannot really restrict
# single-sign-on this can be nearly anything, so we cannot really restrict
# this. However, we'll restrict it to HTTPS.
'form-action': ["{dynamic}", "https:"] + (['http:'] if settings.SITE_URL.startswith('http://') else []),
}
# Only include pay.google.com for wallet detection purposes on the Payment selection page
if (
url.url_name == "event.order.pay.change" or
(url.url_name == "event.checkout" and url.kwargs['step'] == "payment")
):
h['script-src'].append('https://pay.google.com')
h['frame-src'].append('https://pay.google.com')
h['connect-src'].append('https://google.com/pay')
if settings.LOG_CSP:
h['report-uri'] = ["/csp_report/"]
if 'Content-Security-Policy' in resp:
@@ -0,0 +1,23 @@
# Generated by Django 4.1.9 on 2023-06-26 10:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0242_auto_20230512_1008'),
]
operations = [
migrations.AddField(
model_name='device',
name='os_name',
field=models.CharField(max_length=190, null=True),
),
migrations.AddField(
model_name='device',
name='os_version',
field=models.CharField(max_length=190, null=True),
),
]
@@ -0,0 +1,35 @@
# Generated by Django 3.2.18 on 2023-05-17 11:32
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0243_device_os_name_and_os_version'),
]
operations = [
migrations.AddField(
model_name='device',
name='rsa_pubkey',
field=models.TextField(null=True),
),
migrations.CreateModel(
name='MediumKeySet',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('public_id', models.BigIntegerField(unique=True)),
('media_type', models.CharField(max_length=100)),
('active', models.BooleanField(default=True)),
('uid_key', models.BinaryField()),
('diversification_key', models.BinaryField()),
('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='medium_key_sets', to='pretixbase.organizer')),
],
),
migrations.AddConstraint(
model_name='mediumkeyset',
constraint=models.UniqueConstraint(condition=models.Q(('active', True)), fields=('organizer', 'media_type'), name='keyset_unique_active'),
),
]
+1 -1
View File
@@ -97,7 +97,7 @@ def _transactions_mark_order_dirty(order_id, using=None):
if getattr(dirty_transactions, 'order_ids', None) is None:
dirty_transactions.order_ids = set()
if _check_for_dirty_orders not in [func for savepoint_id, func in conn.run_on_commit]:
if _check_for_dirty_orders not in [func for (savepoint_id, func, *__) in conn.run_on_commit]:
transaction.on_commit(_check_for_dirty_orders, using)
dirty_transactions.order_ids.clear() # This is necessary to clean up after old threads with rollbacked transactions
+12
View File
@@ -143,6 +143,14 @@ class Device(LoggedModel):
max_length=190,
null=True, blank=True
)
os_name = models.CharField(
max_length=190,
null=True, blank=True
)
os_version = models.CharField(
max_length=190,
null=True, blank=True
)
software_brand = models.CharField(
max_length=190,
null=True, blank=True
@@ -158,6 +166,10 @@ class Device(LoggedModel):
null=True,
blank=False
)
rsa_pubkey = models.TextField(
null=True,
blank=True,
)
info = models.JSONField(
null=True, blank=True,
)
+3
View File
@@ -1277,6 +1277,9 @@ class Event(EventMixin, LoggedModel):
return not self.orders.exists() and not self.invoices.exists()
def delete_sub_objects(self):
from .checkin import Checkin
Checkin.all.filter(successful=False, list__event=self).delete()
self.cartposition_set.filter(addon_to__isnull=False).delete()
self.cartposition_set.all().delete()
self.vouchers.all().delete()
+8 -3
View File
@@ -43,6 +43,7 @@ from typing import Optional, Tuple
from zoneinfo import ZoneInfo
import dateutil.parser
import django_redis
from dateutil.tz import datetime_exists
from django.conf import settings
from django.core.exceptions import ValidationError
@@ -57,7 +58,6 @@ from django.utils.functional import cached_property
from django.utils.timezone import is_naive, make_aware, now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_countries.fields import Country
from django_redis import get_redis_connection
from django_scopes import ScopedManager
from i18nfield.fields import I18nCharField, I18nTextField
@@ -1910,8 +1910,13 @@ class Quota(LoggedModel):
def rebuild_cache(self, now_dt=None):
if settings.HAS_REDIS:
rc = get_redis_connection("redis")
rc.hdel(f'quotas:{self.event_id}:availabilitycache', str(self.pk))
rc = django_redis.get_redis_connection("redis")
p = rc.pipeline()
p.hdel(f'quotas:{self.event_id}:availabilitycache', str(self.pk))
p.hdel(f'quotas:{self.event_id}:availabilitycache:nocw', str(self.pk))
p.hdel(f'quotas:{self.event_id}:availabilitycache:igcl', str(self.pk))
p.hdel(f'quotas:{self.event_id}:availabilitycache:nocw:igcl', str(self.pk))
p.execute()
self.availability(now_dt=now_dt)
def availability(
+1 -3
View File
@@ -88,9 +88,7 @@ class LogEntry(models.Model):
class Meta:
ordering = ('-datetime', '-id')
index_together = [
['datetime', 'id']
]
indexes = [models.Index(fields=["datetime", "id"])]
def display(self):
from ..signals import logentry_display
+26 -1
View File
@@ -121,5 +121,30 @@ class ReusableMedium(LoggedModel):
class Meta:
unique_together = (("identifier", "type", "organizer"),)
index_together = (("identifier", "type", "organizer"), ("updated", "id"))
indexes = [
models.Index(fields=("identifier", "type", "organizer")),
models.Index(fields=("updated", "id")),
]
ordering = "identifier", "type", "organizer"
class MediumKeySet(models.Model):
organizer = models.ForeignKey('Organizer', on_delete=models.CASCADE, related_name='medium_key_sets')
public_id = models.BigIntegerField(
unique=True,
)
media_type = models.CharField(max_length=100)
active = models.BooleanField(default=True)
uid_key = models.BinaryField()
diversification_key = models.BinaryField()
objects = ScopedManager(organizer='organizer')
class Meta:
constraints = [
models.UniqueConstraint(
fields=["organizer", "media_type"],
condition=Q(active=True),
name="keyset_unique_active",
),
]
+46 -7
View File
@@ -270,9 +270,9 @@ class Order(LockModel, LoggedModel):
verbose_name = _("Order")
verbose_name_plural = _("Orders")
ordering = ("-datetime", "-pk")
index_together = [
["datetime", "id"],
["last_modified", "id"],
indexes = [
models.Index(fields=["datetime", "id"]),
models.Index(fields=["last_modified", "id"]),
]
def __str__(self):
@@ -896,6 +896,33 @@ class Order(LockModel, LoggedModel):
), tz)
return term_last
@property
def payment_term_expire_date(self):
delay = self.event.settings.get('payment_term_expire_delay_days', as_type=int)
if not delay: # performance saver + backwards compatibility
return self.expires
term_last = self.payment_term_last
if term_last and self.expires > term_last: # backwards compatibility
return self.expires
expires = self.expires.date() + timedelta(days=delay)
if self.event.settings.get('payment_term_weekdays'):
if expires.weekday() == 5:
expires += timedelta(days=2)
elif expires.weekday() == 6:
expires += timedelta(days=1)
tz = ZoneInfo(self.event.settings.timezone)
expires = make_aware(datetime.combine(
expires,
time(hour=23, minute=59, second=59)
), tz)
if term_last:
return min(expires, term_last)
else:
return expires
def _can_be_paid(self, count_waitinglist=True, ignore_date=False, force=False) -> Union[bool, str]:
error_messages = {
'late_lastdate': _("The payment can not be accepted as the last date of payments configured in the "
@@ -1645,12 +1672,13 @@ class OrderPayment(models.Model):
if status_change:
self.order.create_transactions()
def fail(self, info=None, user=None, auth=None, log_data=None):
def fail(self, info=None, user=None, auth=None, log_data=None, send_mail=True):
"""
Marks the order as failed and sets info to ``info``, but only if the order is in ``created`` or ``pending``
state. This is equivalent to setting ``state`` to ``OrderPayment.PAYMENT_STATE_FAILED`` and logging a failure,
but it adds strong database logging since we do not want to report a failure for an order that has just
but it adds strong database locking since we do not want to report a failure for an order that has just
been marked as paid.
:param send_mail: Whether an email should be sent to the user about this event (default: ``True``).
"""
with transaction.atomic():
locked_instance = OrderPayment.objects.select_for_update(of=OF_SELF).get(pk=self.pk)
@@ -1675,6 +1703,17 @@ class OrderPayment(models.Model):
'info': info,
'data': log_data,
}, user=user, auth=auth)
if send_mail:
with language(self.order.locale, self.order.event.settings.region):
email_subject = self.order.event.settings.mail_subject_order_payment_failed
email_template = self.order.event.settings.mail_text_order_payment_failed
email_context = get_email_context(event=self.order.event, order=self.order)
self.order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.payment_failed', user=user, auth=auth,
)
return True
def confirm(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text='',
@@ -2717,8 +2756,8 @@ class Transaction(models.Model):
class Meta:
ordering = 'datetime', 'pk'
index_together = [
['datetime', 'id']
indexes = [
models.Index(fields=['datetime', 'id'])
]
def save(self, *args, **kwargs):
+30 -3
View File
@@ -28,6 +28,7 @@ import pycountry
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import EmailValidator
from django.db.models import Q
from django.utils import formats
from django.utils.functional import cached_property
from django.utils.translation import (
@@ -42,8 +43,8 @@ from phonenumbers import SUPPORTED_REGIONS
from pretix.base.channels import get_all_sales_channels
from pretix.base.forms.questions import guess_country
from pretix.base.models import (
ItemVariation, OrderPosition, Question, QuestionAnswer, QuestionOption,
Seat, SubEvent,
Customer, ItemVariation, OrderPosition, Question, QuestionAnswer,
QuestionOption, Seat, SubEvent,
)
from pretix.base.services.pricing import get_price
from pretix.base.settings import (
@@ -804,7 +805,7 @@ class QuestionColumn(ImportColumn):
return self.q.clean_answer(value)
def assign(self, value, order, position, invoice_address, **kwargs):
if value:
if value is not None:
if not hasattr(order, '_answers'):
order._answers = []
if isinstance(value, QuestionOption):
@@ -826,6 +827,28 @@ class QuestionColumn(ImportColumn):
a.options.add(*a._options)
class CustomerColumn(ImportColumn):
identifier = 'customer'
verbose_name = gettext_lazy('Customer')
def clean(self, value, previous_values):
if value:
try:
value = self.event.organizer.customers.get(
Q(identifier=value) | Q(email__iexact=value) | Q(external_identifier=value)
)
except Customer.MultipleObjectsReturned:
value = self.event.organizer.customers.get(
Q(identifier=value)
)
except Customer.DoesNotExist:
raise ValidationError(_('No matching customer was found.'))
return value
def assign(self, value, order, position, invoice_address, **kwargs):
order.customer = value
def get_all_columns(event):
default = []
if event.has_subevents:
@@ -837,6 +860,10 @@ def get_all_columns(event):
Variation(event),
InvoiceAddressCompany(event),
]
if event.settings.customer_accounts:
default += [
CustomerColumn(event),
]
scheme = PERSON_NAME_SCHEMES.get(event.settings.name_scheme)
for n, l, w in scheme['fields']:
default.append(InvoiceAddressNamePart(event, n, l))
+29 -2
View File
@@ -60,7 +60,7 @@ from pretix.base.channels import get_all_sales_channels
from pretix.base.forms import PlaceholderValidator
from pretix.base.models import (
CartPosition, Event, GiftCard, InvoiceAddress, Order, OrderPayment,
OrderRefund, Quota,
OrderRefund, Quota, TaxRule,
)
from pretix.base.reldate import RelativeDateField, RelativeDateWrapper
from pretix.base.settings import SettingsSandbox
@@ -78,6 +78,16 @@ from pretix.presale.views.cart import cart_session, get_or_create_cart_id
logger = logging.getLogger(__name__)
class WalletQueries:
APPLEPAY = 'applepay'
GOOGLEPAY = 'googlepay'
WALLETS = (
(APPLEPAY, pgettext_lazy('payment', 'Apple Pay')),
(GOOGLEPAY, pgettext_lazy('payment', 'Google Pay')),
)
class PaymentProviderForm(Form):
def clean(self):
cleaned_data = super().clean()
@@ -436,6 +446,19 @@ class BasePaymentProvider:
d['_restrict_to_sales_channels']._as_type = list
return d
@property
def walletqueries(self):
"""
.. warning:: This property is considered **experimental**. It might change or get removed at any time without
prior notice.
A list of wallet payment methods that should be dynamically joined to the public name of the payment method,
if they are available to the user.
The detection is made on a best effort basis with no guarantees of correctness and actual availability.
Wallets that pretix can check for are exposed through ``pretix.base.payment.WalletQueries``.
"""
return []
def settings_form_clean(self, cleaned_data):
"""
Overriding this method allows you to inject custom validation into the settings form.
@@ -1015,7 +1038,11 @@ class FreeOrderProvider(BasePaymentProvider):
cart = get_cart(request)
total = get_cart_total(request)
total += sum([f.value for f in get_fees(self.event, request, total, None, None, cart)])
try:
total += sum([f.value for f in get_fees(self.event, request, total, None, None, cart)])
except TaxRule.SaleNotAllowed:
# ignore for now, will fail on order creation
pass
return total == 0
def order_change_allowed(self, order: Order) -> bool:
+122 -28
View File
@@ -43,16 +43,18 @@ import subprocess
import tempfile
import unicodedata
import uuid
from collections import OrderedDict
from collections import OrderedDict, defaultdict
from functools import partial
from io import BytesIO
import jsonschema
import reportlab.rl_config
from bidi.algorithm import get_display
from django.conf import settings
from django.contrib.staticfiles import finders
from django.core.exceptions import ValidationError
from django.db.models import Max, Min
from django.db.models.fields.files import FieldFile
from django.dispatch import receiver
from django.utils.deconstruct import deconstructible
from django.utils.formats import date_format
@@ -60,7 +62,8 @@ from django.utils.html import conditional_escape
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext
from i18nfield.strings import LazyI18nString
from pypdf import PdfReader
from pypdf import PdfReader, PdfWriter, Transformation
from pypdf.generic import RectangleObject
from reportlab.graphics import renderPDF
from reportlab.graphics.barcode.qr import QrCodeWidget
from reportlab.graphics.shapes import Drawing
@@ -85,6 +88,9 @@ from pretix.presale.style import get_fonts
logger = logging.getLogger(__name__)
if not settings.DEBUG:
reportlab.rl_config.shapeChecking = 0
DEFAULT_VARIABLES = OrderedDict((
("secret", {
@@ -102,7 +108,10 @@ DEFAULT_VARIABLES = OrderedDict((
("positionid", {
"label": _("Order position number"),
"editor_sample": "1",
"evaluate": lambda orderposition, order, event: str(orderposition.positionid)
"evaluate": lambda orderposition, order, event: str(orderposition.positionid),
# There is no performance gain in using evaluate_bulk here, but we want to make sure it is used somewhere
# in core to make sure we notice if the implementation of the API breaks.
"evaluate_bulk": lambda orderpositions: [str(p.positionid) for p in orderpositions],
}),
("order_positionid", {
"label": _("Order code and position number"),
@@ -355,14 +364,9 @@ DEFAULT_VARIABLES = OrderedDict((
}),
("addons", {
"label": _("List of Add-Ons"),
"editor_sample": _("Add-on 1\nAdd-on 2"),
"editor_sample": _("Add-on 1\n2x Add-on 2"),
"evaluate": lambda op, order, ev: "\n".join([
'{} - {}'.format(p.item.name, p.variation.value) if p.variation else str(p.item.name)
for p in (
op.addons.all() if 'addons' in getattr(op, '_prefetched_objects_cache', {})
else op.addons.select_related('item', 'variation')
)
if not p.canceled
str(p) for p in generate_compressed_addon_list(op, order, ev)
])
}),
("organizer", {
@@ -696,6 +700,30 @@ def get_seat(op: OrderPosition):
return None
def generate_compressed_addon_list(op, order, event):
itemcount = defaultdict(int)
addons = (
op.addons.all() if 'addons' in getattr(op, '_prefetched_objects_cache', {})
else op.addons.select_related('item', 'variation')
)
for pos in addons:
itemcount[pos.item, pos.variation] += 1
addonlist = []
for (item, variation), count in itemcount.items():
if variation:
if count > 1:
addonlist.append('{}x {} - {}'.format(count, item.name, variation.value))
else:
addonlist.append('{} - {}'.format(item.name, variation.value))
else:
if count > 1:
addonlist.append('{}x {}'.format(count, item.name))
else:
addonlist.append(item.name)
return addonlist
class Renderer:
def __init__(self, event, layout, background_file):
@@ -860,22 +888,37 @@ class Renderer:
image_file = None
if image_file:
ir = ThumbnailingImageReader(image_file)
try:
ir = ThumbnailingImageReader(image_file)
ir.resize(float(o['width']) * mm, float(o['height']) * mm, 300)
canvas.drawImage(
image=ir,
x=float(o['left']) * mm,
y=float(o['bottom']) * mm,
width=float(o['width']) * mm,
height=float(o['height']) * mm,
preserveAspectRatio=True,
anchor='c', # centered in frame
mask='auto'
)
if isinstance(image_file, FieldFile):
# ThumbnailingImageReader "closes" the file, so it's no use to use the same file pointer
# in case we need it again. For FieldFile, fortunately, there is an easy way to make the file
# refresh itself when it is used next.
del image_file.file
except:
logger.exception("Can not resize image")
pass
canvas.drawImage(
image=ir,
x=float(o['left']) * mm,
y=float(o['bottom']) * mm,
width=float(o['width']) * mm,
height=float(o['height']) * mm,
preserveAspectRatio=True,
anchor='c', # centered in frame
mask='auto'
)
logger.exception("Can not load or resize image")
canvas.saveState()
canvas.setFillColorRGB(.8, .8, .8, alpha=1)
canvas.rect(
x=float(o['left']) * mm,
y=float(o['bottom']) * mm,
width=float(o['width']) * mm,
height=float(o['height']) * mm,
stroke=0,
fill=1,
)
canvas.restoreState()
else:
canvas.saveState()
canvas.setFillColorRGB(.8, .8, .8, alpha=1)
@@ -929,7 +972,7 @@ class Renderer:
# reportlab does not support unicode combination characters
# It's important we do this before we use ArabicReshaper
text = unicodedata.normalize("NFKC", text)
text = unicodedata.normalize("NFC", text)
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
# to resolve all ligatures and python-bidi to switch RTL texts.
@@ -982,7 +1025,10 @@ class Renderer:
elif o['type'] == "poweredby":
self._draw_poweredby(canvas, op, o)
if self.bg_pdf:
page_size = (self.bg_pdf.pages[0].mediabox[2], self.bg_pdf.pages[0].mediabox[3])
page_size = (
self.bg_pdf.pages[0].mediabox[2] - self.bg_pdf.pages[0].mediabox[0],
self.bg_pdf.pages[0].mediabox[3] - self.bg_pdf.pages[0].mediabox[1]
)
if self.bg_pdf.pages[0].get('/Rotate') in (90, 270):
# swap dimensions due to pdf being rotated
page_size = page_size[::-1]
@@ -1010,14 +1056,12 @@ class Renderer:
with open(os.path.join(d, 'out.pdf'), 'rb') as f:
return BytesIO(f.read())
else:
from pypdf import PdfReader, PdfWriter, Transformation
from pypdf.generic import RectangleObject
buffer.seek(0)
new_pdf = PdfReader(buffer)
output = PdfWriter()
for i, page in enumerate(new_pdf.pages):
bg_page = copy.copy(self.bg_pdf.pages[i])
bg_page = copy.deepcopy(self.bg_pdf.pages[i])
bg_rotation = bg_page.get('/Rotate')
if bg_rotation:
# /Rotate is clockwise, transformation.rotate is counter-clockwise
@@ -1054,6 +1098,56 @@ class Renderer:
return outbuffer
def merge_background(fg_pdf, bg_pdf, out_file, compress):
if settings.PDFTK:
with tempfile.TemporaryDirectory() as d:
fg_filename = os.path.join(d, 'fg.pdf')
bg_filename = os.path.join(d, 'bg.pdf')
fg_pdf.write(fg_filename)
bg_pdf.write(bg_filename)
pdftk_cmd = [
settings.PDFTK,
fg_filename,
'multibackground',
bg_filename,
'output',
'-',
]
if compress:
pdftk_cmd.append('compress')
subprocess.run(pdftk_cmd, check=True, stdout=out_file)
else:
output = PdfWriter()
for i, page in enumerate(fg_pdf.pages):
bg_page = copy.deepcopy(bg_pdf.pages[i])
bg_rotation = bg_page.get('/Rotate')
if bg_rotation:
# /Rotate is clockwise, transformation.rotate is counter-clockwise
t = Transformation().rotate(bg_rotation)
w = float(page.mediabox.getWidth())
h = float(page.mediabox.getHeight())
if bg_rotation in (90, 270):
# offset due to rotation base
if bg_rotation == 90:
t = t.translate(h, 0)
else:
t = t.translate(0, w)
# rotate mediabox as well
page.mediabox = RectangleObject((
page.mediabox.left.as_numeric(),
page.mediabox.bottom.as_numeric(),
page.mediabox.top.as_numeric(),
page.mediabox.right.as_numeric(),
))
page.trimbox = page.mediabox
elif bg_rotation == 180:
t = t.translate(w, h)
page.add_transformation(t)
bg_page.merge_page(page)
output.add_page(bg_page)
output.write(out_file)
@deconstructible
class PdfLayoutValidator:
def __call__(self, value):
+7 -3
View File
@@ -141,9 +141,10 @@ error_messages = {
'price_not_a_number': gettext_lazy('The entered price is not a number.'),
'price_too_high': gettext_lazy('The entered price is to high.'),
'voucher_invalid': gettext_lazy('This voucher code is not known in our database.'),
'voucher_min_usages': gettext_lazy(
'The voucher code "%(voucher)s" can only be used if you select at least %(number)s '
'matching products.'
'voucher_min_usages': ngettext_lazy(
'The voucher code "%(voucher)s" can only be used if you select at least %(number)s matching products.',
'The voucher code "%(voucher)s" can only be used if you select at least %(number)s matching products.',
'number'
),
'voucher_min_usages_removed': ngettext_lazy(
'The voucher code "%(voucher)s" can only be used if you select at least %(number)s matching products. '
@@ -317,6 +318,9 @@ class CartManager:
def _delete_out_of_timeframe(self):
err = None
for cp in self.positions:
if not cp.pk:
continue
if cp.subevent and cp.subevent.presale_start and self.now_dt < cp.subevent.presale_start:
err = error_messages['some_subevent_not_started']
cp.addons.all().delete()
+7 -7
View File
@@ -86,8 +86,8 @@ def _build_time(t=None, value=None, ev=None, now_dt=None):
return ev.date_admission or ev.date_from
def _logic_annotate_for_graphic_explain(rules, ev, rule_data):
logic_environment = _get_logic_environment(ev)
def _logic_annotate_for_graphic_explain(rules, ev, rule_data, now_dt):
logic_environment = _get_logic_environment(ev, now_dt)
event = ev if isinstance(ev, Event) else ev.event
def _evaluate_inners(r):
@@ -152,7 +152,7 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
get in before 17:00". In the middle of the night it would switch to "You can only get in after 09:00".
"""
now_dt = now_dt or now()
logic_environment = _get_logic_environment(ev)
logic_environment = _get_logic_environment(ev, now_dt)
_var_values = {'False': False, 'True': True}
_var_explanations = {}
@@ -229,7 +229,7 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
for vname, data in _var_explanations.items():
var, operator, rhs = data['var'], data['operator'], data['rhs']
if var == 'now':
compare_to = _build_time(*rhs[0]['buildTime'], ev=ev).astimezone(ev.timezone)
compare_to = _build_time(*rhs[0]['buildTime'], ev=ev, now_dt=now_dt).astimezone(ev.timezone)
tolerance = timedelta(minutes=float(rhs[1])) if len(rhs) > 1 and rhs[1] else timedelta(seconds=0)
if operator == 'isBefore':
compare_to += tolerance
@@ -337,7 +337,7 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
return ', '.join(var_texts[v] for v in paths_with_min_weight[0] if not _var_values[v])
def _get_logic_environment(ev):
def _get_logic_environment(ev, now_dt):
# Every change to our supported JSON logic must be done
# * in pretix.base.services.checkin
# * in pretix.base.models.checkin
@@ -354,7 +354,7 @@ def _get_logic_environment(ev):
logic.add_operation('objectList', lambda *objs: list(objs))
logic.add_operation('lookup', lambda model, pk, str: int(pk))
logic.add_operation('inList', lambda a, b: a in b)
logic.add_operation('buildTime', partial(_build_time, ev=ev))
logic.add_operation('buildTime', partial(_build_time, ev=ev, now_dt=now_dt))
logic.add_operation('isBefore', is_before)
logic.add_operation('isAfter', lambda t1, t2, tol=None: is_before(t2, t1, tol))
return logic
@@ -861,7 +861,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
if type == Checkin.TYPE_ENTRY and clist.rules:
rule_data = LazyRuleVars(op, clist, dt)
logic = _get_logic_environment(op.subevent or clist.event)
logic = _get_logic_environment(op.subevent or clist.event, now_dt=dt)
if not logic.apply(clist.rules, rule_data):
if force:
force_used = True
+14 -3
View File
@@ -26,7 +26,7 @@ from typing import Any, Dict, Union
from celery.exceptions import MaxRetriesExceededError
from django.conf import settings
from django.core.files.base import ContentFile
from django.db import connection, transaction
from django.db import close_old_connections, connection, transaction
from django.dispatch import receiver
from django.utils.timezone import now, override
from django.utils.translation import gettext
@@ -86,9 +86,12 @@ def export(self, event: Event, fileid: str, provider: str, form_data: Dict[str,
gettext('Your export did not contain any data.')
)
file.filename, file.type, data = d
close_old_connections() # This task can run very long, we might need a new DB connection
f = ContentFile(data)
file.file.save(cachedfile_name(file, file.filename), f)
return file.pk
return str(file.pk)
@app.task(base=ProfiledOrganizerUserTask, throws=(ExportError,), bind=True)
@@ -154,9 +157,12 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
gettext('Your export did not contain any data.')
)
file.filename, file.type, data = d
close_old_connections() # This task can run very long, we might need a new DB connection
f = ContentFile(data)
file.file.save(cachedfile_name(file, file.filename), f)
return file.pk
return str(file.pk)
def _run_scheduled_export(schedule, context: Union[Event, Organizer], exporter, config_url, retry_func, has_permission):
@@ -214,6 +220,11 @@ def _run_scheduled_export(schedule, context: Union[Event, Organizer], exporter,
raise ExportError(
gettext('Your exported data exceeded the size limit for scheduled exports.')
)
conn = transaction.get_connection()
if not conn.in_atomic_block: # atomic execution only happens during tests or with celery always_eager on
close_old_connections() # This task can run very long, we might need a new DB connection
f = ContentFile(data)
file.file.save(cachedfile_name(file, file.filename), f)
except ExportEmptyError as e:
+72
View File
@@ -0,0 +1,72 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import secrets
from django.db import IntegrityError
from django.db.models import Q
from django_scopes import scopes_disabled
from pretix.base.models import GiftCardAcceptance
from pretix.base.models.media import MediumKeySet
def create_nfc_mf0aes_keyset(organizer):
for i in range(20):
public_id = secrets.randbelow(2 ** 32)
uid_key = secrets.token_bytes(16)
diversification_key = secrets.token_bytes(16)
try:
return MediumKeySet.objects.create(
organizer=organizer,
media_type="nfc_mf0aes",
public_id=public_id,
diversification_key=diversification_key,
uid_key=uid_key,
active=True,
)
except IntegrityError: # either race condition with another thread or duplicate public ID
try:
return MediumKeySet.objects.get(
organizer=organizer,
media_type="nfc_mf0aes",
active=True,
)
except MediumKeySet.DoesNotExist:
continue # duplicate public ID, let's try again
@scopes_disabled()
def get_keysets_for_organizer(organizer):
sets = list(MediumKeySet.objects.filter(
Q(organizer=organizer) | Q(organizer__in=GiftCardAcceptance.objects.filter(
acceptor=organizer,
active=True,
reusable_media=True,
).values_list("issuer_id", flat=True))
))
if organizer.settings.reusable_media_type_nfc_mf0aes and not any(
ks.organizer == organizer and ks.media_type == "nfc_mf0aes" for ks in sets
):
new_set = create_nfc_mf0aes_keyset(organizer)
if new_set:
sets.append(new_set)
return sets
+15 -4
View File
@@ -935,7 +935,10 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
raise OrderError(e.message)
require_approval = any(p.requires_approval(invoice_address=address) for p in positions)
fees = _get_fees(positions, payment_requests, address, meta_info, event, require_approval=require_approval)
try:
fees = _get_fees(positions, payment_requests, address, meta_info, event, require_approval=require_approval)
except TaxRule.SaleNotAllowed:
raise OrderError(error_messages['country_blocked'])
total = pending_sum = sum([c.price for c in positions]) + sum([c.value for c in fees])
order = Order(
@@ -968,7 +971,10 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
for fee in fees:
fee.order = order
fee._calculate_tax()
try:
fee._calculate_tax()
except TaxRule.SaleNotAllowed:
raise OrderError(error_messages['country_blocked'])
if fee.tax_rule and not fee.tax_rule.pk:
fee.tax_rule = None # TODO: deprecate
fee.save()
@@ -1264,12 +1270,12 @@ def expire_orders(sender, **kwargs):
Exists(
OrderFee.objects.filter(order_id=OuterRef('pk'), fee_type=OrderFee.FEE_TYPE_CANCELLATION)
)
).select_related('event').order_by('event_id')
).prefetch_related('event').order_by('event_id')
for o in qs:
if o.event_id != event_id:
expire = o.event.settings.get('payment_term_expire_automatically', as_type=bool)
event_id = o.event_id
if expire:
if expire and now() >= o.payment_term_expire_date:
mark_order_expired(o)
@@ -2470,6 +2476,11 @@ class OrderChangeManager:
split_order.status = Order.STATUS_PAID
else:
split_order.status = Order.STATUS_PENDING
if self.order.status == Order.STATUS_PAID:
split_order.set_expires(
now(),
list(set(p.subevent_id for p in split_positions))
)
split_order.save()
if offset_amount > Decimal('0.00'):
+15 -12
View File
@@ -24,13 +24,13 @@ import time
from collections import Counter, defaultdict
from itertools import zip_longest
import django_redis
from django.conf import settings
from django.db import models
from django.db.models import (
Case, Count, F, Func, Max, OuterRef, Q, Subquery, Sum, Value, When,
)
from django.utils.timezone import now
from django_redis import get_redis_connection
from pretix.base.models import (
CartPosition, Checkin, Order, OrderPosition, Quota, Voucher,
@@ -102,6 +102,12 @@ class QuotaAvailability:
self.count_waitinglist = defaultdict(int)
self.count_cart = defaultdict(int)
self._cache_key_suffix = ""
if not self._count_waitinglist:
self._cache_key_suffix += ":nocw"
if self._ignore_closed:
self._cache_key_suffix += ":igcl"
self.sizes = {}
def queue(self, *quota):
@@ -121,17 +127,14 @@ class QuotaAvailability:
if self._full_results:
raise ValueError("You cannot combine full_results and allow_cache.")
elif not self._count_waitinglist:
raise ValueError("If you set allow_cache, you need to set count_waitinglist.")
elif settings.HAS_REDIS:
rc = get_redis_connection("redis")
rc = django_redis.get_redis_connection("redis")
quotas_by_event = defaultdict(list)
for q in [_q for _q in self._queue if _q.id in quota_ids_set]:
quotas_by_event[q.event_id].append(q)
for eventid, evquotas in quotas_by_event.items():
d = rc.hmget(f'quotas:{eventid}:availabilitycache', [str(q.pk) for q in evquotas])
d = rc.hmget(f'quotas:{eventid}:availabilitycache{self._cache_key_suffix}', [str(q.pk) for q in evquotas])
for redisval, q in zip(d, evquotas):
if redisval is not None:
data = [rv for rv in redisval.decode().split(',')]
@@ -164,12 +167,12 @@ class QuotaAvailability:
if not settings.HAS_REDIS or not quotas:
return
rc = get_redis_connection("redis")
rc = django_redis.get_redis_connection("redis")
# We write the computed availability to redis in a per-event hash as
#
# quota_id -> (availability_state, availability_number, timestamp).
#
# We store this in a hash instead of inidividual values to avoid making two many redis requests
# We store this in a hash instead of individual values to avoid making too many redis requests
# which would introduce latency.
# The individual entries in the hash are "valid" for 120 seconds. This means in a typical peak scenario with
@@ -179,16 +182,16 @@ class QuotaAvailability:
# these quotas. We choose 10 seconds since that should be well above the duration of a write.
lock_name = '_'.join([str(p) for p in sorted([q.pk for q in quotas])])
if rc.exists(f'quotas:availabilitycachewrite:{lock_name}'):
if rc.exists(f'quotas:availabilitycachewrite:{lock_name}{self._cache_key_suffix}'):
return
rc.setex(f'quotas:availabilitycachewrite:{lock_name}', '1', 10)
rc.setex(f'quotas:availabilitycachewrite:{lock_name}{self._cache_key_suffix}', '1', 10)
update = defaultdict(list)
for q in quotas:
update[q.event_id].append(q)
for eventid, quotas in update.items():
rc.hmset(f'quotas:{eventid}:availabilitycache', {
rc.hmset(f'quotas:{eventid}:availabilitycache{self._cache_key_suffix}', {
str(q.id): ",".join(
[str(i) for i in self.results[q]] +
[str(int(time.time()))]
@@ -197,7 +200,7 @@ class QuotaAvailability:
# To make sure old events do not fill up our redis instance, we set an expiry on the cache. However, we set it
# on 7 days even though we mostly ignore values older than 2 monites. The reasoning is that we have some places
# where we set allow_cache_stale and use the old entries anyways to save on performance.
rc.expire(f'quotas:{eventid}:availabilitycache', 3600 * 24 * 7)
rc.expire(f'quotas:{eventid}:availabilitycache{self._cache_key_suffix}', 3600 * 24 * 7)
# We used to also delete item_quota_cache:* from the event cache here, but as the cache
# gets more complex, this does not seem worth it. The cache is only present for up to
+66 -6
View File
@@ -20,6 +20,7 @@
# <https://www.gnu.org/licenses/>.
#
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
#
@@ -31,7 +32,7 @@
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import inspect
import json
from datetime import timedelta
from tempfile import NamedTemporaryFile
@@ -41,10 +42,13 @@ from zipfile import ZipFile
from dateutil.parser import parse
from django.conf import settings
from django.utils.crypto import get_random_string
from django.utils.formats import date_format
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from pretix.base.models import CachedFile, Event, cachedfile_name
from pretix.base.i18n import language
from pretix.base.models import CachedFile, Event, User, cachedfile_name
from pretix.base.services.mail import SendMailException, mail
from pretix.base.services.tasks import ProfiledEventTask
from pretix.base.shredder import ShredError
from pretix.celery_app import app
@@ -101,8 +105,20 @@ def export(event: Event, shredders: List[str], session_key=None, cfid=None) -> N
return cf.pk
@app.task(base=ProfiledEventTask, throws=(ShredError,))
def shred(event: Event, fileid: str, confirm_code: str) -> None:
@app.task(base=ProfiledEventTask, throws=(ShredError,), bind=True)
def shred(self, event: Event, fileid: str, confirm_code: str, user: int=None, locale: str='en') -> None:
steps = []
if user:
user = User.objects.get(pk=user)
def set_progress(val):
if not self.request.called_directly:
self.update_state(
state='PROGRESS',
meta={'value': val, 'steps': steps}
)
known_shredders = event.get_data_shredders()
try:
cf = CachedFile.objects.get(pk=fileid)
@@ -124,8 +140,52 @@ def shred(event: Event, fileid: str, confirm_code: str) -> None:
if event.logentry_set.filter(datetime__gte=parse(indexdata['time'])):
raise ShredError(_("Something happened in your event after the export, please try again."))
for shredder in shredders:
shredder.shred_data()
event.log_action(
'pretix.event.shredder.started', user=user, data={
'indexdata': indexdata
}
)
for i, shredder in enumerate(shredders):
with language(locale):
steps.append({'label': str(shredder.verbose_name), 'done': False})
set_progress(i * 100 / len(shredders))
if 'progress_callback' in inspect.signature(shredder.shred_data).parameters:
shredder.shred_data(
progress_callback=lambda y: set_progress(
i * 100 / len(shredders) + min(max(y, 0), 100) / 100 * 100 / len(shredders)
)
)
else:
shredder.shred_data()
steps[-1]['done'] = True
cf.file.delete(save=False)
cf.delete()
event.log_action(
'pretix.event.shredder.completed', user=user, data={
'indexdata': indexdata
}
)
if user:
with language(user.locale):
try:
mail(
user.email,
_('Data shredding completed'),
'pretixbase/email/shred_completed.txt',
{
'user': user,
'organizer': event.organizer.name,
'event': str(event.name),
'start_time': date_format(parse(indexdata['time']).astimezone(event.timezone), 'SHORT_DATETIME_FORMAT'),
'shredders': ', '.join([str(s.verbose_name) for s in shredders])
},
event=None,
user=user,
locale=user.locale,
)
except SendMailException:
pass # Already logged
+81 -5
View File
@@ -259,6 +259,46 @@ DEFAULTS = {
label=_("Gift card currency"),
)
},
'reusable_media_type_nfc_mf0aes': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Active"),
)
},
'reusable_media_type_nfc_mf0aes_autocreate_giftcard': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Automatically create a new gift card if a new chip is encoded"),
)
},
'reusable_media_type_nfc_mf0aes_autocreate_giftcard_currency': {
'default': 'EUR',
'type': str,
'form_class': forms.ChoiceField,
'serializer_class': serializers.ChoiceField,
'serializer_kwargs': dict(
choices=[(c.alpha_3, c.alpha_3 + " - " + c.name) for c in settings.CURRENCIES],
),
'form_kwargs': dict(
choices=[(c.alpha_3, c.alpha_3 + " - " + c.name) for c in settings.CURRENCIES],
label=_("Gift card currency"),
)
},
'reusable_media_type_nfc_mf0aes_random_uid': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Use UID protection feature of NFC chip"),
)
},
'max_items_per_order': {
'default': '10',
'type': int,
@@ -893,6 +933,28 @@ DEFAULTS = {
"the pool and can be ordered by other people."),
)
},
'payment_term_expire_delay_days': {
'default': '0',
'type': int,
'form_class': forms.IntegerField,
'serializer_class': serializers.IntegerField,
'form_kwargs': dict(
label=_('Expiration delay'),
help_text=_("The order will only actually expire this many days after the expiration date communicated "
"to the customer. If you select \"Only end payment terms on weekdays\" above, this will also "
"be respected. However, this will not delay beyond the \"last date of payments\" "
"configured above, which is always enforced."),
# Every order in between the official expiry date and the delayed expiry date has a performance penalty
# for the cron job, so we limit this feature to 30 days to prevent arbitrary numbers of orders needing
# to be checked.
min_value=0,
max_value=30,
),
'serializer_kwargs': dict(
min_value=0,
max_value=30,
),
},
'payment_pending_hidden': {
'default': 'False',
'type': bool,
@@ -2279,6 +2341,24 @@ missing additional payment of **{pending_sum}**.
You can view the payment information and the status of your order at
{url}
Best regards,
Your {event} team""")) # noqa: W291
},
'mail_subject_order_payment_failed': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("Payment failed for your order: {code}")),
},
'mail_text_order_payment_failed': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
your payment attempt for your order for {event} has failed.
Your order is still valid and you can try to pay again using the same or a different payment method. Please complete your payment before {expire_date}.
You can retry the payment and view the status of your order at
{url}
Best regards,
Your {event} team""")) # noqa: W291
},
@@ -3635,14 +3715,10 @@ def validate_organizer_settings(organizer, settings_dict):
# This is not doing anything for the time being.
# But earlier we called validate_event_settings for the organizer, too - and that didn't do anything for
# organizer-settings either.
#
# N.B.: When actually fleshing out this stub, adding it to the OrganizerUpdateForm should be considered.
"""
if settings_dict.get('reusable_media_type_ntag_pretix1') and settings_dict.get('reusable_media_type_nfc_uid'):
if settings_dict.get('reusable_media_type_nfc_mf0aes') and settings_dict.get('reusable_media_type_nfc_uid'):
raise ValidationError({
'reusable_media_type_nfc_uid': _('This needs to be disabled if other NFC-based types are active.')
})
"""
def global_settings_object(holder):
+232 -66
View File
@@ -32,11 +32,12 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import copy
import json
import os
import time
from typing import List, Tuple
from django.db import transaction
from django.db.models import Max, Q
from django.db.models.functions import Greatest
from django.dispatch import receiver
@@ -99,11 +100,13 @@ class BaseDataShredder:
"""
raise NotImplementedError() # NOQA
def shred_data(self):
def shred_data(self, progress_callback=None):
"""
This method is called to actually remove the data from the system. You should remove any database objects
here.
You can call ``progress_callback`` with an integer value between 0 and 100 to communicate back your progress.
You should never delete ``LogEntry`` objects, but you might modify them to remove personal data. In this
case, set the ``LogEntry.shredded`` attribute to ``True`` to show that this is no longer original log data.
"""
@@ -151,6 +154,7 @@ class BaseDataShredder:
def shred_log_fields(logentry, banlist=None, whitelist=None):
d = logentry.parsed_data
initial_data = copy.copy(d)
shredded = False
if whitelist:
for k, v in d.items():
@@ -162,9 +166,61 @@ def shred_log_fields(logentry, banlist=None, whitelist=None):
if f in d:
d[f] = ''
shredded = True
logentry.data = json.dumps(d)
logentry.shredded = logentry.shredded or shredded
logentry.save(update_fields=['data', 'shredded'])
if d != initial_data:
logentry.data = json.dumps(d)
logentry.shredded = logentry.shredded or shredded
logentry.save(update_fields=['data', 'shredded'])
def slow_update(qs, batch_size=1000, sleep_time=.5, progress_callback=None, progress_offset=0, progress_total=None, **update):
"""
Doing UPDATE queries on hundreds of thousands of rows can cause outages due to high write load on the database.
This provides a throttled way to update rows. The condition for this to work properly is that the queryset has a
filter condition that no longer applies after the update!
Otherwise, this will be an endless loop!
"""
total_updated = 0
while True:
updated = qs.order_by().filter(
pk__in=qs.order_by().values_list('pk', flat=True)[:batch_size]
).update(**update)
total_updated += updated
if not updated:
break
if total_updated >= 0.8 * batch_size:
time.sleep(sleep_time)
if progress_callback and progress_total:
progress_callback((progress_offset + total_updated) / progress_total)
return total_updated
def slow_delete(qs, batch_size=1000, sleep_time=.5, progress_callback=None, progress_offset=0, progress_total=None):
"""
Doing DELETE queries on hundreds of thousands of rows can cause outages due to high write load on the database.
This provides a throttled way to update rows.
"""
total_deleted = 0
while True:
deleted = qs.order_by().filter(
pk__in=qs.order_by().values_list('pk', flat=True)[:batch_size]
).delete()[0]
total_deleted += deleted
if not deleted:
break
if total_deleted >= 0.8 * batch_size:
time.sleep(sleep_time)
return total_deleted
def _progress_helper(queryset, progress_callback, offset, total):
if not progress_callback:
yield from queryset
else:
for i, o in enumerate(queryset):
yield o
if i % 10 == 0:
progress_callback((i + offset) / total * 100)
class PhoneNumberShredder(BaseDataShredder):
@@ -177,18 +233,26 @@ class PhoneNumberShredder(BaseDataShredder):
o.code: o.phone for o in self.event.orders.filter(phone__isnull=False)
}, cls=CustomJSONEncoder, indent=4)
@transaction.atomic
def shred_data(self):
for o in self.event.orders.all():
def shred_data(self, progress_callback=None):
qs_orders = self.event.orders.all()
qs_orders_cnt = qs_orders.count()
qs_le = self.event.logentry_set.filter(action_type="pretix.event.order.phone.changed")
qs_le_cnt = qs_le.count()
total = qs_le_cnt + qs_orders_cnt
for o in _progress_helper(qs_orders, progress_callback, 0, total):
changed = bool(o.phone)
o.phone = None
d = o.meta_info_data
if d:
if 'contact_form_data' in d and 'phone' in d['contact_form_data']:
changed = True
del d['contact_form_data']['phone']
o.meta_info = json.dumps(d)
o.save(update_fields=['meta_info', 'phone'])
o.meta_info = json.dumps(d)
if changed:
o.save(update_fields=['meta_info', 'phone'])
for le in self.event.logentry_set.filter(action_type="pretix.event.order.phone.changed"):
for le in _progress_helper(qs_le, progress_callback, qs_orders_cnt, total):
shred_log_fields(le, banlist=['old_phone', 'new_phone'])
@@ -207,37 +271,66 @@ class EmailAddressShredder(BaseDataShredder):
for op in OrderPosition.all.filter(order__event=self.event, attendee_email__isnull=False)
}, indent=4)
@transaction.atomic
def shred_data(self):
OrderPosition.all.filter(order__event=self.event, attendee_email__isnull=False).update(attendee_email=None)
def shred_data(self, progress_callback=None):
qs_op = OrderPosition.all.filter(order__event=self.event, attendee_email__isnull=False)
qs_op_cnt = qs_op.count()
for o in self.event.orders.all():
qs_orders = self.event.orders.all()
qs_orders_cnt = qs_orders.count()
qs_le = self.event.logentry_set.filter(
Q(action_type__contains="order.email") | Q(action_type__contains="position.email") |
Q(action_type="pretix.event.order.contact.changed") |
Q(action_type="pretix.event.order.modified")
).exclude(data="")
qs_le_cnt = qs_le.count()
total = qs_op_cnt + qs_orders_cnt + qs_le_cnt
slow_update(
qs_op,
attendee_email=None,
progress_callback=progress_callback,
progress_offset=0,
progress_total=total,
# Updates to order position table are slow, since PostgreSQL needs to update many indexes, so let's
# take them really slowly to not overwhelm the database.
batch_size=100,
sleep_time=2,
)
for o in _progress_helper(qs_orders, progress_callback, qs_op_cnt, total):
changed = bool(o.email) or bool(o.customer)
o.email = None
o.customer = None
d = o.meta_info_data
if d:
if 'contact_form_data' in d and 'email' in d['contact_form_data']:
del d['contact_form_data']['email']
o.meta_info = json.dumps(d)
o.save(update_fields=['meta_info', 'email', 'customer'])
changed = True
o.meta_info = json.dumps(d)
if 'contact_form_data' in d and 'email_repeat' in d['contact_form_data']:
del d['contact_form_data']['email_repeat']
changed = True
if changed:
if d:
o.meta_info = json.dumps(d)
o.save(update_fields=['meta_info', 'email', 'customer'])
for le in self.event.logentry_set.filter(
Q(action_type__contains="order.email") | Q(action_type__contains="position.email"),
):
shred_log_fields(le, banlist=['recipient', 'message', 'subject', 'full_mail'])
for le in self.event.logentry_set.filter(action_type="pretix.event.order.contact.changed"):
shred_log_fields(le, banlist=['old_email', 'new_email'])
for le in self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data=""):
d = le.parsed_data
if 'data' in d:
for row in d['data']:
if 'attendee_email' in row:
row['attendee_email'] = ''
le.data = json.dumps(d)
le.shredded = True
le.save(update_fields=['data', 'shredded'])
for le in _progress_helper(qs_le, progress_callback, qs_op_cnt + qs_orders_cnt, total):
if le.action_type == "pretix.event.order.modified":
d = le.parsed_data
if 'data' in d:
for row in d['data']:
if 'attendee_email' in row:
row['attendee_email'] = ''
le.data = json.dumps(d)
le.shredded = True
le.save(update_fields=['data', 'shredded'])
else:
shred_log_fields(le, banlist=[
'recipient', 'message', 'subject', 'full_mail', 'old_email', 'new_email'
])
class WaitingListShredder(BaseDataShredder):
@@ -251,16 +344,35 @@ class WaitingListShredder(BaseDataShredder):
for wle in self.event.waitinglistentries.all()
], indent=4)
@transaction.atomic
def shred_data(self):
self.event.waitinglistentries.update(name_cached=None, name_parts={'_shredded': True}, email='', phone='')
def shred_data(self, progress_callback=None):
qs_wle = self.event.waitinglistentries.exclude(email='')
qs_wle_cnt = qs_wle.count()
for wle in self.event.waitinglistentries.select_related('voucher').filter(voucher__isnull=False):
qs_voucher = self.event.waitinglistentries.select_related('voucher').filter(voucher__isnull=False)
qs_voucher_cnt = qs_voucher.count()
qs_le = self.event.logentry_set.filter(action_type="pretix.voucher.added.waitinglist").exclude(data="")
qs_le_cnt = qs_le.count()
total = qs_voucher_cnt + qs_wle_cnt + qs_le_cnt
slow_update(
qs_wle,
name_cached=None,
name_parts={'_shredded': True},
email='',
phone='',
progress_callback=progress_callback,
progress_offset=0,
progress_total=total,
)
for wle in _progress_helper(qs_voucher, progress_callback, qs_wle_cnt, total):
if '@' in wle.voucher.comment:
wle.voucher.comment = ''
wle.voucher.save(update_fields=['comment'])
for le in self.event.logentry_set.filter(action_type="pretix.voucher.added.waitinglist").exclude(data=""):
for le in _progress_helper(qs_le, progress_callback, qs_wle_cnt + qs_voucher_cnt, total):
d = le.parsed_data
if 'name' in d:
d['name'] = ''
@@ -298,17 +410,41 @@ class AttendeeInfoShredder(BaseDataShredder):
)
}, indent=4)
@transaction.atomic
def shred_data(self):
OrderPosition.all.filter(
def shred_data(self, progress_callback=None):
qs_op = OrderPosition.all.filter(
order__event=self.event
).filter(
Q(attendee_name_cached__isnull=False) | Q(attendee_name_parts__isnull=False) |
Q(company__isnull=False) | Q(street__isnull=False) | Q(zipcode__isnull=False) | Q(city__isnull=False)
).update(attendee_name_cached=None, attendee_name_parts={'_shredded': True}, company=None, street=None,
zipcode=None, city=None)
Q(attendee_name_cached__isnull=False) |
Q(company__isnull=False) |
Q(street__isnull=False) |
Q(zipcode__isnull=False) |
Q(city__isnull=False)
)
qs_op_cnt = qs_op.count()
for le in self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data=""):
qs_le = self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data="")
qs_le_cnt = qs_le.count()
total = qs_op_cnt + qs_le_cnt
slow_update(
qs_op,
attendee_name_cached=None,
attendee_name_parts={'_shredded': True},
company=None,
street=None,
zipcode=None,
city=None,
progress_callback=progress_callback,
progress_total=total,
progress_offset=0,
# Updates to order position table are slow, since PostgreSQL needs to update many indexes, so let's
# take them really slowly to not overwhelm the database.
batch_size=100,
sleep_time=2,
)
for le in _progress_helper(qs_le, progress_callback, qs_op_cnt, total):
d = le.parsed_data
if 'data' in d:
for i, row in enumerate(d['data']):
@@ -343,11 +479,18 @@ class InvoiceAddressShredder(BaseDataShredder):
for ia in InvoiceAddress.objects.filter(order__event=self.event)
}, indent=4)
@transaction.atomic
def shred_data(self):
InvoiceAddress.objects.filter(order__event=self.event).delete()
def shred_data(self, progress_callback=None):
qs_ia = InvoiceAddress.objects.filter(order__event=self.event)
qs_ia_cnt = qs_ia.count()
for le in self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data=""):
qs_le = self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data="")
qs_le_cnt = qs_le.count()
total = qs_ia_cnt + qs_le_cnt
slow_delete(qs_ia, progress_callback=progress_callback, progress_total=total, progress_offset=0)
for le in _progress_helper(qs_le, progress_callback, qs_ia_cnt, total):
d = le.parsed_data
if 'invoice_data' in d and not isinstance(d['invoice_data'], bool):
for field in d['invoice_data']:
@@ -375,11 +518,18 @@ class QuestionAnswerShredder(BaseDataShredder):
).data
yield 'question-answers.json', 'application/json', json.dumps(d, indent=4)
@transaction.atomic
def shred_data(self):
QuestionAnswer.objects.filter(orderposition__order__event=self.event).delete()
def shred_data(self, progress_callback=None):
qs_qa = QuestionAnswer.objects.filter(orderposition__order__event=self.event)
qs_qa_cnt = qs_qa.count()
for le in self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data=""):
qs_le = self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data="")
qs_le_cnt = qs_le.count()
total = qs_qa_cnt + qs_le_cnt
slow_delete(qs_qa, progress_callback=progress_callback, progress_total=total, progress_offset=0)
for le in _progress_helper(qs_le, progress_callback, qs_qa_cnt, total):
d = le.parsed_data
if 'data' in d:
for i, row in enumerate(d['data']):
@@ -408,9 +558,11 @@ class InvoiceShredder(BaseDataShredder):
yield 'invoices/{}.pdf'.format(i.number), 'application/pdf', i.file.read()
i.file.close()
@transaction.atomic
def shred_data(self):
for i in self.event.invoices.filter(shredded=False):
def shred_data(self, progress_callback=None):
qs_i = self.event.invoices.filter(shredded=False)
total = qs_i.count()
for i in _progress_helper(qs_i, progress_callback, 0, total):
if i.file:
i.file.delete()
i.shredded = True
@@ -430,10 +582,17 @@ class CachedTicketShredder(BaseDataShredder):
def generate_files(self) -> List[Tuple[str, str, str]]:
pass
@transaction.atomic
def shred_data(self):
CachedTicket.objects.filter(order_position__order__event=self.event).delete()
CachedCombinedTicket.objects.filter(order__event=self.event).delete()
def shred_data(self, progress_callback=None):
qs_1 = CachedTicket.objects.filter(order_position__order__event=self.event)
qs_1_cnt = qs_1.count()
qs_2 = CachedCombinedTicket.objects.filter(order__event=self.event)
qs_2_cnt = qs_2.count()
total = qs_1_cnt + qs_2_cnt
slow_delete(qs_1, progress_callback=progress_callback, progress_total=total, progress_offset=0)
slow_delete(qs_2, progress_callback=progress_callback, progress_total=total, progress_offset=qs_1_cnt)
class PaymentInfoShredder(BaseDataShredder):
@@ -446,14 +605,21 @@ class PaymentInfoShredder(BaseDataShredder):
def generate_files(self) -> List[Tuple[str, str, str]]:
pass
@transaction.atomic
def shred_data(self):
def shred_data(self, progress_callback=None):
qs_p = OrderPayment.objects.filter(order__event=self.event)
qs_p_count = qs_p.count()
qs_r = OrderRefund.objects.filter(order__event=self.event)
qs_r_count = qs_r.count()
total = qs_p_count + qs_r_count
provs = self.event.get_payment_providers()
for obj in OrderPayment.objects.filter(order__event=self.event):
for obj in _progress_helper(qs_p, progress_callback, 0, total):
pprov = provs.get(obj.provider)
if pprov:
pprov.shred_payment_info(obj)
for obj in OrderRefund.objects.filter(order__event=self.event):
for obj in _progress_helper(qs_r, progress_callback, qs_p_count, total):
pprov = provs.get(obj.provider)
if pprov:
pprov.shred_payment_info(obj)
+25 -1
View File
@@ -683,12 +683,16 @@ dictionaries as values that contain keys like in the following example::
"product": {
"label": _("Product name"),
"editor_sample": _("Sample product"),
"evaluate": lambda orderposition, order, event: str(orderposition.item)
"evaluate": lambda orderposition, order, event: str(orderposition.item),
"evaluate_bulk": lambda orderpositions: [str(op.item) for op in orderpositions],
}
}
The ``evaluate`` member will be called with the order position, order and event as arguments. The event might
also be a subevent, if applicable.
The ``evaluate_bulk`` member is optional but can significantly improve performance in some situations because you
can perform database fetches in bulk instead of single queries for every position.
"""
@@ -787,3 +791,23 @@ return a dictionary mapping names of attributes in the settings store to DRF ser
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
customer_created = GlobalSignal()
"""
Arguments: ``customer``
This signal is sent out every time a customer account is created. The ``customer``
object is given as the first argument.
The ``sender`` keyword argument will contain the organizer.
"""
customer_signed_in = GlobalSignal()
"""
Arguments: ``customer``
This signal is sent out every time a customer signs in. The ``customer`` object
is given as the first argument.
The ``sender`` keyword argument will contain the organizer.
"""
@@ -0,0 +1,17 @@
{% load i18n %}
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
we hereby confirm that the following data shredding job has been completed:
Organizer: {{ organizer }}
Event: {{ event }}
Data selection: {{ shredders }}
Start time: {{ start_time }} (new data added after this time might not have been deleted)
Best regards,
Your pretix team
{% endblocktrans %}
+135 -25
View File
@@ -48,6 +48,8 @@ from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.safestring import mark_safe
from markdown import Extension
from markdown.inlinepatterns import SubstituteTagInlineProcessor
from markdown.postprocessors import Postprocessor
from markdown.treeprocessors import UnescapeTreeprocessor
from tlds import tld_set
register = template.Library()
@@ -108,6 +110,8 @@ URL_RE = SimpleLazyObject(lambda: build_url_re(tlds=sorted(tld_set, key=len, rev
EMAIL_RE = SimpleLazyObject(lambda: build_email_re(tlds=sorted(tld_set, key=len, reverse=True)))
DOT_ESCAPE = "|escaped-dot-sGnY9LMK|"
def safelink_callback(attrs, new=False):
"""
@@ -185,6 +189,109 @@ class EmailNl2BrExtension(Extension):
md.inlinePatterns.register(br_tag, 'nl', 5)
class LinkifyPostprocessor(Postprocessor):
def __init__(self, linker):
self.linker = linker
super().__init__()
def run(self, text):
return self.linker.linkify(text)
class CleanPostprocessor(Postprocessor):
def __init__(self, tags, attributes, protocols, strip):
self.tags = tags
self.attributes = attributes
self.protocols = protocols
self.strip = strip
super().__init__()
def run(self, text):
return bleach.clean(
text,
tags=self.tags,
attributes=self.attributes,
protocols=self.protocols,
strip=self.strip
)
class CustomUnescapeTreeprocessor(UnescapeTreeprocessor):
"""
This un-escapes everything except \\.
"""
def _unescape(self, m):
if m.group(1) == "46": # 46 is the ASCII position of .
return DOT_ESCAPE
return chr(int(m.group(1)))
class CustomUnescapePostprocessor(Postprocessor):
"""
Restore escaped .
"""
def run(self, text):
return text.replace(DOT_ESCAPE, ".")
class LinkifyAndCleanExtension(Extension):
r"""
We want to do:
input --> markdown --> bleach clean --> linkify --> output
Internally, the markdown library does:
source --> parse --> (tree|inline)processors --> serializing --> postprocessors
All escaped characters such as \. will be turned to something like <STX>46<ETX> in the processors
step and then will be converted to . back again in the last tree processor, before serialization.
Therefore, linkify does not see the escaped character anymore. This is annoying for the one case
where you want to type "rich_text.py" and *not* have it turned into a link, since you can't type
"rich_text\.py" either.
A simple solution would be to run linkify before markdown, but that may cause other issues when
linkify messes with the markdown syntax and it makes handling our attributes etc. harder.
So we do a weird hack where we modify the unescape processor to unescape everything EXCEPT for the
dot and then unescape that one manually after linkify. However, to make things even harder, the bleach
clean step removes any invisible characters, so we need to cheat a bit more.
"""
def __init__(self, linker, tags, attributes, protocols, strip):
self.linker = linker
self.tags = tags
self.attributes = attributes
self.protocols = protocols
self.strip = strip
super().__init__()
def extendMarkdown(self, md):
md.treeprocessors.deregister('unescape')
md.treeprocessors.register(
CustomUnescapeTreeprocessor(md),
'unescape',
0
)
md.postprocessors.register(
CleanPostprocessor(self.tags, self.attributes, self.protocols, self.strip),
'clean',
2
)
md.postprocessors.register(
LinkifyPostprocessor(self.linker),
'linkify',
1
)
md.postprocessors.register(
CustomUnescapePostprocessor(self.linker),
'unescape_dot',
0
)
def markdown_compile_email(source):
linker = bleach.Linker(
url_re=URL_RE,
@@ -192,18 +299,20 @@ def markdown_compile_email(source):
callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
parse_email=True
)
return linker.linkify(bleach.clean(
markdown.markdown(
source,
extensions=[
'markdown.extensions.sane_lists',
EmailNl2BrExtension(),
]
),
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES,
protocols=ALLOWED_PROTOCOLS,
))
return markdown.markdown(
source,
extensions=[
'markdown.extensions.sane_lists',
EmailNl2BrExtension(),
LinkifyAndCleanExtension(
linker,
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES,
protocols=ALLOWED_PROTOCOLS,
strip=False,
)
]
)
class SnippetExtension(markdown.extensions.Extension):
@@ -213,23 +322,24 @@ class SnippetExtension(markdown.extensions.Extension):
md.parser.blockprocessors.deregister('quote')
def markdown_compile(source, snippet=False):
def markdown_compile(source, linker, snippet=False):
tags = ALLOWED_TAGS_SNIPPET if snippet else ALLOWED_TAGS
exts = [
'markdown.extensions.sane_lists',
'markdown.extensions.nl2br'
'markdown.extensions.nl2br',
LinkifyAndCleanExtension(
linker,
tags=tags,
attributes=ALLOWED_ATTRIBUTES,
protocols=ALLOWED_PROTOCOLS,
strip=snippet,
)
]
if snippet:
exts.append(SnippetExtension())
return bleach.clean(
markdown.markdown(
source,
extensions=exts
),
strip=snippet,
tags=tags,
attributes=ALLOWED_ATTRIBUTES,
protocols=ALLOWED_PROTOCOLS,
return markdown.markdown(
source,
extensions=exts
)
@@ -245,7 +355,7 @@ def rich_text(text: str, **kwargs):
callbacks=DEFAULT_CALLBACKS + ([truelink_callback, safelink_callback] if kwargs.get('safelinks', True) else [truelink_callback, abslink_callback]),
parse_email=True
)
body_md = linker.linkify(markdown_compile(text))
body_md = markdown_compile(text, linker)
return mark_safe(body_md)
@@ -261,5 +371,5 @@ def rich_text_snippet(text: str, **kwargs):
callbacks=DEFAULT_CALLBACKS + ([truelink_callback, safelink_callback] if kwargs.get('safelinks', True) else [truelink_callback, abslink_callback]),
parse_email=True
)
body_md = linker.linkify(markdown_compile(text, snippet=True))
body_md = markdown_compile(text, linker, snippet=True)
return mark_safe(body_md)
+2 -1
View File
@@ -115,7 +115,8 @@ class AsyncMixin:
elif state == 'PROGRESS':
data.update({
'started': True,
'percentage': info.get('value', 0) if isinstance(info, dict) else 0
'percentage': info.get('value', 0) if isinstance(info, dict) else 0,
'steps': info.get('steps', []) if isinstance(info, dict) else None,
})
elif state == 'STARTED':
data.update({
+13
View File
@@ -749,6 +749,7 @@ class PaymentSettingsForm(EventSettingsValidationMixin, SettingsForm):
'payment_term_minutes',
'payment_term_last',
'payment_term_expire_automatically',
'payment_term_expire_delay_days',
'payment_term_accept_late',
'payment_pending_hidden',
'payment_explanation',
@@ -1115,6 +1116,16 @@ class MailSettingsForm(SettingsForm):
help_text=_("This email only applies to payment methods that can receive incomplete payments, "
"such as bank transfer."),
)
mail_subject_order_payment_failed = I18nFormField(
label=_("Subject"),
required=False,
widget=I18nTextInput,
)
mail_text_order_payment_failed = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
)
mail_subject_waiting_list = I18nFormField(
label=_("Subject"),
required=False,
@@ -1288,6 +1299,8 @@ class MailSettingsForm(SettingsForm):
'mail_subject_order_pending_warning': ['event', 'order'],
'mail_text_order_incomplete_payment': ['event', 'order', 'pending_sum'],
'mail_subject_order_incomplete_payment': ['event', 'order'],
'mail_text_order_payment_failed': ['event', 'order'],
'mail_subject_order_payment_failed': ['event', 'order'],
'mail_text_order_custom_mail': ['event', 'order'],
'mail_text_download_reminder': ['event', 'order'],
'mail_subject_download_reminder': ['event', 'order'],
+4 -4
View File
@@ -1732,8 +1732,8 @@ class CheckinListAttendeeFilterForm(FilterForm):
'-timestamp': (OrderBy(F('last_entry'), nulls_last=True, descending=True), '-order__code'),
'item': ('item__name', 'variation__value', 'order__code'),
'-item': ('-item__name', '-variation__value', '-order__code'),
'seat': ('seat__sorting_rank', 'seat__guid'),
'-seat': ('-seat__sorting_rank', '-seat__guid'),
'seat': ('seat__sorting_rank', 'seat__seat_guid'),
'-seat': ('-seat__sorting_rank', '-seat__seat_guid'),
'date': ('subevent__date_from', 'subevent__id', 'order__code'),
'-date': ('-subevent__date_from', 'subevent__id', '-order__code'),
'name': {'_order': F('display_name').asc(nulls_first=True),
@@ -1940,7 +1940,7 @@ class VoucherFilterForm(FilterForm):
'item__category__position',
'item__category',
'item__position',
'item__variation__position',
'variation__position',
'quota__name',
),
'subevent': 'subevent__date_from',
@@ -1950,7 +1950,7 @@ class VoucherFilterForm(FilterForm):
'-item__category__position',
'-item__category',
'-item__position',
'-item__variation__position',
'-variation__position',
'-quota__name',
)
}
-5
View File
@@ -461,11 +461,6 @@ class ItemCreateForm(I18nModelForm):
)
if self.cleaned_data.get('copy_from'):
for mv in self.cleaned_data['copy_from'].meta_values.all():
mv.pk = None
mv.item = instance
mv.save(force_insert=True)
for question in self.cleaned_data['copy_from'].questions.all():
question.items.add(instance)
question.log_action('pretix.event.question.changed', user=self.user, data={
+4
View File
@@ -412,6 +412,10 @@ class OrganizerSettingsForm(SettingsForm):
'reusable_media_type_nfc_uid',
'reusable_media_type_nfc_uid_autocreate_giftcard',
'reusable_media_type_nfc_uid_autocreate_giftcard_currency',
'reusable_media_type_nfc_mf0aes',
'reusable_media_type_nfc_mf0aes_autocreate_giftcard',
'reusable_media_type_nfc_mf0aes_autocreate_giftcard_currency',
'reusable_media_type_nfc_mf0aes_random_uid',
]
organizer_logo_image = ExtFileField(
+5
View File
@@ -341,6 +341,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.giftcards.acceptance.added': _('Gift card acceptance for another organizer has been added.'),
'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'),
'pretix.giftcards.acceptance.acceptor.invited': _('A new gift card acceptor has been invited.'),
'pretix.giftcards.acceptance.acceptor.removed': _('A gift card acceptor has been removed.'),
'pretix.giftcards.acceptance.issuer.removed': _('A gift card issuer has been removed or declined.'),
'pretix.giftcards.acceptance.issuer.accepted': _('A new gift card issuer has been accepted.'),
'pretix.webhook.created': _('The webhook has been created.'),
@@ -368,10 +369,13 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.reusable_medium.created.auto': _('The reusable medium has been created automatically.'),
'pretix.reusable_medium.changed': _('The reusable medium has been changed.'),
'pretix.reusable_medium.linked_orderposition.changed': _('The medium has been connected to a new ticket.'),
'pretix.reusable_medium.linked_giftcard.changed': _('The medium has been connected to a new gift card.'),
'pretix.email.error': _('Sending of an email has failed.'),
'pretix.event.comment': _('The event\'s internal comment has been updated.'),
'pretix.event.canceled': _('The event has been canceled.'),
'pretix.event.deleted': _('An event has been deleted.'),
'pretix.event.shredder.started': _('A removal process for personal data has been started.'),
'pretix.event.shredder.completed': _('A removal process for personal data has been completed.'),
'pretix.event.order.modified': _('The order details have been changed.'),
'pretix.event.order.unpaid': _('The order has been marked as unpaid.'),
'pretix.event.order.secret.changed': _('The order\'s secret has been changed.'),
@@ -430,6 +434,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'the order has been received and requires '
'approval.'),
'pretix.event.order.email.resend': _('An email with a link to the order detail page has been resent to the user.'),
'pretix.event.order.email.payment_failed': _('An email has been sent to notify the user that the payment failed.'),
'pretix.event.order.payment.confirmed': _('Payment {local_id} has been confirmed.'),
'pretix.event.order.payment.canceled': _('Payment {local_id} has been canceled.'),
'pretix.event.order.payment.canceled.failed': _('Canceling payment {local_id} has failed.'),
+4
View File
@@ -304,6 +304,10 @@ an instance of a form class that you bind yourself when appropriate. Your form w
as part of the standard validation and rendering cycle and rendered using default bootstrap
styles. It is advisable to set a prefix for your form to avoid clashes with other plugins.
Your forms may also have two special properties: ``template`` with a template that will be
included to render the form, and ``title``, which will be used as a headline. Your template
will be passed a ``form`` variable with your form.
As with all plugin signals, the ``sender`` keyword argument will contain the event.
"""
@@ -470,6 +470,8 @@
<div class="progress-bar progress-bar-success">
</div>
</div>
<div class="steps">
</div>
</div>
</div>
</div>
@@ -20,7 +20,7 @@
</div>
<div class="panel-body form-horizontal">
{% if spf_warning %}
<div class="alert alert-warning">
<div class="alert alert-danger">
<p>
{{ spf_warning }}
</p>
@@ -70,10 +70,18 @@
</div>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
{% if spf_warning %}
<div class="form-group submit-group">
<a href="" class="btn btn-default btn-save">
{% trans "Cancel" %}
</a>
</div>
{% else %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
{% endif %}
</form>
{% endblock %}
@@ -0,0 +1,27 @@
{% load i18n %}
<ul class="dropdown-menu dropdown-menu-right">
<li>
<a href="{% url "control:event.qrcode" event=request.event.slug organizer=request.organizer.slug filetype="png" %}{% if url %}?url={{ url|urlencode }}{% endif %}"
target="_blank" download>
{% blocktrans with filetype="PNG" %}Download QR code as {{ filetype }} image{% endblocktrans %}
</a>
</li>
<li>
<a href="{% url "control:event.qrcode" event=request.event.slug organizer=request.organizer.slug filetype="svg" %}{% if url %}?url={{ url|urlencode }}{% endif %}"
target="_blank" download>
{% blocktrans with filetype="SVG" %}Download QR code as {{ filetype }} image{% endblocktrans %}
</a>
</li>
<li>
<a href="{% url "control:event.qrcode" event=request.event.slug organizer=request.organizer.slug filetype="jpeg" %}{% if url %}?url={{ url|urlencode }}{% endif %}"
target="_blank" download>
{% blocktrans with filetype="JPG" %}Download QR code as {{ filetype }} image{% endblocktrans %}
</a>
</li>
<li>
<a href="{% url "control:event.qrcode" event=request.event.slug organizer=request.organizer.slug filetype="gif" %}{% if url %}?url={{ url|urlencode }}{% endif %}"
target="_blank" download>
{% blocktrans with filetype="GIF" %}Download QR code as {{ filetype }} image{% endblocktrans %}
</a>
</li>
</ul>
@@ -27,28 +27,7 @@
<button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown" title="{% trans "Create QR code" %}" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-qrcode" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li>
<a href="{% url "control:event.qrcode" event=request.event.slug organizer=request.organizer.slug filetype="png" %}" target="_blank" download>
{% blocktrans with filetype="PNG" %}Download QR code as {{ filetype }} image{% endblocktrans %}
</a>
</li>
<li>
<a href="{% url "control:event.qrcode" event=request.event.slug organizer=request.organizer.slug filetype="svg" %}" target="_blank" download>
{% blocktrans with filetype="SVG" %}Download QR code as {{ filetype }} image{% endblocktrans %}
</a>
</li>
<li>
<a href="{% url "control:event.qrcode" event=request.event.slug organizer=request.organizer.slug filetype="jpeg" %}" target="_blank" download>
{% blocktrans with filetype="JPG" %}Download QR code as {{ filetype }} image{% endblocktrans %}
</a>
</li>
<li>
<a href="{% url "control:event.qrcode" event=request.event.slug organizer=request.organizer.slug filetype="gif" %}" target="_blank" download>
{% blocktrans with filetype="GIF" %}Download QR code as {{ filetype }} image{% endblocktrans %}
</a>
</li>
</ul>
{% include "pretixcontrol/event/fragment_qr_dropdown.html" with url=0 %}
</div>
<div class="clearfix"></div>
</div>
@@ -105,6 +105,9 @@
{% 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_subject_order_expire_warning,mail_text_order_expire_warning,mail_subject_order_pending_warning,mail_text_order_pending_warning,mail_subject_order_incomplete_payment,mail_text_order_incomplete_payment" exclude="mail_days_order_expire_warning" %}
{% blocktrans asvar title_payment_failed %}Payment failed{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="payment_failed" title=title_payment_failed items="mail_subject_order_payment_failed,mail_text_order_payment_failed" %}
{% 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_subject_waiting_list,mail_text_waiting_list" %}
@@ -65,6 +65,8 @@
{% bootstrap_field form.payment_term_minutes layout="control" %}
{% bootstrap_field form.payment_term_last layout="control" %}
{% bootstrap_field form.payment_term_expire_automatically layout="control" %}
{% trans "days" context "unit" as days %}
{% bootstrap_field form.payment_term_expire_delay_days layout="control" addon_after=days %}
{% bootstrap_field form.payment_term_accept_late layout="control" %}
{% bootstrap_field form.payment_pending_hidden layout="control" %}
</fieldset>
@@ -337,7 +337,7 @@
<div class="alert alert-info">
{% blocktrans trimmed %}
The waiting list currently is not compatible with some advanced features of pretix such as
add-on products or product bundles.
hidden products, add-on products or product bundles.
{% endblocktrans %}
</div>
<div class="alert alert-info">
@@ -186,8 +186,12 @@
{% bootstrap_field form.media_type layout="control" %}
{% endif %}
{% for f in plugin_forms %}
{% if f.is_layouts %}
{% bootstrap_form f layout="control" %}
{% if f.is_layouts and not f.title %}
{% if f.template and not "template" in f.fields %}
{% include f.template with form=f %}
{% else %}
{% bootstrap_form f layout="control" %}
{% endif %}
{% endif %}
{% endfor %}
</fieldset>
@@ -256,11 +260,27 @@
{% endif %}
{% bootstrap_field form.show_quota_left layout="control" %}
{% for f in plugin_forms %}
{% if not f.is_layouts %}
{% bootstrap_form f layout="control" %}
{% if not f.is_layouts and not f.title %}
{% if f.template and not "template" in f.fields %}
{% include f.template with form=f %}
{% else %}
{% bootstrap_form f layout="control" %}
{% endif %}
{% endif %}
{% endfor %}
</fieldset>
{% for f in plugin_forms %}
{% if not f.is_layouts and f.title %}
<fieldset>
<legend>{{ f.title }}</legend>
{% if f.template and not "template" in f.fields %}
{% include f.template with form=f %}
{% else %}
{% bootstrap_form f layout="control" %}
{% endif %}
</fieldset>
{% endif %}
{% endfor %}
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
@@ -69,7 +69,7 @@
<td></td>
<td class="text-right flip">
<strong>
{{ sums.count }}
{{ sums.sum_count }}
</strong>
</td>
<td></td>
@@ -262,6 +262,32 @@
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">NFC Mifare Ultralight AES</h4>
</div>
<div class="panel-body">
<p class="help-block">
{% blocktrans trimmed %}
This medium type works only with NFC chips of the type Mifare Ultralight AES
made by NXP. This provides a higher level of security than other approaches, but
requires all chips to be encoded prior to use.
{% endblocktrans %}
{% blocktrans trimmed %}
NFC media can currently only be connected to gift cards.
{% endblocktrans %}
</p>
{% bootstrap_field sform.reusable_media_type_nfc_mf0aes layout="control" %}
<div data-display-dependency="#{{ sform.reusable_media_type_nfc_mf0aes.id_for_label }}">
{% bootstrap_field sform.reusable_media_type_nfc_mf0aes_autocreate_giftcard layout="control" %}
<div data-display-dependency="#{{ sform.reusable_media_type_nfc_mf0aes_autocreate_giftcard.id_for_label }}">
{% bootstrap_field sform.reusable_media_type_nfc_mf0aes_autocreate_giftcard_currency layout="control" %}
</div>
{% bootstrap_field sform.reusable_media_type_nfc_mf0aes_random_uid layout="control" %}
</div>
</div>
</div>
</div>
</fieldset>
<fieldset>
@@ -269,6 +295,11 @@
{% bootstrap_field sform.invoice_regenerate_allowed layout="control" %}
</fieldset>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</div>
<div class="col-xs-12 col-lg-2">
<div class="panel panel-default">
@@ -281,10 +312,5 @@
</div>
</div>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}
@@ -8,7 +8,7 @@
{% trans "Data shredder" %}
</h1>
<form action="{% url "control:event.shredder.shred" event=request.event.slug organizer=request.organizer.slug %}"
method="post" class="form-horizontal" data-asynctask>
method="post" class="form-horizontal" data-asynctask data-asynctask-long>
{% csrf_token %}
<fieldset>
{% if download_on_shred %}
@@ -55,6 +55,12 @@
</fieldset>
{% endif %}
<input type="hidden" name="file" value="{{ file.pk }}">
<div class="alert alert-info">
{% blocktrans trimmed %}
Depending on the amount of data in your event, the following step may take a while to complete.
We will inform you via email once it has been completed.
{% endblocktrans %}
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Continue" %}
@@ -42,7 +42,7 @@
</div>
{% else %}
<form action="{% url "control:event.shredder.export" event=request.event.slug organizer=request.organizer.slug %}"
method="post" class="form-horizontal" data-asynctask>
method="post" class="form-horizontal" data-asynctask data-asynctask-long>
<legend>{% trans "Data selection" %}</legend>
{% csrf_token %}
<div class="panel-group" id="payment_accordion">
@@ -17,7 +17,7 @@
placeholder="{% trans "Prefix (optional)" %}">
<div class="input-group">
<input type="number" class="form-control input-xs"
id="voucher-bulk-codes-num"
id="voucher-bulk-codes-num" max="100000"
placeholder="{% trans "Number" context "number_of_things" %}">
<div class="input-group-btn">
<button class="btn btn-default" type="button" id="voucher-bulk-codes-generate"
@@ -42,10 +42,18 @@
<div class="form-group">
<label class="col-md-3 control-label" for="id_url">{% trans "Voucher link" %}</label>
<div class="col-md-9">
<input type="text" name="url"
value="{% abseventurl request.event "presale:event.redeem" %}?voucher={{ voucher.code|urlencode }}{% if voucher.subevent_id %}&subevent={{ voucher.subevent_id }}{% endif %}"
class="form-control"
id="id_url" readonly>
<div class="input-group">
<input type="text" name="url"
value="{{ url }}"
class="form-control"
id="id_url" readonly>
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" title="{% trans "Create QR code" %}" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-qrcode" aria-hidden="true"></i>
</button>
{% include "pretixcontrol/event/fragment_qr_dropdown.html" with url=url %}
</div>
</div>
</div>
</div>
{% endif %}
@@ -96,7 +96,9 @@
<tr>
<th>
{% if "can_change_vouchers" in request.eventpermset %}
<input type="checkbox" data-toggle-table />
<label aria-label="{% trans "select all rows for batch-operation" %}" class="batch-select-label">
<input type="checkbox" data-toggle-table />
</label>
{% endif %}
</th>
<th>
@@ -139,7 +141,9 @@
<tr>
<td>
{% if "can_change_vouchers" in request.eventpermset %}
<input type="checkbox" name="voucher" class="" value="{{ v.pk }}"/>
<label aria-label="{% trans "select row for batch-operation" %}" class="batch-select-label">
<input type="checkbox" name="voucher" class="batch-select-checkbox" value="{{ v.pk }}"/>
</label>
{% endif %}
</td>
<td>
@@ -194,9 +198,12 @@
</table>
</div>
{% if "can_change_vouchers" in request.eventpermset %}
<button type="submit" class="btn btn-default btn-save" name="action" value="delete">
{% trans "Delete selected" %}
</button>
<div class="batch-select-actions">
<button type="submit" class="btn btn-danger" name="action" value="delete">
<i class="fa fa-trash" aria-hidden="true"></i>
{% trans "Delete selected" %}
</button>
</div>
{% endif %}
</form>
{% include "pretixcontrol/pagination.html" %}
@@ -212,11 +212,21 @@
<span class="label label-warning">{% trans "Voucher assigned" %}</span>
{% endif %}
{% elif e.availability.0 == 100 %}
<span class="label label-warning">
{% blocktrans with num=e.availability.1 %}
Waiting, product {{ num }}x available
{% endblocktrans %}
</span>
{% if e.availability.1|default_if_none:"none" == "none" %}
<span class="label label-danger" data-toggle="tooltip"
title="{% trans "For safety reasons, the waiting list does not run if the quota is set to unlimited." %}">
<span class="fa fa-ban" aria-hidden="true"></span>
{% blocktrans trimmed %}
Quota unlimited
{% endblocktrans %}
</span>
{% else %}
<span class="label label-warning">
{% blocktrans with num=e.availability.1 %}
Waiting, product {{ num }}x available
{% endblocktrans %}
</span>
{% endif %}
{% else %}
<span class="label label-danger">{% trans "Waiting, product unavailable" %}</span>
{% endif %}
@@ -226,7 +236,7 @@
<a href="{% url "control:event.voucher" organizer=request.event.organizer.slug event=request.event.slug voucher=e.voucher.pk %}">
{{ e.voucher }}
</a>
{% elif not e.voucher and e.availability.0 == 100 %}
{% elif not e.voucher and e.availability.0 == 100 and e.availability.1|default_if_none:"none" != "none" %}
<button name="assign" value="{{ e.pk }}" class="btn btn-default btn-xs">
{% trans "Send a voucher" %}
</button>
+2 -1
View File
@@ -530,7 +530,8 @@ class CheckInListSimulator(EventPermissionRequiredMixin, FormView):
and (self.result["status"] in ("ok", "incomplete") or self.result["reason"] == "rules"):
op = OrderPosition.objects.get(pk=self.result["position"]["id"])
rule_data = LazyRuleVars(op, self.list, form.cleaned_data["datetime"])
rule_graph = _logic_annotate_for_graphic_explain(self.list.rules, op.subevent or self.list.event, rule_data)
rule_graph = _logic_annotate_for_graphic_explain(self.list.rules, op.subevent or self.list.event, rule_data,
form.cleaned_data["datetime"])
self.result["rule_graph"] = rule_graph
if self.result.get("questions"):
+9 -1
View File
@@ -40,7 +40,7 @@ from collections import OrderedDict
from decimal import Decimal
from io import BytesIO
from itertools import groupby
from urllib.parse import urlsplit
from urllib.parse import urlparse, urlsplit
from zoneinfo import ZoneInfo
import bleach
@@ -50,6 +50,7 @@ from django.apps import apps
from django.conf import settings
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.core.files import File
from django.db import transaction
from django.db.models import ProtectedError
@@ -61,6 +62,7 @@ from django.http import (
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.timezone import now
from django.utils.translation import gettext, gettext_lazy as _, gettext_noop
from django.views.generic import FormView, ListView
@@ -1530,6 +1532,12 @@ class EventQRCode(EventPermissionRequiredMixin, View):
def get(self, request, *args, filetype, **kwargs):
url = build_absolute_uri(request.event, 'presale:event.index')
if "url" in request.GET:
if url_has_allowed_host_and_scheme(request.GET["url"], allowed_hosts=[urlparse(url).netloc]):
url = request.GET["url"]
else:
raise PermissionDenied("Untrusted URL")
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_M,
+38 -4
View File
@@ -88,6 +88,10 @@ from ...helpers.compat import CompatDeleteView
from . import ChartContainingView, CreateView, PaginationMixin, UpdateView
def has_truthy_attr(cls, attr):
return hasattr(cls, attr) and getattr(cls, attr)
class ItemList(ListView):
model = Item
context_object_name = 'items'
@@ -1184,30 +1188,46 @@ class MetaDataEditorMixin:
@cached_property
def meta_forms(self):
if hasattr(self, 'object') and self.object:
if getattr(self, 'object', None):
val_instances = {
v.property_id: v for v in self.object.meta_values.all()
}
else:
val_instances = {}
if getattr(self, 'copy_from', None):
defaults = {
v.property_id: v.value for v in self.copy_from.meta_values.all()
}
else:
defaults = {}
formlist = []
for p in self.request.event.item_meta_properties.all():
formlist.append(self._make_meta_form(p, val_instances))
formlist.append(self._make_meta_form(p, val_instances, defaults))
return formlist
def _make_meta_form(self, p, val_instances):
def _make_meta_form(self, p, val_instances, defaults):
return self.meta_form(
prefix='prop-{}'.format(p.pk),
property=p,
instance=val_instances.get(p.pk, self.meta_model(property=p, item=self.object)),
instance=val_instances.get(
p.pk,
self.meta_model(
property=p,
item=self.object if getattr(self, 'object', None) else None,
value=defaults.get(p.pk, None)
)
),
data=(self.request.POST if self.request.method == "POST" else None)
)
def save_meta(self):
for f in self.meta_forms:
if f.cleaned_data.get('value'):
if not f.instance.item_id:
f.instance.item = self.object
f.save()
elif f.instance and f.instance.pk:
f.instance.delete()
@@ -1253,6 +1273,7 @@ class ItemCreate(EventPermissionRequiredMixin, MetaDataEditorMixin, CreateView):
messages.success(self.request, _('Your changes have been saved.'))
ret = super().form_valid(form)
self.save_meta()
form.instance.log_action('pretix.event.item.added', user=self.request.user, data={
k: (form.cleaned_data.get(k).name
if isinstance(form.cleaned_data.get(k), File)
@@ -1279,6 +1300,14 @@ class ItemCreate(EventPermissionRequiredMixin, MetaDataEditorMixin, CreateView):
ctx['meta_forms'] = self.meta_forms
return ctx
def post(self, request, *args, **kwargs):
self.object = None
form = self.get_form()
if form.is_valid() and all([f.is_valid() for f in self.meta_forms]):
return self.form_valid(form)
else:
return self.form_invalid(form)
class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataEditorMixin, UpdateView):
form_class = ItemUpdateForm
@@ -1293,6 +1322,11 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
forms.extend(resp)
else:
forms.append(resp)
for form in forms:
if has_truthy_attr(form, "title") and has_truthy_attr(form, "is_layout"):
raise ValueError("`title` and `is_layout` must not both be truthy values")
return forms
def get_success_url(self) -> str:
+5 -4
View File
@@ -192,8 +192,8 @@ class MailSettingsSetupView(TemplateView):
spf_record = get_spf_record(hostname)
if not spf_record:
spf_warning = _(
'We could not find an SPF record set for the domain you are trying to use. You can still '
'proceed, but it will increase the chance of emails going to spam or being rejected. We '
'We could not find an SPF record set for the domain you are trying to use. This means that '
'there is a very high change most of the emails will be rejected or marked as spam. We '
'strongly recommend setting an SPF record on the domain. You can do so through the DNS '
'settings at the provider you registered your domain with.'
)
@@ -205,7 +205,8 @@ class MailSettingsSetupView(TemplateView):
'this system in the SPF record.'
)
if settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED:
verification = settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED and not spf_warning
if verification:
if 'verification' in self.request.POST:
messages.error(request, _('The verification code was incorrect, please try again.'))
else:
@@ -230,7 +231,7 @@ class MailSettingsSetupView(TemplateView):
context={
'basetpl': self.basetpl,
'object': self.object,
'verification': settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED,
'verification': verification,
'spf_warning': spf_warning,
'spf_record': spf_record,
'spf_key': settings.MAIL_CUSTOM_SENDER_SPF_STRING,
+3 -3
View File
@@ -418,7 +418,7 @@ class OrderTransactions(OrderView):
'item', 'variation', 'subevent'
).order_by('datetime')
ctx['sums'] = self.order.transactions.aggregate(
count=Sum('count'),
sum_count=Sum('count'),
full_price=Sum(F('count') * F('price')),
full_tax_value=Sum(F('count') * F('tax_value')),
)
@@ -1918,7 +1918,7 @@ class OrderContactChange(OrderView):
'pretix.event.order.contact.changed',
data={
'old_email': old_email,
'new_email': self.form.cleaned_data['email'],
'new_email': self.form.cleaned_data.get('email'),
},
user=self.request.user,
)
@@ -1930,7 +1930,7 @@ class OrderContactChange(OrderView):
'pretix.event.order.phone.changed',
data={
'old_phone': old_phone,
'new_phone': self.form.cleaned_data['phone'],
'new_phone': self.form.cleaned_data.get('phone'),
},
user=self.request.user,
)
+1
View File
@@ -396,6 +396,7 @@ class OrganizerDelete(AdministratorPermissionRequiredMixin, FormView):
)
self.request.organizer.delete_sub_objects()
self.request.organizer.delete()
self.request.organizer = None
messages.success(self.request, _('The organizer has been deleted.'))
return redirect(self.get_success_url())
except ProtectedError as e:
+15 -3
View File
@@ -37,10 +37,11 @@ import logging
from collections import OrderedDict
from zipfile import ZipFile
from django.shortcuts import get_object_or_404
from django.contrib import messages
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from django.utils.translation import get_language, gettext_lazy as _
from django.views import View
from django.views.generic import TemplateView
@@ -62,6 +63,16 @@ class ShredderMixin:
sorted(self.request.event.get_data_shredders().items(), key=lambda s: s[1].verbose_name)
)
def dispatch(self, request, *args, **kwargs):
try:
return super().dispatch(request, *args, **kwargs)
except ShredError as e:
messages.error(request, str(e))
return redirect(reverse('control:event.shredder.start', kwargs={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug
}))
class StartShredView(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixin, ShredderMixin, TemplateView):
permission = 'can_change_orders'
@@ -167,4 +178,5 @@ class ShredDoView(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixi
if request.event.slug != request.POST.get("slug"):
return self.error(ShredError(_("The slug you entered was not correct.")))
return self.do(self.request.event.id, request.POST.get("file"), request.POST.get("confirm_code"))
return self.do(self.request.event.id, request.POST.get("file"), request.POST.get("confirm_code"),
self.request.user.pk, get_language())
+3 -3
View File
@@ -546,7 +546,7 @@ def variations_select2(request, **kwargs):
F('item__category__position').asc(nulls_first=True),
'item__category_id',
'item__position',
'item__pk'
'item__pk',
'position',
'value'
).select_related('item')
@@ -718,7 +718,7 @@ def itemvarquota_select2(request, **kwargs):
itemqs = request.event.items.prefetch_related('variations').filter(
Q(name__icontains=i18ncomp(query)) | Q(internal_name__icontains=query)
)
quotaqs = request.event.quotas.filter(quotaf).select_related('subevent')
quotaqs = request.event.quotas.filter(quotaf).select_related('subevent').order_by('-subevent__date_from', 'name')
more = False
else:
if page == 1:
@@ -727,7 +727,7 @@ def itemvarquota_select2(request, **kwargs):
)
else:
itemqs = request.event.items.none()
quotaqs = request.event.quotas.filter(name__icontains=query).select_related('subevent')
quotaqs = request.event.quotas.filter(name__icontains=query).select_related('subevent').order_by('-subevent__date_from', 'name')
total = quotaqs.count()
pagesize = 20
offset = (page - 1) * pagesize
+11
View File
@@ -34,6 +34,7 @@
# License for the specific language governing permissions and limitations under the License.
import io
from urllib.parse import urlencode
import bleach
from defusedcsv import csv
@@ -75,6 +76,7 @@ from pretix.control.views import PaginationMixin
from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.format import format_map
from pretix.helpers.models import modelcopy
from pretix.multidomain.urlreverse import build_absolute_uri
class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
@@ -315,6 +317,13 @@ class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
expires__gte=now()
).count()
ctx['redeemed_in_carts'] = redeemed_in_carts
url_params = {
'voucher': self.object.code
}
if self.object.subevent_id:
url_params['subevent'] = self.object.subevent_id
ctx['url'] = build_absolute_uri(self.request.event, "presale:event.redeem") + "?" + urlencode(url_params)
return ctx
@@ -569,6 +578,8 @@ class VoucherRNG(EventPermissionRequiredMixin, View):
def get(self, request, *args, **kwargs):
try:
num = int(request.GET.get('num', '5'))
if num > 100_000:
return HttpResponseBadRequest()
except ValueError: # NOQA
return HttpResponseBadRequest()
+4 -2
View File
@@ -91,7 +91,7 @@ class WaitingListQuerySetMixin:
return self.request.POST
return self.request.GET
def get_queryset(self):
def get_queryset(self, force_filtered=False):
qs = WaitingListEntry.objects.filter(
event=self.request.event
).select_related('item', 'variation', 'voucher').prefetch_related(
@@ -135,6 +135,8 @@ class WaitingListQuerySetMixin:
qs = qs.filter(
id__in=self.request_data.getlist('entry')
)
elif force_filtered and '__ALL' not in self.request_data:
qs = qs.none()
return qs
@@ -158,7 +160,7 @@ class WaitingListActionView(EventPermissionRequiredMixin, WaitingListQuerySetMix
'forbidden': self.get_queryset().filter(voucher__isnull=False),
})
elif request.POST.get('action') == 'delete_confirm':
for obj in self.get_queryset():
for obj in self.get_queryset(force_filtered=True):
if not obj.voucher_id:
obj.log_action('pretix.event.orders.waitinglist.deleted', user=self.request.user)
obj.delete()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+42 -37
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-06-16 14:35+0000\n"
"POT-Creation-Date: 2023-07-21 11:46+0000\n"
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -39,6 +39,7 @@ msgid "Venmo"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:36
#: pretix/static/pretixpresale/js/walletdetection.js:38
msgid "Apple Pay"
msgstr ""
@@ -138,8 +139,8 @@ msgid "Continue"
msgstr "المتابعة"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:222
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:157
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:188
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:204
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:235
msgid "Confirming your payment …"
msgstr "جاري تأكيد الدفع الخاص بك …"
@@ -161,15 +162,15 @@ msgstr "الطلبات المدفوعة"
msgid "Total revenue"
msgstr "إجمالي الإيرادات"
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:12
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:13
msgid "Contacting Stripe …"
msgstr "جاري الاتصال بStripe …"
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:65
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:70
msgid "Total"
msgstr "المجموع"
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:164
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:211
msgid "Contacting your bank …"
msgstr "جاري الاتصال بالبنك الذي تتعامل معه …"
@@ -330,8 +331,8 @@ msgstr "حاليا بالداخل"
msgid "close"
msgstr "إغلاق"
#: pretix/static/pretixbase/js/asynctask.js:43
#: pretix/static/pretixbase/js/asynctask.js:120
#: pretix/static/pretixbase/js/asynctask.js:58
#: pretix/static/pretixbase/js/asynctask.js:135
msgid ""
"Your request is currently being processed. Depending on the size of your "
"event, this might take up to a few minutes."
@@ -339,13 +340,13 @@ msgstr ""
"تتم معالجة طلبك حاليا. قد يستغرق الأمر بضع دقائق بناء على حجم الفعالية التي "
"اخترت."
#: pretix/static/pretixbase/js/asynctask.js:48
#: pretix/static/pretixbase/js/asynctask.js:125
#: pretix/static/pretixbase/js/asynctask.js:63
#: pretix/static/pretixbase/js/asynctask.js:140
msgid "Your request has been queued on the server and will soon be processed."
msgstr "طلبك قيد الانتظار وستتم معالجته قريبا."
#: pretix/static/pretixbase/js/asynctask.js:54
#: pretix/static/pretixbase/js/asynctask.js:131
#: pretix/static/pretixbase/js/asynctask.js:69
#: pretix/static/pretixbase/js/asynctask.js:146
msgid ""
"Your request arrived on the server but we still wait for it to be processed. "
"If this takes longer than two minutes, please contact us or go back in your "
@@ -354,36 +355,36 @@ msgstr ""
"وصل طلبك للخادم وننتظر تنفيذه. إذا استغرق الأمر أكثر من دقيقتين تواصل معنا "
"أو عاود المحاولة مجددا."
#: pretix/static/pretixbase/js/asynctask.js:90
#: pretix/static/pretixbase/js/asynctask.js:178
#: pretix/static/pretixbase/js/asynctask.js:183
#: pretix/static/pretixbase/js/asynctask.js:105
#: pretix/static/pretixbase/js/asynctask.js:193
#: pretix/static/pretixbase/js/asynctask.js:198
#: pretix/static/pretixcontrol/js/ui/mail.js:24
msgid "An error of type {code} occurred."
msgstr "حدث خطأ من نوع {code}."
#: pretix/static/pretixbase/js/asynctask.js:93
#: pretix/static/pretixbase/js/asynctask.js:108
msgid ""
"We currently cannot reach the server, but we keep trying. Last error code: "
"{code}"
msgstr "لم نتمكن من الاتصال بالخادم، لكن سنواصل المحاولة، رمز آخر خطأ: {code}"
#: pretix/static/pretixbase/js/asynctask.js:145
#: pretix/static/pretixbase/js/asynctask.js:160
#: pretix/static/pretixcontrol/js/ui/mail.js:21
msgid "The request took too long. Please try again."
msgstr "استغرقت الطلب فترة طويلة، الرجاء المحاولة مرة أخرى."
#: pretix/static/pretixbase/js/asynctask.js:186
#: pretix/static/pretixbase/js/asynctask.js:201
#: pretix/static/pretixcontrol/js/ui/mail.js:26
msgid ""
"We currently cannot reach the server. Please try again. Error code: {code}"
msgstr ""
"لا يمكننا الوصول إلى الخادم حاليا، حاول مرة أخرى من فضلك. رمز الخطأ : {code}"
#: pretix/static/pretixbase/js/asynctask.js:215
#: pretix/static/pretixbase/js/asynctask.js:230
msgid "We are processing your request …"
msgstr "جاري معالجة طلبك …"
#: pretix/static/pretixbase/js/asynctask.js:223
#: pretix/static/pretixbase/js/asynctask.js:238
msgid ""
"We are currently sending your request to the server. If this takes longer "
"than one minute, please check your internet connection and then reload this "
@@ -392,7 +393,7 @@ msgstr ""
"نعمل الآن على ارسال طلبك إلى الخادم، إذا أستغرقت العملية أكثر من دقيقة، يرجى "
"التحقق من اتصالك بالإنترنت ثم أعد تحميل الصفحة مرة أخرى."
#: pretix/static/pretixbase/js/asynctask.js:285
#: pretix/static/pretixbase/js/asynctask.js:301
#: pretix/static/pretixcontrol/js/ui/main.js:71
msgid "Close message"
msgstr "أغلق الرسالة"
@@ -559,46 +560,46 @@ msgstr "توليد الرسائل …"
msgid "Unknown error."
msgstr "خطأ غير معروف."
#: pretix/static/pretixcontrol/js/ui/main.js:309
#: pretix/static/pretixcontrol/js/ui/main.js:311
msgid "Your color has great contrast and is very easy to read!"
msgstr "اللون يتمتع بتباين كبير وتسهل قراءته!"
#: pretix/static/pretixcontrol/js/ui/main.js:313
#: pretix/static/pretixcontrol/js/ui/main.js:315
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr "اللون يحظى بتباين معقول ويمكن أن يكون مناسب للقراءة!"
#: pretix/static/pretixcontrol/js/ui/main.js:317
#: pretix/static/pretixcontrol/js/ui/main.js:319
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr "تباين اللون سيئ للخلفية البيضاء، الرجاء اختيار لون غامق."
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:486
#: pretix/static/pretixcontrol/js/ui/main.js:468
#: pretix/static/pretixcontrol/js/ui/main.js:488
msgid "Search query"
msgstr "البحث في الاستفسارات"
#: pretix/static/pretixcontrol/js/ui/main.js:484
#: pretix/static/pretixcontrol/js/ui/main.js:486
msgid "All"
msgstr "الكل"
#: pretix/static/pretixcontrol/js/ui/main.js:485
#: pretix/static/pretixcontrol/js/ui/main.js:487
msgid "None"
msgstr "لا شيء"
#: pretix/static/pretixcontrol/js/ui/main.js:489
#: pretix/static/pretixcontrol/js/ui/main.js:491
msgid "Selected only"
msgstr "المختارة فقط"
#: pretix/static/pretixcontrol/js/ui/main.js:892
#: pretix/static/pretixcontrol/js/ui/main.js:894
msgid "Use a different name internally"
msgstr "قم باستخدم اسم مختلف داخليا"
#: pretix/static/pretixcontrol/js/ui/main.js:932
#: pretix/static/pretixcontrol/js/ui/main.js:934
msgid "Click to close"
msgstr "اضغط لاغلاق الصفحة"
#: pretix/static/pretixcontrol/js/ui/main.js:1007
#: pretix/static/pretixcontrol/js/ui/main.js:1009
msgid "You have unsaved changes!"
msgstr "لم تقم بحفظ التعديلات!"
@@ -672,23 +673,27 @@ msgstr "ستسترد %(currency)%(amount)"
msgid "Please enter the amount the organizer can keep."
msgstr "الرجاء إدخال المبلغ الذي يمكن للمنظم الاحتفاظ به."
#: pretix/static/pretixpresale/js/ui/main.js:436
#: pretix/static/pretixpresale/js/ui/main.js:412
msgid "Please enter a quantity for one of the ticket types."
msgstr "الرجاء إدخال عدد لأحد أنواع التذاكر."
#: pretix/static/pretixpresale/js/ui/main.js:472
#: pretix/static/pretixpresale/js/ui/main.js:448
msgid "required"
msgstr "مطلوب"
#: pretix/static/pretixpresale/js/ui/main.js:575
#: pretix/static/pretixpresale/js/ui/main.js:594
#: pretix/static/pretixpresale/js/ui/main.js:551
#: pretix/static/pretixpresale/js/ui/main.js:570
msgid "Time zone:"
msgstr "المنطقة الزمنية:"
#: pretix/static/pretixpresale/js/ui/main.js:585
#: pretix/static/pretixpresale/js/ui/main.js:561
msgid "Your local time:"
msgstr "التوقيت المحلي:"
#: pretix/static/pretixpresale/js/walletdetection.js:39
msgid "Google Pay"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:17
msgctxt "widget"
msgid "Quantity"
File diff suppressed because it is too large Load Diff
+42 -37
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-06-16 14:35+0000\n"
"POT-Creation-Date: 2023-07-21 11:46+0000\n"
"PO-Revision-Date: 2020-12-19 07:00+0000\n"
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -38,6 +38,7 @@ msgid "Venmo"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:36
#: pretix/static/pretixpresale/js/walletdetection.js:38
msgid "Apple Pay"
msgstr ""
@@ -135,8 +136,8 @@ msgid "Continue"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:222
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:157
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:188
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:204
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:235
msgid "Confirming your payment …"
msgstr ""
@@ -158,15 +159,15 @@ msgstr ""
msgid "Total revenue"
msgstr ""
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:12
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:13
msgid "Contacting Stripe …"
msgstr ""
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:65
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:70
msgid "Total"
msgstr ""
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:164
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:211
msgid "Contacting your bank …"
msgstr ""
@@ -316,62 +317,62 @@ msgstr ""
msgid "close"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:43
#: pretix/static/pretixbase/js/asynctask.js:120
#: pretix/static/pretixbase/js/asynctask.js:58
#: pretix/static/pretixbase/js/asynctask.js:135
msgid ""
"Your request is currently being processed. Depending on the size of your "
"event, this might take up to a few minutes."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:48
#: pretix/static/pretixbase/js/asynctask.js:125
#: pretix/static/pretixbase/js/asynctask.js:63
#: pretix/static/pretixbase/js/asynctask.js:140
msgid "Your request has been queued on the server and will soon be processed."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:54
#: pretix/static/pretixbase/js/asynctask.js:131
#: pretix/static/pretixbase/js/asynctask.js:69
#: pretix/static/pretixbase/js/asynctask.js:146
msgid ""
"Your request arrived on the server but we still wait for it to be processed. "
"If this takes longer than two minutes, please contact us or go back in your "
"browser and try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:90
#: pretix/static/pretixbase/js/asynctask.js:178
#: pretix/static/pretixbase/js/asynctask.js:183
#: pretix/static/pretixbase/js/asynctask.js:105
#: pretix/static/pretixbase/js/asynctask.js:193
#: pretix/static/pretixbase/js/asynctask.js:198
#: pretix/static/pretixcontrol/js/ui/mail.js:24
msgid "An error of type {code} occurred."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:93
#: pretix/static/pretixbase/js/asynctask.js:108
msgid ""
"We currently cannot reach the server, but we keep trying. Last error code: "
"{code}"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:145
#: pretix/static/pretixbase/js/asynctask.js:160
#: pretix/static/pretixcontrol/js/ui/mail.js:21
msgid "The request took too long. Please try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:186
#: pretix/static/pretixbase/js/asynctask.js:201
#: pretix/static/pretixcontrol/js/ui/mail.js:26
msgid ""
"We currently cannot reach the server. Please try again. Error code: {code}"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:215
#: pretix/static/pretixbase/js/asynctask.js:230
msgid "We are processing your request …"
msgstr "Estem processant la vostra sol·licitud …"
#: pretix/static/pretixbase/js/asynctask.js:223
#: pretix/static/pretixbase/js/asynctask.js:238
msgid ""
"We are currently sending your request to the server. If this takes longer "
"than one minute, please check your internet connection and then reload this "
"page and try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:285
#: pretix/static/pretixbase/js/asynctask.js:301
#: pretix/static/pretixcontrol/js/ui/main.js:71
msgid "Close message"
msgstr ""
@@ -538,46 +539,46 @@ msgstr ""
msgid "Unknown error."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:309
#: pretix/static/pretixcontrol/js/ui/main.js:311
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:313
#: pretix/static/pretixcontrol/js/ui/main.js:315
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:317
#: pretix/static/pretixcontrol/js/ui/main.js:319
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:486
#: pretix/static/pretixcontrol/js/ui/main.js:468
#: pretix/static/pretixcontrol/js/ui/main.js:488
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:484
#: pretix/static/pretixcontrol/js/ui/main.js:486
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:485
#: pretix/static/pretixcontrol/js/ui/main.js:487
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:489
#: pretix/static/pretixcontrol/js/ui/main.js:491
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:892
#: pretix/static/pretixcontrol/js/ui/main.js:894
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:932
#: pretix/static/pretixcontrol/js/ui/main.js:934
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1007
#: pretix/static/pretixcontrol/js/ui/main.js:1009
msgid "You have unsaved changes!"
msgstr ""
@@ -641,25 +642,29 @@ msgstr ""
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:436
#: pretix/static/pretixpresale/js/ui/main.js:412
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:472
#: pretix/static/pretixpresale/js/ui/main.js:448
#, fuzzy
#| msgid "Cart expired"
msgid "required"
msgstr "Cistella expirada"
#: pretix/static/pretixpresale/js/ui/main.js:575
#: pretix/static/pretixpresale/js/ui/main.js:594
#: pretix/static/pretixpresale/js/ui/main.js:551
#: pretix/static/pretixpresale/js/ui/main.js:570
msgid "Time zone:"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:585
#: pretix/static/pretixpresale/js/ui/main.js:561
msgid "Your local time:"
msgstr ""
#: pretix/static/pretixpresale/js/walletdetection.js:39
msgid "Google Pay"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:17
msgctxt "widget"
msgid "Quantity"
File diff suppressed because it is too large Load Diff
+44 -37
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-06-16 14:35+0000\n"
"POT-Creation-Date: 2023-07-21 11:46+0000\n"
"PO-Revision-Date: 2023-03-19 06:00+0000\n"
"Last-Translator: Michael <michael.happl@gmx.at>\n"
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -38,6 +38,7 @@ msgid "Venmo"
msgstr "Venmo"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:36
#: pretix/static/pretixpresale/js/walletdetection.js:38
msgid "Apple Pay"
msgstr "Apple Pay"
@@ -135,8 +136,8 @@ msgid "Continue"
msgstr "Pokračovat"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:222
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:157
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:188
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:204
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:235
msgid "Confirming your payment …"
msgstr "Potvrzuji vaši platbu …"
@@ -158,15 +159,15 @@ msgstr "Zaplacené objednávky"
msgid "Total revenue"
msgstr "Celkové příjmy"
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:12
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:13
msgid "Contacting Stripe …"
msgstr "Kontaktuji Stripe …"
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:65
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:70
msgid "Total"
msgstr "Celkem"
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:164
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:211
msgid "Contacting your bank …"
msgstr "Kontaktuji vaši banku …"
@@ -316,8 +317,8 @@ msgstr "Aktuálně uvnitř"
msgid "close"
msgstr "zavřít"
#: pretix/static/pretixbase/js/asynctask.js:43
#: pretix/static/pretixbase/js/asynctask.js:120
#: pretix/static/pretixbase/js/asynctask.js:58
#: pretix/static/pretixbase/js/asynctask.js:135
msgid ""
"Your request is currently being processed. Depending on the size of your "
"event, this might take up to a few minutes."
@@ -325,13 +326,13 @@ msgstr ""
"Váš požadavek je právě zpracováván. V závislosti na velikosti vaší udalosti, "
"to může trvat několik minut."
#: pretix/static/pretixbase/js/asynctask.js:48
#: pretix/static/pretixbase/js/asynctask.js:125
#: pretix/static/pretixbase/js/asynctask.js:63
#: pretix/static/pretixbase/js/asynctask.js:140
msgid "Your request has been queued on the server and will soon be processed."
msgstr "Váš požadavek byl vložem do fronty serveru a brzy bude zpracován."
#: pretix/static/pretixbase/js/asynctask.js:54
#: pretix/static/pretixbase/js/asynctask.js:131
#: pretix/static/pretixbase/js/asynctask.js:69
#: pretix/static/pretixbase/js/asynctask.js:146
msgid ""
"Your request arrived on the server but we still wait for it to be processed. "
"If this takes longer than two minutes, please contact us or go back in your "
@@ -341,14 +342,14 @@ msgstr ""
"Pokud to trvá více jak dvě minuty, prosím kontaktuje nás nebo se vraťte do "
"vašeho prohlížeče a zkuste to znovu."
#: pretix/static/pretixbase/js/asynctask.js:90
#: pretix/static/pretixbase/js/asynctask.js:178
#: pretix/static/pretixbase/js/asynctask.js:183
#: pretix/static/pretixbase/js/asynctask.js:105
#: pretix/static/pretixbase/js/asynctask.js:193
#: pretix/static/pretixbase/js/asynctask.js:198
#: pretix/static/pretixcontrol/js/ui/mail.js:24
msgid "An error of type {code} occurred."
msgstr "Vyskytla se chyba {code}."
#: pretix/static/pretixbase/js/asynctask.js:93
#: pretix/static/pretixbase/js/asynctask.js:108
msgid ""
"We currently cannot reach the server, but we keep trying. Last error code: "
"{code}"
@@ -356,12 +357,12 @@ msgstr ""
"Momentálně nemůžeme kontaktovat server, ale stále se o to pokoušíme. "
"Poslední chybový kód: {code}"
#: pretix/static/pretixbase/js/asynctask.js:145
#: pretix/static/pretixbase/js/asynctask.js:160
#: pretix/static/pretixcontrol/js/ui/mail.js:21
msgid "The request took too long. Please try again."
msgstr "Zpracování požadavku trvá příliš dlouho. Prosím zkuste to znovu."
#: pretix/static/pretixbase/js/asynctask.js:186
#: pretix/static/pretixbase/js/asynctask.js:201
#: pretix/static/pretixcontrol/js/ui/mail.js:26
msgid ""
"We currently cannot reach the server. Please try again. Error code: {code}"
@@ -369,11 +370,11 @@ msgstr ""
"Momentálně nemůžeme kontaktovat server. Prosím zkuste to znovu. Chybový kód: "
"{code}"
#: pretix/static/pretixbase/js/asynctask.js:215
#: pretix/static/pretixbase/js/asynctask.js:230
msgid "We are processing your request …"
msgstr "Zpracováváme váš požadavek …"
#: pretix/static/pretixbase/js/asynctask.js:223
#: pretix/static/pretixbase/js/asynctask.js:238
msgid ""
"We are currently sending your request to the server. If this takes longer "
"than one minute, please check your internet connection and then reload this "
@@ -383,7 +384,7 @@ msgstr ""
"prosím zkontrolujte své internetové připojení a znovu načtěte stránku a "
"zkuste to znovu."
#: pretix/static/pretixbase/js/asynctask.js:285
#: pretix/static/pretixbase/js/asynctask.js:301
#: pretix/static/pretixcontrol/js/ui/main.js:71
msgid "Close message"
msgstr "Zavřít zprávu"
@@ -550,16 +551,16 @@ msgstr "Vytváření zpráv…"
msgid "Unknown error."
msgstr "Neznámá chyba."
#: pretix/static/pretixcontrol/js/ui/main.js:309
#: pretix/static/pretixcontrol/js/ui/main.js:311
msgid "Your color has great contrast and is very easy to read!"
msgstr "Tato barva má velmi dobrý kontrast a je velmi dobře čitelná!"
#: pretix/static/pretixcontrol/js/ui/main.js:313
#: pretix/static/pretixcontrol/js/ui/main.js:315
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
"Tato barva má slušný kontrast a pravděpodobně je dostatečně dobře čitelná!"
#: pretix/static/pretixcontrol/js/ui/main.js:317
#: pretix/static/pretixcontrol/js/ui/main.js:319
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
@@ -567,32 +568,32 @@ msgstr ""
"Tato barva je pro text na bílém pozadí špatně kontrastní, zvolte prosím "
"tmavší odstín."
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:486
#: pretix/static/pretixcontrol/js/ui/main.js:468
#: pretix/static/pretixcontrol/js/ui/main.js:488
msgid "Search query"
msgstr "Hledaný výraz"
#: pretix/static/pretixcontrol/js/ui/main.js:484
#: pretix/static/pretixcontrol/js/ui/main.js:486
msgid "All"
msgstr "Všechny"
#: pretix/static/pretixcontrol/js/ui/main.js:485
#: pretix/static/pretixcontrol/js/ui/main.js:487
msgid "None"
msgstr "Žádný"
#: pretix/static/pretixcontrol/js/ui/main.js:489
#: pretix/static/pretixcontrol/js/ui/main.js:491
msgid "Selected only"
msgstr "Pouze vybrané"
#: pretix/static/pretixcontrol/js/ui/main.js:892
#: pretix/static/pretixcontrol/js/ui/main.js:894
msgid "Use a different name internally"
msgstr "Interně používat jiný název"
#: pretix/static/pretixcontrol/js/ui/main.js:932
#: pretix/static/pretixcontrol/js/ui/main.js:934
msgid "Click to close"
msgstr "Kliknutím zavřete"
#: pretix/static/pretixcontrol/js/ui/main.js:1007
#: pretix/static/pretixcontrol/js/ui/main.js:1009
msgid "You have unsaved changes!"
msgstr "Máte neuložené změny!"
@@ -659,23 +660,29 @@ msgstr "Dostanete %(currency)s %(amount)s zpět"
msgid "Please enter the amount the organizer can keep."
msgstr "Zadejte částku, kterou si organizátor může ponechat."
#: pretix/static/pretixpresale/js/ui/main.js:436
#: pretix/static/pretixpresale/js/ui/main.js:412
msgid "Please enter a quantity for one of the ticket types."
msgstr "Zadejte prosím množství pro jeden z typů vstupenek."
#: pretix/static/pretixpresale/js/ui/main.js:472
#: pretix/static/pretixpresale/js/ui/main.js:448
msgid "required"
msgstr "povinný"
#: pretix/static/pretixpresale/js/ui/main.js:575
#: pretix/static/pretixpresale/js/ui/main.js:594
#: pretix/static/pretixpresale/js/ui/main.js:551
#: pretix/static/pretixpresale/js/ui/main.js:570
msgid "Time zone:"
msgstr "Časové pásmo:"
#: pretix/static/pretixpresale/js/ui/main.js:585
#: pretix/static/pretixpresale/js/ui/main.js:561
msgid "Your local time:"
msgstr "Místní čas:"
#: pretix/static/pretixpresale/js/walletdetection.js:39
#, fuzzy
#| msgid "Apple Pay"
msgid "Google Pay"
msgstr "Apple Pay"
#: pretix/static/pretixpresale/js/widget/widget.js:17
msgctxt "widget"
msgid "Quantity"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+44 -37
View File
@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-06-16 14:35+0000\n"
"POT-Creation-Date: 2023-07-21 11:46+0000\n"
"PO-Revision-Date: 2022-12-01 17:00+0000\n"
"Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -37,6 +37,7 @@ msgid "Venmo"
msgstr "Venmo"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:36
#: pretix/static/pretixpresale/js/walletdetection.js:38
msgid "Apple Pay"
msgstr "Apple Pay"
@@ -139,8 +140,8 @@ msgid "Continue"
msgstr "Fortsæt"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:222
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:157
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:188
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:204
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:235
msgid "Confirming your payment …"
msgstr "Bekræfter din betaling …"
@@ -162,15 +163,15 @@ msgstr "Betalte bestillinger"
msgid "Total revenue"
msgstr "Omsætning i alt"
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:12
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:13
msgid "Contacting Stripe …"
msgstr "Kontakter Stripe …"
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:65
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:70
msgid "Total"
msgstr "Total"
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:164
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:211
msgid "Contacting your bank …"
msgstr "Kontakter din bank …"
@@ -328,8 +329,8 @@ msgstr "Inde i øjeblikket"
msgid "close"
msgstr "Luk"
#: pretix/static/pretixbase/js/asynctask.js:43
#: pretix/static/pretixbase/js/asynctask.js:120
#: pretix/static/pretixbase/js/asynctask.js:58
#: pretix/static/pretixbase/js/asynctask.js:135
#, fuzzy
#| msgid ""
#| "Your request has been queued on the server and will now be processed. "
@@ -341,8 +342,8 @@ msgstr ""
"Din forespørgsel er under behandling. Alt efter størrelsen af din event kan "
"der gå op til et par minutter."
#: pretix/static/pretixbase/js/asynctask.js:48
#: pretix/static/pretixbase/js/asynctask.js:125
#: pretix/static/pretixbase/js/asynctask.js:63
#: pretix/static/pretixbase/js/asynctask.js:140
#, fuzzy
#| msgid ""
#| "Your request has been queued on the server and will now be processed. "
@@ -352,8 +353,8 @@ msgstr ""
"Din forespørgsel er under behandling. Alt efter størrelsen af din event kan "
"der gå op til et par minutter."
#: pretix/static/pretixbase/js/asynctask.js:54
#: pretix/static/pretixbase/js/asynctask.js:131
#: pretix/static/pretixbase/js/asynctask.js:69
#: pretix/static/pretixbase/js/asynctask.js:146
msgid ""
"Your request arrived on the server but we still wait for it to be processed. "
"If this takes longer than two minutes, please contact us or go back in your "
@@ -362,14 +363,14 @@ msgstr ""
"Din forespørgsel er under behandling. Hvis der går mere end to minutter, så "
"kontakt os eller gå tilbage og prøv igen."
#: pretix/static/pretixbase/js/asynctask.js:90
#: pretix/static/pretixbase/js/asynctask.js:178
#: pretix/static/pretixbase/js/asynctask.js:183
#: pretix/static/pretixbase/js/asynctask.js:105
#: pretix/static/pretixbase/js/asynctask.js:193
#: pretix/static/pretixbase/js/asynctask.js:198
#: pretix/static/pretixcontrol/js/ui/mail.js:24
msgid "An error of type {code} occurred."
msgstr "Der er sket en fejl ({code})."
#: pretix/static/pretixbase/js/asynctask.js:93
#: pretix/static/pretixbase/js/asynctask.js:108
msgid ""
"We currently cannot reach the server, but we keep trying. Last error code: "
"{code}"
@@ -377,12 +378,12 @@ msgstr ""
"Vi kan ikke komme i kontakt med serveren, men prøver igen. Seneste fejlkode: "
"{code}"
#: pretix/static/pretixbase/js/asynctask.js:145
#: pretix/static/pretixbase/js/asynctask.js:160
#: pretix/static/pretixcontrol/js/ui/mail.js:21
msgid "The request took too long. Please try again."
msgstr "Forespørgslen tog for lang tid. Prøv igen."
#: pretix/static/pretixbase/js/asynctask.js:186
#: pretix/static/pretixbase/js/asynctask.js:201
#: pretix/static/pretixcontrol/js/ui/mail.js:26
msgid ""
"We currently cannot reach the server. Please try again. Error code: {code}"
@@ -390,11 +391,11 @@ msgstr ""
"Vi kan i øjeblikket ikke komme i kontakt med serveren. Prøv igen. Fejlkode: "
"{code}"
#: pretix/static/pretixbase/js/asynctask.js:215
#: pretix/static/pretixbase/js/asynctask.js:230
msgid "We are processing your request …"
msgstr "Vi behandler din bestilling …"
#: pretix/static/pretixbase/js/asynctask.js:223
#: pretix/static/pretixbase/js/asynctask.js:238
msgid ""
"We are currently sending your request to the server. If this takes longer "
"than one minute, please check your internet connection and then reload this "
@@ -403,7 +404,7 @@ msgstr ""
"Din forespørgsel bliver sendt til serveren. Hvis det tager mere end et "
"minut, så tjek din internetforbindelse, genindlæs siden og prøv igen."
#: pretix/static/pretixbase/js/asynctask.js:285
#: pretix/static/pretixbase/js/asynctask.js:301
#: pretix/static/pretixcontrol/js/ui/main.js:71
msgid "Close message"
msgstr "Luk besked"
@@ -573,46 +574,46 @@ msgstr "Opretter beskeder …"
msgid "Unknown error."
msgstr "Ukendt fejl."
#: pretix/static/pretixcontrol/js/ui/main.js:309
#: pretix/static/pretixcontrol/js/ui/main.js:311
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:313
#: pretix/static/pretixcontrol/js/ui/main.js:315
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:317
#: pretix/static/pretixcontrol/js/ui/main.js:319
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:486
#: pretix/static/pretixcontrol/js/ui/main.js:468
#: pretix/static/pretixcontrol/js/ui/main.js:488
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:484
#: pretix/static/pretixcontrol/js/ui/main.js:486
msgid "All"
msgstr "Alle"
#: pretix/static/pretixcontrol/js/ui/main.js:485
#: pretix/static/pretixcontrol/js/ui/main.js:487
msgid "None"
msgstr "Ingen"
#: pretix/static/pretixcontrol/js/ui/main.js:489
#: pretix/static/pretixcontrol/js/ui/main.js:491
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:892
#: pretix/static/pretixcontrol/js/ui/main.js:894
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:932
#: pretix/static/pretixcontrol/js/ui/main.js:934
msgid "Click to close"
msgstr "Klik for at lukke"
#: pretix/static/pretixcontrol/js/ui/main.js:1007
#: pretix/static/pretixcontrol/js/ui/main.js:1009
msgid "You have unsaved changes!"
msgstr "Du har ændringer, der ikke er gemt!"
@@ -686,25 +687,31 @@ msgstr "fra %(currency)s %(price)s"
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:436
#: pretix/static/pretixpresale/js/ui/main.js:412
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:472
#: pretix/static/pretixpresale/js/ui/main.js:448
#, fuzzy
#| msgid "Cart expired"
msgid "required"
msgstr "Kurv udløbet"
#: pretix/static/pretixpresale/js/ui/main.js:575
#: pretix/static/pretixpresale/js/ui/main.js:594
#: pretix/static/pretixpresale/js/ui/main.js:551
#: pretix/static/pretixpresale/js/ui/main.js:570
msgid "Time zone:"
msgstr "Tidszone:"
#: pretix/static/pretixpresale/js/ui/main.js:585
#: pretix/static/pretixpresale/js/ui/main.js:561
msgid "Your local time:"
msgstr "Din lokaltid:"
#: pretix/static/pretixpresale/js/walletdetection.js:39
#, fuzzy
#| msgid "Apple Pay"
msgid "Google Pay"
msgstr "Apple Pay"
#: pretix/static/pretixpresale/js/widget/widget.js:17
msgctxt "widget"
msgid "Quantity"
File diff suppressed because it is too large Load Diff

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