Compare commits

..

179 Commits

Author SHA1 Message Date
Richard Schreiber 6db9b1711b add client-side csv-file for answer options 2022-11-15 12:56:21 +01:00
Raphael Michel 3d82058269 Do not show internal name in cart tooltips 2022-11-15 09:55:53 +01:00
Richard Schreiber 4f21bf8001 Calendar: add label „continued“ to event’s title 2022-11-15 08:19:41 +01:00
Raphael Michel e32e7e2a50 Add clever handling of plus button in cart with voucher (#2893) 2022-11-14 16:55:39 +01:00
Raphael Michel 5b8228bea0 PPv2: Improve error handling (#2899) 2022-11-14 16:55:30 +01:00
Raphael Michel a628f605a6 Send refund webhooks correctly when refunds are created via API 2022-11-14 12:23:49 +01:00
Martin Gross e658744f67 PPv2: Do not PATCH custom_id and description for APMs (#2898) 2022-11-14 11:46:35 +01:00
Raphael Michel 776c5e9fa2 Set autocomplete="one-time-code" on TOTP field 2022-11-14 10:37:00 +01:00
Raphael Michel 46b5055aec Bump zeep to 4.2.* 2022-11-11 17:01:35 +01:00
Raphael Michel ef227deb2e Bump phonenumberslite to 8.13.* 2022-11-11 17:00:32 +01:00
Raphael Michel 30cfe1ef3c Bump pytest-xdist to 3.0.* 2022-11-11 16:59:46 +01:00
Raphael Michel 4d5c828e2a PDF editor: Update pdfjs from 1.7 to 3.0.279 2022-11-11 16:58:58 +01:00
Raphael Michel f509306b35 PDF editor: Fix browser detection 2022-11-11 16:58:58 +01:00
Richard Schreiber 706e479cff Update vue to 2.7.14 (#2897) 2022-11-11 14:26:16 +01:00
Martin Gross a5be7dcff5 PayPal2: Allow all https-pages to be framed, addressing CSP+popover issues (Z#23111577) 2022-11-10 19:10:03 +01:00
Raphael Michel 845b3a866b Fix switching from SMTP to custom email 2022-11-10 17:38:05 +01:00
Raphael Michel 91e1e079e1 Allow private SMTP servers by default in debug version 2022-11-10 17:38:03 +01:00
Raphael Michel 9075c75a93 Fix test for exception type 2022-11-10 14:59:22 +01:00
Raphael Michel 7b97204f2f Port b9feceba (Do not show a price if there are mandatory non-free addons) to voucher redemption 2022-11-10 14:48:55 +01:00
Raphael Michel dfedf09656 PDF renderer: Normalize unicode before printing text 2022-11-10 13:53:15 +01:00
Raphael Michel 655cfe0afd Bump django-redis to 5.2.* 2022-11-10 09:17:26 +01:00
Raphael Michel faf17f824e Bump django-hijack to 3.2.* 2022-11-10 09:17:26 +01:00
Raphael Michel fbf52a5219 Bump Pillow to 9.3.* 2022-11-10 09:17:26 +01:00
Richard Schreiber 9466c57c35 Translations: Update Greek
Currently translated at 58.2% (2847 of 4888 strings)

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

powered by weblate
2022-11-10 09:17:11 +01:00
exbu 806ef8477b Translations: Update Dutch (informal) (nl_Informal)
Currently translated at 82.6% (4040 of 4888 strings)

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

powered by weblate
2022-11-10 09:17:11 +01:00
Raphael Michel 7cb654706a Translations: Update German (informal) (de_Informal)
Currently translated at 99.6% (4873 of 4888 strings)

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

powered by weblate
2022-11-10 09:17:11 +01:00
Raphael Michel dea448e0f8 Translations: Update German
Currently translated at 99.6% (4889 of 4904 strings)

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

powered by weblate
2022-11-10 09:17:11 +01:00
Dennis Lichtenthäler 98b413249a Translations: Update German
Currently translated at 100.0% (4889 of 4889 strings)

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

powered by weblate
2022-11-10 09:17:11 +01:00
Raphael Michel 4630c1fe8b Allow to charge a cancellation fee on unpaid orders (#2845) 2022-11-10 09:11:43 +01:00
Raphael Michel bb718375e9 Stripe: Allow to set a custom statement descriptor suffix (#2883) 2022-11-10 09:11:35 +01:00
Martin Gross 7d2dd722bd PayPal: Fix loading of Smart Payment Buttons on APM payment page (regression introduced in #2875) 2022-11-09 15:04:35 +01:00
Raphael Michel 2adbd3cd4a Fix isort complain 2022-11-08 18:24:33 +01:00
Raphael Michel fb483ad00e Add comment to test 2022-11-08 18:07:50 +01:00
Raphael Michel 9cef65f359 API: Fix carts with addons/bundles not being created correctly 2022-11-08 18:03:16 +01:00
Raphael Michel ceeb69856b API: Support is_bundled during order creation 2022-11-08 16:55:35 +01:00
Raphael Michel c184187e59 Improve error handling for CSV parsing in voucher bulk creation 2022-11-08 10:27:31 +01:00
Raphael Michel 8ca38bdbaf Badges: Use ExportError instead of OrderError 2022-11-08 10:24:56 +01:00
Raphael Michel 3ae42b0c57 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2022-11-07 16:22:46 +01:00
Raphael Michel 6368954ecb Translations: Update Ukrainian
Currently translated at 74.1% (3615 of 4878 strings)

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

powered by weblate
2022-11-07 16:22:13 +01:00
Fazenda Dengo 26ebdb7113 Translations: Update Portuguese
Currently translated at 3.8% (187 of 4878 strings)

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

powered by weblate
2022-11-07 16:22:13 +01:00
Fazenda Dengo a1cb0b386b Translations: Update Portuguese (Portugal)
Currently translated at 63.1% (127 of 201 strings)

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

powered by weblate
2022-11-07 16:22:13 +01:00
Fazenda Dengo d46e1aba52 Translations: Update Portuguese (Portugal)
Currently translated at 76.6% (3737 of 4878 strings)

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

powered by weblate
2022-11-07 16:22:13 +01:00
Dennis Lichtenthäler 1f41184f9e Translations: Update German
Currently translated at 100.0% (4878 of 4878 strings)

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

powered by weblate
2022-11-07 16:22:13 +01:00
dependabot[bot] 2c746dffb2 Bump @babel/preset-env from 7.19.3 to 7.20.2 in /src/pretix/static/npm_dir (#2886)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-07 16:06:25 +01:00
dependabot[bot] 84bd4e0e94 Bump @rollup/plugin-node-resolve from 14.1.0 to 15.0.1 in /src/pretix/static/npm_dir (#2877)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-07 16:06:12 +01:00
Raphael Michel 93f8b38745 SMTP settings: Don't replace password with ***** 2022-11-07 16:05:33 +01:00
Raphael Michel 4110d6ec15 Do some basic cleaning on dynamic subjects 2022-11-07 15:58:18 +01:00
Raphael Michel 9bea383ff0 Make all email subjects configurable (#2884)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2022-11-07 15:50:09 +01:00
Raphael Michel 2287c8b34c Bump django-filter to 22.1 2022-11-07 15:35:31 +01:00
Raphael Michel f7a129854e Bump pytest-mock to 3.10 2022-11-07 15:35:31 +01:00
Raphael Michel a96fccef63 Bump pyjwt to 2.6.* 2022-11-07 15:35:31 +01:00
Raphael Michel dc5a85b39e PDF: Fix another crash if unknown font is used
see also PRETIXEU-7K4
2022-11-07 15:35:31 +01:00
dependabot[bot] 23f9fb4a9a Bump @rollup/plugin-babel in /src/pretix/static/npm_dir
Bumps [@rollup/plugin-babel](https://github.com/rollup/plugins/tree/HEAD/packages/babel) from 5.3.1 to 6.0.2.
- [Release notes](https://github.com/rollup/plugins/releases)
- [Changelog](https://github.com/rollup/plugins/blob/master/packages/babel/CHANGELOG.md)
- [Commits](https://github.com/rollup/plugins/commits/babel-v6.0.2/packages/babel)

---
updated-dependencies:
- dependency-name: "@rollup/plugin-babel"
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-07 15:33:54 +01:00
dependabot[bot] 6130c45b3e Bump vue and vue-template-compiler in /src/pretix/static/npm_dir
Bumps [vue](https://github.com/vuejs/core) and [vue-template-compiler](https://github.com/vuejs/vue). These dependencies needed to be updated together.

Updates `vue` from 2.7.10 to 2.7.13
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/commits)

Updates `vue-template-compiler` from 2.7.10 to 2.7.13
- [Release notes](https://github.com/vuejs/vue/releases)
- [Changelog](https://github.com/vuejs/vue/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/vue/compare/v2.7.10...v2.7.13)

---
updated-dependencies:
- dependency-name: vue
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: vue-template-compiler
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-07 15:33:44 +01:00
dependabot[bot] 83840c4024 Bump @babel/core from 7.19.3 to 7.19.6 in /src/pretix/static/npm_dir (#2880)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-07 15:33:33 +01:00
Maciej Szymczak 02d1d1e0c3 Translations: Update Polish
Currently translated at 14.1% (688 of 4878 strings)

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

powered by weblate
2022-11-07 09:08:18 +01:00
Maciej Szymczak f641f0fdd1 Translations: Update Polish
Currently translated at 13.8% (677 of 4878 strings)

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

powered by weblate
2022-11-07 09:08:18 +01:00
Raphael Michel 0c827c94a8 Fix mail text preview for languages down below in the list 2022-11-04 13:18:07 +01:00
Raphael Michel 4fb76f1b55 API: Fix overriding date_admission during event clone 2022-11-04 10:39:21 +01:00
Raphael Michel cb3b1f3ac5 API: Add discount to order position serializer 2022-11-03 15:29:20 +01:00
Richard Schreiber 0b95f89882 Fix paypal disabling continue button (Z#23110784) (#2875) 2022-11-03 13:27:30 +01:00
Raphael Michel bccd7cd1a4 API: Fix setting plugins during event creation 2022-11-01 18:39:01 +01:00
Richard Schreiber 9c33078a40 Fix isort error 2022-11-01 17:15:27 +01:00
Raphael Michel 6403e5370a Don't crash if a exporter signal returns None 2022-11-01 13:40:22 +01:00
Raphael Michel 3fe2a0455f Fix crash in CartManager 2022-11-01 12:14:30 +01:00
pretix translation bot 6956b198ae Update translations (#2874)
Co-authored-by: Raphael Michel <michel@rami.io>
2022-11-01 11:27:26 +01:00
Raphael Michel 36f7a3d3a3 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2022-10-31 16:56:46 +01:00
Fazenda Dengo 587e1a1c96 Translations: Update Portuguese (Portugal)
Currently translated at 76.6% (3737 of 4877 strings)

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

powered by weblate
2022-10-31 16:56:14 +01:00
Raphael Michel 8707ab5277 Use a more human-friendly file name for calendar attachments 2022-10-31 16:55:49 +01:00
Raphael Michel 4f6fa84fa7 Fix locking timeout no longer working after redis version change 2022-10-31 16:02:42 +01:00
Raphael Michel e76d13bf8e Improve logging of periodic command errors 2022-10-31 15:23:32 +01:00
Raphael Michel 39449ecbbe Sentry: Set propagate_traces=False 2022-10-31 14:13:59 +01:00
Raphael Michel 0204b42587 Revert "Attempt downgrade to sentry-sdk 1.8.*"
This reverts commit c1d1e437cc.
2022-10-31 14:10:11 +01:00
Raphael Michel c1d1e437cc Attempt downgrade to sentry-sdk 1.8.* 2022-10-31 12:30:03 +01:00
Raphael Michel 2fe0ceb4c7 PDF: Fail gracefully on unknown font 2022-10-31 09:53:06 +01:00
Raphael Michel 4cba292b57 Bump to 4.15.0.dev0 2022-10-28 13:34:05 +02:00
Raphael Michel 9e91197c5d Bump to 4.14.0 2022-10-28 13:32:30 +02:00
Raphael Michel 10a8cf3758 Split OverviewReport into modular functions 2022-10-27 22:41:05 +02:00
Raphael Michel d1deb35711 Add support for base_qs parameter in order_overview function 2022-10-27 22:41:05 +02:00
Raphael Michel c4d2b0bff7 Fix handling of default ticket layouts during event cloning 2022-10-27 21:55:08 +02:00
Raphael Michel 2d8ceb3255 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (4877 of 4877 strings)

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

powered by weblate
2022-10-26 21:13:19 +02:00
Raphael Michel 176e5f115b Translations: Update German
Currently translated at 100.0% (4877 of 4877 strings)

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

powered by weblate
2022-10-26 21:13:19 +02:00
Raphael Michel 9939793e91 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2022-10-26 20:57:05 +02:00
Raphael Michel 7d3cd16785 Add workaround for https://github.com/getsentry/sentry-python/issues/1700 2022-10-26 11:58:44 +02:00
Raphael Michel 7c5fac306a Bank transfer: Match orders based on invoice number (#2867) 2022-10-26 11:06:45 +02:00
Raphael Michel 37683781d0 Fix incorrect variable use in test 2022-10-26 10:49:48 +02:00
Raphael Michel 89dda69205 Allow to sort export of all tickets or badges by question answer (#2865) 2022-10-26 10:43:13 +02:00
Raphael Michel f2c72e5ff8 Bump pytest to 7.2.* 2022-10-26 09:39:40 +02:00
Raphael Michel 780ebfe120 Bump sentry-sdk to 1.10.* 2022-10-26 09:39:40 +02:00
Raphael Michel c7d5b687f3 Bump django-countries to 7.4.* 2022-10-26 09:39:40 +02:00
Raphael Michel 5fcb51f372 Bump sepaxml to 2.5.* 2022-10-26 09:39:40 +02:00
FlorianKohlerb88f86e20d524626 9b08f1b286 Translations: Update French
Currently translated at 47.0% (2291 of 4870 strings)

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

powered by weblate
2022-10-26 09:21:54 +02:00
Raphael Michel 4f35be7a25 Fix isort issue 2022-10-25 22:35:16 +02:00
Raphael Michel 884dbff4b8 Log details of API exceptions 2022-10-25 17:57:25 +02:00
Raphael Michel 51768eaef9 Add support for request ID headers 2022-10-25 17:17:59 +02:00
Raphael Michel 45f579caf2 Vouchers: Fix label on redemption page 2022-10-25 15:00:00 +02:00
fyksen a29dbd88ac Translations: Update Norwegian Bokmål
Currently translated at 7.0% (344 of 4870 strings)

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

powered by weblate
2022-10-25 14:55:12 +02:00
fyksen 957337b091 Translations: Update Norwegian Bokmål
Currently translated at 7.0% (343 of 4870 strings)

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

powered by weblate
2022-10-25 14:55:12 +02:00
Raphael Michel 4983073172 API: Fix crash with deletion of cart positions with add-ons 2022-10-25 12:08:58 +02:00
Raphael Michel b99d21df69 Fix crash in event creation with very long event names 2022-10-25 12:04:52 +02:00
Raphael Michel 2cfffe6526 Fix edge case in item add-on formset validation 2022-10-25 11:55:21 +02:00
Raphael Michel 87a413ea42 API: Enforce that Item.default_price can't be null 2022-10-25 11:39:48 +02:00
Raphael Michel 4146437380 Do not ask people to enter an address if they can't 2022-10-25 09:27:02 +02:00
Richard Schreiber b4a7369642 Fix: make hidden form inputs visible, if invalid (Z#23110236) 2022-10-21 11:11:45 +02:00
Raphael Michel f9b51a8abb Fix incorrect handling of native customer logins 2022-10-20 18:07:46 +02:00
Raphael Michel d69d70cfb1 Voucher: Add min_usages parameter (#2853) 2022-10-20 18:07:24 +02:00
Martin Gross ba2d908a89 Security Profiles: Add stripeterminal.paymentintent to POS (#2850) 2022-10-19 17:57:44 +02:00
Raphael Michel c05abcbccd Bump stripe to 4.2.* and raise Stripe API version 2022-10-19 17:55:55 +02:00
Martin Gross e16fd61bec Stripe Connect: Fix account name retrieval (#2857) 2022-10-19 17:55:34 +02:00
Raphael Michel a29d69d8f7 Fix subevent calender closed after month switch 2022-10-19 17:35:40 +02:00
Raphael Michel e063ad7dda Set payment_banktransfer_invoice_immediately by default 2022-10-19 17:28:22 +02:00
Raphael Michel 7c2bacf3b5 Fix crash on rendering error 404 page 2022-10-19 16:52:35 +02:00
Raphael Michel c921ca4e65 API: Fix crash when sorting orderpositions by attendee name 2022-10-19 15:32:29 +02:00
Raphael Michel 29a36057ed Fix border of navbar (that appeared out of nowhere?) 2022-10-19 10:52:47 +02:00
robbi5 5eeecf9214 Set default ticket layout QR code content explicitly to secret (#2858) 2022-10-18 17:24:28 +02:00
Raphael Michel 5992abcb7d Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (4870 of 4870 strings)

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

powered by weblate
2022-10-12 10:30:54 +02:00
Fazenda Dengo 0db7ec3169 Translations: Update Portuguese (Portugal)
Currently translated at 76.7% (3737 of 4870 strings)

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

powered by weblate
2022-10-12 09:06:41 +02:00
Raphael Michel 8046bf98b7 Make link more visible on redirect.html page 2022-10-11 18:04:38 +02:00
Raphael Michel 9ed39ab0fa Stripe: Prevent lost session with firefox tracking protection 2022-10-11 18:04:30 +02:00
Raphael Michel 7e79fc8b5e Add title scheme "dr_prof_he" for person names 2022-10-11 14:57:19 +02:00
Raphael Michel 9da68645da Replace phrase "presale period" with "booking period" 2022-10-11 11:34:23 +02:00
Fazenda Dengo f7a4b66da1 Translations: Update Portuguese (Portugal)
Currently translated at 75.9% (3697 of 4869 strings)

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

powered by weblate
2022-10-10 16:20:22 +02:00
Fazenda Dengo c9212a483b Translations: Update Portuguese (Portugal)
Currently translated at 75.4% (3675 of 4869 strings)

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

powered by weblate
2022-10-10 16:20:22 +02:00
Raphael Michel cc4e946d95 API: Fix order creation with nested cart positions 2022-10-10 13:45:11 +02:00
Raphael Michel 9d1cfd1eb6 Clarify cart order (#2844) 2022-10-10 12:59:49 +02:00
Raphael Michel 38969747f4 API: New implementation for cart creation (#2833) 2022-10-10 12:59:38 +02:00
Raphael Michel 6e7af4c64b API: Add device info to all security profiles 2022-10-10 12:36:27 +02:00
Raphael Michel fb45f9f08c Fix readthedocs build 2022-10-10 11:47:00 +02:00
Raphael Michel 6848ce24eb Attempt to fix readthedocs build config 2022-10-10 09:53:36 +02:00
Raphael Michel dac4fd8d3c Attempt to fix readthedocs build config 2022-10-10 09:49:49 +02:00
Raphael Michel 6905d3e801 Attempt to fix readthedocs build config 2022-10-10 09:41:29 +02:00
Raphael Michel 909b16be64 Attempt to fix readthedocs build config 2022-10-10 09:33:42 +02:00
Raphael Michel a18162cc47 Attempt to fix readthedocs build config 2022-10-10 09:21:43 +02:00
Raphael Michel 6f0fc9ed49 Fix form validation of cancellation form 2022-10-07 12:39:39 +02:00
Raphael Michel 2409c513d6 Remove useless margins at the end of panel boxes 2022-10-07 10:17:29 +02:00
Raphael Michel 0a95f90012 OIDC RP: Use a separator value in state that is less likely to get lost in transit 2022-10-07 09:42:50 +02:00
Julian Rother edbd24e942 Checkout: do not show bundled products as "Selected add-ons" in questions step (#2820) 2022-10-07 09:12:13 +02:00
Martin Gross 3940af868b Mail: Fix retry on non-permanent failures (PRETIXEU-7E3) 2022-10-06 18:17:12 +02:00
Raphael Michel 8b4197d868 Bump djangorestframework to 3.14.* 2022-10-06 16:05:35 +02:00
Raphael Michel 632e441c24 Bump django-statici18n to 2.3.* 2022-10-06 14:31:04 +02:00
Andreas Grillenberger c73ede81ae Docs: Fix incorrect endpoint URL (#2829) 2022-10-06 14:25:08 +02:00
Raphael Michel c4b7aeaaa2 Consistently set default background PDFs on server, not client (#2840)
Co-authored-by: Martin Gross <gross@rami.io>
2022-10-06 14:14:56 +02:00
Raphael Michel b5bd98336a Docs: Update API docs for digital content plugin 2022-10-06 10:57:00 +02:00
Raphael Michel 5af52f6087 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (4869 of 4869 strings)

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

powered by weblate
2022-10-06 10:51:04 +02:00
Raphael Michel c5e4d06921 Translations: Update German
Currently translated at 100.0% (4869 of 4869 strings)

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

powered by weblate
2022-10-06 10:51:04 +02:00
Raphael Michel 917cc00091 Translations: Update German
Currently translated at 100.0% (4869 of 4869 strings)

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

powered by weblate
2022-10-06 10:51:04 +02:00
Raphael Michel 63cb88bfb8 Fix crash in OrderChangeManager log entry generation 2022-10-06 09:41:38 +02:00
Raphael Michel ac1fe15b6c Fix order-level exports through the API 2022-10-05 17:38:28 +02:00
Raphael Michel ddaa0570bc Revert "Use a temporary file for exports for more stable writing" 2022-10-05 13:39:20 +02:00
Raphael Michel 07352743f2 Fix missing seek call in export task 2022-10-05 13:31:53 +02:00
Raphael Michel f99ef5fff2 Fix regression when exporting empty data 2022-10-05 13:22:46 +02:00
Raphael Michel 9d686072e2 Fix regression in export task 2022-10-05 12:56:28 +02:00
Raphael Michel 4e44a2809b Fix safe_openpyxl implementation to not leak memory in WriteOnlyWorksheet 2022-10-05 12:27:29 +02:00
Raphael Michel 370e4eafc2 Use a temporary file for exports for more stable writing 2022-10-05 12:26:36 +02:00
Raphael Michel b7ec372ebc Add exporter for list of customers 2022-10-05 10:36:57 +02:00
Raphael Michel 60cdfe4029 Allow organizer-level exports with separate permission and no event selection 2022-10-05 10:36:57 +02:00
Raphael Michel 74e14285ee Remove hack for gift card exporters, it's not required 2022-10-05 10:36:57 +02:00
Richard Schreiber 8f56ab54a4 PDF/Badges: Improve performance/reduce filesize when creating multiple badges (#2824)
* improve bg performance by using pdftk

* fix handling of rotated background-PDFs
2022-10-05 06:12:23 +02:00
Raphael Michel 4ac58654a0 Run isort on generated protobuf code 2022-10-04 18:03:49 +02:00
Raphael Michel 167eb06aeb Bump django-debug-toolbar to 3.7 2022-10-04 18:03:49 +02:00
Raphael Michel 9a0cc7e8c1 Bump pytest-mock to 3.9 2022-10-04 18:03:49 +02:00
dependabot[bot] d4ff1808d5 Bump @babel/preset-env in /src/pretix/static/npm_dir
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.19.1 to 7.19.3.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.19.3/packages/babel-preset-env)

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-04 18:02:05 +02:00
Raphael Michel 0ff22786cb Fix typeahead for item meta values with limited access 2022-10-04 16:58:11 +02:00
yvovandoorn abfb53872c bump css-inline from 0.7.x to 0.8.x to allow for successful arm64 installs 2022-10-04 16:58:09 +02:00
dependabot[bot] 67f60a9e09 Bump rollup from 2.79.0 to 2.79.1 in /src/pretix/static/npm_dir
Bumps [rollup](https://github.com/rollup/rollup) from 2.79.0 to 2.79.1.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.79.0...v2.79.1)

---
updated-dependencies:
- dependency-name: rollup
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-04 16:45:12 +02:00
Raphael Michel 1d04d40507 Bump protobuf to 4.21.*, regenerate protobuf file 2022-10-04 16:43:34 +02:00
Raphael Michel 14fdd7cfca Bump django-compressor to 4.1.* 2022-10-04 16:43:34 +02:00
Raphael Michel 402ed61756 Bump PyPDF2 to 2.11.* 2022-10-04 16:43:34 +02:00
dependabot[bot] 66c75cbb1b Bump @babel/core from 7.19.1 to 7.19.3 in /src/pretix/static/npm_dir
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.19.1 to 7.19.3.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.19.3/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-04 16:28:39 +02:00
dependabot[bot] c32791c7dd Bump @rollup/plugin-node-resolve in /src/pretix/static/npm_dir
Bumps [@rollup/plugin-node-resolve](https://github.com/rollup/plugins/tree/HEAD/packages/node-resolve) from 13.3.0 to 14.1.0.
- [Release notes](https://github.com/rollup/plugins/releases)
- [Changelog](https://github.com/rollup/plugins/blob/master/packages/node-resolve/CHANGELOG.md)
- [Commits](https://github.com/rollup/plugins/commits/node-resolve-v14.1.0/packages/node-resolve)

---
updated-dependencies:
- dependency-name: "@rollup/plugin-node-resolve"
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-04 16:28:32 +02:00
Richard Schreiber d6846d8415 Cart: change icon from checkbox to arrow-right for voucher submit (#2832) 2022-10-04 11:06:39 +02:00
Raphael Michel b1c8efa33f AsyncFormView/AsyncPostView: Allow to report status back 2022-09-30 13:58:07 +02:00
Raphael Michel f14d031de4 Fix semantics of LockTimeoutException and LockReleaseException 2022-09-30 13:41:51 +02:00
Raphael Michel 25c86db6f5 Do not try to unserialize empty string as phone number 2022-09-30 13:28:02 +02:00
Richard Schreiber 7205d0689e Badges: fix pagesizes for 8 A7 on A4-page 2022-09-30 09:18:54 +02:00
Raphael Michel cde46012cb Add .badge-variant styles 2022-09-29 18:05:21 +02:00
Richard Schreiber e4a0122938 fix pagesizes and offsets 2022-09-29 17:30:38 +02:00
Raphael Michel 77c08cb710 Fix whitespace issue in EPC QR generation 2022-09-29 13:55:50 +02:00
Raphael Michel af49a02047 Bump version to 4.14.0.dev0a 2022-09-29 13:37:55 +02:00
220 changed files with 149302 additions and 122466 deletions
-1
View File
@@ -1 +0,0 @@
-r doc/requirements.txt
+15
View File
@@ -0,0 +1,15 @@
version: 2
sphinx:
configuration: doc/conf.py
build:
os: ubuntu-22.04
tools:
python: "3.8"
nodejs: "16"
apt_packages:
- gettext
python:
install:
- method: pip
path: ./src/
- requirements: doc/requirements.rtd.txt
+3
View File
@@ -117,6 +117,9 @@ Example::
``loglevel``
Set console and file log level (``DEBUG``, ``INFO``, ``WARNING``, ``ERROR`` or ``CRITICAL``). Defaults to ``INFO``.
``request_id_header``
Specifies the name of a header that should be used for logging request IDs. Off by default.
Locale settings
---------------
+25 -8
View File
@@ -17,8 +17,8 @@ The cart position resource contains the following public fields:
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the cart position
cart_id string Identifier of the cart this belongs to. Needs to end
in "@api" for API-created positions.
cart_id string Identifier of the cart this belongs to, needs to end
in "@api" for API-created positions
datetime datetime Time of creation
expires datetime The cart position will expire at this time and no longer block quota
item integer ID of the item
@@ -29,14 +29,15 @@ attendee_name_parts object of strings Composition of
attendee_email string Specified attendee email address for this position (or ``null``)
voucher integer Internal ID of the voucher used for this position (or ``null``)
addon_to integer Internal ID of the position this position is an add-on for (or ``null``)
subevent integer ID of the date inside an event series this position belongs to (or ``null``).
is_bundled boolean If ``addon_to`` is set, this shows whether this is a bundled product or an addon product
subevent integer ID of the date inside an event series this position belongs to (or ``null``)
answers list of objects Answers to user-defined questions
├ question integer Internal ID of the answered question
├ answer string Text representation of the answer
├ question_identifier string The question's ``identifier`` field
├ options list of integers Internal IDs of selected option(s)s (only for choice types)
└ option_identifiers list of strings The ``identifier`` fields of the selected option(s)s
seat objects The assigned seat. Can be ``null``.
seat objects The assigned seat (or ``null``)
├ id integer Internal ID of the seat instance
├ name string Human-readable seat name
└ seat_guid string Identifier of the seat within the seating plan
@@ -46,6 +47,10 @@ seat objects The assigned se
This ``seat`` attribute has been added.
.. versionchanged:: 4.14
This ``is_bundled`` attribute has been added and the cart creation endpoints have been updated.
Cart position endpoints
-----------------------
@@ -87,6 +92,7 @@ Cart position endpoints
"attendee_email": null,
"voucher": null,
"addon_to": null,
"is_bundled": false,
"subevent": null,
"datetime": "2018-06-11T10:00:00Z",
"expires": "2018-06-11T10:00:00Z",
@@ -133,6 +139,7 @@ Cart position endpoints
"attendee_email": null,
"voucher": null,
"addon_to": null,
"is_bundled": false,
"subevent": null,
"datetime": "2018-06-11T10:00:00Z",
"expires": "2018-06-11T10:00:00Z",
@@ -168,7 +175,7 @@ Cart position endpoints
* does not validate if the event's ticket sales are already over or haven't started
* does not support add-on products at the moment
* does not validate constraints on add-on products at the moment
* does not check or calculate prices but believes any prices you send
@@ -176,6 +183,8 @@ Cart position endpoints
* does not support file upload questions
Note that more validation might be added in the future, so please do not rely on missing validation.
You can supply the following fields of the resource:
* ``cart_id`` (optional, needs to end in ``@api``)
@@ -190,6 +199,8 @@ Cart position endpoints
* ``includes_tax`` (optional, **deprecated**, do not use, will be removed)
* ``sales_channel`` (optional)
* ``voucher`` (optional, expect a voucher code)
* ``addons`` (optional, expect a list of nested objects of cart positions)
* ``bundled`` (optional, expect a list of nested objects of cart positions)
* ``answers``
* ``question``
@@ -221,6 +232,12 @@ Cart position endpoints
"options": []
}
],
"addons": [
{
"item": 2,
"variation": null,
}
],
"subevent": null
}
@@ -232,7 +249,7 @@ Cart position endpoints
Vary: Accept
Content-Type: application/json
(Full cart position resource, see above.)
(Full cart position resource, see above, with additional nested objects "addons" and "bundled".)
:param organizer: The ``slug`` field of the organizer of the event to create a position for
:param event: The ``slug`` field of the event to create a position for
@@ -244,8 +261,8 @@ Cart position endpoints
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/cartpositions/bulk_create/
Creates multiple new cart position. This operation is deliberately not atomic, so each cart position can succeed
or fail individually, so the response code of the response is not the only thing to look at!
Creates multiple new cart position. **This operation is deliberately not atomic, so each cart position can succeed
or fail individually, so the response code of the response is not the only thing to look at!**
.. warning:: This endpoint is considered **experimental**. It might change at any time without prior notice.
+6 -1
View File
@@ -178,6 +178,7 @@ tax_rule integer The ID of the u
secret string Secret code printed on the tickets for validation
addon_to integer Internal ID of the position this position is an add-on for (or ``null``)
subevent integer ID of the date inside an event series this position belongs to (or ``null``).
discount integer ID of a discount that has been used during the creation of this position in some way (or ``null``).
pseudonymization_id string A random ID, e.g. for use in lead scanning apps
checkins list of objects List of **successful** check-ins with this ticket
├ id integer Internal ID of the check-in event
@@ -371,6 +372,7 @@ List of all orders
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
"addon_to": null,
"subevent": null,
"discount": null,
"pseudonymization_id": "MQLJvANO3B",
"seat": null,
"checkins": [
@@ -546,6 +548,7 @@ Fetching individual orders
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
"addon_to": null,
"subevent": null,
"discount": null,
"pseudonymization_id": "MQLJvANO3B",
"seat": null,
"checkins": [
@@ -1487,6 +1490,7 @@ List of all order positions
"tax_rule": null,
"tax_value": "0.00",
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
"discount": null,
"pseudonymization_id": "MQLJvANO3B",
"seat": null,
"addon_to": null,
@@ -1597,6 +1601,7 @@ Fetching individual positions
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
"addon_to": null,
"subevent": null,
"discount": null,
"pseudonymization_id": "MQLJvANO3B",
"seat": null,
"checkins": [
@@ -1925,7 +1930,7 @@ otherwise, such as splitting an order or changing fees.
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/ HTTP/1.1
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/change/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
+2
View File
@@ -19,6 +19,8 @@ max_usages integer The maximum num
redeemed (default: 1).
redeemed integer The number of times this voucher already has been
redeemed.
min_usages integer The minimum number of times this voucher must be
redeemed on first usage (default: 1).
valid_until datetime The voucher expiration date (or ``null``).
block_quota boolean If ``true``, quota is blocked for this voucher.
allow_ignore_quota boolean If ``true``, this voucher can be redeemed even if a
+7 -1
View File
@@ -60,7 +60,13 @@ The exporter class
.. py:attribute:: BaseExporter.event
The default constructor sets this property to the event we are currently
working for.
working for. This will be ``None`` if the exporter is run for multiple
events.
.. py:attribute:: BaseExporter.events
The default constructor sets this property to the list of events to work
on, regardless of whether the exporter is called for one or multiple events.
.. autoattribute:: identifier
+6
View File
@@ -93,6 +93,7 @@ id integer Internal conten
title multi-lingual string The content title (required)
content_type string The type of content, valid values are ``webinar``, ``video``, ``livestream``, ``link``, ``file``
url string The location of the digital content
file file A downloadable file. Either ``url`` or ``file`` must be ``null``.
description multi-lingual string A public description of the item. May contain Markdown
syntax and is not required.
available_from datetime The first date time at which this content will be shown
@@ -144,6 +145,7 @@ API Endpoints
},
"content_type": "link",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"file": null,
"description": {
"en": "Watch our event live here on YouTube!"
},
@@ -191,6 +193,7 @@ API Endpoints
},
"content_type": "link",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"file": null,
"description": {
"en": "Watch our event live here on YouTube!"
},
@@ -229,6 +232,7 @@ API Endpoints
},
"content_type": "link",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"file": null,
"description": {
"en": "Watch our event live here on YouTube!"
},
@@ -255,6 +259,7 @@ API Endpoints
},
"content_type": "link",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"file": null,
"description": {
"en": "Watch our event live here on YouTube!"
},
@@ -309,6 +314,7 @@ API Endpoints
},
"content_type": "link",
"url": "https://mywebsite.com",
"file": null,
"description": {
"en": "Watch our event live here on YouTube!"
},
+10
View File
@@ -0,0 +1,10 @@
sphinx==2.3.*
jinja2==3.0.*
sphinx-rtd-theme
sphinxcontrib-httpdomain
sphinxcontrib-images
sphinxcontrib-spelling==4.*
sphinxemoji
pygments-markdown-lexer
# See https://github.com/rfk/pyenchant/pull/130
git+https://github.com/raphaelm/pyenchant.git@patch-1#egg=pyenchant
-6
View File
@@ -1,6 +0,0 @@
build:
image: latest
python:
version: 3.6
+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.13.0"
__version__ = "4.15.0.dev0"
+5
View File
@@ -46,6 +46,7 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:version'),
('GET', 'api-v1:device.eventselection'),
('GET', 'api-v1:idempotency.query'),
('GET', 'api-v1:device.info'),
('POST', 'api-v1:device.update'),
('POST', 'api-v1:device.revoke'),
('POST', 'api-v1:device.roll'),
@@ -80,6 +81,7 @@ class PretixScanNoSyncNoSearchSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:version'),
('GET', 'api-v1:device.eventselection'),
('GET', 'api-v1:idempotency.query'),
('GET', 'api-v1:device.info'),
('POST', 'api-v1:device.update'),
('POST', 'api-v1:device.revoke'),
('POST', 'api-v1:device.roll'),
@@ -112,6 +114,7 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:version'),
('GET', 'api-v1:device.eventselection'),
('GET', 'api-v1:idempotency.query'),
('GET', 'api-v1:device.info'),
('POST', 'api-v1:device.update'),
('POST', 'api-v1:device.revoke'),
('POST', 'api-v1:device.roll'),
@@ -145,6 +148,7 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:version'),
('GET', 'api-v1:device.eventselection'),
('GET', 'api-v1:idempotency.query'),
('GET', 'api-v1:device.info'),
('POST', 'api-v1:device.update'),
('POST', 'api-v1:device.revoke'),
('POST', 'api-v1:device.roll'),
@@ -192,6 +196,7 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
('POST', 'plugins:pretix_posbackend:posdebuglogentry-bulk-create'),
('GET', 'plugins:pretix_posbackend:poscashier-list'),
('POST', 'plugins:pretix_posbackend:stripeterminal.token'),
('POST', 'plugins:pretix_posbackend:stripeterminal.paymentintent'),
('PUT', 'plugins:pretix_posbackend:file.upload'),
('GET', 'api-v1:revokedsecrets-list'),
('GET', 'api-v1:event.settings'),
+9
View File
@@ -19,11 +19,17 @@
# 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 logging
import ujson
from rest_framework import exceptions
from rest_framework.response import Response
from rest_framework.views import exception_handler, status
from pretix.base.services.locking import LockTimeoutException
logger = logging.getLogger(__name__)
def custom_exception_handler(exc, context):
response = exception_handler(exc, context)
@@ -37,4 +43,7 @@ def custom_exception_handler(exc, context):
}
)
if isinstance(exc, exceptions.APIException):
logger.info(f'API Exception [{exc.status_code}]: {ujson.dumps(exc.detail)}')
return response
+182 -137
View File
@@ -23,8 +23,7 @@ import os
from datetime import timedelta
from django.core.files import File
from django.db.models import Q
from django.utils.crypto import get_random_string
from django.db.models import prefetch_related_objects
from django.utils.timezone import now
from django.utils.translation import gettext_lazy
from rest_framework import serializers
@@ -34,7 +33,7 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.order import (
AnswerCreateSerializer, AnswerSerializer, InlineSeatSerializer,
)
from pretix.base.models import Quota, Seat, Voucher
from pretix.base.models import Seat, Voucher
from pretix.base.models.orders import CartPosition
@@ -52,148 +51,18 @@ class CartPositionSerializer(I18nAwareModelSerializer):
model = CartPosition
fields = ('id', 'cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
'attendee_email', 'voucher', 'addon_to', 'subevent', 'datetime', 'expires', 'includes_tax',
'answers', 'seat')
'answers', 'seat', 'is_bundled')
class CartPositionCreateSerializer(I18nAwareModelSerializer):
class BaseCartPositionCreateSerializer(I18nAwareModelSerializer):
answers = AnswerCreateSerializer(many=True, required=False)
expires = serializers.DateTimeField(required=False)
attendee_name = serializers.CharField(required=False, allow_null=True)
seat = serializers.CharField(required=False, allow_null=True)
sales_channel = serializers.CharField(required=False, default='sales_channel')
includes_tax = serializers.BooleanField(required=False, allow_null=True)
voucher = serializers.CharField(required=False, allow_null=True)
class Meta:
model = CartPosition
fields = ('cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
'subevent', 'expires', 'includes_tax', 'answers', 'seat', 'sales_channel', 'voucher')
def create(self, validated_data):
answers_data = validated_data.pop('answers')
if not validated_data.get('cart_id'):
cid = "{}@api".format(get_random_string(48))
while CartPosition.objects.filter(cart_id=cid).exists():
cid = "{}@api".format(get_random_string(48))
validated_data['cart_id'] = cid
if not validated_data.get('expires'):
validated_data['expires'] = now() + timedelta(
minutes=self.context['event'].settings.get('reservation_time', as_type=int)
)
new_quotas = (validated_data.get('variation').quotas.filter(subevent=validated_data.get('subevent'))
if validated_data.get('variation')
else validated_data.get('item').quotas.filter(subevent=validated_data.get('subevent')))
if len(new_quotas) == 0:
raise ValidationError(
gettext_lazy('The product "{}" is not assigned to a quota.').format(
str(validated_data.get('item'))
)
)
for quota in new_quotas:
avail = quota.availability(_cache=self.context['quota_cache'])
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < 1):
raise ValidationError(
gettext_lazy('There is not enough quota available on quota "{}" to perform '
'the operation.').format(
quota.name
)
)
for quota in new_quotas:
oldsize = self.context['quota_cache'][quota.pk][1]
newsize = oldsize - 1 if oldsize is not None else None
self.context['quota_cache'][quota.pk] = (
Quota.AVAILABILITY_OK if newsize is None or newsize > 0 else Quota.AVAILABILITY_GONE,
newsize
)
attendee_name = validated_data.pop('attendee_name', '')
if attendee_name and not validated_data.get('attendee_name_parts'):
validated_data['attendee_name_parts'] = {
'_legacy': attendee_name
}
seated = validated_data.get('item').seat_category_mappings.filter(subevent=validated_data.get('subevent')).exists()
if validated_data.get('seat'):
if not seated:
raise ValidationError('The specified product does not allow to choose a seat.')
try:
seat = self.context['event'].seats.get(seat_guid=validated_data['seat'], subevent=validated_data.get('subevent'))
except Seat.DoesNotExist:
raise ValidationError('The specified seat does not exist.')
except Seat.MultipleObjectsReturned:
raise ValidationError('The specified seat ID is not unique.')
else:
validated_data['seat'] = seat
elif seated:
raise ValidationError('The specified product requires to choose a seat.')
if validated_data.get('voucher'):
try:
voucher = self.context['event'].vouchers.get(code__iexact=validated_data.get('voucher'))
except Voucher.DoesNotExist:
raise ValidationError('The specified voucher does not exist.')
if voucher and not voucher.applies_to(validated_data.get('item'), validated_data.get('variation')):
raise ValidationError('The specified voucher is not valid for the given item and variation.')
if voucher and voucher.seat and voucher.seat != validated_data.get('seat'):
raise ValidationError('The specified voucher is not valid for this seat.')
if voucher and voucher.subevent_id and (not validated_data.get('subevent') or voucher.subevent_id != validated_data['subevent'].pk):
raise ValidationError('The specified voucher is not valid for this subevent.')
if voucher.valid_until is not None and voucher.valid_until < now():
raise ValidationError('The specified voucher is expired.')
redeemed_in_carts = CartPosition.objects.filter(
Q(voucher=voucher) & Q(event=self.context['event']) & Q(expires__gte=now())
)
cart_count = redeemed_in_carts.count()
v_avail = voucher.max_usages - voucher.redeemed - cart_count
if v_avail < 1:
raise ValidationError('The specified voucher has already been used the maximum number of times.')
validated_data['voucher'] = voucher
if validated_data.get('seat'):
if not validated_data['seat'].is_available(
sales_channel=validated_data.get('sales_channel', 'web'),
distance_ignore_cart_id=validated_data['cart_id'],
ignore_voucher_id=validated_data['voucher'].pk if validated_data.get('voucher') else None,
):
raise ValidationError(
gettext_lazy('The selected seat "{seat}" is not available.').format(seat=validated_data['seat'].name))
validated_data.pop('sales_channel')
# todo: does this make sense?
validated_data['custom_price_input'] = validated_data['price']
# todo: listed price, etc?
# currently does not matter because there is no way to transform an API cart position into an order that keeps
# prices, cart positions are just quota/voucher placeholders
validated_data['custom_price_input_is_net'] = not validated_data.pop('includes_tax', True)
cp = CartPosition.objects.create(event=self.context['event'], **validated_data)
for answ_data in answers_data:
options = answ_data.pop('options')
if isinstance(answ_data['answer'], File):
an = answ_data.pop('answer')
answ = cp.answers.create(**answ_data, answer='')
answ.file.save(os.path.basename(an.name), an, save=False)
answ.answer = 'file://' + answ.file.name
answ.save()
an.close()
else:
answ = cp.answers.create(**answ_data)
answ.options.add(*options)
return cp
def validate_cart_id(self, cid):
if cid and not cid.endswith('@api'):
raise ValidationError('Cart ID should end in @api or be empty.')
return cid
fields = ('item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
'subevent', 'includes_tax', 'answers')
def validate_item(self, item):
if item.event != self.context['event']:
@@ -240,4 +109,180 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
raise ValidationError(
{'attendee_name': ['Do not specify attendee_name if you specified attendee_name_parts.']}
)
if not data.get('expires'):
data['expires'] = now() + timedelta(
minutes=self.context['event'].settings.get('reservation_time', as_type=int)
)
quotas_for_item_cache = self.context.get('quotas_for_item_cache', {})
quotas_for_variation_cache = self.context.get('quotas_for_variation_cache', {})
seated = data.get('item').seat_category_mappings.filter(subevent=data.get('subevent')).exists()
if data.get('seat'):
if not seated:
raise ValidationError({'seat': ['The specified product does not allow to choose a seat.']})
try:
seat = self.context['event'].seats.get(seat_guid=data['seat'], subevent=data.get('subevent'))
except Seat.DoesNotExist:
raise ValidationError({'seat': ['The specified seat does not exist.']})
except Seat.MultipleObjectsReturned:
raise ValidationError({'seat': ['The specified seat ID is not unique.']})
else:
data['seat'] = seat
elif seated:
raise ValidationError({'seat': ['The specified product requires to choose a seat.']})
if data.get('voucher'):
try:
voucher = self.context['event'].vouchers.get(code__iexact=data['voucher'])
except Voucher.DoesNotExist:
raise ValidationError({'voucher': ['The specified voucher does not exist.']})
if voucher and not voucher.applies_to(data['item'], data.get('variation')):
raise ValidationError({'voucher': ['The specified voucher is not valid for the given item and variation.']})
if voucher and voucher.seat and voucher.seat != data.get('seat'):
raise ValidationError({'voucher': ['The specified voucher is not valid for this seat.']})
if voucher and voucher.subevent_id and (not data.get('subevent') or voucher.subevent_id != data['subevent'].pk):
raise ValidationError({'voucher': ['The specified voucher is not valid for this subevent.']})
if voucher.valid_until is not None and voucher.valid_until < now():
raise ValidationError({'voucher': ['The specified voucher is expired.']})
data['voucher'] = voucher
if not data.get('voucher') or (not data['voucher'].allow_ignore_quota and not data['voucher'].block_quota):
if data.get('variation'):
if data['variation'].pk not in quotas_for_variation_cache:
quotas_for_variation_cache[data['variation'].pk] = data['variation'].quotas.filter(subevent=data.get('subevent'))
data['_quotas'] = quotas_for_variation_cache[data['variation'].pk]
else:
if data['item'].pk not in quotas_for_item_cache:
quotas_for_item_cache[data['item'].pk] = data['item'].quotas.filter(subevent=data.get('subevent'))
data['_quotas'] = quotas_for_item_cache[data['item'].pk]
if len(data['_quotas']) == 0:
raise ValidationError(
gettext_lazy('The product "{}" is not assigned to a quota.').format(
str(data.get('item'))
)
)
else:
data['_quotas'] = []
return data
def create(self, validated_data):
validated_data.pop('_quotas')
answers_data = validated_data.pop('answers')
attendee_name = validated_data.pop('attendee_name', '')
if attendee_name and not validated_data.get('attendee_name_parts'):
validated_data['attendee_name_parts'] = {
'_legacy': attendee_name
}
# todo: does this make sense?
validated_data['custom_price_input'] = validated_data['price']
# todo: listed price, etc?
# currently does not matter because there is no way to transform an API cart position into an order that keeps
# prices, cart positions are just quota/voucher placeholders
validated_data['custom_price_input_is_net'] = not validated_data.pop('includes_tax', True)
cp = CartPosition.objects.create(event=self.context['event'], **validated_data)
for answ_data in answers_data:
options = answ_data.pop('options')
if isinstance(answ_data['answer'], File):
an = answ_data.pop('answer')
answ = cp.answers.create(**answ_data, answer='')
answ.file.save(os.path.basename(an.name), an, save=False)
answ.answer = 'file://' + answ.file.name
answ.save()
an.close()
else:
answ = cp.answers.create(**answ_data)
answ.options.add(*options)
return cp
class CartPositionCreateSerializer(BaseCartPositionCreateSerializer):
expires = serializers.DateTimeField(required=False)
addons = BaseCartPositionCreateSerializer(many=True, required=False)
bundled = BaseCartPositionCreateSerializer(many=True, required=False)
seat = serializers.CharField(required=False, allow_null=True)
sales_channel = serializers.CharField(required=False, default='sales_channel')
voucher = serializers.CharField(required=False, allow_null=True)
class Meta:
model = CartPosition
fields = BaseCartPositionCreateSerializer.Meta.fields + (
'cart_id', 'expires', 'addons', 'bundled', 'seat', 'sales_channel', 'voucher'
)
def validate_cart_id(self, cid):
if cid and not cid.endswith('@api'):
raise ValidationError('Cart ID should end in @api or be empty.')
return cid
def create(self, validated_data):
validated_data.pop('sales_channel')
addons_data = validated_data.pop('addons', None)
bundled_data = validated_data.pop('bundled', None)
cp = super().create(validated_data)
if addons_data:
for addon_data in addons_data:
addon_data['addon_to'] = cp
addon_data['is_bundled'] = False
addon_data['cart_id'] = cp.cart_id
super().create(addon_data)
if bundled_data:
for bundle_data in bundled_data:
bundle_data['addon_to'] = cp
bundle_data['is_bundled'] = True
bundle_data['cart_id'] = cp.cart_id
super().create(bundle_data)
return cp
def validate(self, data):
data = super().validate(data)
# This is currently only a very basic validation of add-ons and bundled products, we don't validate their number
# or price. We can always go stricter, as the endpoint is documented as experimental.
# However, this serializer should always be *at least* as strict as the order creation serializer.
if data.get('item') and data.get('addons'):
prefetch_related_objects([data['item']], 'addons')
for sub_data in data['addons']:
if not any(a.addon_category_id == sub_data['item'].category_id for a in data['item'].addons.all()):
raise ValidationError({
'addons': [
'The product "{prod}" can not be used as an add-on product for "{main}".'.format(
prod=str(sub_data['item']),
main=str(data['item']),
)
]
})
if data.get('item') and data.get('bundled'):
prefetch_related_objects([data['item']], 'bundles')
for sub_data in data['bundled']:
if not any(
a.bundled_item_id == sub_data['item'].pk and
a.bundled_variation_id == (sub_data['variation'].pk if sub_data.get('variation') else None)
for a in data['item'].bundles.all()
):
raise ValidationError({
'bundled': [
'The product "{prod}" can not be used as an bundled product for "{main}".'.format(
prod=str(sub_data['item']),
main=str(data['item']),
)
]
})
return data
+9 -1
View File
@@ -411,7 +411,8 @@ class CloneEventSerializer(EventSerializer):
has_subevents = validated_data.pop('has_subevents', None)
tz = validated_data.pop('timezone', None)
sales_channels = validated_data.pop('sales_channels', None)
new_event = super().create(validated_data)
date_admission = validated_data.pop('date_admission', None)
new_event = super().create({**validated_data, 'plugins': None})
event = Event.objects.filter(slug=self.context['event'], organizer=self.context['organizer'].pk).first()
new_event.copy_data_from(event)
@@ -426,6 +427,10 @@ class CloneEventSerializer(EventSerializer):
new_event.sales_channels = sales_channels
if has_subevents is not None:
new_event.has_subevents = has_subevents
if has_subevents is not None:
new_event.has_subevents = has_subevents
if date_admission is not None:
new_event.date_admission = date_admission
new_event.save()
if tz:
new_event.settings.timezone = tz
@@ -755,6 +760,9 @@ class EventSettingsSerializer(SettingsSerializer):
'invoice_logo_image',
'cancel_allow_user',
'cancel_allow_user_until',
'cancel_allow_user_unpaid_keep',
'cancel_allow_user_unpaid_keep_fees',
'cancel_allow_user_unpaid_keep_percentage',
'cancel_allow_user_paid',
'cancel_allow_user_paid_until',
'cancel_allow_user_paid_keep',
+9 -2
View File
@@ -23,6 +23,8 @@ from django import forms
from django.http import QueryDict
from rest_framework import serializers
from pretix.base.exporter import OrganizerLevelExportMixin
class FormFieldWrapperField(serializers.Field):
def __init__(self, *args, **kwargs):
@@ -49,7 +51,6 @@ simple_mappings = (
(forms.EmailField, serializers.EmailField, ()),
(forms.UUIDField, serializers.UUIDField, ()),
(forms.URLField, serializers.URLField, ()),
(forms.NullBooleanField, serializers.NullBooleanField, ()),
(forms.BooleanField, serializers.BooleanField, ()),
)
@@ -87,7 +88,7 @@ class JobRunSerializer(serializers.Serializer):
ex = kwargs.pop('exporter')
events = kwargs.pop('events', None)
super().__init__(*args, **kwargs)
if events is not None:
if events is not None and not isinstance(ex, OrganizerLevelExportMixin):
self.fields["events"] = serializers.SlugRelatedField(
queryset=events,
required=True,
@@ -106,6 +107,12 @@ class JobRunSerializer(serializers.Serializer):
)
break
if isinstance(v, forms.NullBooleanField):
self.fields[k] = serializers.BooleanField(
required=v.required,
allow_null=True,
validators=v.validators,
)
if isinstance(v, forms.ModelMultipleChoiceField):
self.fields[k] = PrimaryKeyRelatedField(
queryset=v.queryset,
+2
View File
@@ -184,6 +184,8 @@ class ItemSerializer(I18nAwareModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['default_price'].allow_null = False
self.fields['default_price'].required = True
if not self.read_only:
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
self.fields['grant_membership_type'].queryset = self.context['event'].organizer.membership_types.all()
+10 -3
View File
@@ -410,13 +410,13 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
class Meta:
model = OrderPosition
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
'company', 'street', 'zipcode', 'city', 'country', 'state',
'company', 'street', 'zipcode', 'city', 'country', 'state', 'discount',
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled')
read_only_fields = (
'id', 'order', 'positionid', 'item', 'variation', 'price', 'voucher', 'tax_rate', 'tax_value', 'secret',
'addon_to', 'subevent', 'checkins', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data',
'seat', 'canceled'
'seat', 'canceled', 'discount',
)
def __init__(self, *args, **kwargs):
@@ -721,7 +721,7 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
class Meta:
model = OrderPosition
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
'company', 'street', 'zipcode', 'city', 'country', 'state',
'company', 'street', 'zipcode', 'city', 'country', 'state', 'is_bundled',
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher')
def __init__(self, *args, **kwargs):
@@ -1086,6 +1086,10 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
seated = pos_data.get('item').seat_category_mappings.filter(subevent=pos_data.get('subevent')).exists()
if pos_data.get('seat'):
if pos_data.get('addon_to'):
errs[i]['seat'] = ['Seats are currently not supported for add-on products.']
continue
if not seated:
errs[i]['seat'] = ['The specified product does not allow to choose a seat.']
try:
@@ -1281,6 +1285,9 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
if not simulate:
for cp in delete_cps:
if cp.addon_to_id:
continue
cp.addons.all().delete()
cp.delete()
order.total = sum([p.price for p in pos_map.values()])
+1 -1
View File
@@ -61,7 +61,7 @@ class VoucherSerializer(I18nAwareModelSerializer):
class Meta:
model = Voucher
fields = ('id', 'code', 'max_usages', 'redeemed', 'valid_until', 'block_quota',
fields = ('id', 'code', 'max_usages', 'redeemed', 'min_usages', 'valid_until', 'block_quota',
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
'tag', 'comment', 'subevent', 'show_hidden_items', 'seat')
read_only_fields = ('id', 'redeemed')
+172 -43
View File
@@ -19,19 +19,28 @@
# 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/>.
#
from collections import Counter
from typing import List
from django.db import transaction
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.translation import gettext as _
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.filters import OrderingFilter
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin
from rest_framework.response import Response
from rest_framework.settings import api_settings
from rest_framework.serializers import as_serializer_error
from pretix.api.serializers.cart import (
CartPositionCreateSerializer, CartPositionSerializer,
)
from pretix.base.models import CartPosition
from pretix.base.services.cart import (
_get_quota_availability, _get_voucher_availability, error_messages,
)
from pretix.base.services.locking import NoLockManager
@@ -54,18 +63,17 @@ class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['quota_cache'] = {}
ctx['quotas_for_item_cache'] = {}
ctx['quotas_for_variation_cache'] = {}
return ctx
def create(self, request, *args, **kwargs):
serializer = CartPositionCreateSerializer(data=request.data, context=self.get_serializer_context())
ctx = self.get_serializer_context()
serializer = CartPositionCreateSerializer(data=request.data, context=ctx)
serializer.is_valid(raise_exception=True)
with transaction.atomic(), self.request.event.lock():
self.perform_create(serializer)
cp = serializer.instance
serializer = CartPositionSerializer(cp, context=serializer.context)
results = self._create(serializers=[serializer], raise_exception=True, ctx=ctx)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
return Response(results[0]['data'], status=status.HTTP_201_CREATED, headers=headers)
@action(detail=False, methods=['POST'])
def bulk_create(self, request, *args, **kwargs):
@@ -73,42 +81,163 @@ class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
return Response({"error": "Please supply a list"}, status=status.HTTP_400_BAD_REQUEST)
ctx = self.get_serializer_context()
with transaction.atomic():
serializers = [
CartPositionCreateSerializer(data=d, context=ctx)
for d in request.data
]
lockfn = self.request.event.lock
if not any(s.is_valid(raise_exception=False) for s in serializers):
lockfn = NoLockManager
results = []
with lockfn():
for s in serializers:
if s.is_valid(raise_exception=False):
try:
cp = s.save()
except ValidationError as e:
results.append({
'success': False,
'data': None,
'errors': {api_settings.NON_FIELD_ERRORS_KEY: e.detail},
})
else:
results.append({
'success': True,
'data': CartPositionSerializer(cp, context=ctx).data,
'errors': None,
})
else:
results.append({
'success': False,
'data': None,
'errors': s.errors,
})
serializers = [
CartPositionCreateSerializer(data=d, context=ctx)
for d in request.data
]
results = self._create(serializers=serializers, raise_exception=False, ctx=ctx)
return Response({'results': results}, status=status.HTTP_200_OK)
def perform_create(self, serializer):
serializer.save()
raise NotImplementedError()
@transaction.atomic()
def perform_destroy(self, instance):
instance.addons.all().delete()
instance.delete()
def _require_locking(self, quota_diff, voucher_use_diff, seat_diff):
if voucher_use_diff or seat_diff:
# If any vouchers or seats are used, we lock to make sure we don't redeem them to often
return True
if quota_diff and any(q.size is not None for q in quota_diff):
# If any quotas are affected that are not unlimited, we lock
return True
return False
@cached_property
def _create_default_cart_id(self):
cid = "{}@api".format(get_random_string(48))
while CartPosition.objects.filter(cart_id=cid).exists():
cid = "{}@api".format(get_random_string(48))
return cid
def _create(self, serializers: List[CartPositionCreateSerializer], ctx, raise_exception=False):
voucher_use_diff = Counter()
quota_diff = Counter()
seat_diff = Counter()
results = [{} for pserializer in serializers]
for i, pserializer in enumerate(serializers):
if not pserializer.is_valid(raise_exception=raise_exception):
results[i] = {
'success': False,
'data': None,
'errors': pserializer.errors,
}
for pserializer in serializers:
if pserializer.errors:
continue
validated_data = pserializer.validated_data
if not validated_data.get('cart_id'):
validated_data['cart_id'] = self._create_default_cart_id
if validated_data.get('voucher'):
voucher_use_diff[validated_data['voucher']] += 1
if validated_data.get('seat'):
seat_diff[validated_data['seat']] += 1
for q in validated_data['_quotas']:
quota_diff[q] += 1
for sub_data in validated_data.get('addons', []) + validated_data.get('bundled', []):
for q in sub_data['_quotas']:
quota_diff[q] += 1
seats_seen = set()
lockfn = NoLockManager
if self._require_locking(quota_diff, voucher_use_diff, seat_diff):
lockfn = self.request.event.lock
with lockfn() as now_dt, transaction.atomic():
vouchers_ok, vouchers_depend_on_cart = _get_voucher_availability(
self.request.event,
voucher_use_diff,
now_dt,
exclude_position_ids=[],
)
quotas_ok = _get_quota_availability(quota_diff, now_dt)
for i, pserializer in enumerate(serializers):
if results[i]:
continue
try:
validated_data = pserializer.validated_data
if validated_data.get('seat'):
# Assumption: Add-ons currently can't have seats
if validated_data['seat'] in seats_seen:
raise ValidationError(error_messages['seat_multiple'])
seats_seen.add(validated_data['seat'])
quotas_needed = Counter()
for q in validated_data['_quotas']:
quotas_needed[q] += 1
for sub_data in validated_data.get('addons', []) + validated_data.get('bundled', []):
for q in sub_data['_quotas']:
quotas_needed[q] += 1
for q, needed in quotas_needed.items():
if quotas_ok[q] < needed:
raise ValidationError(
_('There is not enough quota available on quota "{}" to perform the operation.').format(
q.name
)
)
if validated_data.get('voucher'):
# Assumption: Add-ons currently can't have vouchers, thus we only need to check the main voucher
if vouchers_ok[validated_data['voucher']] < 1:
raise ValidationError(
{'voucher': [_('The specified voucher has already been used the maximum number of times.')]}
)
if validated_data.get('seat'):
# Assumption: Add-ons currently can't have seats, thus we only need to check the main product
if not validated_data['seat'].is_available(
sales_channel=validated_data.get('sales_channel', 'web'),
distance_ignore_cart_id=validated_data['cart_id'],
ignore_voucher_id=validated_data['voucher'].pk if validated_data.get('voucher') else None,
):
raise ValidationError(
{'seat': [_('The selected seat "{seat}" is not available.').format(seat=validated_data['seat'].name)]}
)
for q, needed in quotas_needed.items():
quotas_ok[q] -= needed
if validated_data.get('voucher'):
vouchers_ok[validated_data['voucher']] -= 1
if any(qa < 0 for qa in quotas_ok.values()):
# Safeguard, should never happen because of conditions above
raise ValidationError(error_messages['unavailable'])
cp = pserializer.create(validated_data)
d = CartPositionSerializer(cp, context=ctx).data
addons = sorted(cp.addons.all(), key=lambda a: a.pk) # order of creation, safe since they are created in the same transaction
d['addons'] = CartPositionSerializer([a for a in addons if not a.is_bundled], many=True, context=ctx).data
d['bundled'] = CartPositionSerializer([a for a in addons if a.is_bundled], many=True, context=ctx).data
results[i] = {
'success': True,
'data': d,
'errors': None,
}
except ValidationError as e:
if raise_exception:
raise
results[i] = {
'success': False,
'data': None,
'errors': as_serializer_error(e),
}
return results
+12 -2
View File
@@ -33,6 +33,7 @@
# License for the specific language governing permissions and limitations under the License.
import django_filters
from django.conf import settings
from django.db import transaction
from django.db.models import Prefetch, ProtectedError, Q
from django.utils.timezone import now
@@ -241,13 +242,17 @@ class EventViewSet(viewsets.ModelViewSet):
except Event.DoesNotExist:
raise ValidationError('Event to copy from was not found')
# Ensure that .installed() is only called when we NOT clone
plugins = serializer.validated_data.pop('plugins', None)
serializer.validated_data['plugins'] = None
new_event = serializer.save(organizer=self.request.organizer)
if copy_from:
new_event.copy_data_from(copy_from)
if 'plugins' in serializer.validated_data:
new_event.set_active_plugins(serializer.validated_data['plugins'])
if plugins is not None:
new_event.set_active_plugins(plugins)
if 'is_public' in serializer.validated_data:
new_event.is_public = serializer.validated_data['is_public']
if 'testmode' in serializer.validated_data:
@@ -256,12 +261,17 @@ class EventViewSet(viewsets.ModelViewSet):
new_event.sales_channels = serializer.validated_data['sales_channels']
if 'has_subevents' in serializer.validated_data:
new_event.has_subevents = serializer.validated_data['has_subevents']
if 'date_admission' in serializer.validated_data:
new_event.date_admission = serializer.validated_data['date_admission']
new_event.save()
if 'timezone' in serializer.validated_data:
new_event.settings.timezone = serializer.validated_data['timezone']
else:
serializer.instance.set_defaults()
new_event.set_active_plugins(plugins if plugins is not None else settings.PRETIX_PLUGINS_DEFAULT.split(','))
new_event.save(update_fields=['plugins'])
serializer.instance.log_action(
'pretix.event.added',
user=self.request.user,
+15 -2
View File
@@ -35,7 +35,8 @@ from rest_framework.reverse import reverse
from pretix.api.serializers.exporters import (
ExporterSerializer, JobRunSerializer,
)
from pretix.base.models import CachedFile, Device, TeamAPIToken
from pretix.base.exporter import OrganizerLevelExportMixin
from pretix.base.models import CachedFile, Device, Event, TeamAPIToken
from pretix.base.services.export import export, multiexport
from pretix.base.signals import (
register_data_exporters, register_multievent_data_exporters,
@@ -155,7 +156,19 @@ class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
organizer=self.request.organizer
)
responses = register_multievent_data_exporters.send(self.request.organizer)
for ex in sorted([response(events, self.request.organizer) for r, response in responses if response], key=lambda ex: str(ex.verbose_name)):
raw_exporters = [
response(Event.objects.none() if issubclass(response, OrganizerLevelExportMixin) else events, self.request.organizer)
for r, response in responses
if response
]
raw_exporters = [
ex for ex in raw_exporters
if (
not isinstance(ex, OrganizerLevelExportMixin) or
perm_holder.has_organizer_permission(self.request.organizer, ex.organizer_required_permission, self.request)
)
]
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
ex._serializer = JobRunSerializer(exporter=ex, events=events)
exporters.append(ex)
return exporters
+21 -4
View File
@@ -61,6 +61,7 @@ from pretix.api.serializers.orderchange import (
OrderPositionCreateForExistingOrderSerializer,
OrderPositionInfoPatchSerializer,
)
from pretix.api.views import RichOrderingFilter
from pretix.base.i18n import language
from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Checkin, Device, EventMetaValue,
@@ -679,28 +680,33 @@ class OrderViewSet(viewsets.ModelViewSet):
)
if order.require_approval:
email_template = request.event.settings.mail_text_order_placed_require_approval
subject_template = request.event.settings.mail_subject_order_placed_require_approval
log_entry = 'pretix.event.order.email.order_placed_require_approval'
email_attendees = False
elif free_flow:
email_template = request.event.settings.mail_text_order_free
subject_template = request.event.settings.mail_subject_order_free
log_entry = 'pretix.event.order.email.order_free'
email_attendees = request.event.settings.mail_send_order_free_attendee
email_attendees_template = request.event.settings.mail_text_order_free_attendee
subject_attendees_template = request.event.settings.mail_subject_order_free_attendee
else:
email_template = request.event.settings.mail_text_order_placed
subject_template = request.event.settings.mail_subject_order_placed
log_entry = 'pretix.event.order.email.order_placed'
email_attendees = request.event.settings.mail_send_order_placed_attendee
email_attendees_template = request.event.settings.mail_text_order_placed_attendee
subject_attendees_template = request.event.settings.mail_subject_order_placed_attendee
_order_placed_email(
request.event, order, payment.payment_provider if payment else None, email_template,
request.event, order, payment.payment_provider if payment else None, email_template, subject_template,
log_entry, invoice, payment, is_free=free_flow
)
if email_attendees:
for p in order.positions.all():
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
_order_placed_email_attendee(request.event, order, p, email_attendees_template, log_entry,
is_free=free_flow)
_order_placed_email_attendee(request.event, order, p, email_attendees_template, subject_attendees_template,
log_entry, is_free=free_flow)
if not free_flow and order.status == Order.STATUS_PAID and payment:
payment._send_paid_mail(invoice, None, '')
@@ -930,7 +936,7 @@ with scopes_disabled():
class OrderPositionViewSet(viewsets.ModelViewSet):
serializer_class = OrderPositionSerializer
queryset = OrderPosition.all.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
filter_backends = (DjangoFilterBackend, RichOrderingFilter)
ordering = ('order__datetime', 'positionid')
ordering_fields = ('order__code', 'order__datetime', 'positionid', 'attendee_name', 'order__status',)
filterset_class = OrderPositionFilter
@@ -1604,6 +1610,17 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
user=request.user if request.user.is_authenticated else None,
auth=request.auth
)
if r.state in (OrderRefund.REFUND_STATE_DONE, OrderRefund.REFUND_STATE_CANCELED, OrderRefund.REFUND_STATE_FAILED):
r.order.log_action(
f'pretix.event.order.refund.{r.state}', {
'local_id': r.local_id,
'provider': r.provider,
},
user=request.user if request.user.is_authenticated else None,
auth=request.auth
)
if mark_refunded:
try:
mark_order_refunded(
+12 -2
View File
@@ -51,7 +51,7 @@ from pretix.helpers.safe_openpyxl import ( # NOQA: backwards compatibility for
SafeWorkbook, remove_invalid_excel_chars as excel_safe,
)
__ = excel_safe # just so the compatbility import above is "used" and doesn't get removed by linter
__ = excel_safe # just so the compatibility import above is "used" and doesn't get removed by linter
class BaseExporter:
@@ -80,7 +80,7 @@ class BaseExporter:
def verbose_name(self) -> str:
"""
A human-readable name for this exporter. This should be short but
self-explaining. Good examples include 'JSON' or 'Microsoft Excel'.
self-explaining. Good examples include 'Orders as JSON' or 'Orders as Microsoft Excel'.
"""
raise NotImplementedError() # NOQA
@@ -137,6 +137,16 @@ class BaseExporter:
raise NotImplementedError() # NOQA
class OrganizerLevelExportMixin:
@property
def organizer_required_permission(self) -> str:
"""
The permission level required to use this exporter. Only useful for organizer-level exports,
not for event-level exports.
"""
return 'can_view_orders'
class ListExporter(BaseExporter):
ProgressSetTotal = namedtuple('ProgressSetTotal', 'total')
+1
View File
@@ -20,6 +20,7 @@
# <https://www.gnu.org/licenses/>.
#
from .answers import * # noqa
from .customers import * # noqa
from .dekodi import * # noqa
from .events import * # noqa
from .invoices import * # noqa
+113
View File
@@ -0,0 +1,113 @@
#
# 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/>.
#
# 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>.
#
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
#
# This file contains Apache-licensed contributions copyrighted by: Benjamin Hättasch, Tobias Kunze
#
# 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.
from collections import OrderedDict
from django.dispatch import receiver
from django.utils.timezone import get_current_timezone
from django.utils.translation import gettext as _, gettext_lazy
from pretix.base.settings import PERSON_NAME_SCHEMES
from ..exporter import ListExporter, OrganizerLevelExportMixin
from ..signals import register_multievent_data_exporters
class CustomerListExporter(OrganizerLevelExportMixin, ListExporter):
identifier = 'customerlist'
verbose_name = gettext_lazy('Customer accounts')
organizer_required_permission = 'can_manage_customers'
@property
def additional_form_fields(self):
return OrderedDict(
[]
)
def iterate_list(self, form_data):
qs = self.organizer.customers.prefetch_related('provider')
headers = [
_('Customer ID'),
_('SSO provider'),
_('External identifier'),
_('E-mail'),
_('Phone number'),
_('Full name'),
]
name_scheme = PERSON_NAME_SCHEMES[self.organizer.settings.name_scheme]
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
headers.append(_('Name') + ': ' + str(label))
headers += [
_('Account active'),
_('Verified email address'),
_('Last login'),
_('Registration date'),
_('Language'),
_('Notes'),
]
yield headers
tz = get_current_timezone()
for obj in qs:
row = [
obj.identifier,
obj.provider.name if obj.provider else None,
obj.external_identifier,
obj.email or '',
obj.phone or '',
obj.name,
]
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(obj.name_parts.get(k, ''))
row += [
_('Yes') if obj.is_active else _('No'),
_('Yes') if obj.is_verified else _('No'),
obj.last_login.astimezone(tz).date().strftime('%Y-%m-%d') if obj.last_login else '',
obj.date_joined.astimezone(tz).date().strftime('%Y-%m-%d') if obj.date_joined else '',
obj.get_locale_display(),
obj.notes or '',
]
yield row
def get_filename(self):
return '{}_customers'.format(self.organizer.slug)
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_customerlist")
def register_multievent_i_customerlist_exporter(sender, **kwargs):
return CustomerListExporter
+172 -173
View File
@@ -60,7 +60,9 @@ from pretix.base.settings import PERSON_NAME_SCHEMES
from ...control.forms.filter import get_all_payment_providers
from ...helpers import GroupConcat
from ...helpers.iter import chunked_iterable
from ..exporter import ListExporter, MultiSheetListExporter
from ..exporter import (
ListExporter, MultiSheetListExporter, OrganizerLevelExportMixin,
)
from ..signals import (
register_data_exporters, register_multievent_data_exporters,
)
@@ -884,76 +886,75 @@ class QuotaListExporter(ListExporter):
return '{}_quotas'.format(self.event.slug)
def generate_GiftCardTransactionListExporter(organizer): # hackhack
class GiftcardTransactionListExporter(ListExporter):
identifier = 'giftcardtransactionlist'
verbose_name = gettext_lazy('Gift card transactions')
class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
identifier = 'giftcardtransactionlist'
verbose_name = gettext_lazy('Gift card transactions')
organizer_required_permission = 'can_manage_gift_cards'
@property
def additional_form_fields(self):
d = [
('date_from',
forms.DateField(
label=_('Start date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
)),
('date_to',
forms.DateField(
label=_('End date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
)),
@property
def additional_form_fields(self):
d = [
('date_from',
forms.DateField(
label=_('Start date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
)),
('date_to',
forms.DateField(
label=_('End date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
)),
]
d = OrderedDict(d)
return d
def iterate_list(self, form_data):
qs = GiftCardTransaction.objects.filter(
card__issuer=self.organizer,
).order_by('datetime').select_related('card', 'order', 'order__event')
if form_data.get('date_from'):
date_value = form_data.get('date_from')
if isinstance(date_value, str):
date_value = dateutil.parser.parse(date_value).date()
qs = qs.filter(
datetime__gte=make_aware(datetime.combine(date_value, time(0, 0, 0)), self.timezone)
)
if form_data.get('date_to'):
date_value = form_data.get('date_to')
if isinstance(date_value, str):
date_value = dateutil.parser.parse(date_value).date()
qs = qs.filter(
datetime__lte=make_aware(datetime.combine(date_value, time(23, 59, 59, 999999)), self.timezone)
)
headers = [
_('Gift card code'),
_('Test mode'),
_('Date'),
_('Amount'),
_('Currency'),
_('Order'),
]
yield headers
for obj in qs:
row = [
obj.card.secret,
_('TEST MODE') if obj.card.testmode else '',
obj.datetime.astimezone(self.timezone).strftime('%Y-%m-%d %H:%M:%S'),
obj.value,
obj.card.currency,
obj.order.full_code if obj.order else None,
]
d = OrderedDict(d)
return d
yield row
def iterate_list(self, form_data):
qs = GiftCardTransaction.objects.filter(
card__issuer=organizer,
).order_by('datetime').select_related('card', 'order', 'order__event')
if form_data.get('date_from'):
date_value = form_data.get('date_from')
if isinstance(date_value, str):
date_value = dateutil.parser.parse(date_value).date()
qs = qs.filter(
datetime__gte=make_aware(datetime.combine(date_value, time(0, 0, 0)), self.timezone)
)
if form_data.get('date_to'):
date_value = form_data.get('date_to')
if isinstance(date_value, str):
date_value = dateutil.parser.parse(date_value).date()
qs = qs.filter(
datetime__lte=make_aware(datetime.combine(date_value, time(23, 59, 59, 999999)), self.timezone)
)
headers = [
_('Gift card code'),
_('Test mode'),
_('Date'),
_('Amount'),
_('Currency'),
_('Order'),
]
yield headers
for obj in qs:
row = [
obj.card.secret,
_('TEST MODE') if obj.card.testmode else '',
obj.datetime.astimezone(self.timezone).strftime('%Y-%m-%d %H:%M:%S'),
obj.value,
obj.card.currency,
obj.order.full_code if obj.order else None,
]
yield row
def get_filename(self):
return '{}_giftcardtransactions'.format(organizer.slug)
return GiftcardTransactionListExporter
def get_filename(self):
return '{}_giftcardtransactions'.format(self.organizer.slug)
class GiftcardRedemptionListExporter(ListExporter):
@@ -1000,114 +1001,112 @@ class GiftcardRedemptionListExporter(ListExporter):
return '{}_giftcardredemptions'.format(self.event.slug)
def generate_GiftCardListExporter(organizer): # hackhack
class GiftcardListExporter(ListExporter):
identifier = 'giftcardlist'
verbose_name = gettext_lazy('Gift cards')
class GiftcardListExporter(OrganizerLevelExportMixin, ListExporter):
identifier = 'giftcardlist'
verbose_name = gettext_lazy('Gift cards')
organizer_required_permission = 'can_manage_gift_cards'
@property
def additional_form_fields(self):
return OrderedDict(
[
('date', forms.DateTimeField(
label=_('Show value at'),
initial=now(),
)),
('testmode', forms.ChoiceField(
label=_('Test mode'),
choices=(
('', _('All')),
('yes', _('Test mode')),
('no', _('Live')),
),
initial='no',
required=False
)),
('state', forms.ChoiceField(
label=_('Status'),
choices=(
('', _('All')),
('empty', _('Empty')),
('valid_value', _('Valid and with value')),
('expired_value', _('Expired and with value')),
('expired', _('Expired')),
),
initial='valid_value',
required=False
))
]
)
def iterate_list(self, form_data):
s = GiftCardTransaction.objects.filter(
card=OuterRef('pk'),
datetime__lte=form_data['date']
).order_by().values('card').annotate(s=Sum('value')).values('s')
qs = organizer.issued_gift_cards.filter(
issuance__lte=form_data['date']
).annotate(
cached_value=Coalesce(Subquery(s), Decimal('0.00')),
).order_by('issuance').prefetch_related(
'transactions', 'transactions__order', 'transactions__order__event', 'transactions__order__invoices'
)
if form_data.get('testmode') == 'yes':
qs = qs.filter(testmode=True)
elif form_data.get('testmode') == 'no':
qs = qs.filter(testmode=False)
if form_data.get('state') == 'empty':
qs = qs.filter(cached_value=0)
elif form_data.get('state') == 'valid_value':
qs = qs.exclude(cached_value=0).filter(Q(expires__isnull=True) | Q(expires__gte=form_data['date']))
elif form_data.get('state') == 'expired_value':
qs = qs.exclude(cached_value=0).filter(expires__lt=form_data['date'])
elif form_data.get('state') == 'expired':
qs = qs.filter(expires__lt=form_data['date'])
headers = [
_('Gift card code'),
_('Test mode card'),
_('Creation date'),
_('Expiry date'),
_('Special terms and conditions'),
_('Currency'),
_('Current value'),
_('Created in order'),
_('Last invoice number of order'),
_('Last invoice date of order'),
@property
def additional_form_fields(self):
return OrderedDict(
[
('date', forms.DateTimeField(
label=_('Show value at'),
initial=now(),
)),
('testmode', forms.ChoiceField(
label=_('Test mode'),
choices=(
('', _('All')),
('yes', _('Test mode')),
('no', _('Live')),
),
initial='no',
required=False
)),
('state', forms.ChoiceField(
label=_('Status'),
choices=(
('', _('All')),
('empty', _('Empty')),
('valid_value', _('Valid and with value')),
('expired_value', _('Expired and with value')),
('expired', _('Expired')),
),
initial='valid_value',
required=False
))
]
yield headers
)
tz = get_current_timezone()
for obj in qs:
o = None
i = None
trans = list(obj.transactions.all())
if trans:
o = trans[0].order
if o:
invs = list(o.invoices.all())
if invs:
i = invs[-1]
row = [
obj.secret,
_('Yes') if obj.testmode else _('No'),
obj.issuance.astimezone(tz).date().strftime('%Y-%m-%d'),
obj.expires.astimezone(tz).date().strftime('%Y-%m-%d') if obj.expires else '',
obj.conditions or '',
obj.currency,
obj.cached_value,
o.full_code if o else '',
i.number if i else '',
i.date.strftime('%Y-%m-%d') if i else '',
]
yield row
def iterate_list(self, form_data):
s = GiftCardTransaction.objects.filter(
card=OuterRef('pk'),
datetime__lte=form_data['date']
).order_by().values('card').annotate(s=Sum('value')).values('s')
qs = self.organizer.issued_gift_cards.filter(
issuance__lte=form_data['date']
).annotate(
cached_value=Coalesce(Subquery(s), Decimal('0.00')),
).order_by('issuance').prefetch_related(
'transactions', 'transactions__order', 'transactions__order__event', 'transactions__order__invoices'
)
def get_filename(self):
return '{}_giftcards'.format(organizer.slug)
if form_data.get('testmode') == 'yes':
qs = qs.filter(testmode=True)
elif form_data.get('testmode') == 'no':
qs = qs.filter(testmode=False)
return GiftcardListExporter
if form_data.get('state') == 'empty':
qs = qs.filter(cached_value=0)
elif form_data.get('state') == 'valid_value':
qs = qs.exclude(cached_value=0).filter(Q(expires__isnull=True) | Q(expires__gte=form_data['date']))
elif form_data.get('state') == 'expired_value':
qs = qs.exclude(cached_value=0).filter(expires__lt=form_data['date'])
elif form_data.get('state') == 'expired':
qs = qs.filter(expires__lt=form_data['date'])
headers = [
_('Gift card code'),
_('Test mode card'),
_('Creation date'),
_('Expiry date'),
_('Special terms and conditions'),
_('Currency'),
_('Current value'),
_('Created in order'),
_('Last invoice number of order'),
_('Last invoice date of order'),
]
yield headers
tz = get_current_timezone()
for obj in qs:
o = None
i = None
trans = list(obj.transactions.all())
if trans:
o = trans[0].order
if o:
invs = list(o.invoices.all())
if invs:
i = invs[-1]
row = [
obj.secret,
_('Yes') if obj.testmode else _('No'),
obj.issuance.astimezone(tz).date().strftime('%Y-%m-%d'),
obj.expires.astimezone(tz).date().strftime('%Y-%m-%d') if obj.expires else '',
obj.conditions or '',
obj.currency,
obj.cached_value,
o.full_code if o else '',
i.number if i else '',
i.date.strftime('%Y-%m-%d') if i else '',
]
yield row
def get_filename(self):
return '{}_giftcards'.format(self.organizer.slug)
@receiver(register_data_exporters, dispatch_uid="exporter_orderlist")
@@ -1147,9 +1146,9 @@ def register_multievent_i_giftcardredemptionlist_exporter(sender, **kwargs):
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_giftcardlist")
def register_multievent_i_giftcardlist_exporter(sender, **kwargs):
return generate_GiftCardListExporter(sender)
return GiftcardListExporter
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_giftcardtransactionlist")
def register_multievent_i_giftcardtransactionlist_exporter(sender, **kwargs):
return generate_GiftCardTransactionListExporter(sender)
return GiftcardTransactionListExporter
@@ -103,6 +103,8 @@ class Command(BaseCommand):
with language(locale), override(timezone):
for receiver, response in signal_result:
if not response:
return None
ex = response(e, o, report_status)
if ex.identifier == options['export_provider']:
params = json.loads(options.get('parameters') or '{}')
@@ -79,9 +79,9 @@ class Command(BaseCommand):
if settings.SENTRY_ENABLED:
from sentry_sdk import capture_exception
capture_exception(err)
self.stdout.write(self.style.ERROR(f'ERROR runperiodic {str(err)}\n'))
self.stdout.write(self.style.ERROR(f'ERROR {name}: {str(err)}\n'))
else:
self.stdout.write(self.style.ERROR(f'ERROR runperiodic {str(err)}\n'))
self.stdout.write(self.style.ERROR(f'ERROR {name}: {str(err)}\n'))
traceback.print_exc()
else:
if options.get('verbosity') > 1:
@@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2022-10-12 09:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0222_alter_question_unique_together'),
]
operations = [
migrations.AddField(
model_name='voucher',
name='min_usages',
field=models.PositiveIntegerField(default=1),
),
]
+2 -1
View File
@@ -78,6 +78,7 @@ class Customer(LoggedModel):
organizer = models.ForeignKey(Organizer, related_name='customers', on_delete=models.CASCADE)
provider = models.ForeignKey(CustomerSSOProvider, related_name='customers', on_delete=models.PROTECT, null=True, blank=True)
identifier = models.CharField(
verbose_name=_('Customer ID'),
max_length=190,
db_index=True,
help_text=_('You can enter any value here to make it easier to match the data with other sources. If you do '
@@ -261,7 +262,7 @@ class Customer(LoggedModel):
) + '?id=' + self.identifier + '&token=' + token
mail(
self.email,
_('Activate your account at {organizer}').format(organizer=self.organizer.name),
self.organizer.settings.mail_subject_customer_registration,
self.organizer.settings.mail_text_customer_registration,
ctx,
locale=self.locale,
+1
View File
@@ -590,6 +590,7 @@ class Event(EventMixin, LoggedModel):
self.settings.event_list_type = 'calendar'
self.settings.invoice_email_attachment = True
self.settings.name_scheme = 'given_family'
self.settings.payment_banktransfer_invoice_immediately = True
@property
def social_image(self):
+52 -18
View File
@@ -564,17 +564,30 @@ class Order(LockModel, LoggedModel):
@cached_property
def user_cancel_fee(self):
fee = Decimal('0.00')
if self.event.settings.cancel_allow_user_paid_keep_fees:
fee += self.fees.filter(
fee_type__in=(OrderFee.FEE_TYPE_PAYMENT, OrderFee.FEE_TYPE_SHIPPING, OrderFee.FEE_TYPE_SERVICE,
OrderFee.FEE_TYPE_CANCELLATION)
).aggregate(
s=Sum('value')
)['s'] or 0
if self.event.settings.cancel_allow_user_paid_keep_percentage:
fee += self.event.settings.cancel_allow_user_paid_keep_percentage / Decimal('100.0') * (self.total - fee)
if self.event.settings.cancel_allow_user_paid_keep:
fee += self.event.settings.cancel_allow_user_paid_keep
if self.status == Order.STATUS_PAID:
if self.event.settings.cancel_allow_user_paid_keep_fees:
fee += self.fees.filter(
fee_type__in=(OrderFee.FEE_TYPE_PAYMENT, OrderFee.FEE_TYPE_SHIPPING, OrderFee.FEE_TYPE_SERVICE,
OrderFee.FEE_TYPE_CANCELLATION)
).aggregate(
s=Sum('value')
)['s'] or 0
if self.event.settings.cancel_allow_user_paid_keep_percentage:
fee += self.event.settings.cancel_allow_user_paid_keep_percentage / Decimal('100.0') * (self.total - fee)
if self.event.settings.cancel_allow_user_paid_keep:
fee += self.event.settings.cancel_allow_user_paid_keep
else:
if self.event.settings.cancel_allow_user_unpaid_keep_fees:
fee += self.fees.filter(
fee_type__in=(OrderFee.FEE_TYPE_PAYMENT, OrderFee.FEE_TYPE_SHIPPING, OrderFee.FEE_TYPE_SERVICE,
OrderFee.FEE_TYPE_CANCELLATION)
).aggregate(
s=Sum('value')
)['s'] or 0
if self.event.settings.cancel_allow_user_unpaid_keep_percentage:
fee += self.event.settings.cancel_allow_user_unpaid_keep_percentage / Decimal('100.0') * (self.total - fee)
if self.event.settings.cancel_allow_user_unpaid_keep:
fee += self.event.settings.cancel_allow_user_unpaid_keep
return round_decimal(min(fee, self.total), self.event.currency)
@property
@@ -642,10 +655,12 @@ class Order(LockModel, LoggedModel):
if self.user_cancel_deadline and now() > self.user_cancel_deadline:
return False
if self.status == Order.STATUS_PAID or self.payment_refund_sum > Decimal('0.00'):
if self.status == Order.STATUS_PAID:
if self.total == Decimal('0.00'):
return self.event.settings.cancel_allow_user
return self.event.settings.cancel_allow_user_paid
elif self.payment_refund_sum > Decimal('0.00'):
return False
elif self.status == Order.STATUS_PENDING:
return self.event.settings.cancel_allow_user
return False
@@ -1027,7 +1042,7 @@ class Order(LockModel, LoggedModel):
with language(self.locale, self.event.settings.region):
email_template = self.event.settings.mail_text_resend_link
email_context = get_email_context(event=self.event, order=self)
email_subject = _('Your order: %(code)s') % {'code': self.code}
email_subject = self.event.settings.mail_subject_resend_link
self.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.resend', user=user, auth=auth,
@@ -1738,8 +1753,8 @@ class OrderPayment(models.Model):
with language(self.order.locale, self.order.event.settings.region):
email_template = self.order.event.settings.mail_text_order_paid_attendee
email_subject = self.order.event.settings.mail_subject_order_paid_attendee
email_context = get_email_context(event=self.order.event, order=self.order, position=position)
email_subject = _('Event registration confirmed: %(code)s') % {'code': self.order.code}
try:
position.send_mail(
email_subject, email_template, email_context,
@@ -1756,8 +1771,8 @@ class OrderPayment(models.Model):
with language(self.order.locale, self.order.event.settings.region):
email_template = self.order.event.settings.mail_text_order_paid
email_subject = self.order.event.settings.mail_subject_order_paid
email_context = get_email_context(event=self.order.event, order=self.order, payment_info=mail_text)
email_subject = _('Payment received for your order: %(code)s') % {'code': self.order.code}
try:
self.order.send_mail(
email_subject, email_template, email_context,
@@ -2237,7 +2252,7 @@ class OrderPosition(AbstractPosition):
@cached_property
def sort_key(self):
return self.addon_to.positionid if self.addon_to else self.positionid, self.addon_to_id or 0
return self.addon_to.positionid if self.addon_to else self.positionid, self.addon_to_id or 0, self.positionid
@property
def checkins(self):
@@ -2263,7 +2278,7 @@ class OrderPosition(AbstractPosition):
ops = []
cp_mapping = {}
# The sorting key ensures that all addons come directly after the position they refer to
for i, cartpos in enumerate(sorted(cp, key=lambda c: (c.addon_to_id or c.pk, c.addon_to_id or 0))):
for i, cartpos in enumerate(sorted(cp, key=lambda c: c.sort_key)):
op = OrderPosition(order=order)
for f in AbstractPosition._meta.fields:
if f.name == 'addon_to':
@@ -2437,7 +2452,7 @@ class OrderPosition(AbstractPosition):
with language(self.order.locale, self.order.event.settings.region):
email_template = self.event.settings.mail_text_resend_link
email_context = get_email_context(event=self.order.event, order=self.order, position=self)
email_subject = _('Your event registration: %(code)s') % {'code': self.order.code}
email_subject = self.event.settings.mail_subject_resend_link
self.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.resend', user=user, auth=auth,
@@ -2659,6 +2674,20 @@ class CartPosition(AbstractPosition):
self.event.currency)
return self.price - net
@cached_property
def sort_key(self):
subevent_key = (self.subevent.date_from, str(self.subevent.name), self.subevent_id) if self.subevent_id else (0, "", 0)
category_key = (self.item.category.position, self.item.category.id) if self.item.category_id is not None else (0, 0)
item_key = self.item.position, self.item_id
variation_key = (self.variation.position, self.variation.id) if self.variation_id is not None else (0, 0)
line_key = (self.price, (self.voucher_id or 0), (self.seat.sorting_rank if self.seat_id else None), self.pk)
sort_key = subevent_key + category_key + item_key + variation_key + line_key
if self.addon_to_id:
return self.addon_to.sort_key + (1 if self.is_bundled else 2,) + sort_key
else:
return sort_key
def update_listed_price_and_voucher(self, voucher_only=False, max_discount=None):
from pretix.base.services.pricing import (
get_listed_price, is_included_for_free,
@@ -2714,6 +2743,11 @@ class CartPosition(AbstractPosition):
self.tax_rate = line_price.rate
self.save(update_fields=['line_price_gross', 'tax_rate'])
@property
def addons_without_bundled(self):
addons = [op for op in self.addons.all() if not op.is_bundled]
return sorted(addons, key=lambda cp: cp.sort_key)
class InvoiceAddress(models.Model):
last_modified = models.DateTimeField(auto_now=True)
+19 -1
View File
@@ -137,6 +137,8 @@ class Voucher(LoggedModel):
:type max_usages: int
:param redeemed: The number of times this voucher already has been redeemed
:type redeemed: int
:param min_usages: The minimum number of times this voucher must be redeemed
:type min_usages: int
:param valid_until: The expiration date of this voucher (optional)
:type valid_until: datetime
:param block_quota: If set to true, this voucher will reserve quota for its holder
@@ -199,6 +201,14 @@ class Voucher(LoggedModel):
verbose_name=_("Redeemed"),
default=0
)
min_usages = models.PositiveIntegerField(
verbose_name=_("Minimum usages"),
help_text=_("If set to more than one, the voucher must be redeemed for this many products when it is used for "
"the first time. On later usages, it can also be used for lower numbers of products. Note that "
"this means that the total number of usages in some cases can be lower than this limit, e.g. in "
"case of cancellations."),
default=1
)
budget = models.DecimalField(
verbose_name=_("Maximum discount budget"),
help_text=_("This is the maximum monetary amount that will be discounted using this voucher across all usages. "
@@ -350,6 +360,10 @@ class Voucher(LoggedModel):
'redeemed': redeemed
}
)
if data.get('max_usages', 1) < data.get('min_usages', 1):
raise ValidationError(
_('The maximum number of usages may not be lower than the minimum number of usages.'),
)
@staticmethod
def clean_subevent(data, event):
@@ -464,7 +478,7 @@ class Voucher(LoggedModel):
if quota:
raise ValidationError(_('You need to choose a specific product if you select a seat.'))
if data.get('max_usages', 1) > 1:
if data.get('max_usages', 1) > 1 or data.get('min_usages', 1) > 1:
raise ValidationError(_('Seat-specific vouchers can only be used once.'))
if item and seat.product != item:
@@ -567,6 +581,10 @@ class Voucher(LoggedModel):
else:
return bool(subevent.seating_plan) if subevent else self.event.seating_plan
@property
def min_usages_remaining(self):
return max(1, self.min_usages - self.redeemed)
@classmethod
def annotate_budget_used_orders(cls, qs):
opq = OrderPosition.objects.filter(
+1 -1
View File
@@ -216,7 +216,7 @@ class WaitingListEntry(LoggedModel):
with language(self.locale, self.event.settings.region):
mail(
self.email,
_('You have been selected from the waitinglist for {event}').format(event=str(self.event)),
self.event.settings.mail_subject_waiting_list,
self.event.settings.mail_text_waiting_list,
get_email_context(event=self.event, waiting_list_entry=self),
self.event,
+41 -3
View File
@@ -40,6 +40,7 @@ import os
import re
import subprocess
import tempfile
import unicodedata
import uuid
from collections import OrderedDict
from functools import partial
@@ -827,6 +828,13 @@ class Renderer:
if o['italic']:
font += ' I'
try:
ad = getAscentDescent(font, float(o['fontsize']))
except KeyError: # font not known, fall back
logger.warning(f'Use of unknown font "{font}"')
font = 'Open Sans'
ad = getAscentDescent(font, float(o['fontsize']))
align_map = {
'left': TA_LEFT,
'center': TA_CENTER,
@@ -853,10 +861,12 @@ class Renderer:
except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
# reportlab does not support unicode combination characters
text = unicodedata.normalize("NFKC", text)
p = Paragraph(text, style=style)
w, h = p.wrapOn(canvas, float(o['width']) * mm, 1000 * mm)
# p_size = p.wrap(float(o['width']) * mm, 1000 * mm)
ad = getAscentDescent(font, float(o['fontsize']))
canvas.saveState()
# The ascent/descent offsets here are not really proven to be correct, they're just empirical values to get
# reportlab render similarly to browser canvas.
@@ -891,7 +901,11 @@ class Renderer:
elif o['type'] == "poweredby":
self._draw_poweredby(canvas, op, o)
if self.bg_pdf:
canvas.setPageSize((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[3])
if self.bg_pdf.pages[0].get('/Rotate') in (90, 270):
# swap dimensions due to pdf being rotated
page_size = page_size[::-1]
canvas.setPageSize(page_size)
if show_page:
canvas.showPage()
@@ -915,13 +929,37 @@ class Renderer:
with open(os.path.join(d, 'out.pdf'), 'rb') as f:
return BytesIO(f.read())
else:
from PyPDF2 import PdfReader, PdfWriter
from PyPDF2 import PdfReader, PdfWriter, Transformation
from PyPDF2.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_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)
@@ -23,11 +23,12 @@
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: pretix_sig1.proto
"""Generated protocol buffer code."""
from google.protobuf import (
descriptor as _descriptor, message as _message, reflection as _reflection,
descriptor as _descriptor, descriptor_pool as _descriptor_pool,
symbol_database as _symbol_database,
)
from google.protobuf.internal import builder as _builder
# @@protoc_insertion_point(imports)
@@ -36,80 +37,14 @@ _sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor.FileDescriptor(
name='pretix_sig1.proto',
package='',
syntax='proto3',
serialized_options=b'\n\026eu.pretix.secrets.sig1B\014TicketProtos',
create_key=_descriptor._internal_create_key,
serialized_pb=b'\n\x11pretix_sig1.proto\"I\n\x06Ticket\x12\x0c\n\x04seed\x18\x01 \x01(\t\x12\x0c\n\x04item\x18\x02 \x01(\x03\x12\x11\n\tvariation\x18\x03 \x01(\x03\x12\x10\n\x08subevent\x18\x04 \x01(\x03\x42&\n\x16\x65u.pretix.secrets.sig1B\x0cTicketProtosb\x06proto3'
)
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x11pretix_sig1.proto\"I\n\x06Ticket\x12\x0c\n\x04seed\x18\x01 \x01(\t\x12\x0c\n\x04item\x18\x02 \x01(\x03\x12\x11\n\tvariation\x18\x03 \x01(\x03\x12\x10\n\x08subevent\x18\x04 \x01(\x03\x42\x33\n#eu.pretix.libpretixsync.crypto.sig1B\x0cTicketProtosb\x06proto3')
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'pretix_sig1_pb2', globals())
if _descriptor._USE_C_DESCRIPTORS == False:
_TICKET = _descriptor.Descriptor(
name='Ticket',
full_name='Ticket',
filename=None,
file=DESCRIPTOR,
containing_type=None,
create_key=_descriptor._internal_create_key,
fields=[
_descriptor.FieldDescriptor(
name='seed', full_name='Ticket.seed', index=0,
number=1, type=9, cpp_type=9, label=1,
has_default_value=False, default_value=b"".decode('utf-8'),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='item', full_name='Ticket.item', index=1,
number=2, type=3, cpp_type=2, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='variation', full_name='Ticket.variation', index=2,
number=3, type=3, cpp_type=2, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='subevent', full_name='Ticket.subevent', index=3,
number=4, type=3, cpp_type=2, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
],
extensions=[
],
nested_types=[],
enum_types=[
],
serialized_options=None,
is_extendable=False,
syntax='proto3',
extension_ranges=[],
oneofs=[
],
serialized_start=21,
serialized_end=94,
)
DESCRIPTOR.message_types_by_name['Ticket'] = _TICKET
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
Ticket = _reflection.GeneratedProtocolMessageType('Ticket', (_message.Message,), {
'DESCRIPTOR' : _TICKET,
'__module__' : 'pretix_sig1_pb2'
# @@protoc_insertion_point(class_scope:Ticket)
})
_sym_db.RegisterMessage(Ticket)
DESCRIPTOR._options = None
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n#eu.pretix.libpretixsync.crypto.sig1B\014TicketProtos'
_TICKET._serialized_start=21
_TICKET._serialized_end=94
# @@protoc_insertion_point(module_scope)
+127 -42
View File
@@ -101,15 +101,20 @@ error_messages = {
'min_items_per_product': _("You need to select at least %(min)s items of the product %(product)s."),
'min_items_per_product_removed': _("We removed %(product)s from your cart as you can not buy less than "
"%(min)s items of it."),
'not_started': _('The presale period for this event has not yet started.'),
'ended': _('The presale period for this event has ended.'),
'not_started': _('The booking period for this event has not yet started.'),
'ended': _('The booking period for this event has ended.'),
'payment_ended': _('All payments for this event need to be confirmed already, so no new orders can be created.'),
'some_subevent_not_started': _('The presale period for this event has not yet started. The affected positions '
'some_subevent_not_started': _('The booking period for this event has not yet started. The affected positions '
'have been removed from your cart.'),
'some_subevent_ended': _('The presale period for one of the events in your cart has ended. The affected '
'some_subevent_ended': _('The booking period for one of the events in your cart has ended. The affected '
'positions have been removed from your cart.'),
'price_too_high': _('The entered price is to high.'),
'voucher_invalid': _('This voucher code is not known in our database.'),
'voucher_min_usages': _('The voucher code "%(voucher)s" can only be used if you select at least %(number)s '
'matching products.'),
'voucher_min_usages_removed': _('The voucher code "%(voucher)s" can only be used if you select at least '
'%(number)s matching products. We have therefore removed some positions from '
'your cart that can no longer be purchased like this.'),
'voucher_redeemed': _('This voucher code has already been used the maximum number of times allowed.'),
'voucher_redeemed_cart': _('This voucher code is currently locked since it is already contained in a cart. This '
'might mean that someone else is redeeming this voucher right now, or that you tried '
@@ -147,11 +152,50 @@ error_messages = {
}
def _get_quota_availability(quota_diff, now_dt):
quotas_ok = defaultdict(int)
qa = QuotaAvailability()
qa.queue(*[k for k, v in quota_diff.items() if v > 0])
qa.compute(now_dt=now_dt)
for quota, count in quota_diff.items():
if count <= 0:
quotas_ok[quota] = 0
break
avail = qa.results[quota]
if avail[1] is not None and avail[1] < count:
quotas_ok[quota] = min(count, avail[1])
else:
quotas_ok[quota] = count
return quotas_ok
def _get_voucher_availability(event, voucher_use_diff, now_dt, exclude_position_ids):
vouchers_ok = {}
_voucher_depend_on_cart = set()
for voucher, count in voucher_use_diff.items():
voucher.refresh_from_db()
if voucher.valid_until is not None and voucher.valid_until < now_dt:
raise CartError(error_messages['voucher_expired'])
redeemed_in_carts = CartPosition.objects.filter(
Q(voucher=voucher) & Q(event=event) &
Q(expires__gte=now_dt)
).exclude(pk__in=exclude_position_ids)
cart_count = redeemed_in_carts.count()
v_avail = voucher.max_usages - voucher.redeemed - cart_count
if cart_count > 0:
_voucher_depend_on_cart.add(voucher)
vouchers_ok[voucher] = v_avail
return vouchers_ok, _voucher_depend_on_cart
class CartManager:
AddOperation = namedtuple('AddOperation', ('count', 'item', 'variation', 'voucher', 'quotas',
'addon_to', 'subevent', 'bundled', 'seat', 'listed_price',
'price_after_voucher', 'custom_price_input',
'custom_price_input_is_net'))
'custom_price_input_is_net', 'voucher_ignored'))
RemoveOperation = namedtuple('RemoveOperation', ('position',))
VoucherOperation = namedtuple('VoucherOperation', ('position', 'voucher', 'price_after_voucher'))
ExtendOperation = namedtuple('ExtendOperation', ('position', 'count', 'item', 'variation', 'voucher',
@@ -286,12 +330,16 @@ class CartManager:
(isinstance(op, self.ExtendOperation) and op.position.is_bundled)
):
if op.item.require_voucher and op.voucher is None:
if getattr(op, 'voucher_ignored', False):
raise CartError(error_messages['voucher_redeemed'])
raise CartError(error_messages['voucher_required'])
if (
(op.item.hide_without_voucher or (op.variation and op.variation.hide_without_voucher)) and
(op.voucher is None or not op.voucher.show_hidden_items)
):
if getattr(op, 'voucher_ignored', False):
raise CartError(error_messages['voucher_redeemed'])
raise CartError(error_messages['voucher_required'])
if not op.item.is_available() or (op.variation and not op.variation.is_available()):
@@ -436,7 +484,7 @@ class CartManager:
self._check_item_constraints(op)
if cp.voucher:
self._voucher_use_diff[cp.voucher] += 1
self._voucher_use_diff[cp.voucher] += 2
self._operations.append(op)
return err
@@ -485,6 +533,15 @@ class CartManager:
voucher_use_diff[voucher] += 1
ops.append((listed_price - price_after_voucher, self.VoucherOperation(p, voucher, price_after_voucher)))
for voucher, cnt in list(voucher_use_diff.items()):
if 0 < cnt < voucher.min_usages_remaining:
raise CartError(
_(error_messages['voucher_min_usages']) % {
'voucher': voucher.code,
'number': voucher.min_usages_remaining,
}
)
# If there are not enough voucher usages left for the full cart, let's apply them in the order that benefits
# the user the most.
ops.sort(key=lambda k: k[0], reverse=True)
@@ -533,6 +590,7 @@ class CartManager:
item = self._items_cache[i['item']]
variation = self._variations_cache[i['variation']] if i['variation'] is not None else None
voucher = None
voucher_ignored = False
if i.get('voucher'):
try:
@@ -542,6 +600,24 @@ class CartManager:
else:
voucher_use_diff[voucher] += i['count']
if i.get('voucher_ignore_if_redeemed', False):
# This is a special case handling for when a user clicks "+" on an existing line in their cart
# that has a voucher attached. If the voucher still has redemptions left, we'll add another line
# with the same voucher, but if it does not we silently continue as if there was no voucher,
# leading to either a higher-priced ticket or an error. Still, this leads to less error cases
# than either of the possible default assumptions.
predicted_redeemed_after = (
voucher.redeemed +
CartPosition.objects.filter(voucher=voucher, expires__gte=self.now_dt).count() +
self._voucher_use_diff[voucher] +
voucher_use_diff[voucher]
)
if predicted_redeemed_after > voucher.max_usages:
i.pop('voucher')
voucher_ignored = True
voucher = None
voucher_use_diff[voucher] -= i['count']
# Fetch all quotas. If there are no quotas, this item is not allowed to be sold.
quotas = list(item.quotas.filter(subevent=subevent)
if variation is None else variation.quotas.filter(subevent=subevent))
@@ -588,6 +664,7 @@ class CartManager:
price_after_voucher=bundle.designated_price,
custom_price_input=None,
custom_price_input_is_net=False,
voucher_ignored=False,
)
self._check_item_constraints(bop, operations)
bundled.append(bop)
@@ -617,6 +694,7 @@ class CartManager:
price_after_voucher=price_after_voucher,
custom_price_input=custom_price,
custom_price_input_is_net=self.event.settings.display_net_prices,
voucher_ignored=voucher_ignored,
)
self._check_item_constraints(op, operations)
operations.append(op)
@@ -748,6 +826,7 @@ class CartManager:
price_after_voucher=listed_price,
custom_price_input=custom_price,
custom_price_input_is_net=self.event.settings.display_net_prices,
voucher_ignored=False,
)
self._check_item_constraints(op, operations)
operations.append(op)
@@ -819,43 +898,13 @@ class CartManager:
self._quota_diff.update(quota_diff)
self._operations += operations
def _get_quota_availability(self):
quotas_ok = defaultdict(int)
qa = QuotaAvailability()
qa.queue(*[k for k, v in self._quota_diff.items() if v > 0])
qa.compute(now_dt=self.now_dt)
for quota, count in self._quota_diff.items():
if count <= 0:
quotas_ok[quota] = 0
break
avail = qa.results[quota]
if avail[1] is not None and avail[1] < count:
quotas_ok[quota] = min(count, avail[1])
else:
quotas_ok[quota] = count
return quotas_ok
def _get_voucher_availability(self):
vouchers_ok = {}
self._voucher_depend_on_cart = set()
for voucher, count in self._voucher_use_diff.items():
voucher.refresh_from_db()
if voucher.valid_until is not None and voucher.valid_until < self.now_dt:
raise CartError(error_messages['voucher_expired'])
redeemed_in_carts = CartPosition.objects.filter(
Q(voucher=voucher) & Q(event=self.event) &
Q(expires__gte=self.now_dt)
).exclude(pk__in=[
vouchers_ok, self._voucher_depend_on_cart = _get_voucher_availability(
self.event, self._voucher_use_diff, self.now_dt,
exclude_position_ids=[
op.position.id for op in self._operations if isinstance(op, self.ExtendOperation)
])
cart_count = redeemed_in_carts.count()
v_avail = voucher.max_usages - voucher.redeemed - cart_count
if cart_count > 0:
self._voucher_depend_on_cart.add(voucher)
vouchers_ok[voucher] = v_avail
]
)
return vouchers_ok
def _check_min_max_per_product(self):
@@ -906,9 +955,44 @@ class CartManager:
)
return err
def _check_min_per_voucher(self):
vouchers = Counter()
for p in self.positions:
vouchers[p.voucher] += 1
for op in self._operations:
if isinstance(op, self.AddOperation):
vouchers[op.voucher] += op.count
elif isinstance(op, self.RemoveOperation):
vouchers[op.position.voucher] -= 1
err = None
for voucher, count in vouchers.items():
if not voucher or count == 0:
continue
if count < voucher.min_usages_remaining:
self._operations = [o for o in self._operations if not (
isinstance(o, self.AddOperation) and o.voucher and o.voucher.pk == voucher.pk
)]
removals = [o.position.pk for o in self._operations if isinstance(o, self.RemoveOperation)]
for p in self.positions:
if p.voucher_id == voucher.pk and p.pk not in removals:
self._operations.append(self.RemoveOperation(position=p))
err = _(error_messages['voucher_min_usages_removed']) % {
'voucher': voucher.code,
'number': voucher.min_usages_remaining,
}
if not err:
raise CartError(
_(error_messages['voucher_min_usages']) % {
'voucher': voucher.code,
'number': voucher.min_usages_remaining,
}
)
return err
def _perform_operations(self):
vouchers_ok = self._get_voucher_availability()
quotas_ok = self._get_quota_availability()
quotas_ok = _get_quota_availability(self._quota_diff, self.now_dt)
err = None
new_cart_positions = []
@@ -1162,6 +1246,7 @@ class CartManager:
err = self._delete_out_of_timeframe()
err = self.extend_expired_positions() or err
err = err or self._check_min_per_voucher()
lockfn = NoLockManager
if self._require_locking():
+17 -7
View File
@@ -26,6 +26,7 @@ from django.core.files.base import ContentFile
from django.utils.timezone import override
from django.utils.translation import gettext
from pretix.base.exporter import OrganizerLevelExportMixin
from pretix.base.i18n import LazyLocaleException, language
from pretix.base.models import (
CachedFile, Device, Event, Organizer, TeamAPIToken, User, cachedfile_name,
@@ -66,8 +67,8 @@ 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
file.file.save(cachedfile_name(file, file.filename), ContentFile(data))
file.save()
f = ContentFile(data)
file.file.save(cachedfile_name(file, file.filename), f)
return file.pk
@@ -101,9 +102,9 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
timezone = e.settings.timezone
region = e.settings.region
else:
locale = settings.LANGUAGE_CODE
timezone = settings.TIME_ZONE
region = None
locale = organizer.settings.locale or settings.LANGUAGE_CODE
timezone = organizer.settings.timezone or settings.TIME_ZONE
region = organizer.settings.region
with language(locale, region), override(timezone):
if form_data.get('events') is not None:
if isinstance(form_data['events'][0], str):
@@ -119,12 +120,21 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
continue
ex = response(events, organizer, set_progress)
if ex.identifier == provider:
if (
isinstance(ex, OrganizerLevelExportMixin) and
not staff_session and
not (device or token or user).has_organizer_permission(organizer, ex.organizer_required_permission)
):
raise ExportError(
gettext('You do not have sufficient permission to perform this export.')
)
d = ex.render(form_data)
if d is None:
raise ExportError(
gettext('Your export did not contain any data.')
)
file.filename, file.type, data = d
file.file.save(cachedfile_name(file, file.filename), ContentFile(data))
file.save()
f = ContentFile(data)
file.file.save(cachedfile_name(file, file.filename), f)
return file.pk
+3 -3
View File
@@ -77,7 +77,7 @@ class LockTimeoutException(Exception):
pass
class LockReleaseException(Exception):
class LockReleaseException(LockTimeoutException):
pass
@@ -163,7 +163,7 @@ def lock_event_redis(event):
retries = 5
for i in range(retries):
try:
if lock.acquire(False):
if lock.acquire(blocking=False):
return True
except RedisError:
logger.exception('Error locking an event')
@@ -180,5 +180,5 @@ def release_event_redis(event):
lock.release()
except RedisError:
logger.exception('Error releasing an event lock')
raise LockTimeoutException()
raise LockReleaseException()
event._lock = None
+5 -3
View File
@@ -63,6 +63,7 @@ from django.utils.timezone import now, override
from django.utils.translation import gettext as _, pgettext
from django_scopes import scope, scopes_disabled
from i18nfield.strings import LazyI18nString
from text_unidecode import unidecode
from pretix.base.email import ClassicMailRenderer
from pretix.base.i18n import language
@@ -196,7 +197,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
else:
sender = formataddr((settings.PRETIX_INSTANCE_NAME, sender))
subject = raw_subject = str(subject)
subject = raw_subject = str(subject).replace('\n', ' ').replace('\r', '')[:900]
signature = ""
bcc = []
@@ -431,8 +432,9 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
}
)
if attach_ical:
fname = re.sub('[^a-zA-Z0-9 ]', '-', unidecode(pgettext('attachment_filename', 'Calendar invite')))
for i, cal in enumerate(get_private_icals(event, [position] if position else order.positions.all())):
email.attach('event-{}.ics'.format(i), cal.serialize(), 'text/calendar')
email.attach('{}{}.ics'.format(fname, f'-{i + 1}' if i > 0 else ''), cal.serialize(), 'text/calendar')
email = email_filter.send_chained(event, 'message', message=email, order=order, user=user)
@@ -542,7 +544,7 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
if not any(c >= 500 for c in smtp_codes):
# Not a permanent failure (mailbox full, service unavailable), retry later, but with large intervals
try:
self.retry(max_retries=5, countdown=[60, 300, 600, 1200, 1800][self.request.retries])
self.retry(max_retries=5, countdown=[60, 300, 600, 1200, 1800, 1800][self.request.retries])
except MaxRetriesExceededError:
# ignore and go on with logging the error
pass
+58 -30
View File
@@ -54,7 +54,7 @@ from django.db.transaction import get_connection
from django.dispatch import receiver
from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now
from django.utils.translation import gettext as _, gettext_lazy
from django.utils.translation import gettext as _
from django_scopes import scopes_disabled
from pretix.api.models import OAuthApplication
@@ -113,8 +113,10 @@ error_messages = {
"surplus items from your cart."),
'busy': _('We were not able to process your request completely as the '
'server was too busy. Please try again.'),
'not_started': _('The presale period for this event has not yet started.'),
'ended': _('The presale period has ended.'),
'not_started': _('The booking period for this event has not yet started.'),
'ended': _('The booking period has ended.'),
'voucher_min_usages': _('The voucher code "%(voucher)s" can only be used if you select at least %(number)s '
'matching products.'),
'voucher_invalid': _('The voucher code used for one of the items in your cart is not known in our database.'),
'voucher_redeemed': _('The voucher code used for one of the items in your cart has already been used the maximum '
'number of times allowed. We removed this item from your cart.'),
@@ -125,9 +127,9 @@ error_messages = {
'voucher_invalid_item': _('The voucher code used for one of the items in your cart is not valid for this item. We '
'removed this item from your cart.'),
'voucher_required': _('You need a valid voucher code to order one of the products.'),
'some_subevent_not_started': _('The presale period for one of the events in your cart has not yet started. The '
'some_subevent_not_started': _('The booking period for one of the events in your cart has not yet started. The '
'affected positions have been removed from your cart.'),
'some_subevent_ended': _('The presale period for one of the events in your cart has ended. The affected '
'some_subevent_ended': _('The booking period for one of the events in your cart has ended. The affected '
'positions have been removed from your cart.'),
'seat_invalid': _('One of the seats in your order was invalid, we removed the position from your cart.'),
'seat_unavailable': _('One of the seats in your order has been taken in the meantime, we removed the position from your cart.'),
@@ -322,10 +324,10 @@ def approve_order(order, user=None, send_mail: bool=True, auth=None, force=False
with language(order.locale, order.event.settings.region):
if order.total == Decimal('0.00'):
email_template = order.event.settings.mail_text_order_approved_free
email_subject = _('Order approved and confirmed: %(code)s') % {'code': order.code}
email_subject = order.event.settings.mail_subject_order_approved_free
else:
email_template = order.event.settings.mail_text_order_approved
email_subject = _('Order approved and awaiting payment: %(code)s') % {'code': order.code}
email_subject = order.event.settings.mail_subject_order_approved
email_context = get_email_context(event=order.event, order=order)
try:
@@ -371,9 +373,9 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
if send_mail:
email_template = order.event.settings.mail_text_order_denied
email_subject = order.event.settings.mail_subject_order_denied
email_context = get_email_context(event=order.event, order=order, comment=comment)
with language(order.locale, order.event.settings.region):
email_subject = _('Order denied: %(code)s') % {'code': order.code}
try:
order.send_mail(
email_subject, email_template, email_context,
@@ -460,9 +462,13 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
f._calculate_tax()
f.save()
if order.payment_refund_sum < cancellation_fee:
raise OrderError(_('The cancellation fee cannot be higher than the payment credit of this order.'))
order.status = Order.STATUS_PAID
if cancellation_fee > order.total:
raise OrderError(_('The cancellation fee cannot be higher than the total amount of this order.'))
elif order.payment_refund_sum < cancellation_fee:
order.status = Order.STATUS_PENDING
order.set_expires()
else:
order.status = Order.STATUS_PAID
order.total = cancellation_fee
order.cancellation_date = now()
order.save(update_fields=['status', 'cancellation_date', 'total'])
@@ -489,10 +495,10 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
order.create_transactions()
if send_mail:
email_template = order.event.settings.mail_text_order_canceled
with language(order.locale, order.event.settings.region):
email_template = order.event.settings.mail_text_order_canceled
email_subject = order.event.settings.mail_subject_order_canceled
email_context = get_email_context(event=order.event, order=order, comment=comment or "")
email_subject = _('Order canceled: %(code)s') % {'code': order.code}
try:
order.send_mail(
email_subject, email_template, email_context,
@@ -569,6 +575,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
products_seen = Counter()
q_avail = Counter()
v_avail = Counter()
v_usages = Counter()
v_budget = {}
deleted_positions = set()
seats_seen = set()
@@ -606,6 +613,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
break
if cp.voucher:
v_usages[cp.voucher] += 1
if cp.voucher not in v_avail:
redeemed_in_carts = CartPosition.objects.filter(
Q(voucher=cp.voucher) & Q(event=event) & Q(expires__gte=now_dt)
@@ -717,6 +725,13 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
# Sorry, can't let you keep that!
delete(cp)
for voucher, cnt in v_usages.items():
if 0 < cnt < voucher.min_usages_remaining:
raise OrderError(error_messages['voucher_min_usages'], {
'voucher': voucher.code,
'number': voucher.min_usages_remaining,
})
# Check prices
sorted_positions = [cp for cp in sorted_positions if cp.pk and cp.pk not in deleted_positions]
old_total = sum(cp.price for cp in sorted_positions)
@@ -927,13 +942,12 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
return order, p
def _order_placed_email(event: Event, order: Order, pprov: BasePaymentProvider, email_template, log_entry: str,
invoice, payment: OrderPayment, is_free=False):
def _order_placed_email(event: Event, order: Order, pprov: BasePaymentProvider, email_template, subject_template,
log_entry: str, invoice, payment: OrderPayment, is_free=False):
email_context = get_email_context(event=event, order=order, payment=payment if pprov else None)
email_subject = gettext_lazy('Your order: {code}')
try:
order.send_mail(
email_subject, email_template, email_context,
subject_template, email_template, email_context,
log_entry,
invoices=[invoice] if invoice and event.settings.invoice_email_attachment else [],
attach_tickets=True,
@@ -946,13 +960,13 @@ def _order_placed_email(event: Event, order: Order, pprov: BasePaymentProvider,
logger.exception('Order received email could not be sent')
def _order_placed_email_attendee(event: Event, order: Order, position: OrderPosition, email_template, log_entry: str, is_free=False):
def _order_placed_email_attendee(event: Event, order: Order, position: OrderPosition, email_template, subject_template,
log_entry: str, is_free=False):
email_context = get_email_context(event=event, order=order, position=position)
email_subject = gettext_lazy('Your event registration: {code}')
try:
position.send_mail(
email_subject, email_template, email_context,
subject_template, email_template, email_context,
log_entry,
invoices=[],
attach_tickets=True,
@@ -1048,29 +1062,34 @@ def _perform_order(event: Event, payment_provider: str, position_ids: List[str],
if order.email:
if order.require_approval:
email_template = event.settings.mail_text_order_placed_require_approval
subject_template = event.settings.mail_subject_order_placed_require_approval
log_entry = 'pretix.event.order.email.order_placed_require_approval'
email_attendees = False
elif free_order_flow:
email_template = event.settings.mail_text_order_free
subject_template = event.settings.mail_subject_order_free
log_entry = 'pretix.event.order.email.order_free'
email_attendees = event.settings.mail_send_order_free_attendee
email_attendees_template = event.settings.mail_text_order_free_attendee
subject_attendees_template = event.settings.mail_subject_order_free_attendee
else:
email_template = event.settings.mail_text_order_placed
subject_template = event.settings.mail_subject_order_placed
log_entry = 'pretix.event.order.email.order_placed'
email_attendees = event.settings.mail_send_order_placed_attendee
email_attendees_template = event.settings.mail_text_order_placed_attendee
subject_attendees_template = event.settings.mail_subject_order_placed_attendee
if sales_channel in event.settings.mail_sales_channel_placed_paid:
_order_placed_email(event, order, pprov, email_template, log_entry, invoice, payment,
_order_placed_email(event, order, pprov, email_template, subject_template, log_entry, invoice, payment,
is_free=free_order_flow)
if email_attendees:
for p in order.positions.all():
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
_order_placed_email_attendee(event, order, p, email_attendees_template, log_entry,
_order_placed_email_attendee(event, order, p, email_attendees_template, subject_attendees_template, log_entry,
is_free=free_order_flow)
return order.id
@@ -1082,8 +1101,16 @@ def expire_orders(sender, **kwargs):
event_id = None
expire = None
for o in Order.objects.filter(expires__lt=now(), status=Order.STATUS_PENDING,
require_approval=False).select_related('event').order_by('event_id'):
qs = Order.objects.filter(
expires__lt=now(),
status=Order.STATUS_PENDING,
require_approval=False
).exclude(
Exists(
OrderFee.objects.filter(order_id=OuterRef('pk'), fee_type=OrderFee.FEE_TYPE_CANCELLATION)
)
).select_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
@@ -1124,9 +1151,9 @@ def send_expiry_warnings(sender, **kwargs):
email_template = settings.mail_text_order_expire_warning
email_context = get_email_context(event=o.event, order=o)
if settings.payment_term_expire_automatically:
email_subject = _('Your order is about to expire: %(code)s') % {'code': o.code}
email_subject = settings.mail_subject_order_expire_warning
else:
email_subject = _('Your order is pending payment: %(code)s') % {'code': o.code}
email_subject = settings.mail_subject_order_pending_warning
try:
o.send_mail(
@@ -1199,8 +1226,8 @@ def send_download_reminders(sender, **kwargs):
o.download_reminder_sent = True
o.save(update_fields=['download_reminder_sent'])
email_template = event.settings.mail_text_download_reminder
email_subject = event.settings.mail_subject_download_reminder
email_context = get_email_context(event=event, order=o)
email_subject = _('Your ticket is ready for download: %(code)s') % {'code': o.code}
try:
o.send_mail(
email_subject, email_template, email_context,
@@ -1223,6 +1250,7 @@ def send_download_reminders(sender, **kwargs):
continue
if p.addon_to_id is None and p.attendee_email and p.attendee_email != o.email:
email_template = event.settings.mail_text_download_reminder_attendee
email_subject = event.settings.mail_subject_download_reminder_attendee
email_context = get_email_context(event=event, order=o, position=p)
try:
o.send_mail(
@@ -1238,7 +1266,7 @@ def notify_user_changed_order(order, user=None, auth=None, invoices=[]):
with language(order.locale, order.event.settings.region):
email_template = order.event.settings.mail_text_order_changed
email_context = get_email_context(event=order.event, order=order)
email_subject = _('Your order has been changed: %(code)s') % {'code': order.code}
email_subject = order.event.settings.mail_subject_order_changed
try:
order.send_mail(
email_subject, email_template, email_context,
@@ -1944,14 +1972,14 @@ class OrderChangeManager:
'position': op.position.pk,
'positionid': op.position.positionid,
'addon_to': op.position.addon_to_id,
'old_taxrule': op.position.tax_rule.pk if op.position.tax_rate else None,
'old_taxrule': op.position.tax_rule.pk if op.position.tax_rule else None,
'new_taxrule': op.tax_rule.pk
})
elif isinstance(op.position, OrderFee):
self.order.log_action('pretix.event.order.changed.tax_rule', user=self.user, auth=self.auth, data={
'fee': op.position.pk,
'fee_type': op.position.fee_type,
'old_taxrule': op.position.tax_rule.pk if op.position.tax_rate else None,
'old_taxrule': op.position.tax_rule.pk if op.position.tax_rule else None,
'new_taxrule': op.tax_rule.pk
})
op.position._calculate_tax(op.tax_rule)
+2 -2
View File
@@ -112,7 +112,7 @@ def dictsum(*dicts) -> dict:
def order_overview(
event: Event, subevent: SubEvent=None, date_filter='', date_from=None, date_until=None, fees=False,
admission_only=False
admission_only=False, base_qs=None
) -> Tuple[List[Tuple[ItemCategory, List[Item]]], Dict[str, Tuple[Decimal, Decimal]]]:
items = event.items.all().select_related(
'category', # for re-grouping
@@ -120,7 +120,7 @@ def order_overview(
'variations'
).order_by('category__position', 'category_id', 'position', 'name')
qs = OrderPosition.all
qs = OrderPosition.all if base_qs is None else base_qs
if isinstance(subevent, (list, QuerySet)):
qs = qs.filter(subevent__in=subevent)
elif subevent:
+140 -4
View File
@@ -1421,6 +1421,45 @@ DEFAULTS = {
label=_("Customers can cancel their unpaid orders"),
)
},
'cancel_allow_user_unpaid_keep': {
'default': '0.00',
'type': Decimal,
'form_class': forms.DecimalField,
'serializer_class': serializers.DecimalField,
'serializer_kwargs': dict(
max_digits=10, decimal_places=2
),
'form_kwargs': dict(
label=_("Charge a fixed cancellation fee"),
help_text=_("Only affects orders pending payments, a cancellation fee for free orders is never charged. "
"Note that it will be your responsibility to claim the cancellation fee from the user."),
)
},
'cancel_allow_user_unpaid_keep_fees': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Charge payment, shipping and service fees"),
help_text=_("Only affects orders pending payments, a cancellation fee for free orders is never charged. "
"Note that it will be your responsibility to claim the cancellation fee from the user."),
)
},
'cancel_allow_user_unpaid_keep_percentage': {
'default': '0.00',
'type': Decimal,
'form_class': forms.DecimalField,
'serializer_class': serializers.DecimalField,
'serializer_kwargs': dict(
max_digits=10, decimal_places=2
),
'form_kwargs': dict(
label=_("Charge a percentual cancellation fee"),
help_text=_("Only affects orders pending payments, a cancellation fee for free orders is never charged. "
"Note that it will be your responsibility to claim the cancellation fee from the user."),
)
},
'cancel_allow_user_until': {
'default': None,
'type': RelativeDateWrapper,
@@ -1708,6 +1747,14 @@ DEFAULTS = {
'type': LazyI18nString,
'default': ""
},
'mail_subject_resend_link': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("Your order: {code}")),
},
'mail_subject_resend_link_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("Your event registration: {code}")),
},
'mail_text_resend_link': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
@@ -1721,6 +1768,10 @@ You can change your order details and view the status of your order at
Best regards,
Your {event} team"""))
},
'mail_subject_resend_all_links': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("Your orders for {event}")),
},
'mail_text_resend_all_links': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
@@ -1733,6 +1784,10 @@ The list is as follows:
Best regards,
Your {event} team"""))
},
'mail_subject_order_free_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("Your event registration: {code}")),
},
'mail_text_order_free_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello {attendee_name},
@@ -1745,6 +1800,14 @@ You can view the details and status of your ticket here:
Best regards,
Your {event} team"""))
},
'mail_send_order_free_attendee': {
'type': bool,
'default': 'False'
},
'mail_subject_order_free': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("Your order: {code}")),
},
'mail_text_order_free': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
@@ -1758,9 +1821,9 @@ You can change your order details and view the status of your order at
Best regards,
Your {event} team"""))
},
'mail_send_order_free_attendee': {
'type': bool,
'default': 'False'
'mail_subject_order_placed_require_approval': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("Your order: {code}")),
},
'mail_text_order_placed_require_approval': {
'type': LazyI18nString,
@@ -1776,6 +1839,10 @@ You can change your order details and view the status of your order at
Best regards,
Your {event} team"""))
},
'mail_subject_order_placed': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("Your order: {code}")),
},
'mail_text_order_placed': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
@@ -1819,6 +1886,10 @@ Your {event} team"""))
'type': bool,
'default': 'False'
},
'mail_subject_order_placed_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("Your event registration: {code}")),
},
'mail_text_order_placed_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello {attendee_name},
@@ -1831,6 +1902,10 @@ You can view the details and status of your ticket here:
Best regards,
Your {event} team"""))
},
'mail_subject_order_changed': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("Your order has been changed: {code}")),
},
'mail_text_order_changed': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
@@ -1843,6 +1918,10 @@ You can view the status of your order at
Best regards,
Your {event} team"""))
},
'mail_subject_order_paid': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("Payment received for your order: {code}")),
},
'mail_text_order_paid': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
@@ -1861,6 +1940,10 @@ Your {event} team"""))
'type': bool,
'default': 'False'
},
'mail_subject_order_paid_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("Event registration confirmed: {code}")),
},
'mail_text_order_paid_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello {attendee_name},
@@ -1888,6 +1971,14 @@ Your {event} team"""))
'type': int,
'default': '3'
},
'mail_subject_order_expire_warning': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("Your order is about to expire: {code}")),
},
'mail_subject_order_pending_warning': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("Your order is pending payment: {code}")),
},
'mail_text_order_expire_warning': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
@@ -1902,6 +1993,10 @@ You can view the payment information and the status of your order at
Best regards,
Your {event} team"""))
},
'mail_subject_waiting_list': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("You have been selected from the waitinglist for {event}")),
},
'mail_text_waiting_list': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
@@ -1931,6 +2026,10 @@ as possible to the next person on the waiting list:
Best regards,
Your {event} team"""))
},
'mail_subject_order_canceled': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("Order canceled: {code}")),
},
'mail_text_order_canceled': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
@@ -1945,6 +2044,10 @@ You can view the details of your order at
Best regards,
Your {event} team"""))
},
'mail_subject_order_approved': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("Order approved and awaiting payment: {code}")),
},
'mail_text_order_approved': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
@@ -1961,6 +2064,10 @@ You can select a payment method and perform the payment here:
Best regards,
Your {event} team"""))
},
'mail_subject_order_approved_free': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("Order approved and confirmed: {code}")),
},
'mail_text_order_approved_free': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
@@ -1974,6 +2081,10 @@ You can change your order details and view the status of your order at
Best regards,
Your {event} team"""))
},
'mail_subject_order_denied': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("Order denied: {code}")),
},
'mail_text_order_denied': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
@@ -2007,6 +2118,10 @@ Your {event} team"""))
'type': bool,
'default': 'False'
},
'mail_subject_download_reminder_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("Your ticket is ready for download: {code}")),
},
'mail_text_download_reminder_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello {attendee_name},
@@ -2019,6 +2134,10 @@ Your {event} team"""))
Best regards,
Your {event} team"""))
},
'mail_subject_download_reminder': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("Your ticket is ready for download: {code}")),
},
'mail_text_download_reminder': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
@@ -2031,6 +2150,10 @@ If you did not do so already, you can download your ticket here:
Best regards,
Your {event} team"""))
},
'mail_subject_customer_registration': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("Activate your account at {organizer}")),
},
'mail_text_customer_registration': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello {name},
@@ -2049,6 +2172,10 @@ Best regards,
Your {organizer} team"""))
},
'mail_subject_customer_email_change': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("Confirm email address for your account at {organizer}")),
},
'mail_text_customer_email_change': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello {name},
@@ -2067,6 +2194,10 @@ Best regards,
Your {organizer} team"""))
},
'mail_subject_customer_reset': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("Set a new password for your account at {organizer}")),
},
'mail_text_customer_reset': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello {name},
@@ -2756,6 +2887,11 @@ PERSON_NAME_TITLE_GROUPS = OrderedDict([
'Dr.',
'Prof.',
'Prof. Dr.',
))),
('dr_prof_he', ('Dr., Prof., H.E.', (
'Dr.',
'Prof.',
'H.E.',
)))
])
@@ -3042,7 +3178,7 @@ settings_hierarkey.add_type(LazyI18nStringList,
settings_hierarkey.add_type(RelativeDateWrapper,
serialize=lambda rdw: rdw.to_string(),
unserialize=lambda s: RelativeDateWrapper.from_string(s))
settings_hierarkey.add_type(PhoneNumber, lambda pn: pn.as_international, lambda s: parse(s))
settings_hierarkey.add_type(PhoneNumber, lambda pn: pn.as_international, lambda s: parse(s) if s else None)
@settings_hierarkey.set_global(cache_namespace='global')
+4
View File
@@ -64,6 +64,10 @@ class EventPluginSignal(django.dispatch.Signal):
# Send to all events!
return True
# If sentry packed this in a wrapper, unpack that
if "sentry" in receiver.__module__:
receiver = receiver.__wrapped__
# Find the Django application this belongs to
searchpath = receiver.__module__
core_module = any([searchpath.startswith(cm) for cm in settings.CORE_MODULES])
-10
View File
@@ -44,16 +44,6 @@ class BaseQuestionsViewMixin:
form_class = BaseQuestionsForm
all_optional = False
@staticmethod
def _keyfunc(pos):
# Sort addons after the item they are an addon to
if isinstance(pos, OrderPosition):
i = pos.addon_to.positionid if pos.addon_to else pos.positionid
else:
i = pos.addon_to.pk if pos.addon_to else pos.pk
addon_penalty = 1 if pos.addon_to else 0
return i, addon_penalty, pos.pk
@cached_property
def _positions_for_questions(self):
raise NotImplementedError()
+20
View File
@@ -215,6 +215,13 @@ class AsyncFormView(AsyncMixin, FormView):
expected_exceptions = (ValidationError,)
task_base = ProfiledEventTask
def async_set_progress(self, percentage):
if not self._task_self.request.called_directly:
self._task_self.update_state(
state='PROGRESS',
meta={'value': percentage}
)
def __init_subclass__(cls):
def async_execute(self, *, request_path, query_string, form_kwargs, locale, tz, url_kwargs=None, url_args=None,
organizer=None, event=None, user=None, session_key=None):
@@ -240,6 +247,9 @@ class AsyncFormView(AsyncMixin, FormView):
self.SessionStore = engine.SessionStore
view_instance.request.session = self.SessionStore(session_key)
task_self = self
view_instance._task_self = task_self
with translation.override(locale), timezone.override(pytz.timezone(tz)):
form_class = view_instance.get_form_class()
if form_kwargs.get('instance'):
@@ -331,6 +341,13 @@ class AsyncPostView(AsyncMixin, View):
expected_exceptions = (ValidationError,)
task_base = ProfiledEventTask
def async_set_progress(self, percentage):
if not self._task_self.request.called_directly:
self._task_self.update_state(
state='PROGRESS',
meta={'value': percentage}
)
def __init_subclass__(cls):
def async_execute(self, *, request_path, url_args, url_kwargs, query_string, post_data, locale, tz,
organizer=None, event=None, user=None, session_key=None):
@@ -355,6 +372,9 @@ class AsyncPostView(AsyncMixin, View):
self.SessionStore = engine.SessionStore
view_instance.request.session = self.SessionStore(session_key)
task_self = self
view_instance._task_self = task_self
with translation.override(locale), timezone.override(pytz.timezone(tz)):
return view_instance.async_post(view_instance.request, *url_args, **url_kwargs)
+128 -5
View File
@@ -589,7 +589,7 @@ class EventSettingsForm(SettingsForm):
(k, '{scheme}: {samples}'.format(
scheme=v[0],
samples=', '.join(v[1])
))
) if v[0] != ', '.join(v[1]) else v[0])
for k, v in PERSON_NAME_TITLE_GROUPS.items()
]
if not self.event.has_subevents:
@@ -666,6 +666,9 @@ class CancelSettingsForm(SettingsForm):
'cancel_allow_user_until',
'cancel_allow_user_paid',
'cancel_allow_user_paid_until',
'cancel_allow_user_unpaid_keep',
'cancel_allow_user_unpaid_keep_fees',
'cancel_allow_user_unpaid_keep_percentage',
'cancel_allow_user_paid_keep',
'cancel_allow_user_paid_keep_fees',
'cancel_allow_user_paid_keep_percentage',
@@ -927,6 +930,11 @@ class MailSettingsForm(SettingsForm):
required=True,
choices=[]
)
mail_subject_order_placed = I18nFormField(
label=_("Subject sent to order contact address"),
required=False,
widget=I18nTextInput,
)
mail_text_order_placed = I18nFormField(
label=_("Text sent to order contact address"),
required=False,
@@ -938,12 +946,22 @@ class MailSettingsForm(SettingsForm):
'tickets, the following email will be sent out to the attendees.'),
required=False,
)
mail_subject_order_placed_attendee = I18nFormField(
label=_("Subject sent to attendees"),
required=False,
widget=I18nTextInput,
)
mail_text_order_placed_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nTextarea,
)
mail_subject_order_paid = I18nFormField(
label=_("Subject sent to order contact address"),
required=False,
widget=I18nTextInput,
)
mail_text_order_paid = I18nFormField(
label=_("Text sent to order contact address"),
required=False,
@@ -955,12 +973,22 @@ class MailSettingsForm(SettingsForm):
'tickets, the following email will be sent out to the attendees.'),
required=False,
)
mail_subject_order_paid_attendee = I18nFormField(
label=_("Subject sent to attendees"),
required=False,
widget=I18nTextInput,
)
mail_text_order_paid_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nTextarea,
)
mail_subject_order_free = I18nFormField(
label=_("Subject sent to order contact address"),
required=False,
widget=I18nTextInput,
)
mail_text_order_free = I18nFormField(
label=_("Text sent to order contact address"),
required=False,
@@ -972,22 +1000,47 @@ class MailSettingsForm(SettingsForm):
'tickets, the following email will be sent out to the attendees.'),
required=False,
)
mail_subject_order_free_attendee = I18nFormField(
label=_("Subject sent to attendees"),
required=False,
widget=I18nTextInput,
)
mail_text_order_free_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nTextarea,
)
mail_subject_order_changed = I18nFormField(
label=_("Subject"),
required=False,
widget=I18nTextInput,
)
mail_text_order_changed = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
)
mail_subject_resend_link = I18nFormField(
label=_("Subject (sent by admin)"),
required=False,
widget=I18nTextInput,
)
mail_subject_resend_link_attendee = I18nFormField(
label=_("Subject (sent by admin to attendee)"),
required=False,
widget=I18nTextInput,
)
mail_text_resend_link = I18nFormField(
label=_("Text (sent by admin)"),
required=False,
widget=I18nTextarea,
)
mail_subject_resend_all_links = I18nFormField(
label=_("Subject (requested by user)"),
required=False,
widget=I18nTextInput,
)
mail_text_resend_all_links = I18nFormField(
label=_("Text (requested by user)"),
required=False,
@@ -1005,11 +1058,31 @@ class MailSettingsForm(SettingsForm):
required=False,
widget=I18nTextarea,
)
mail_subject_order_expire_warning = I18nFormField(
label=_("Subject (if order will expire automatically)"),
required=False,
widget=I18nTextInput,
)
mail_subject_order_pending_warning = I18nFormField(
label=_("Subject (if order will not expire automatically)"),
required=False,
widget=I18nTextInput,
)
mail_subject_waiting_list = I18nFormField(
label=_("Subject"),
required=False,
widget=I18nTextInput,
)
mail_text_waiting_list = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
)
mail_subject_order_canceled = I18nFormField(
label=_("Subject"),
required=False,
widget=I18nTextInput,
)
mail_text_order_canceled = I18nFormField(
label=_("Text"),
required=False,
@@ -1020,6 +1093,11 @@ class MailSettingsForm(SettingsForm):
required=False,
widget=I18nTextarea,
)
mail_subject_download_reminder = I18nFormField(
label=_("Subject sent to order contact address"),
required=False,
widget=I18nTextInput,
)
mail_text_download_reminder = I18nFormField(
label=_("Text sent to order contact address"),
required=False,
@@ -1031,6 +1109,11 @@ class MailSettingsForm(SettingsForm):
'tickets, the following email will be sent out to the attendees.'),
required=False,
)
mail_subject_download_reminder_attendee = I18nFormField(
label=_("Subject sent to attendees"),
required=False,
widget=I18nTextInput,
)
mail_text_download_reminder_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
@@ -1043,50 +1126,90 @@ class MailSettingsForm(SettingsForm):
help_text=_("This email will be sent out this many days before the order event starts. If the "
"field is empty, the mail will never be sent.")
)
mail_subject_order_placed_require_approval = I18nFormField(
label=_("Subject for received order"),
required=False,
widget=I18nTextInput,
)
mail_text_order_placed_require_approval = I18nFormField(
label=_("Received order"),
label=_("Text for received order"),
required=False,
widget=I18nTextarea,
)
mail_subject_order_approved = I18nFormField(
label=_("Subject for approved order"),
required=False,
widget=I18nTextInput,
)
mail_text_order_approved = I18nFormField(
label=_("Approved order"),
label=_("Text for approved order"),
required=False,
widget=I18nTextarea,
help_text=_("This will only be sent out for non-free orders. Free orders will receive the free order "
"template from below instead."),
)
mail_subject_order_approved_free = I18nFormField(
label=_("Subject for approved free order"),
required=False,
widget=I18nTextInput,
)
mail_text_order_approved_free = I18nFormField(
label=_("Approved free order"),
label=_("Text for approved free order"),
required=False,
widget=I18nTextarea,
help_text=_("This will only be sent out for free orders. Non-free orders will receive the non-free order "
"template from above instead."),
)
mail_subject_order_denied = I18nFormField(
label=_("Subject for denied order"),
required=False,
widget=I18nTextInput,
)
mail_text_order_denied = I18nFormField(
label=_("Denied order"),
label=_("Text for denied order"),
required=False,
widget=I18nTextarea,
)
base_context = {
'mail_text_order_placed': ['event', 'order', 'payment'],
'mail_subject_order_placed': ['event', 'order', 'payment'],
'mail_text_order_placed_attendee': ['event', 'order', 'position'],
'mail_subject_order_placed_attendee': ['event', 'order', 'position'],
'mail_text_order_placed_require_approval': ['event', 'order'],
'mail_subject_order_placed_require_approval': ['event', 'order'],
'mail_text_order_approved': ['event', 'order'],
'mail_subject_order_approved': ['event', 'order'],
'mail_text_order_approved_free': ['event', 'order'],
'mail_subject_order_approved_free': ['event', 'order'],
'mail_text_order_denied': ['event', 'order', 'comment'],
'mail_subject_order_denied': ['event', 'order', 'comment'],
'mail_text_order_paid': ['event', 'order', 'payment_info'],
'mail_subject_order_paid': ['event', 'order', 'payment_info'],
'mail_text_order_paid_attendee': ['event', 'order', 'position'],
'mail_subject_order_paid_attendee': ['event', 'order', 'position'],
'mail_text_order_free': ['event', 'order'],
'mail_subject_order_free': ['event', 'order'],
'mail_text_order_free_attendee': ['event', 'order', 'position'],
'mail_subject_order_free_attendee': ['event', 'order', 'position'],
'mail_text_order_changed': ['event', 'order'],
'mail_subject_order_changed': ['event', 'order'],
'mail_text_order_canceled': ['event', 'order', 'comment'],
'mail_subject_order_canceled': ['event', 'order', 'comment'],
'mail_text_order_expire_warning': ['event', 'order'],
'mail_subject_order_expire_warning': ['event', 'order'],
'mail_subject_order_pending_warning': ['event', 'order'],
'mail_text_order_custom_mail': ['event', 'order'],
'mail_text_download_reminder': ['event', 'order'],
'mail_subject_download_reminder': ['event', 'order'],
'mail_text_download_reminder_attendee': ['event', 'order', 'position'],
'mail_subject_download_reminder_attendee': ['event', 'order', 'position'],
'mail_text_resend_link': ['event', 'order'],
'mail_subject_resend_link': ['event', 'order'],
'mail_subject_resend_link_attendee': ['event', 'order'],
'mail_text_waiting_list': ['event', 'waiting_list_entry'],
'mail_subject_waiting_list': ['event', 'waiting_list_entry'],
'mail_text_resend_all_links': ['event', 'orders'],
'mail_subject_resend_all_links': ['event', 'orders'],
'mail_attach_ical_description': ['event', 'event_or_subevent'],
}
-4
View File
@@ -768,10 +768,6 @@ class ItemAddOnsFormSet(I18nFormSet):
if self._should_delete_form(form):
# This form is going to be deleted so any of its errors
# should not cause the entire formset to be invalid.
try:
categories.remove(form.cleaned_data['addon_category'].pk)
except KeyError:
pass
continue
if 'addon_category' in form.cleaned_data:
+9 -13
View File
@@ -158,7 +158,7 @@ class CancelForm(ForceQuotaConfirmationForm):
localize=True,
label=_('Keep a cancellation fee of'),
help_text=_('If you keep a fee, all positions within this order will be canceled and the order will be reduced '
'to a paid cancellation fee. Payment and shipping fees will be canceled as well, so include them '
'to a cancellation fee. Payment and shipping fees will be canceled as well, so include them '
'in your cancellation fee if you want to keep them. Please always enter a gross value, '
'tax will be calculated automatically.'),
)
@@ -176,23 +176,19 @@ class CancelForm(ForceQuotaConfirmationForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
prs = self.instance.payment_refund_sum
if prs > 0:
change_decimal_field(self.fields['cancellation_fee'], self.instance.event.currency)
self.fields['cancellation_fee'].widget.attrs['placeholder'] = floatformat(
Decimal('0.00'),
settings.CURRENCY_PLACES.get(self.instance.event.currency, 2)
)
self.fields['cancellation_fee'].max_value = prs
else:
del self.fields['cancellation_fee']
change_decimal_field(self.fields['cancellation_fee'], self.instance.event.currency)
self.fields['cancellation_fee'].widget.attrs['placeholder'] = floatformat(
Decimal('0.00'),
settings.CURRENCY_PLACES.get(self.instance.event.currency, 2)
)
self.fields['cancellation_fee'].max_value = self.instance.total
if not self.instance.invoices.exists():
del self.fields['cancel_invoice']
def clean_cancellation_fee(self):
val = self.cleaned_data['cancellation_fee'] or Decimal('0.00')
if val > self.instance.payment_refund_sum:
raise ValidationError(_('The cancellation fee cannot be higher than the payment credit of this order.'))
if val > self.instance.total:
raise ValidationError(_('The cancellation fee cannot be higher than the total amount of this order.'))
return val
+21 -1
View File
@@ -45,7 +45,9 @@ from django.utils.crypto import get_random_string
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes.forms import SafeModelMultipleChoiceField
from i18nfield.forms import I18nFormField, I18nFormSetMixin, I18nTextarea
from i18nfield.forms import (
I18nFormField, I18nFormSetMixin, I18nTextarea, I18nTextInput,
)
from phonenumber_field.formfields import PhoneNumberField
from pytz import common_timezones
@@ -457,16 +459,31 @@ class MailSettingsForm(SettingsForm):
}}
)
mail_subject_customer_registration = I18nFormField(
label=_("Subject"),
required=False,
widget=I18nTextInput,
)
mail_text_customer_registration = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
)
mail_subject_customer_email_change = I18nFormField(
label=_("Subject"),
required=False,
widget=I18nTextInput,
)
mail_text_customer_email_change = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
)
mail_subject_customer_reset = I18nFormField(
label=_("Subject"),
required=False,
widget=I18nTextInput,
)
mail_text_customer_reset = I18nFormField(
label=_("Text"),
required=False,
@@ -475,8 +492,11 @@ class MailSettingsForm(SettingsForm):
base_context = {
'mail_text_customer_registration': ['customer', 'url'],
'mail_subject_customer_registration': ['customer', 'url'],
'mail_text_customer_email_change': ['customer', 'url'],
'mail_subject_customer_email_change': ['customer', 'url'],
'mail_text_customer_reset': ['customer', 'url'],
'mail_subject_customer_reset': ['customer', 'url'],
}
def _get_sample_context(self, base_parameters):
+7 -4
View File
@@ -72,7 +72,7 @@ class VoucherForm(I18nModelForm):
localized_fields = '__all__'
fields = [
'code', 'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag',
'comment', 'max_usages', 'price_mode', 'subevent', 'show_hidden_items', 'budget'
'comment', 'max_usages', 'min_usages', 'price_mode', 'subevent', 'show_hidden_items', 'budget'
]
field_classes = {
'valid_until': SplitDateTimeField,
@@ -308,7 +308,7 @@ class VoucherBulkForm(VoucherForm):
localized_fields = '__all__'
fields = [
'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', 'comment',
'max_usages', 'price_mode', 'subevent', 'show_hidden_items', 'budget'
'max_usages', 'min_usages', 'price_mode', 'subevent', 'show_hidden_items', 'budget'
]
field_classes = {
'valid_until': SplitDateTimeField,
@@ -345,8 +345,11 @@ class VoucherBulkForm(VoucherForm):
if ',' in raw or ';' in raw:
if '@' in r[0]:
raise ValidationError(_('CSV input needs to contain a header row in the first line.'))
dialect = csv.Sniffer().sniff(raw[:1024])
reader = csv.DictReader(StringIO(raw), dialect=dialect)
try:
dialect = csv.Sniffer().sniff(raw[:1024])
reader = csv.DictReader(StringIO(raw), dialect=dialect)
except csv.Error as e:
raise ValidationError(_('CSV parsing failed: {error}.').format(error=str(e)))
if 'email' not in reader.fieldnames:
raise ValidationError(_('CSV input needs to contain a field with the header "{header}".').format(header="email"))
unknown_fields = [f for f in reader.fieldnames if f not in ('email', 'name', 'tag', 'number')]
@@ -11,7 +11,7 @@
{% trans "You configured your account to require authentication with a second medium, e.g. your phone. Please enter your verification code here:" %}
</p>
<div class="form-group">
<input class="form-control" name="token" placeholder="{% trans "Token" %}"
<input class="form-control" name="token" placeholder="{% trans "Token" %}" autocomplete="one-time-code"
type="text" required="required" autofocus="autofocus" id="webauthn-response">
</div>
<div class="sr-only alert alert-danger" id="webauthn-error">
@@ -116,8 +116,12 @@
</form>
{{ items|json_script:"items" }}
{% compress js %}
{% if DEBUG %}
<script type="text/javascript" src="{% static "vuejs/vue.js" %}"></script>
{% else %}
<script type="text/javascript" src="{% static "vuejs/vue.min.js" %}"></script>
{% endif %}
{% compress js %}
<script type="text/javascript" src="{% static "d3/d3.v6.js" %}"></script>
<script type="text/javascript" src="{% static "d3/d3-color.v2.js" %}"></script>
<script type="text/javascript" src="{% static "d3/d3-dispatch.v2.js" %}"></script>
@@ -128,6 +132,8 @@
<script type="text/javascript" src="{% static "d3/d3-transition.v2.js" %}"></script>
<script type="text/javascript" src="{% static "d3/d3-drag.v2.js" %}"></script>
<script type="text/javascript" src="{% static "d3/d3-zoom.v2.js" %}"></script>
{% endcompress %}
{% compress js %}
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/checkinrules/jsonlogic-boolalg.js" %}"></script>
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/datetimefield.vue' %}"></script>
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/timefield.vue' %}"></script>
@@ -11,6 +11,9 @@
<legend>{% trans "Unpaid or free orders" %}</legend>
{% bootstrap_field form.cancel_allow_user layout="control" %}
{% bootstrap_field form.cancel_allow_user_until layout="control" %}
{% bootstrap_field form.cancel_allow_user_unpaid_keep layout="control" %}
{% bootstrap_field form.cancel_allow_user_unpaid_keep_percentage layout="control" %}
{% bootstrap_field form.cancel_allow_user_unpaid_keep_fees layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Paid orders" %}</legend>
@@ -88,37 +88,37 @@
<h4>{% trans "Text" %}</h4>
<div class="panel-group" id="questions_group">
{% blocktrans asvar title_placed_order %}Placed order{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_placed" title=title_placed_order items="mail_text_order_placed,mail_send_order_placed_attendee,mail_text_order_placed_attendee" exclude="mail_send_order_placed_attendee" %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_placed" title=title_placed_order items="mail_subject_order_placed,mail_text_order_placed,mail_send_order_placed_attendee,mail_subject_order_placed_attendee,mail_text_order_placed_attendee" exclude="mail_send_order_placed_attendee" %}
{% blocktrans asvar title_paid_order %}Paid order{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_paid" title=title_paid_order items="mail_text_order_paid,mail_send_order_paid_attendee,mail_text_order_paid_attendee" exclude="mail_send_order_paid_attendee" %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_paid" title=title_paid_order items="mail_subject_order_paid,mail_text_order_paid,mail_send_order_paid_attendee,mail_subject_order_paid_attendee,mail_text_order_paid_attendee" exclude="mail_send_order_paid_attendee" %}
{% blocktrans asvar title_free_order %}Free order{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_free" title=title_free_order items="mail_text_order_free,mail_send_order_free_attendee,mail_text_order_free_attendee" exclude="mail_send_order_free_attendee" %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_free" title=title_free_order items="mail_subject_order_free,mail_text_order_free,mail_send_order_free_attendee,mail_subject_order_free_attendee,mail_text_order_free_attendee" exclude="mail_send_order_free_attendee" %}
{% blocktrans asvar title_resend_link %}Resend link{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="resend_link" title=title_resend_link items="mail_text_resend_link,mail_text_resend_all_links" %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="resend_link" title=title_resend_link items="mail_subject_resend_link,mail_subject_resend_link_attendee,mail_text_resend_link,mail_subject_resend_all_links,mail_text_resend_all_links" %}
{% blocktrans asvar title_order_changed %}Order changed{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_changed" title=title_order_changed items="mail_text_order_changed" %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_changed" title=title_order_changed items="mail_subject_order_changed,mail_text_order_changed" %}
{% blocktrans asvar title_payment_reminder %}Payment reminder{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_expirew" title=title_payment_reminder items="mail_days_order_expire_warning,mail_text_order_expire_warning" exclude="mail_days_order_expire_warning" %}
{% 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_subject_order_pending_warning,mail_text_order_expire_warning" exclude="mail_days_order_expire_warning" %}
{% blocktrans asvar title_waiting_list_notification %}Waiting list notification{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="waiting_list" title=title_waiting_list_notification items="mail_text_waiting_list" %}
{% 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" %}
{% blocktrans asvar title_order_canceled %}Order canceled{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_canceled" title=title_order_canceled items="mail_text_order_canceled" %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_canceled" title=title_order_canceled items="mail_subject_order_canceled,mail_text_order_canceled" %}
{% blocktrans asvar title_order_custom_mail %}Order custom mail{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="custom_mail" title=title_order_custom_mail items="mail_text_order_custom_mail" %}
{% blocktrans asvar title_download_tickets_reminder %}Reminder to download tickets{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_download_tickets_reminder items="mail_days_download_reminder,mail_text_download_reminder,mail_send_download_reminder_attendee,mail_text_download_reminder_attendee,mail_sales_channel_download_reminder" exclude="mail_days_download_reminder,mail_send_download_reminder_attendee,mail_sales_channel_download_reminder" %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_download_tickets_reminder items="mail_days_download_reminder,mail_subject_download_reminder,mail_text_download_reminder,mail_send_download_reminder_attendee,mail_subject_download_reminder_attendee,mail_text_download_reminder_attendee,mail_sales_channel_download_reminder" exclude="mail_days_download_reminder,mail_send_download_reminder_attendee,mail_sales_channel_download_reminder" %}
{% blocktrans asvar title_require_approval %}Order approval process{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_require_approval items="mail_text_order_placed_require_approval,mail_text_order_approved,mail_text_order_approved_free,mail_text_order_denied" %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_require_approval items="mail_subject_order_placed_require_approval,mail_text_order_placed_require_approval,mail_subject_order_approved,mail_text_order_approved,mail_subject_order_approved_free,mail_text_order_approved_free,mail_subject_order_denied,mail_text_order_denied" %}
</div>
<h4>{% trans "Attachments" %}</h4>
{% bootstrap_field form.mail_attachment_new_order layout="control" %}
@@ -118,6 +118,10 @@
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a new option" %}</button>
</p>
<p>
{% trans "or add options from a csv file" %}
<input type="file" id="add-options-csv" accept=".csv" aria-label="{% trans "csv file with answer options" %}">
</p>
</div>
</div>
</fieldset>
@@ -190,7 +190,13 @@
</dd>
{% if order.status == "n" %}
<dt>{% trans "Expiry date" %}</dt>
<dd>{{ order.expires|date:"SHORT_DATETIME_FORMAT" }}</dd>
<dd>
{{ order.expires|date:"SHORT_DATETIME_FORMAT" }}
{% if has_cancellation_fee and request.event.settings.payment_term_expire_automatically %}
<span class="fa fa-warning text-danger" data-toggle="tooltip"
title="{% trans "This order will not expire automatically as it has an open cancellation fee." %}"></span>
{% endif %}
</dd>
{% endif %}
{% if request.organizer.settings.customer_accounts %}
<dt>{% trans "Customer account" %}</dt>
@@ -58,13 +58,13 @@
<legend>{% trans "E-mail content" %}</legend>
<div class="panel-group" id="questions_group">
{% blocktrans asvar title_customer_registration %}Customer account registration{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="customer_registration" title=title_customer_registration items="mail_text_customer_registration" %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="customer_registration" title=title_customer_registration items="mail_subject_customer_registration,mail_text_customer_registration" %}
{% blocktrans asvar title_email_change %}Customer account email change{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="email_change" title=title_email_change items="mail_text_customer_email_change" %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="email_change" title=title_email_change items="mail_subject_customer_email_change,mail_text_customer_email_change" %}
{% blocktrans asvar title_reset %}Customer account password reset{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="reset" title=title_reset items="mail_text_customer_reset" %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="reset" title=title_reset items="mail_subject_customer_reset,mail_text_customer_reset" %}
</div>
</fieldset>
</div>
@@ -73,6 +73,7 @@
<legend>{% trans "Advanced settings" %}</legend>
{% bootstrap_field form.block_quota layout="control" %}
{% bootstrap_field form.allow_ignore_quota layout="control" %}
{% bootstrap_field form.min_usages layout="control" %}
{% bootstrap_field form.budget addon_after=request.event.currency layout="control" %}
{% bootstrap_field form.tag layout="control" %}
{% bootstrap_field form.comment layout="control" %}
@@ -85,6 +85,7 @@
<legend>{% trans "Advanced settings" %}</legend>
{% bootstrap_field form.block_quota layout="control" %}
{% bootstrap_field form.allow_ignore_quota layout="control" %}
{% bootstrap_field form.min_usages layout="control" %}
{% bootstrap_field form.budget addon_after=request.event.currency layout="control" %}
{% bootstrap_field form.tag layout="control" %}
{% bootstrap_field form.comment layout="control" %}
+8 -4
View File
@@ -41,6 +41,7 @@ from decimal import Decimal
from itertools import groupby
from urllib.parse import urlsplit
import bleach
from django.conf import settings
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
@@ -723,7 +724,7 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
if preview_item not in MailSettingsForm.base_context:
return HttpResponseBadRequest(_('invalid item'))
regex = r"^" + re.escape(preview_item) + r"_(?P<idx>[\d+])$"
regex = r"^" + re.escape(preview_item) + r"_(?P<idx>[\d]+)$"
msgs = {}
for k, v in request.POST.items():
# only accept allowed fields
@@ -732,9 +733,12 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
idx = matched.group('idx')
if idx in self.supported_locale:
with language(self.supported_locale[idx], self.request.event.settings.region):
msgs[self.supported_locale[idx]] = markdown_compile_email(
v.format_map(self.placeholders(preview_item))
)
if k.startswith('mail_subject_'):
msgs[self.supported_locale[idx]] = bleach.clean(v).format_map(self.placeholders(preview_item))
else:
msgs[self.supported_locale[idx]] = markdown_compile_email(
v.format_map(self.placeholders(preview_item))
)
return JsonResponse({
'item': preview_item,
+10 -1
View File
@@ -32,6 +32,7 @@ from django.utils.translation import gettext_lazy as _
from django.views.generic import TemplateView
from pretix.base import email
from pretix.base.forms import SECRET_REDACTED
from pretix.base.models import Event
from pretix.base.services.mail import mail
from pretix.control.forms.filter import OrganizerFilterForm
@@ -169,6 +170,13 @@ class MailSettingsSetupView(TemplateView):
)
if allow_save:
self.object.settings.smtp_use_custom = False
del self.object.settings.smtp_host
del self.object.settings.smtp_port
del self.object.settings.smtp_username
del self.object.settings.smtp_password
del self.object.settings.smtp_use_tls
del self.object.settings.smtp_use_ssl
for k, v in self.simple_form.cleaned_data.items():
self.object.settings.set(k, v)
self.log_action(self.simple_form.cleaned_data)
@@ -237,7 +245,8 @@ class MailSettingsSetupView(TemplateView):
if request.POST.get('state') == 'save':
for k, v in self.smtp_form.cleaned_data.items():
self.object.settings.set(k, v)
if v != SECRET_REDACTED:
self.object.settings.set(k, v)
self.object.settings.smtp_use_custom = True
self.log_action({**self.smtp_form.cleaned_data, 'smtp_use_custom': True})
messages.success(request, _('Your changes have been saved.'))
+2 -1
View File
@@ -276,7 +276,8 @@ class EventWizard(SafeSessionWizardView):
t.limit_events.add(event)
elif event.organizer.settings.event_team_provisioning:
t = Team.objects.create(
organizer=event.organizer, name=_('Team {event}').format(event=event.name),
organizer=event.organizer,
name=_('Team {event}').format(event=str(event.name)[:188] + "" if len(str(event.name)) > 190 else str(event.name)),
can_change_event_settings=True, can_change_items=True,
can_view_orders=True, can_change_orders=True, can_view_vouchers=True,
can_change_vouchers=True
+31 -27
View File
@@ -293,6 +293,7 @@ class OrderDetail(OrderView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['items'] = self.get_items()
ctx['has_cancellation_fee'] = any(f.fee_type == OrderFee.FEE_TYPE_CANCELLATION for f in ctx['items']['fees'])
ctx['event'] = self.request.event
ctx['payments'] = self.order.payments.order_by('-created')
ctx['refunds'] = self.order.refunds.select_related('payment').order_by('-created')
@@ -1178,7 +1179,7 @@ class OrderTransition(OrderView):
}
)
def post(self, *args, **kwargs):
def post(self, request, *args, **kwargs):
to = self.request.POST.get('status', '')
if self.order.status in (Order.STATUS_PENDING, Order.STATUS_EXPIRED) and to == 'p' and self.mark_paid_form.is_valid():
ps = self.mark_paid_form.cleaned_data['amount']
@@ -1267,38 +1268,41 @@ class OrderTransition(OrderView):
'confirmation mail.'))
else:
messages.success(self.request, _('The payment has been created successfully.'))
elif self.order.cancel_allowed() and to == 'c' and self.mark_canceled_form.is_valid():
try:
cancel_order(self.order.pk, user=self.request.user,
email_comment=self.mark_canceled_form.cleaned_data['comment'],
send_mail=self.mark_canceled_form.cleaned_data['send_email'],
cancel_invoice=self.mark_canceled_form.cleaned_data.get('cancel_invoice', True),
cancellation_fee=self.mark_canceled_form.cleaned_data.get('cancellation_fee'))
except OrderError as e:
messages.error(self.request, str(e))
else:
self.order.refresh_from_db()
if self.order.pending_sum < 0:
messages.success(self.request, _('The order has been canceled. You can now select how you want to '
'transfer the money back to the user.'))
with language(self.order.locale):
return redirect(reverse('control:event.order.refunds.start', kwargs={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug,
'code': self.order.code
}) + '?start-action=do_nothing&start-mode=partial&start-partial_amount={}&giftcard={}&comment={}'.format(
round_decimal(self.order.pending_sum * -1),
'true' if self.req and self.req.refund_as_giftcard else 'false',
quote(gettext('Order canceled'))
))
elif self.order.cancel_allowed() and to == 'c':
if self.mark_canceled_form.is_valid():
try:
cancel_order(self.order.pk, user=self.request.user,
email_comment=self.mark_canceled_form.cleaned_data['comment'],
send_mail=self.mark_canceled_form.cleaned_data['send_email'],
cancel_invoice=self.mark_canceled_form.cleaned_data.get('cancel_invoice', True),
cancellation_fee=self.mark_canceled_form.cleaned_data.get('cancellation_fee'))
except OrderError as e:
messages.error(self.request, str(e))
else:
self.order.refresh_from_db()
if self.order.pending_sum < 0:
messages.success(self.request, _('The order has been canceled. You can now select how you want to '
'transfer the money back to the user.'))
with language(self.order.locale):
return redirect(reverse('control:event.order.refunds.start', kwargs={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug,
'code': self.order.code
}) + '?start-action=do_nothing&start-mode=partial&start-partial_amount={}&giftcard={}&comment={}'.format(
round_decimal(self.order.pending_sum * -1),
'true' if self.req and self.req.refund_as_giftcard else 'false',
quote(gettext('Order canceled'))
))
messages.success(self.request, _('The order has been canceled.'))
messages.success(self.request, _('The order has been canceled.'))
else:
return self.get(self.request, *args, **kwargs)
elif self.order.status == Order.STATUS_PENDING and to == 'e':
mark_order_expired(self.order, user=self.request.user)
messages.success(self.request, _('The order has been marked as expired.'))
return redirect(self.get_order_url())
def get(self, *args, **kwargs):
def get(self, request, *args, **kwargs):
to = self.request.GET.get('status', '')
if self.order.status in (Order.STATUS_PENDING, Order.STATUS_EXPIRED) and to == 'p':
return render(self.request, 'pretixcontrol/order/pay.html', {
+36 -18
View File
@@ -37,6 +37,7 @@ import re
from datetime import timedelta
from decimal import Decimal
import bleach
from django import forms
from django.conf import settings
from django.contrib import messages
@@ -65,6 +66,7 @@ from pretix.api.models import WebHook
from pretix.api.webhooks import manually_retry_all_calls
from pretix.base.auth import get_auth_backends
from pretix.base.channels import get_all_sales_channels
from pretix.base.exporter import OrganizerLevelExportMixin
from pretix.base.i18n import language
from pretix.base.models import (
CachedFile, Customer, Device, Gate, GiftCard, Invoice, LogEntry,
@@ -323,7 +325,7 @@ class MailSettingsPreview(OrganizerPermissionRequiredMixin, View):
if preview_item not in MailSettingsForm.base_context:
return HttpResponseBadRequest(_('invalid item'))
regex = r"^" + re.escape(preview_item) + r"_(?P<idx>[\d+])$"
regex = r"^" + re.escape(preview_item) + r"_(?P<idx>[\d]+)$"
msgs = {}
for k, v in request.POST.items():
# only accept allowed fields
@@ -332,9 +334,12 @@ class MailSettingsPreview(OrganizerPermissionRequiredMixin, View):
idx = matched.group('idx')
if idx in self.supported_locale:
with language(self.supported_locale[idx], self.request.organizer.settings.region):
msgs[self.supported_locale[idx]] = markdown_compile_email(
v.format_map(self.placeholders(preview_item))
)
if k.startswith('mail_subject_'):
msgs[self.supported_locale[idx]] = bleach.clean(v).format_map(self.placeholders(preview_item))
else:
msgs[self.supported_locale[idx]] = markdown_compile_email(
v.format_map(self.placeholders(preview_item))
)
return JsonResponse({
'item': preview_item,
@@ -1508,7 +1513,19 @@ class ExportMixin:
)
responses = register_multievent_data_exporters.send(self.request.organizer)
id = self.request.GET.get("identifier") or self.request.POST.get("exporter")
for ex in sorted([response(events, self.request.organizer) for r, response in responses if response], key=lambda ex: str(ex.verbose_name)):
raw_exporters = [
response(Event.objects.none() if issubclass(response, OrganizerLevelExportMixin) else events, self.request.organizer)
for r, response in responses
if response
]
raw_exporters = [
ex for ex in raw_exporters
if (
not isinstance(ex, OrganizerLevelExportMixin) or
self.request.user.has_organizer_permission(self.request.organizer, ex.organizer_required_permission, self.request)
)
]
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
if id and ex.identifier != id:
continue
@@ -1526,18 +1543,19 @@ class ExportMixin:
initial=initial
)
ex.form.fields = ex.export_form_fields
ex.form.fields.update([
('events',
forms.ModelMultipleChoiceField(
queryset=events,
initial=events,
widget=forms.CheckboxSelectMultiple(
attrs={'class': 'scrolling-multiple-choice'}
),
label=_('Events'),
required=True
)),
])
if not isinstance(ex, OrganizerLevelExportMixin):
ex.form.fields.update([
('events',
forms.ModelMultipleChoiceField(
queryset=events,
initial=events,
widget=forms.CheckboxSelectMultiple(
attrs={'class': 'scrolling-multiple-choice'}
),
label=_('Events'),
required=True
)),
])
exporters.append(ex)
return exporters
@@ -2222,7 +2240,7 @@ class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
) + '?id=' + self.customer.identifier + '&token=' + token
mail(
self.customer.email,
_('Set a new password for your account at {organizer}').format(organizer=self.request.organizer.name),
self.request.organizer.settings.mail_subject_customer_reset,
self.request.organizer.settings.mail_text_customer_reset,
ctx,
locale=self.customer.locale,
+1 -1
View File
@@ -763,7 +763,7 @@ def item_meta_values(request, organizer, event):
or request.user.teams.filter(all_events=True, organizer=organizer, can_change_items=True).exists()
)
if not all_access:
defaults = matches.filter(
defaults = defaults.filter(
event__id__in=request.user.teams.filter(can_change_items=True).values_list(
'limit_events__id', flat=True
)
+1 -18
View File
@@ -23,8 +23,7 @@ import pyuca
from babel.core import Locale
from django.core.cache import cache
from django.utils import translation
from django.utils.encoding import force_str
from django_countries import Countries, CountryTuple
from django_countries import Countries
from django_countries.fields import CountryField
from phonenumbers.data import _COUNTRY_CODE_TO_REGION_CODE
@@ -61,22 +60,6 @@ class CachedCountries(Countries):
cache.set(cache_key, val, 3600 * 24 * 30)
yield from val
def translate_pair(self, code: str, name=None):
# We need to temporarily override this function until
# https://github.com/SmileyChris/django-countries/issues/364
# is fixed
if name is None:
name = self.countries[code]
if isinstance(name, dict):
if "names" in name:
country_name: str = name["names"][0]
else:
country_name = name["name"]
else:
country_name = name
country_name = force_str(country_name)
return CountryTuple(code, country_name)
class FastCountryField(CountryField):
def __init__(self, *args, **kwargs):
+40
View File
@@ -21,9 +21,49 @@
#
import logging
import sentry_sdk
from django.core.signals import request_finished
from django.dispatch import receiver
try:
from asgiref.local import Local
except ImportError:
from threading import local as Local
from django.conf import settings
class AdminExistsFilter(logging.Filter):
def filter(self, record):
return not settings.DEBUG and len(settings.ADMINS) > 0
local = Local()
class RequestIdFilter(logging.Filter):
def filter(self, record):
record.request_id = getattr(local, 'request_id', None)
return True
class RequestIdMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if settings.REQUEST_ID_HEADER and settings.REQUEST_ID_HEADER in request.headers:
local.request_id = request.request_id = request.headers[settings.REQUEST_ID_HEADER]
if settings.SENTRY_ENABLED:
sentry_sdk.set_tag("request_id", request.request_id)
else:
local.request_id = request.request_id = None
return self.get_response(request)
@receiver(request_finished)
def on_request_finished(sender, **kwargs):
# not part of middleware, since things could be logged after the middleware stack is finished
local.request_id = None
+28 -10
View File
@@ -74,7 +74,30 @@ def SafeCell(*args, value=None, **kwargs):
return c
class SafeAppendMixin:
class SafeWriteOnlyWorksheet(WriteOnlyWorksheet):
def append(self, row):
if not isgenerator(row) and not isinstance(row, (list, tuple, range)):
self._invalid_row(row)
self._get_writer()
if self._rows is None:
self._rows = self._write_rows()
next(self._rows)
filtered_row = []
for content in row:
if isinstance(content, Cell):
filtered_row.append(content)
else:
filtered_row.append(
SafeCell(self, row=1, column=1, value=remove_invalid_excel_chars(content))
)
self._rows.send(filtered_row)
class SafeWorksheet(Worksheet):
def append(self, iterable):
row_idx = self._current_row + 1
@@ -105,21 +128,16 @@ class SafeAppendMixin:
self._current_row = row_idx
class SafeWriteOnlyWorksheet(SafeAppendMixin, WriteOnlyWorksheet):
pass
class SafeWorksheet(SafeAppendMixin, Worksheet):
pass
class SafeWorkbook(Workbook):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self._sheets:
# monkeypatch existing sheets
for s in self._sheets:
s.append = SafeAppendMixin.append
if self.write_only:
s.append = SafeWriteOnlyWorksheet.append
else:
s.append = SafeWorksheet.append
def create_sheet(self, title=None, index=None):
if self.read_only:
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-09-29 09:24+0000\n"
"POT-Creation-Date: 2022-11-07 15:22+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/"
@@ -132,18 +132,18 @@ msgstr ""
msgid "Mercado Pago"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:157
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:159
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
msgid "Continue"
msgstr "المتابعة"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:215
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:217
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
msgid "Confirming your payment …"
msgstr "جاري تأكيد الدفع الخاص بك …"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:240
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:242
msgid "Payment method unavailable"
msgstr ""
@@ -643,32 +643,32 @@ msgstr[3] "سيتم حجز العناصر لك في سلة التسوق لعدة
msgstr[4] "سيتم حجز العناصر لك في سلة التسوق لدقائق {num}."
msgstr[5] "سيتم حجز العناصر لك في سلة التسوق لمدة {num}."
#: pretix/static/pretixpresale/js/ui/main.js:144
#: pretix/static/pretixpresale/js/ui/main.js:152
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr "يحصل المنظم على %(currency) %(amount)"
#: pretix/static/pretixpresale/js/ui/main.js:152
#: pretix/static/pretixpresale/js/ui/main.js:160
msgid "You get %(currency)s %(amount)s back"
msgstr "ستسترد %(currency)%(amount)"
#: pretix/static/pretixpresale/js/ui/main.js:168
#: pretix/static/pretixpresale/js/ui/main.js:176
msgid "Please enter the amount the organizer can keep."
msgstr "الرجاء إدخال المبلغ الذي يمكن للمنظم الاحتفاظ به."
#: pretix/static/pretixpresale/js/ui/main.js:380
#: pretix/static/pretixpresale/js/ui/main.js:388
msgid "Please enter a quantity for one of the ticket types."
msgstr "الرجاء إدخال عدد لأحد أنواع التذاكر."
#: pretix/static/pretixpresale/js/ui/main.js:416
#: pretix/static/pretixpresale/js/ui/main.js:424
msgid "required"
msgstr "مطلوب"
#: pretix/static/pretixpresale/js/ui/main.js:519
#: pretix/static/pretixpresale/js/ui/main.js:538
#: pretix/static/pretixpresale/js/ui/main.js:527
#: pretix/static/pretixpresale/js/ui/main.js:546
msgid "Time zone:"
msgstr "المنطقة الزمنية:"
#: pretix/static/pretixpresale/js/ui/main.js:529
#: pretix/static/pretixpresale/js/ui/main.js:537
msgid "Your local time:"
msgstr "التوقيت المحلي:"
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-09-29 09:24+0000\n"
"POT-Creation-Date: 2022-11-07 15:22+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-"
@@ -129,18 +129,18 @@ msgstr ""
msgid "Mercado Pago"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:157
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:159
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
msgid "Continue"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:215
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:217
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
msgid "Confirming your payment …"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:240
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:242
msgid "Payment method unavailable"
msgstr ""
@@ -616,34 +616,34 @@ msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "El contingut de la cistella ja no el teniu reservat."
msgstr[1] "El contingut de la cistella ja no el teniu reservat."
#: pretix/static/pretixpresale/js/ui/main.js:144
#: pretix/static/pretixpresale/js/ui/main.js:152
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:152
#: pretix/static/pretixpresale/js/ui/main.js:160
msgid "You get %(currency)s %(amount)s back"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:168
#: pretix/static/pretixpresale/js/ui/main.js:176
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:380
#: pretix/static/pretixpresale/js/ui/main.js:388
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:416
#: pretix/static/pretixpresale/js/ui/main.js:424
#, fuzzy
#| msgid "Cart expired"
msgid "required"
msgstr "Cistella expirada"
#: pretix/static/pretixpresale/js/ui/main.js:519
#: pretix/static/pretixpresale/js/ui/main.js:538
#: pretix/static/pretixpresale/js/ui/main.js:527
#: pretix/static/pretixpresale/js/ui/main.js:546
msgid "Time zone:"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:529
#: pretix/static/pretixpresale/js/ui/main.js:537
msgid "Your local time:"
msgstr ""
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-09-29 09:24+0000\n"
"POT-Creation-Date: 2022-11-07 15:22+0000\n"
"PO-Revision-Date: 2021-12-06 23:00+0000\n"
"Last-Translator: Ondřej Sokol <osokol@treesoft.cz>\n"
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -131,18 +131,18 @@ msgstr ""
msgid "Mercado Pago"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:157
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:159
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
msgid "Continue"
msgstr "Pokračovat"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:215
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:217
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
msgid "Confirming your payment …"
msgstr "Potvrzuji vaši platbu …"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:240
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:242
msgid "Payment method unavailable"
msgstr ""
@@ -636,32 +636,32 @@ msgstr[1] ""
msgstr[2] ""
"Produkty v nákupním košíku jsou pro vás rezervovány na dalších {num} minut."
#: pretix/static/pretixpresale/js/ui/main.js:144
#: pretix/static/pretixpresale/js/ui/main.js:152
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr "Organizátor si ponechává %(currency)s %(amount)s"
#: pretix/static/pretixpresale/js/ui/main.js:152
#: pretix/static/pretixpresale/js/ui/main.js:160
msgid "You get %(currency)s %(amount)s back"
msgstr "Dostanete %(currency)s %(amount)s zpět"
#: pretix/static/pretixpresale/js/ui/main.js:168
#: pretix/static/pretixpresale/js/ui/main.js:176
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:380
#: pretix/static/pretixpresale/js/ui/main.js:388
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:416
#: pretix/static/pretixpresale/js/ui/main.js:424
msgid "required"
msgstr "povinný"
#: pretix/static/pretixpresale/js/ui/main.js:519
#: pretix/static/pretixpresale/js/ui/main.js:538
#: pretix/static/pretixpresale/js/ui/main.js:527
#: pretix/static/pretixpresale/js/ui/main.js:546
msgid "Time zone:"
msgstr "Časové pásmo:"
#: pretix/static/pretixpresale/js/ui/main.js:529
#: pretix/static/pretixpresale/js/ui/main.js:537
msgid "Your local time:"
msgstr "Místní čas:"
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-09-29 09:24+0000\n"
"POT-Creation-Date: 2022-11-07 15:22+0000\n"
"PO-Revision-Date: 2022-04-01 13:36+0000\n"
"Last-Translator: Anna-itk <abc@aarhus.dk>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -130,7 +130,7 @@ msgstr ""
msgid "Mercado Pago"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:157
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:159
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
#, fuzzy
#| msgctxt "widget"
@@ -138,13 +138,13 @@ msgstr ""
msgid "Continue"
msgstr "Fortsæt"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:215
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:217
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
msgid "Confirming your payment …"
msgstr "Bekræfter din betaling …"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:240
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:242
msgid "Payment method unavailable"
msgstr ""
@@ -665,40 +665,40 @@ msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "Varerne i din kurv er reserveret for dig i et minut."
msgstr[1] "Varerne i din kurv er reserveret for dig i {num} minutter."
#: pretix/static/pretixpresale/js/ui/main.js:144
#: pretix/static/pretixpresale/js/ui/main.js:152
#, fuzzy
#| msgctxt "widget"
#| msgid "from %(currency)s %(price)s"
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr "fra %(currency)s %(price)s"
#: pretix/static/pretixpresale/js/ui/main.js:152
#: pretix/static/pretixpresale/js/ui/main.js:160
#, fuzzy
#| msgctxt "widget"
#| msgid "from %(currency)s %(price)s"
msgid "You get %(currency)s %(amount)s back"
msgstr "fra %(currency)s %(price)s"
#: pretix/static/pretixpresale/js/ui/main.js:168
#: pretix/static/pretixpresale/js/ui/main.js:176
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:380
#: pretix/static/pretixpresale/js/ui/main.js:388
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:416
#: pretix/static/pretixpresale/js/ui/main.js:424
#, fuzzy
#| msgid "Cart expired"
msgid "required"
msgstr "Kurv udløbet"
#: pretix/static/pretixpresale/js/ui/main.js:519
#: pretix/static/pretixpresale/js/ui/main.js:538
#: pretix/static/pretixpresale/js/ui/main.js:527
#: pretix/static/pretixpresale/js/ui/main.js:546
msgid "Time zone:"
msgstr "Tidszone:"
#: pretix/static/pretixpresale/js/ui/main.js:529
#: pretix/static/pretixpresale/js/ui/main.js:537
msgid "Your local time:"
msgstr "Din lokaltid:"
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-09-29 09:24+0000\n"
"POT-Creation-Date: 2022-11-07 15:22+0000\n"
"PO-Revision-Date: 2022-07-26 08:58+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -129,18 +129,18 @@ msgstr "WeChat Pay"
msgid "Mercado Pago"
msgstr "Mercado Pago"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:157
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:159
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
msgid "Continue"
msgstr "Fortfahren"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:215
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:217
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
msgid "Confirming your payment …"
msgstr "Zahlung wird bestätigt …"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:240
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:242
msgid "Payment method unavailable"
msgstr "Zahlungsmethode nicht verfügbar"
@@ -638,32 +638,32 @@ msgstr[0] ""
msgstr[1] ""
"Die Produkte in Ihrem Warenkorb sind noch {num} Minuten für Sie reserviert."
#: pretix/static/pretixpresale/js/ui/main.js:144
#: pretix/static/pretixpresale/js/ui/main.js:152
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr "Der Veranstalter behält %(currency)s %(amount)s ein"
#: pretix/static/pretixpresale/js/ui/main.js:152
#: pretix/static/pretixpresale/js/ui/main.js:160
msgid "You get %(currency)s %(amount)s back"
msgstr "Sie erhalten %(currency)s %(amount)s zurück"
#: pretix/static/pretixpresale/js/ui/main.js:168
#: pretix/static/pretixpresale/js/ui/main.js:176
msgid "Please enter the amount the organizer can keep."
msgstr "Bitte geben Sie den Betrag ein, den der Veranstalter einbehalten darf."
#: pretix/static/pretixpresale/js/ui/main.js:380
#: pretix/static/pretixpresale/js/ui/main.js:388
msgid "Please enter a quantity for one of the ticket types."
msgstr "Bitte tragen Sie eine Menge für eines der Produkte ein."
#: pretix/static/pretixpresale/js/ui/main.js:416
#: pretix/static/pretixpresale/js/ui/main.js:424
msgid "required"
msgstr "verpflichtend"
#: pretix/static/pretixpresale/js/ui/main.js:519
#: pretix/static/pretixpresale/js/ui/main.js:538
#: pretix/static/pretixpresale/js/ui/main.js:527
#: pretix/static/pretixpresale/js/ui/main.js:546
msgid "Time zone:"
msgstr "Zeitzone:"
#: pretix/static/pretixpresale/js/ui/main.js:529
#: pretix/static/pretixpresale/js/ui/main.js:537
msgid "Your local time:"
msgstr "Deine lokale Zeit:"
File diff suppressed because it is too large Load Diff
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-09-29 09:24+0000\n"
"POT-Creation-Date: 2022-11-07 15:22+0000\n"
"PO-Revision-Date: 2022-07-26 08:58+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
@@ -129,18 +129,18 @@ msgstr "WeChat Pay"
msgid "Mercado Pago"
msgstr "Mercado Pago"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:157
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:159
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
msgid "Continue"
msgstr "Fortfahren"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:215
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:217
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
msgid "Confirming your payment …"
msgstr "Zahlung wird bestätigt …"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:240
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:242
msgid "Payment method unavailable"
msgstr "Zahlungsmethode nicht verfügbar"
@@ -637,32 +637,32 @@ msgstr[0] ""
msgstr[1] ""
"Die Produkte in deinem Warenkorb sind noch {num} Minuten für dich reserviert."
#: pretix/static/pretixpresale/js/ui/main.js:144
#: pretix/static/pretixpresale/js/ui/main.js:152
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr "Der Veranstalter behält %(currency)s %(amount)s ein"
#: pretix/static/pretixpresale/js/ui/main.js:152
#: pretix/static/pretixpresale/js/ui/main.js:160
msgid "You get %(currency)s %(amount)s back"
msgstr "Du erhältst %(currency)s %(amount)s zurück"
#: pretix/static/pretixpresale/js/ui/main.js:168
#: pretix/static/pretixpresale/js/ui/main.js:176
msgid "Please enter the amount the organizer can keep."
msgstr "Bitte gib den Betrag ein, den der Veranstalter einbehalten darf."
#: pretix/static/pretixpresale/js/ui/main.js:380
#: pretix/static/pretixpresale/js/ui/main.js:388
msgid "Please enter a quantity for one of the ticket types."
msgstr "Bitte trage eine Menge für eines der Produkte ein."
#: pretix/static/pretixpresale/js/ui/main.js:416
#: pretix/static/pretixpresale/js/ui/main.js:424
msgid "required"
msgstr "verpflichtend"
#: pretix/static/pretixpresale/js/ui/main.js:519
#: pretix/static/pretixpresale/js/ui/main.js:538
#: pretix/static/pretixpresale/js/ui/main.js:527
#: pretix/static/pretixpresale/js/ui/main.js:546
msgid "Time zone:"
msgstr "Zeitzone:"
#: pretix/static/pretixpresale/js/ui/main.js:529
#: pretix/static/pretixpresale/js/ui/main.js:537
msgid "Your local time:"
msgstr "Deine lokale Zeit:"
+1509 -1395
View File
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-09-29 09:24+0000\n"
"POT-Creation-Date: 2022-11-07 15:22+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -128,18 +128,18 @@ msgstr ""
msgid "Mercado Pago"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:157
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:159
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
msgid "Continue"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:215
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:217
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
msgid "Confirming your payment …"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:240
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:242
msgid "Payment method unavailable"
msgstr ""
@@ -611,32 +611,32 @@ msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] ""
msgstr[1] ""
#: pretix/static/pretixpresale/js/ui/main.js:144
#: pretix/static/pretixpresale/js/ui/main.js:152
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:152
#: pretix/static/pretixpresale/js/ui/main.js:160
msgid "You get %(currency)s %(amount)s back"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:168
#: pretix/static/pretixpresale/js/ui/main.js:176
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:380
#: pretix/static/pretixpresale/js/ui/main.js:388
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:416
#: pretix/static/pretixpresale/js/ui/main.js:424
msgid "required"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:519
#: pretix/static/pretixpresale/js/ui/main.js:538
#: pretix/static/pretixpresale/js/ui/main.js:527
#: pretix/static/pretixpresale/js/ui/main.js:546
msgid "Time zone:"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:529
#: pretix/static/pretixpresale/js/ui/main.js:537
msgid "Your local time:"
msgstr ""
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-09-29 09:24+0000\n"
"POT-Creation-Date: 2022-11-07 15:22+0000\n"
"PO-Revision-Date: 2019-10-03 19:00+0000\n"
"Last-Translator: Chris Spy <chrispiropoulou@hotmail.com>\n"
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -131,7 +131,7 @@ msgstr ""
msgid "Mercado Pago"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:157
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:159
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
#, fuzzy
#| msgctxt "widget"
@@ -139,13 +139,13 @@ msgstr ""
msgid "Continue"
msgstr "Συνέχεια"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:215
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:217
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
msgid "Confirming your payment …"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:240
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:242
msgid "Payment method unavailable"
msgstr ""
@@ -677,40 +677,40 @@ msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "Τα είδη στο καλάθι θα παραμείνουν δεσμευμένα για ένα λεπτό."
msgstr[1] "Τα είδη στο καλάθι θα παραμείνουν δεσμευμένα για {num} λεπτά."
#: pretix/static/pretixpresale/js/ui/main.js:144
#: pretix/static/pretixpresale/js/ui/main.js:152
#, fuzzy
#| msgctxt "widget"
#| msgid "from %(currency)s %(price)s"
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr "απο %(currency)s %(price)s"
#: pretix/static/pretixpresale/js/ui/main.js:152
#: pretix/static/pretixpresale/js/ui/main.js:160
#, fuzzy
#| msgctxt "widget"
#| msgid "from %(currency)s %(price)s"
msgid "You get %(currency)s %(amount)s back"
msgstr "απο %(currency)s %(price)s"
#: pretix/static/pretixpresale/js/ui/main.js:168
#: pretix/static/pretixpresale/js/ui/main.js:176
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:380
#: pretix/static/pretixpresale/js/ui/main.js:388
msgid "Please enter a quantity for one of the ticket types."
msgstr "Εισαγάγετε μια ποσότητα για έναν από τους τύπους εισιτηρίων."
#: pretix/static/pretixpresale/js/ui/main.js:416
#: pretix/static/pretixpresale/js/ui/main.js:424
#, fuzzy
#| msgid "Cart expired"
msgid "required"
msgstr "Το καλάθι έληξε"
#: pretix/static/pretixpresale/js/ui/main.js:519
#: pretix/static/pretixpresale/js/ui/main.js:538
#: pretix/static/pretixpresale/js/ui/main.js:527
#: pretix/static/pretixpresale/js/ui/main.js:546
msgid "Time zone:"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:529
#: pretix/static/pretixpresale/js/ui/main.js:537
msgid "Your local time:"
msgstr ""
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-09-29 09:24+0000\n"
"POT-Creation-Date: 2022-11-07 15:22+0000\n"
"PO-Revision-Date: 2021-11-25 21:00+0000\n"
"Last-Translator: Ismael Menéndez Fernández <ismael.menendez@balidea.com>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -131,18 +131,18 @@ msgstr ""
msgid "Mercado Pago"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:157
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:159
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
msgid "Continue"
msgstr "Continuar"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:215
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:217
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
msgid "Confirming your payment …"
msgstr "Confirmando el pago…"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:240
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:242
msgid "Payment method unavailable"
msgstr ""
@@ -637,32 +637,32 @@ msgstr[0] ""
msgstr[1] ""
"Los elementos en su carrito de compras se han reservado por {num} minutos."
#: pretix/static/pretixpresale/js/ui/main.js:144
#: pretix/static/pretixpresale/js/ui/main.js:152
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr "El organizador se queda %(currency)s %(price)s"
#: pretix/static/pretixpresale/js/ui/main.js:152
#: pretix/static/pretixpresale/js/ui/main.js:160
msgid "You get %(currency)s %(amount)s back"
msgstr "Obtienes %(currency)s %(price)s de vuelta"
#: pretix/static/pretixpresale/js/ui/main.js:168
#: pretix/static/pretixpresale/js/ui/main.js:176
msgid "Please enter the amount the organizer can keep."
msgstr "Por favor, ingrese el monto que el organizador puede quedarse."
#: pretix/static/pretixpresale/js/ui/main.js:380
#: pretix/static/pretixpresale/js/ui/main.js:388
msgid "Please enter a quantity for one of the ticket types."
msgstr "Por favor, introduzca un valor para cada tipo de entrada."
#: pretix/static/pretixpresale/js/ui/main.js:416
#: pretix/static/pretixpresale/js/ui/main.js:424
msgid "required"
msgstr "campo requerido"
#: pretix/static/pretixpresale/js/ui/main.js:519
#: pretix/static/pretixpresale/js/ui/main.js:538
#: pretix/static/pretixpresale/js/ui/main.js:527
#: pretix/static/pretixpresale/js/ui/main.js:546
msgid "Time zone:"
msgstr "Zona horaria:"
#: pretix/static/pretixpresale/js/ui/main.js:529
#: pretix/static/pretixpresale/js/ui/main.js:537
msgid "Your local time:"
msgstr "Su hora local:"
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-09-29 09:24+0000\n"
"POT-Creation-Date: 2022-11-07 15:22+0000\n"
"PO-Revision-Date: 2021-11-10 05:00+0000\n"
"Last-Translator: Jaakko Rinta-Filppula <jaakko@r-f.fi>\n"
"Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -131,7 +131,7 @@ msgstr ""
msgid "Mercado Pago"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:157
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:159
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
#, fuzzy
#| msgctxt "widget"
@@ -139,13 +139,13 @@ msgstr ""
msgid "Continue"
msgstr "Jatka"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:215
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:217
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
msgid "Confirming your payment …"
msgstr "Maksuasi vahvistetaan …"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:240
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:242
msgid "Payment method unavailable"
msgstr ""
@@ -636,34 +636,34 @@ msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "Ostoskorissasi olevat tuotteet eivät ole enää varattu sinulle."
msgstr[1] "Ostoskorissasi olevat tuotteet eivät ole enää varattu sinulle."
#: pretix/static/pretixpresale/js/ui/main.js:144
#: pretix/static/pretixpresale/js/ui/main.js:152
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:152
#: pretix/static/pretixpresale/js/ui/main.js:160
msgid "You get %(currency)s %(amount)s back"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:168
#: pretix/static/pretixpresale/js/ui/main.js:176
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:380
#: pretix/static/pretixpresale/js/ui/main.js:388
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:416
#: pretix/static/pretixpresale/js/ui/main.js:424
#, fuzzy
#| msgid "Cart expired"
msgid "required"
msgstr "Ostoskori on vanhentunut"
#: pretix/static/pretixpresale/js/ui/main.js:519
#: pretix/static/pretixpresale/js/ui/main.js:538
#: pretix/static/pretixpresale/js/ui/main.js:527
#: pretix/static/pretixpresale/js/ui/main.js:546
msgid "Time zone:"
msgstr "Aikavyöhyke:"
#: pretix/static/pretixpresale/js/ui/main.js:529
#: pretix/static/pretixpresale/js/ui/main.js:537
msgid "Your local time:"
msgstr ""
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: French\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-09-29 09:24+0000\n"
"POT-Creation-Date: 2022-11-07 15:22+0000\n"
"PO-Revision-Date: 2022-04-07 10:40+0000\n"
"Last-Translator: Eva-Maria Obermann <obermann@rami.io>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -130,7 +130,7 @@ msgstr ""
msgid "Mercado Pago"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:157
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:159
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
#, fuzzy
#| msgctxt "widget"
@@ -138,13 +138,13 @@ msgstr ""
msgid "Continue"
msgstr "Continuer"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:215
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:217
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
msgid "Confirming your payment …"
msgstr "Confirmation de votre paiment…"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:240
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:242
msgid "Payment method unavailable"
msgstr ""
@@ -665,40 +665,40 @@ msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "Les articles de votre panier sont réservés pour une minute."
msgstr[1] "Les articles de votre panier sont réservés pendant {num} minutes."
#: pretix/static/pretixpresale/js/ui/main.js:144
#: pretix/static/pretixpresale/js/ui/main.js:152
#, fuzzy
#| msgctxt "widget"
#| msgid "from %(currency)s %(price)s"
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr "de %(currency)s %(price)s"
#: pretix/static/pretixpresale/js/ui/main.js:152
#: pretix/static/pretixpresale/js/ui/main.js:160
#, fuzzy
#| msgctxt "widget"
#| msgid "from %(currency)s %(price)s"
msgid "You get %(currency)s %(amount)s back"
msgstr "de %(currency)s %(price)s"
#: pretix/static/pretixpresale/js/ui/main.js:168
#: pretix/static/pretixpresale/js/ui/main.js:176
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:380
#: pretix/static/pretixpresale/js/ui/main.js:388
msgid "Please enter a quantity for one of the ticket types."
msgstr "SVP entrez une quantité pour un de vos types de billets."
#: pretix/static/pretixpresale/js/ui/main.js:416
#: pretix/static/pretixpresale/js/ui/main.js:424
#, fuzzy
#| msgid "Cart expired"
msgid "required"
msgstr "Panier expiré"
#: pretix/static/pretixpresale/js/ui/main.js:519
#: pretix/static/pretixpresale/js/ui/main.js:538
#: pretix/static/pretixpresale/js/ui/main.js:527
#: pretix/static/pretixpresale/js/ui/main.js:546
msgid "Time zone:"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:529
#: pretix/static/pretixpresale/js/ui/main.js:537
msgid "Your local time:"
msgstr ""
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-09-29 09:24+0000\n"
"POT-Creation-Date: 2022-11-07 15:22+0000\n"
"PO-Revision-Date: 2022-02-22 22:00+0000\n"
"Last-Translator: Ismael Menéndez Fernández <ismael.menendez@balidea.com>\n"
"Language-Team: Galician <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -131,18 +131,18 @@ msgstr ""
msgid "Mercado Pago"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:157
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:159
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
msgid "Continue"
msgstr "Continuar"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:215
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:217
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
msgid "Confirming your payment …"
msgstr "Confirmando o pagamento…"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:240
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:242
msgid "Payment method unavailable"
msgstr ""
@@ -634,32 +634,32 @@ msgstr[0] "Os artigos da túa cesta están reservados para ti durante un minuto.
msgstr[1] ""
"Os artigos da túa cesta están reservados para ti durante {num} minutos."
#: pretix/static/pretixpresale/js/ui/main.js:144
#: pretix/static/pretixpresale/js/ui/main.js:152
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr "O organizador queda %(currency)s %(price)s"
#: pretix/static/pretixpresale/js/ui/main.js:152
#: pretix/static/pretixpresale/js/ui/main.js:160
msgid "You get %(currency)s %(amount)s back"
msgstr "Obtés %(currency)s %(price)s de volta"
#: pretix/static/pretixpresale/js/ui/main.js:168
#: pretix/static/pretixpresale/js/ui/main.js:176
msgid "Please enter the amount the organizer can keep."
msgstr "Por favor, ingrese a cantidade que pode conservar o organizador."
#: pretix/static/pretixpresale/js/ui/main.js:380
#: pretix/static/pretixpresale/js/ui/main.js:388
msgid "Please enter a quantity for one of the ticket types."
msgstr "Por favor, introduza un valor para cada tipo de entrada."
#: pretix/static/pretixpresale/js/ui/main.js:416
#: pretix/static/pretixpresale/js/ui/main.js:424
msgid "required"
msgstr "campo requirido"
#: pretix/static/pretixpresale/js/ui/main.js:519
#: pretix/static/pretixpresale/js/ui/main.js:538
#: pretix/static/pretixpresale/js/ui/main.js:527
#: pretix/static/pretixpresale/js/ui/main.js:546
msgid "Time zone:"
msgstr "Zona horaria:"
#: pretix/static/pretixpresale/js/ui/main.js:529
#: pretix/static/pretixpresale/js/ui/main.js:537
msgid "Your local time:"
msgstr "A súa hora local:"
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-09-29 09:24+0000\n"
"POT-Creation-Date: 2022-11-07 15:22+0000\n"
"PO-Revision-Date: 2021-09-24 13:54+0000\n"
"Last-Translator: ofirtro <ofir.tro@gmail.com>\n"
"Language-Team: Hebrew <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -130,18 +130,18 @@ msgstr ""
msgid "Mercado Pago"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:157
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:159
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
msgid "Continue"
msgstr "המשך"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:215
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:217
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
msgid "Confirming your payment …"
msgstr "מאמת את התשלום שלך…"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:240
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:242
msgid "Payment method unavailable"
msgstr ""
@@ -619,32 +619,32 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
#: pretix/static/pretixpresale/js/ui/main.js:144
#: pretix/static/pretixpresale/js/ui/main.js:152
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:152
#: pretix/static/pretixpresale/js/ui/main.js:160
msgid "You get %(currency)s %(amount)s back"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:168
#: pretix/static/pretixpresale/js/ui/main.js:176
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:380
#: pretix/static/pretixpresale/js/ui/main.js:388
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:416
#: pretix/static/pretixpresale/js/ui/main.js:424
msgid "required"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:519
#: pretix/static/pretixpresale/js/ui/main.js:538
#: pretix/static/pretixpresale/js/ui/main.js:527
#: pretix/static/pretixpresale/js/ui/main.js:546
msgid "Time zone:"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:529
#: pretix/static/pretixpresale/js/ui/main.js:537
msgid "Your local time:"
msgstr ""
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-09-29 09:24+0000\n"
"POT-Creation-Date: 2022-11-07 15:22+0000\n"
"PO-Revision-Date: 2020-01-24 08:00+0000\n"
"Last-Translator: Prokaj Miklós <mixolid0@gmail.com>\n"
"Language-Team: Hungarian <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -131,7 +131,7 @@ msgstr ""
msgid "Mercado Pago"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:157
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:159
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
#, fuzzy
#| msgctxt "widget"
@@ -139,13 +139,13 @@ msgstr ""
msgid "Continue"
msgstr "Folytatás"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:215
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:217
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
msgid "Confirming your payment …"
msgstr "A fizetés megerősítése…"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:240
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:242
msgid "Payment method unavailable"
msgstr ""
@@ -665,40 +665,40 @@ msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "A kosár tartalma egy percig foglalva van számodra."
msgstr[1] "A kosár tartalma {num} percig foglalva van számodra."
#: pretix/static/pretixpresale/js/ui/main.js:144
#: pretix/static/pretixpresale/js/ui/main.js:152
#, fuzzy
#| msgctxt "widget"
#| msgid "from %(currency)s %(price)s"
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr "%(currency) %(price)-tól"
#: pretix/static/pretixpresale/js/ui/main.js:152
#: pretix/static/pretixpresale/js/ui/main.js:160
#, fuzzy
#| msgctxt "widget"
#| msgid "from %(currency)s %(price)s"
msgid "You get %(currency)s %(amount)s back"
msgstr "%(currency) %(price)-tól"
#: pretix/static/pretixpresale/js/ui/main.js:168
#: pretix/static/pretixpresale/js/ui/main.js:176
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:380
#: pretix/static/pretixpresale/js/ui/main.js:388
msgid "Please enter a quantity for one of the ticket types."
msgstr "Adjon meg egy mennyiséget az egyik jegytípusból."
#: pretix/static/pretixpresale/js/ui/main.js:416
#: pretix/static/pretixpresale/js/ui/main.js:424
#, fuzzy
#| msgid "Cart expired"
msgid "required"
msgstr "A kosár lejárt"
#: pretix/static/pretixpresale/js/ui/main.js:519
#: pretix/static/pretixpresale/js/ui/main.js:538
#: pretix/static/pretixpresale/js/ui/main.js:527
#: pretix/static/pretixpresale/js/ui/main.js:546
msgid "Time zone:"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:529
#: pretix/static/pretixpresale/js/ui/main.js:537
msgid "Your local time:"
msgstr ""
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-09-29 09:24+0000\n"
"POT-Creation-Date: 2022-11-07 15:22+0000\n"
"PO-Revision-Date: 2022-05-08 19:00+0000\n"
"Last-Translator: Emanuele Signoretta <signorettae@gmail.com>\n"
"Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -133,18 +133,18 @@ msgstr ""
msgid "Mercado Pago"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:157
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:159
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
msgid "Continue"
msgstr "Continua"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:215
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:217
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
msgid "Confirming your payment …"
msgstr "Stiamo processando il tuo pagamento …"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:240
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:242
msgid "Payment method unavailable"
msgstr ""
@@ -632,32 +632,32 @@ msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "Gli elementi nel tuo carrello sono riservati per 1 minuto."
msgstr[1] "Gli elementi nel tuo carrello sono riservati per {num} minuti."
#: pretix/static/pretixpresale/js/ui/main.js:144
#: pretix/static/pretixpresale/js/ui/main.js:152
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr "L'organizzatore trattiene %(currency)s %(amount)s"
#: pretix/static/pretixpresale/js/ui/main.js:152
#: pretix/static/pretixpresale/js/ui/main.js:160
msgid "You get %(currency)s %(amount)s back"
msgstr "Ricevi indietro %(currency)s %(amount)s"
#: pretix/static/pretixpresale/js/ui/main.js:168
#: pretix/static/pretixpresale/js/ui/main.js:176
msgid "Please enter the amount the organizer can keep."
msgstr "Inserisci l'importo che l'organizzatore può trattenere."
#: pretix/static/pretixpresale/js/ui/main.js:380
#: pretix/static/pretixpresale/js/ui/main.js:388
msgid "Please enter a quantity for one of the ticket types."
msgstr "Inserisci la quantità per una tipologia di biglietto."
#: pretix/static/pretixpresale/js/ui/main.js:416
#: pretix/static/pretixpresale/js/ui/main.js:424
msgid "required"
msgstr "richiesto"
#: pretix/static/pretixpresale/js/ui/main.js:519
#: pretix/static/pretixpresale/js/ui/main.js:538
#: pretix/static/pretixpresale/js/ui/main.js:527
#: pretix/static/pretixpresale/js/ui/main.js:546
msgid "Time zone:"
msgstr "Fuso orario:"
#: pretix/static/pretixpresale/js/ui/main.js:529
#: pretix/static/pretixpresale/js/ui/main.js:537
msgid "Your local time:"
msgstr "Ora locale:"

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