Compare commits

...
Author SHA1 Message Date
Richard Schreiber 3c014a91b0 Fix missing CSS in seatingframe 2024-06-27 08:55:11 +02:00
Raphael Michel 9fe6916ab5 Item index: use internal category names again 2024-06-26 15:25:04 +02:00
Raphael Michel 634263f1ba Item form: Validate empty dynamic validity 2024-06-26 09:44:49 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
67265e94a0 Update importlib-metadata requirement from ==7.* to ==8.* (#4264)
Updates the requirements on [importlib-metadata](https://github.com/python/importlib_metadata) to permit the latest version.
- [Release notes](https://github.com/python/importlib_metadata/releases)
- [Changelog](https://github.com/python/importlib_metadata/blob/main/NEWS.rst)
- [Commits](https://github.com/python/importlib_metadata/compare/v7.0.0...v8.0.0)

---
updated-dependencies:
- dependency-name: importlib-metadata
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-26 09:27:04 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0fa2e9b5dd Update webauthn requirement from ==2.1.* to ==2.2.* (#4263)
Updates the requirements on [webauthn](https://github.com/duo-labs/py_webauthn) to permit the latest version.
- [Release notes](https://github.com/duo-labs/py_webauthn/releases)
- [Changelog](https://github.com/duo-labs/py_webauthn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/duo-labs/py_webauthn/compare/v2.1.0...v2.2.0)

---
updated-dependencies:
- dependency-name: webauthn
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-26 09:26:58 +02:00
Raphael Michel c99d93a078 CSS: Fix minor bugs 2024-06-25 15:55:48 +02:00
Raphael Michel 9e20fac0da CSS: Fix handling of white background 2024-06-25 15:22:16 +02:00
Raphael Michel 3e4ccc53be Fix primary color in control 2024-06-25 14:28:47 +02:00
Mira Weller ce88dfa530 add missing semicolon 2024-06-25 14:23:27 +02:00
f0a06cd9fe Replace SCSS compilation with CSS variables (#4191)
* Replace SCSS compilation with CSS variables

* Update tests

* Update src/pretix/presale/style.py

Co-authored-by: Mira <weller@rami.io>

* Update src/pretix/presale/context.py

Co-authored-by: Mira <weller@rami.io>

* Update src/pretix/presale/views/widget.py

Co-authored-by: Mira <weller@rami.io>

* Update src/pretix/presale/context.py

Co-authored-by: Mira <weller@rami.io>

* Update src/pretix/static/pretixbase/scss/_variables.scss

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Last minor changes

* Rename file

---------

Co-authored-by: Mira <weller@rami.io>
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-06-25 13:01:20 +02:00
Erik LöfmanandRaphael Michel 7672e6274d Translations: Update Swedish
Currently translated at 39.4% (2232 of 5664 strings)

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

powered by weblate
2024-06-25 13:01:13 +02:00
Anarion DunedainandRaphael Michel 061f578b29 Translations: Update Polish
Currently translated at 76.7% (4346 of 5664 strings)

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

powered by weblate
2024-06-25 13:01:13 +02:00
Erik LöfmanandRaphael Michel 79f8501a09 Translations: Update Swedish
Currently translated at 37.8% (2145 of 5664 strings)

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

powered by weblate
2024-06-25 13:01:13 +02:00
Erik LöfmanandRaphael Michel c5237b5021 Translations: Update Swedish
Currently translated at 36.3% (2057 of 5664 strings)

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

powered by weblate
2024-06-25 13:01:13 +02:00
Anarion DunedainandRaphael Michel 0d6f7e74a3 Translations: Update Polish
Currently translated at 76.4% (4332 of 5664 strings)

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

powered by weblate
2024-06-25 13:01:13 +02:00
Reece NeedhamandRaphael Michel 21bd4a86a7 Translations: Update Spanish
Currently translated at 100.0% (231 of 231 strings)

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

powered by weblate
2024-06-25 13:01:13 +02:00
Reece NeedhamandRaphael Michel 750f641018 Translations: Update Spanish
Currently translated at 87.6% (4966 of 5664 strings)

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

powered by weblate
2024-06-25 13:01:13 +02:00
Felix HartnagelandRaphael Michel 2a385d14c4 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5664 of 5664 strings)

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

powered by weblate
2024-06-25 13:01:13 +02:00
Felix HartnagelandRaphael Michel 6a7ab1bdf5 Translations: Update German
Currently translated at 100.0% (5664 of 5664 strings)

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

powered by weblate
2024-06-25 13:01:13 +02:00
MiraandGitHub a73c4ad937 Improve List Sorting UI (#4215)
Improve product list UI (allow move between categories, more useful columns and links)
and hide "move up/down" arrows in lists by default if drag-drop is available
2024-06-25 12:54:11 +02:00
Raphael Michel 043e2eb9cf Order denial email: Use correct language for context 2024-06-25 11:57:42 +02:00
Raphael Michel c0fb93ea3b Waiting list: Add additional explanation (Z#23157006) 2024-06-25 11:31:16 +02:00
4f9297e7d8 Show minimal check-in status in order export (Z#23154920) (#4223)
* Show minimal check-in status in order export (Z#23154920)

* Update src/pretix/helpers/database.py

Co-authored-by: Mira <weller@rami.io>

* Review note

---------

Co-authored-by: Mira <weller@rami.io>
2024-06-24 17:34:10 +02:00
Erik LöfmanandMira 70b48fdd4b Translations: Update Swedish
Currently translated at 35.2% (1994 of 5664 strings)

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

powered by weblate
2024-06-24 17:20:24 +02:00
Anarion DunedainandMira e7b5317431 Translations: Update Polish
Currently translated at 75.1% (4259 of 5664 strings)

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

powered by weblate
2024-06-24 17:20:24 +02:00
MiraandMira 63ef2e70e2 Translations: Update Spanish
Currently translated at 75.3% (174 of 231 strings)

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

powered by weblate
2024-06-24 17:20:24 +02:00
MiraandMira c4db2a48b6 Translations: Update Spanish
Currently translated at 87.6% (4964 of 5664 strings)

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

powered by weblate
2024-06-24 17:20:24 +02:00
Reece NeedhamandMira de255b021e Translations: Update Spanish
Currently translated at 87.6% (4964 of 5664 strings)

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

powered by weblate
2024-06-24 17:20:24 +02:00
Raphael Michel d3fce71b7f Bump version to 2024.7.0.dev0 2024-06-24 11:05:53 +02:00
Raphael Michel 37dea068ce Bump version to 2024.6.0 2024-06-24 11:04:38 +02:00
Raphael MichelandRaphael Michel b5ef49cd3c Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5664 of 5664 strings)

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

powered by weblate
2024-06-24 11:00:19 +02:00
Raphael MichelandRaphael Michel 245c5972c6 Translations: Update German
Currently translated at 100.0% (5664 of 5664 strings)

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

powered by weblate
2024-06-24 11:00:19 +02:00
Raphael MichelandRaphael Michel 6597977752 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5664 of 5664 strings)

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

powered by weblate
2024-06-24 11:00:19 +02:00
Raphael MichelandRaphael Michel 580137577e Translations: Update German
Currently translated at 100.0% (5664 of 5664 strings)

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

powered by weblate
2024-06-24 11:00:19 +02:00
Raphael Michel a09550ce02 Translations:Add TWINT to wordlist 2024-06-24 10:55:26 +02:00
Raphael Michel 1b444b780d Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2024-06-24 10:20:56 +02:00
Anarion DunedainandRaphael Michel f41d8bb761 Translations: Update Polish
Currently translated at 68.3% (3850 of 5634 strings)

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

powered by weblate
2024-06-24 09:24:57 +02:00
Maciej SzymczakandRaphael Michel 365dbe7a14 Translations: Update Polish
Currently translated at 63.5% (3582 of 5634 strings)

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

powered by weblate
2024-06-24 09:24:57 +02:00
Anarion DunedainandRaphael Michel bf6078efb6 Translations: Update Polish
Currently translated at 63.5% (3582 of 5634 strings)

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

powered by weblate
2024-06-24 09:24:57 +02:00
Anarion DunedainandRaphael Michel 695b9a2ed6 Translations: Update Polish
Currently translated at 63.5% (3579 of 5634 strings)

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

powered by weblate
2024-06-24 09:24:57 +02:00
Anarion DunedainandRaphael Michel 25a15069ed Translations: Update Polish
Currently translated at 51.8% (2921 of 5634 strings)

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

powered by weblate
2024-06-24 09:24:57 +02:00
Anarion DunedainandRaphael Michel 11a4ea7b77 Translations: Update Polish
Currently translated at 100.0% (229 of 229 strings)

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

powered by weblate
2024-06-24 09:24:57 +02:00
Anarion DunedainandRaphael Michel 4e5e7df201 Translations: Update Polish
Currently translated at 49.5% (2789 of 5634 strings)

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

powered by weblate
2024-06-24 09:24:57 +02:00
Raphael MichelandRaphael Michel 8f515aa327 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (229 of 229 strings)

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

powered by weblate
2024-06-24 09:24:57 +02:00
Tinna SandströmandRaphael Michel dbc7fda2f8 Translations: Update Swedish
Currently translated at 27.0% (1522 of 5634 strings)

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

powered by weblate
2024-06-24 09:24:57 +02:00
Erik LöfmanandRaphael Michel 5e686674ae Translations: Update Swedish
Currently translated at 27.0% (1522 of 5634 strings)

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

powered by weblate
2024-06-24 09:24:57 +02:00
Raphael MichelandRaphael Michel 5660cd7f93 Translations: Update German
Currently translated at 100.0% (229 of 229 strings)

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

powered by weblate
2024-06-24 09:24:57 +02:00
Raphael Michel a7a33ed165 Voucher bulk form: Strip spaces from email addresses before validation 2024-06-20 17:16:21 +02:00
Richard SchreiberandGitHub 9ffdf979f4 PDF: ignore outline, annots, etc. when merging background-pdf (#4249) 2024-06-20 12:07:04 +02:00
7338381e58 Stripe: Support for TWINT (#4216)
* Stripe: Support for TWINT

* Update src/pretix/plugins/stripe/payment.py

Co-authored-by: Richard Schreiber <schreiber@rami.io>

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-06-20 10:22:43 +02:00
Raphael Michel ce06672334 Event list: Do not carry page through filter form 2024-06-19 18:19:51 +02:00
Raphael Michel 223f095611 Add Slovak and Catalan to selectable languages 2024-06-19 18:11:41 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b625dc9ec8 Bump braces from 3.0.2 to 3.0.3 in /src/pretix/static/npm_dir (#4230)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-19 18:01:25 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
50c4a1c376 Update flake8 requirement from ==7.0.* to ==7.1.* (#4240)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-19 18:01:18 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6fd2e42426 Bump django-compressor from 4.4 to 4.5 (#4241)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-19 18:00:56 +02:00
Erik LöfmanandRaphael Michel da651df4f0 Translations: Update Swedish
Currently translated at 25.7% (1450 of 5634 strings)

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

powered by weblate
2024-06-19 18:00:44 +02:00
NikolaiandRaphael Michel a6f527e32d Translations: Update Danish
Currently translated at 31.3% (1764 of 5634 strings)

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

powered by weblate
2024-06-19 18:00:44 +02:00
Thilo-Alexander GinkelandRaphael Michel 3ea61fbd1f Translations: Update German
Currently translated at 100.0% (5634 of 5634 strings)

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

powered by weblate
2024-06-19 18:00:44 +02:00
simonDandRaphael Michel 6a959d4220 Translations: Update French
Currently translated at 96.5% (5440 of 5634 strings)

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

powered by weblate
2024-06-19 18:00:44 +02:00
L. PereiraandRaphael Michel 92959dbb1f Translations: Update Portuguese (Brazil)
Currently translated at 12.9% (730 of 5634 strings)

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

powered by weblate
2024-06-19 18:00:44 +02:00
Anarion DunedainandRaphael Michel 6d6f3c4af8 Translations: Update Polish
Currently translated at 39.9% (2251 of 5634 strings)

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

powered by weblate
2024-06-19 18:00:44 +02:00
Thilo-Alexander GinkelandRaphael Michel 8d14a285ca Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5634 of 5634 strings)

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

powered by weblate
2024-06-19 18:00:44 +02:00
Raphael MichelandGitHub a6b8cd8a54 Stripe: Move Multibanco to payment intents (#4243) 2024-06-19 18:00:23 +02:00
cb95cdc6ce item_forms: Allow signals to return None (#4237)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-06-18 16:24:39 +02:00
Erik LöfmanandRaphael Michel cc7b00e206 Translations: Update Swedish
Currently translated at 21.3% (1203 of 5634 strings)

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

powered by weblate
2024-06-18 15:34:40 +02:00
Kristian FeldsamandRaphael Michel 136c54b9a8 Translations: Update Slovak
Currently translated at 92.7% (5226 of 5634 strings)

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

powered by weblate
2024-06-18 15:34:40 +02:00
Kristian FeldsamandRaphael Michel 28ba434e45 Translations: Update Slovak
Currently translated at 0.4% (1 of 229 strings)

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

powered by weblate
2024-06-18 15:34:40 +02:00
Erik LöfmanandRaphael Michel 09320093ad Translations: Update Swedish
Currently translated at 21.2% (1200 of 5634 strings)

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

powered by weblate
2024-06-17 10:15:25 +02:00
Kristian FeldsamandRaphael Michel 89cfab6cad Translations: Update Slovak
Currently translated at 91.0% (5129 of 5634 strings)

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

powered by weblate
2024-06-17 10:15:25 +02:00
alemairebeandRaphael Michel a180ce4c51 Translations: Update French
Currently translated at 96.6% (5443 of 5634 strings)

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

powered by weblate
2024-06-17 10:15:25 +02:00
1200274ebf Export: Do not rely on cached answer option values (Z#23152831) (#4225)
* Export: Do not rely on cached answer option values

* refactor duplicate code

---------

Co-authored-by: Mira Weller <weller@rami.io>
2024-06-17 10:12:15 +02:00
Raphael MichelandGitHub 877401d8c0 Remove subevent.items (#4220) 2024-06-14 14:49:55 +02:00
Martin GrossandGitHub 44170c1b93 Doc: Add missing backquote in dev/structure (ref #4222) 2024-06-14 12:31:35 +02:00
Renne RochaandRaphael Michel ce34bd0a13 Translations: Update Portuguese (Brazil)
Currently translated at 11.9% (672 of 5634 strings)

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

powered by weblate
2024-06-14 12:03:39 +02:00
Erik LöfmanandRaphael Michel b58f05efd0 Translations: Update Swedish
Currently translated at 21.1% (1190 of 5634 strings)

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

powered by weblate
2024-06-14 12:03:39 +02:00
Tinna SandströmandRaphael Michel 3ac70e6e3a Translations: Update Swedish
Currently translated at 21.1% (1190 of 5634 strings)

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

powered by weblate
2024-06-14 12:03:39 +02:00
Raphael MichelandRaphael Michel 90123d6a58 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5634 of 5634 strings)

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

powered by weblate
2024-06-14 12:03:39 +02:00
Raphael MichelandRaphael Michel 48a3984db6 Translations: Update German
Currently translated at 100.0% (5634 of 5634 strings)

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

powered by weblate
2024-06-14 12:03:39 +02:00
simonDandRaphael Michel fe6ee4437f Translations: Update French
Currently translated at 96.6% (5443 of 5634 strings)

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

powered by weblate
2024-06-14 12:03:39 +02:00
simonDandRaphael Michel f470389cd8 Translations: Update French
Currently translated at 93.0% (213 of 229 strings)

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

powered by weblate
2024-06-14 12:03:39 +02:00
simonDandRaphael Michel c848594c21 Translations: Update French
Currently translated at 96.5% (5442 of 5634 strings)

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

powered by weblate
2024-06-14 12:03:39 +02:00
e9a95b0b09 Add system report for pretix Enterprise (#4213)
* Add system report for pretix Enterprise

* Update src/pretix/control/sysreport.py

Co-authored-by: Mira <weller@rami.io>

* ADd missing license header

---------

Co-authored-by: Mira <weller@rami.io>
2024-06-13 17:08:36 +02:00
Richard SchreiberandGitHub 3b48b0782d PDF: when merging bg.pdf with fg.pdf use the higher PDF-version (#4171) 2024-06-11 12:16:57 +02:00
Tinna SandströmandRaphael Michel b55bd8f75a Translations: Update Swedish
Currently translated at 20.8% (1174 of 5634 strings)

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

powered by weblate
2024-06-11 09:00:13 +02:00
Raphael Michel 39caadb335 Compatibility of safe_openpyxl with openpyxl==3.1.3 2024-06-10 17:19:29 +02:00
dd6ebd7a48 Improve validation of email templates (#4184)
* Improve validation of email templates

* simplify SafeFormatter (skip attribute access code path altogether instead of blocklisting characters)

* let SafeFormatter optionally raise on missing key

* simplify placeholder validation

* rename parameter

* Remove unused import

---------

Co-authored-by: Raphael Michel <michel@rami.io>
2024-06-10 16:41:52 +02:00
Raphael MichelandGitHub ab576bb643 Update sentry-sdk requirement from ==1.45.* to ==2.5.* (#4176)
* Update sentry-sdk requirement from ==1.45.* to ==2.3.*

* Review notes
2024-06-10 16:25:08 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8bc16af36e Bump @babel/core from 7.24.5 to 7.24.7 in /src/pretix/static/npm_dir (#4209)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.24.5 to 7.24.7.
- [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.24.7/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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-10 15:13:24 +02:00
537044bdc8 Bank transfer: Ignore checksum for blocklist (Z#23154934) (#4194)
* Bank transfer: Ignore checksum for blocklist (Z#23154934)

* Update src/pretix/plugins/banktransfer/payment.py

Co-authored-by: Richard Schreiber <schreiber@rami.io>

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-06-10 14:49:51 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1ac7d03bb8 Bump pug-code-gen and @vue/component-compiler in /src/pretix/static/npm_dir (#4210)
Bumps [pug-code-gen](https://github.com/pugjs/pug) and [@vue/component-compiler](https://github.com/vuejs/vue-component-compiler). These dependencies needed to be updated together.

Updates `pug-code-gen` from 2.0.3 to 3.0.3
- [Release notes](https://github.com/pugjs/pug/releases)
- [Commits](https://github.com/pugjs/pug/compare/pug@2.0.3...pug-code-gen@3.0.3)

Updates `@vue/component-compiler` from 4.2.3 to 4.2.4
- [Release notes](https://github.com/vuejs/vue-component-compiler/releases)
- [Changelog](https://github.com/vuejs/vue-component-compiler/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vuejs/vue-component-compiler/compare/v4.2.3...v4.2.4)

---
updated-dependencies:
- dependency-name: pug-code-gen
  dependency-type: indirect
- dependency-name: "@vue/component-compiler"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-10 14:48:55 +02:00
Raphael MichelandGitHub b939fad1c0 Sendmail: Prevent confusion around setting attach_tickets (Z#23155893) (#4211) 2024-06-10 14:48:47 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fb3a608c54 Update django-hijack requirement from ==3.4.* to ==3.5.* (#4198)
Updates the requirements on [django-hijack](https://github.com/django-hijack/django-hijack) to permit the latest version.
- [Release notes](https://github.com/django-hijack/django-hijack/releases)
- [Changelog](https://github.com/django-hijack/django-hijack/blob/master/docs/release-button.png)
- [Commits](https://github.com/django-hijack/django-hijack/compare/3.4.0...3.5.0)

---
updated-dependencies:
- dependency-name: django-hijack
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-10 14:31:17 +02:00
Raphael MichelandGitHub ebda10542e Add IE11 banner to frontend (#4207) 2024-06-10 14:29:17 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
93dd6bf34d Bump @babel/preset-env from 7.24.5 to 7.24.6 in /src/pretix/static/npm_dir (#4189)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.24.5 to 7.24.6.
- [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.24.6/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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-10 14:12:02 +02:00
David VazandRaphael Michel 52148ebb7a Translations: Update Portuguese (Portugal)
Currently translated at 56.3% (129 of 229 strings)

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

powered by weblate
2024-06-10 14:11:41 +02:00
David VazandRaphael Michel 82b4fe2733 Translations: Update Portuguese (Portugal)
Currently translated at 88.0% (4962 of 5634 strings)

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

powered by weblate
2024-06-10 14:11:41 +02:00
alemairebeandRaphael Michel 79750e4f4b Translations: Update French
Currently translated at 96.2% (5423 of 5634 strings)

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

powered by weblate
2024-06-10 14:11:41 +02:00
MiraandRaphael Michel dbabbf7aab Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5634 of 5634 strings)

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

powered by weblate
2024-06-10 14:11:41 +02:00
MiraandRaphael Michel 6158a1f2a4 Translations: Update German
Currently translated at 100.0% (5634 of 5634 strings)

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

powered by weblate
2024-06-10 14:11:41 +02:00
Charlie LundbergandRaphael Michel 68f6f921b5 Translations: Update Swedish
Currently translated at 20.7% (1171 of 5634 strings)

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

powered by weblate
2024-06-10 14:11:41 +02:00
Charlie LundbergandRaphael Michel d0e672435a Translations: Update Swedish
Currently translated at 20.6% (1163 of 5634 strings)

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

powered by weblate
2024-06-10 14:11:41 +02:00
Raphael MichelandGitHub 5bc622bcfe PayPal: Validate proper payee set with ISU integration (#4206) 2024-06-10 14:10:02 +02:00
Raphael Michel d0184c1f48 PayPal: Fix issue in ASV setup (PRETIXEU-A5V) 2024-06-10 12:28:57 +02:00
Raphael Michel e28bbb7ea0 Voucher creation: Fix TypeError in validation (PRETIXEU-A52) 2024-06-10 12:23:20 +02:00
Raphael MichelandGitHub fe54a42fc7 Web checkin: Render special cases of pending state in search (Z#23154934) (#4193) 2024-06-04 21:53:30 +02:00
Raphael MichelandGitHub 7365f165ad Thumbnails: Keep frame durations of GIFs (#4183) 2024-06-04 21:53:20 +02:00
Raphael MichelandGitHub 90ce802a33 Item form: Prevent combining validity_mode with gift cards (#4187) 2024-06-04 11:57:49 +02:00
Raphael MichelandGitHub d463878514 Do not use price suggestion if voucher is used (Z#23155018) (#4195) 2024-06-04 11:57:26 +02:00
Charlie LundbergandRaphael Michel dd7ee84d29 Translations: Update Swedish
Currently translated at 20.2% (1143 of 5634 strings)

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

powered by weblate
2024-06-03 12:48:45 +02:00
Jo SiebertandRaphael Michel f1e2d1f44c Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5634 of 5634 strings)

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

powered by weblate
2024-06-03 12:48:45 +02:00
Jo SiebertandRaphael Michel 87a6a58f32 Translations: Update German
Currently translated at 100.0% (5634 of 5634 strings)

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

powered by weblate
2024-06-03 12:48:45 +02:00
Luan ThienandRaphael Michel b2dd56bd41 Translations: Update Vietnamese
Currently translated at 1.5% (90 of 5634 strings)

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

powered by weblate
2024-06-03 12:48:45 +02:00
Charlie LundbergandRaphael Michel f0ceab2305 Translations: Update Swedish
Currently translated at 19.7% (1113 of 5634 strings)

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

powered by weblate
2024-06-03 12:48:45 +02:00
danijossnetandRaphael Michel 14e3316dd9 Translations: Update Greek
Currently translated at 49.7% (2803 of 5634 strings)

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

powered by weblate
2024-06-03 12:48:45 +02:00
Luan ThienandRaphael Michel eb28fdcba9 Translations: Update Vietnamese
Currently translated at 55.0% (126 of 229 strings)

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

powered by weblate
2024-06-03 12:48:45 +02:00
Luan ThienandRaphael Michel 49e4a0faa0 Translations: Update Vietnamese
Currently translated at 1.5% (89 of 5634 strings)

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

powered by weblate
2024-06-03 12:48:45 +02:00
NikolaiandRaphael Michel e486089590 Translations: Update Danish
Currently translated at 62.4% (143 of 229 strings)

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

powered by weblate
2024-06-03 12:48:45 +02:00
NikolaiandRaphael Michel 4bb02d4ad9 Translations: Update Danish
Currently translated at 31.2% (1761 of 5634 strings)

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

powered by weblate
2024-06-03 12:48:45 +02:00
Luan ThienandRaphael Michel 8ad852d9cb Translations: Update Vietnamese
Currently translated at 0.8% (46 of 5634 strings)

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

powered by weblate
2024-06-03 12:48:45 +02:00
NikolaiandRaphael Michel 868fcfc471 Translations: Update Danish
Currently translated at 30.9% (1745 of 5634 strings)

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

powered by weblate
2024-06-03 12:48:45 +02:00
NikolaiandRaphael Michel d847c9a095 Translations: Update Danish
Currently translated at 30.9% (1744 of 5634 strings)

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

powered by weblate
2024-06-03 12:48:45 +02:00
Charlie LundbergandRaphael Michel d1c6b22624 Translations: Update Swedish
Currently translated at 19.4% (1093 of 5634 strings)

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

powered by weblate
2024-06-03 12:48:45 +02:00
mathbritoandRaphael Michel 842987f48e Translations: Update Portuguese (Brazil)
Currently translated at 11.7% (664 of 5634 strings)

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

powered by weblate
2024-06-03 12:48:45 +02:00
JannikandGitHub 58fc13ed91 Docs: Fix metrics names (#4192) 2024-06-03 12:33:28 +02:00
Raphael Michel 8010d2e6bb Update GitLab CI script 2024-05-31 17:46:45 +02:00
Raphael Michel 1566f54764 VAT ID validation: Fix crash with invalid Norwegian IDs (PRETIXEU-A3J) 2024-05-29 09:31:58 +02:00
Richard SchreiberandGitHub 9d380557e1 SEO improvements - add h1.sr-only if only header-image is used
* add hidden h1 with event-title if header-image only

* add event-title to alt-attribute of header-image

* add hidden setting for google_site_verification
2024-05-28 09:18:15 +02:00
Martin GrossandGitHub 5758e0dd68 PPv2 APM: Create referenced PPObjects for APM Orders; enable webhooks to capture them (#3958) 2024-05-27 13:45:37 +02:00
Martin Gross b4629e24a5 Downgrade requests to 2.31.* again while waiting for 2.33.3 release 2024-05-27 12:11:40 +02:00
Raphael Michel 27f5121211 Bump version to 2024.6.0.dev0 2024-05-24 14:11:21 +02:00
Raphael Michel a5007e4bd6 Bump version to 2024.5.0. 2024-05-24 14:10:29 +02:00
Richard SchreiberandGitHub fb3046210b Harden timing when getting order with secret check (#4177) 2024-05-24 14:09:18 +02:00
Raphael MichelandRaphael Michel 37908bd042 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5634 of 5634 strings)

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

powered by weblate
2024-05-24 11:04:49 +02:00
Raphael MichelandRaphael Michel 74bcbe8f07 Translations: Update German
Currently translated at 100.0% (5634 of 5634 strings)

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

powered by weblate
2024-05-24 11:04:49 +02:00
Raphael Michel 29f378c58b Update wordlists 2024-05-24 10:56:10 +02:00
Raphael Michel 9b537aeb5c Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2024-05-24 10:47:49 +02:00
Raphael Michel 4b5cd35a0e Add a "the" 2024-05-24 10:47:49 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e0675233d5 Update protobuf requirement from ==5.26.* to ==5.27.* (#4174)
Updates the requirements on [protobuf](https://github.com/protocolbuffers/protobuf) to permit the latest version.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Changelog](https://github.com/protocolbuffers/protobuf/blob/main/protobuf_release.bzl)
- [Commits](https://github.com/protocolbuffers/protobuf/compare/v5.26.0-rc1...v5.27.0)

---
updated-dependencies:
- dependency-name: protobuf
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-24 10:47:32 +02:00
Raphael Michel e9726a5227 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2024-05-24 10:10:01 +02:00
78b65d0757 Document exhibitor_tags field in API docs (#4172)
* Document exhibitor_tags field in API

* Update doc/plugins/exhibitors.rst

---------

Co-authored-by: Raphael Michel <michel@rami.io>
2024-05-24 10:00:44 +02:00
Serhii HorichenkoandRaphael Michel ef620ceb37 Translations: Update Ukrainian
Currently translated at 63.8% (3587 of 5621 strings)

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

powered by weblate
2024-05-24 09:50:45 +02:00
Charlie LundbergandRaphael Michel 8575a5f1cd Translations: Update Swedish
Currently translated at 19.0% (1070 of 5621 strings)

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

powered by weblate
2024-05-24 09:50:45 +02:00
Sinan SarıçınarandRaphael Michel 5762ffc035 Translations: Update Turkish
Currently translated at 44.3% (2494 of 5621 strings)

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

powered by weblate
2024-05-24 09:50:45 +02:00
Raphael MichelandRaphael Michel 79286bb051 Translations: Update French
Currently translated at 96.4% (5424 of 5621 strings)

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

powered by weblate
2024-05-24 09:50:45 +02:00
MiraandGitHub 05a2f411db Improve order secret handling (#4139)
- use hmac.compare_digest for all secret comparisons
- use salted_hmac with sha256 instead of plain sha1 for hashed secrets
- move secret handling into helper functions
2024-05-23 14:30:16 +02:00
Raphael MichelandGitHub e93e5c047c Waiting list: Fix pathological performance on large series with seating (#4169) 2024-05-23 11:51:48 +02:00
Raphael MichelandGitHub 2619a658c9 Widget: Allow passing more invoice data fields (#4162) 2024-05-23 11:19:59 +02:00
Raphael MichelandGitHub 808775c76b Widget: Do not open seating plan when pressing return in input field (#4163) 2024-05-23 11:18:09 +02:00
Raphael MichelandGitHub 9f297fbd25 Widget: Fix data-invoice-address-country being overridden by GeoIP (#4168) 2024-05-23 11:17:54 +02:00
Serhii HorichenkoandRaphael Michel d882da0adb Translations: Update Ukrainian
Currently translated at 63.6% (3577 of 5621 strings)

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

powered by weblate
2024-05-23 11:17:02 +02:00
Serhii HorichenkoandRaphael Michel 73bd4a746e Translations: Update Ukrainian
Currently translated at 98.2% (225 of 229 strings)

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

powered by weblate
2024-05-23 11:17:02 +02:00
Serhii HorichenkoandRaphael Michel bc5d0763f3 Translations: Update Ukrainian
Currently translated at 63.6% (3576 of 5621 strings)

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

powered by weblate
2024-05-23 11:17:02 +02:00
Charlie LundbergandRaphael Michel ff084f04b1 Translations: Update Swedish
Currently translated at 19.0% (1069 of 5621 strings)

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

powered by weblate
2024-05-23 11:17:02 +02:00
Richard SchreiberandGitHub 71af40a08b Simplify merging rotated background PDFs (#4166)
* Simplify merging rotated background PDFs

* fix code style issues
2024-05-23 08:55:51 +02:00
ef60093bae Update pypdf requirement from ==3.9.* to ==4.2.* (#4159)
* Change PDF merging for compatibility with pypdf 4

* Update pypdf requirement from ==3.9.* to ==4.2.*

* fix pypdf API-changes

* fix missing removal of deepcopy

* fix rotated PDF-backgrounds

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-05-22 14:56:21 +02:00
MiraandGitHub 49c4cc639f Fix time machine permission via pretix_event_access sessions for staff users (#4160) 2024-05-22 13:13:50 +02:00
Charlie LundbergandRaphael Michel e2800019f6 Translations: Update Swedish
Currently translated at 86.0% (197 of 229 strings)

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

powered by weblate
2024-05-22 09:15:52 +02:00
Charlie LundbergandRaphael Michel c44ea8aa81 Translations: Update Swedish
Currently translated at 18.2% (1026 of 5621 strings)

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

powered by weblate
2024-05-22 09:15:52 +02:00
Serhii HorichenkoandRaphael Michel 47a03e1b2a Translations: Update Ukrainian
Currently translated at 63.5% (3573 of 5621 strings)

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

powered by weblate
2024-05-22 09:15:52 +02:00
Raphael Michel 86ddca15ca Keep Python 3.9 compatibility for now / Revert "Fix more Python 3.12 warnings"
This reverts commit 294b3966b0.
2024-05-22 09:02:25 +02:00
Raphael Michel 294b3966b0 Fix more Python 3.12 warnings 2024-05-21 14:40:41 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fecc00231b Update requests requirement from ==2.31.* to ==2.32.* (#4155)
updated-dependencies:
- dependency-name: requests
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-21 14:24:46 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b3dfc459f5 Update pep8-naming requirement from ==0.13.* to ==0.14.* (#4152)
Updates the requirements on [pep8-naming](https://github.com/PyCQA/pep8-naming) to permit the latest version.
- [Release notes](https://github.com/PyCQA/pep8-naming/releases)
- [Changelog](https://github.com/PyCQA/pep8-naming/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/PyCQA/pep8-naming/compare/0.13.0...0.14.1)

---
updated-dependencies:
- dependency-name: pep8-naming
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-21 14:24:25 +02:00
Krisztián Henrik PappandRaphael Michel e21d63a7be Translations: Update Hungarian
Currently translated at 2.0% (115 of 5621 strings)

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

powered by weblate
2024-05-21 14:23:50 +02:00
MiraandGitHub 9a807df158 Fix pretix_event_access (custom domain) sessions for staff users (#4158) 2024-05-21 13:26:12 +02:00
Raphael Michel e95d551711 Silence deprecation warnings on Python 3.12 2024-05-21 12:59:58 +02:00
7188e44fe5 Stripe: Add support for Swish (#4149)
* Stripe: Add support for Swish

* Update src/pretix/plugins/stripe/payment.py

Co-authored-by: Martin Gross <gross@rami.io>

---------

Co-authored-by: Martin Gross <gross@rami.io>
2024-05-17 13:33:03 +02:00
Richard SchreiberandRaphael Michel a6a93555b6 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5621 of 5621 strings)

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

powered by weblate
2024-05-17 11:53:57 +02:00
Richard SchreiberandRaphael Michel 94eb473e42 Translations: Update German
Currently translated at 100.0% (5621 of 5621 strings)

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

powered by weblate
2024-05-17 11:53:57 +02:00
Serhii HorichenkoandRaphael Michel cbc3a344c1 Translations: Update Ukrainian
Currently translated at 98.2% (225 of 229 strings)

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

powered by weblate
2024-05-17 11:53:57 +02:00
Serhii HorichenkoandRaphael Michel 47db52d75f Translations: Update Ukrainian
Currently translated at 63.3% (3559 of 5621 strings)

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

powered by weblate
2024-05-17 11:53:57 +02:00
MiraandGitHub ba99fe597c Use correct condition for time machine check in template (#4151) 2024-05-17 11:29:54 +02:00
b638c00952 Time machine mode [Z#23129725] (#3961)
Allows organizers to test their shop as if it were a different date and time.

Implemented using a time_machine_now() function which is used instead of regular now(), which can overlay the real date time with a value from a ContextVar, assigned from a session value in EventMiddleware.

For more information, see doc/development/implementation/timemachine.rst

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
Co-authored-by: Raphael Michel <michel@rami.io>
2024-05-17 10:52:17 +02:00
Raphael Michel bfcca7046a Vouchers: Add copy button for URL 2024-05-16 11:57:50 +02:00
Raphael Michel ad5d10ff67 Fix missing serializer class 2024-05-15 11:01:32 +02:00
Raphael Michel 54d327deea Badges: Fix name clash of cache property 2024-05-15 09:46:56 +02:00
Raphael Michel d6505f946f API: Allow setting payment_giftcard__enabled 2024-05-14 15:45:42 +02:00
Raphael Michel 9c4efa7dcf Fix link on orders with memberships without customer (PRETIXEU-A1W) 2024-05-14 11:59:16 +02:00
Raphael Michel e6d26c4962 Add comment on pypdf upgrade 2024-05-14 11:09:02 +02:00
Serhii HorichenkoandRaphael Michel 7ddbbe21f7 Translations: Update Ukrainian
Currently translated at 62.8% (3534 of 5621 strings)

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

powered by weblate
2024-05-14 11:04:24 +02:00
Serhii HorichenkoandRaphael Michel 8d5ad0bd9e Translations: Update Russian
Currently translated at 19.5% (1101 of 5621 strings)

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

powered by weblate
2024-05-14 11:04:24 +02:00
Raphael Michel aff6a6f022 Revert "Update pypdf requirement from ==3.9.* to ==4.2.* (#4145)"
This reverts commit 46008818ce.
2024-05-14 10:59:43 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
46008818ce Update pypdf requirement from ==3.9.* to ==4.2.* (#4145)
Updates the requirements on [pypdf](https://github.com/py-pdf/pypdf) to permit the latest version.
- [Release notes](https://github.com/py-pdf/pypdf/releases)
- [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/py-pdf/pypdf/compare/3.9.0...4.2.0)

---
updated-dependencies:
- dependency-name: pypdf
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-14 09:33:26 +02:00
Raphael Michel 95db04bad2 Revert "Update pypdf requirement from ==3.9.* to ==4.2.* (#4055)"
This reverts commit 46b2214836.
2024-05-13 18:29:41 +02:00
Serhii HorichenkoandRaphael Michel d0c62ec1cf Translations: Update Ukrainian
Currently translated at 62.8% (3532 of 5621 strings)

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

powered by weblate
2024-05-13 10:06:04 +02:00
Serhii HorichenkoandRaphael Michel d6cbb130bd Translations: Update Ukrainian
Currently translated at 62.8% (3532 of 5621 strings)

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

powered by weblate
2024-05-13 10:06:04 +02:00
Martin GrossandRaphael Michel 097d2fcda0 Translations: Update Norwegian Bokmål
Currently translated at 97.0% (5455 of 5621 strings)

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

powered by weblate
2024-05-13 10:06:04 +02:00
RyoandRaphael Michel 41a7c13970 Translations: Update Japanese
Currently translated at 3.4% (194 of 5621 strings)

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

powered by weblate
2024-05-13 10:06:04 +02:00
Raphael MichelandRaphael Michel 1b725810dd Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5621 of 5621 strings)

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

powered by weblate
2024-05-08 17:54:00 +02:00
Raphael MichelandRaphael Michel 251f486480 Translations: Update German
Currently translated at 100.0% (5621 of 5621 strings)

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

powered by weblate
2024-05-08 17:54:00 +02:00
Adam KaputandRaphael Michel a7afcdf753 Translations: Update Polish
Currently translated at 40.0% (2243 of 5607 strings)

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

powered by weblate
2024-05-08 17:54:00 +02:00
NikolaiandRaphael Michel 0722341073 Translations: Update Danish
Currently translated at 31.1% (1744 of 5607 strings)

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

powered by weblate
2024-05-08 17:54:00 +02:00
Raphael Michel 207bf101b8 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2024-05-08 15:19:10 +02:00
e8f7cea1bf Allow attendees to modify their data (Z#23152886) (#4138)
* Allow attendees to modify their data

* Allow attendees to change ticket information

* Update src/pretix/control/templates/pretixcontrol/event/settings.html

Co-authored-by: Mira <weller@rami.io>

* Update src/pretix/presale/views/order.py

Co-authored-by: Mira <weller@rami.io>

* Update src/pretix/base/services/placeholders.py

Co-authored-by: Mira <weller@rami.io>

* Tests fix

* Fix test

---------

Co-authored-by: Mira <weller@rami.io>
2024-05-08 15:18:33 +02:00
Raphael Michel aa55eb2de2 Reactivate order: Fix incorrect signal being sent 2024-05-08 09:56:37 +02:00
MiraandGitHub 9dc5c1b266 Prevent transferring files from priv/ to pub/ on event clone (#3956)
* Prevent transferring files from priv/ to pub/ on event clone

* Also detect file names with node prefix

* Only transfer files in explicitly declared file fields

* Update django-hierarkey

* Add note to documentation about the new behaviour
2024-05-08 09:33:23 +02:00
Shintaro OkamatsuandRaphael Michel 514f1def4d Translations: Update Japanese
Currently translated at 3.4% (193 of 5607 strings)

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

powered by weblate
2024-05-08 09:32:48 +02:00
AbdelatifAitBaraandRaphael Michel c2bc97a0d8 Translations: Update Arabic
Currently translated at 66.4% (3728 of 5607 strings)

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

powered by weblate
2024-05-08 09:32:48 +02:00
Serhii HorichenkoandRaphael Michel 7fba473426 Translations: Update Ukrainian
Currently translated at 98.2% (225 of 229 strings)

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

powered by weblate
2024-05-08 09:32:48 +02:00
Serhii HorichenkoandRaphael Michel be87ba0000 Translations: Update Ukrainian
Currently translated at 62.9% (3531 of 5607 strings)

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

powered by weblate
2024-05-08 09:32:48 +02:00
Adam KaputandRaphael Michel 76b7643c39 Translations: Update Polish
Currently translated at 94.7% (217 of 229 strings)

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

powered by weblate
2024-05-08 09:32:48 +02:00
Adam KaputandRaphael Michel b1a3963b33 Translations: Update Polish
Currently translated at 39.7% (2226 of 5607 strings)

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

powered by weblate
2024-05-08 09:32:48 +02:00
Raphael MichelandGitHub 586e694ff3 Order import: Add expires column (Z#23152985) (#4137) 2024-05-08 09:09:55 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f4383c67a4 Update fakeredis requirement from ==2.22.* to ==2.23.* (#4140)
Updates the requirements on [fakeredis](https://github.com/cunla/fakeredis-py) to permit the latest version.
- [Release notes](https://github.com/cunla/fakeredis-py/releases)
- [Commits](https://github.com/cunla/fakeredis-py/compare/v2.22.0...v2.23.0)

---
updated-dependencies:
- dependency-name: fakeredis
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-08 09:07:51 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
46b2214836 Update pypdf requirement from ==3.9.* to ==4.2.* (#4055)
Updates the requirements on [pypdf](https://github.com/py-pdf/pypdf) to permit the latest version.
- [Release notes](https://github.com/py-pdf/pypdf/releases)
- [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/py-pdf/pypdf/compare/3.9.0...4.2.0)

---
updated-dependencies:
- dependency-name: pypdf
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-07 14:32:48 +02:00
Raphael MichelandGitHub 0e20d897d2 Prevent parallel refunds for the same order (Z#23152965) (#4136) 2024-05-07 14:32:02 +02:00
Raphael Michel 0c09cccd4f Docs: Add docker compose guide 2024-05-07 13:55:52 +02:00
Raphael MichelandGitHub 5ca0833db1 Vouchers: Fix validation of quota when copying a blocking voucher (Z#23152799) (#4133)
* Vouchers: Fix validation of quota when copying a blocking voucher

* Bugfixes
2024-05-07 09:50:16 +02:00
Raphael MichelandGitHub 7a63498333 PDF editor: Add variables for purchase date (Z#23152887) (#4134)
* PDF editor: Add variables for purchase date (Z#23152887)

* Fix order and add time
2024-05-07 09:48:05 +02:00
Raphael Michel b8c0887f79 Widget: Fix CORS for cached JS 2024-05-07 09:31:56 +02:00
Raphael Michel 9da65f60d7 Voucher import: Fix validation quirks 2024-05-03 16:06:40 +02:00
Raphael Michel 806124304a Voucher get code validation 2024-05-03 14:34:51 +02:00
Serhii HorichenkoandRaphael Michel 0d57673a47 Translations: Update Ukrainian
Currently translated at 62.9% (3529 of 5607 strings)

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

powered by weblate
2024-05-03 09:52:42 +02:00
Raphael MichelandGitHub 166b5e4f3b Change default password hash to argon2id (#4121)
* Change default password hash to argon2id

* Install argon2
2024-05-02 18:22:02 +02:00
541b8f5bd6 Discounts: Fix edge case in computation (#4126)
* Add new test case for discounts:

Two discounts:
- "For every 1 item1, you get three item2 for 10 % off."
- "For every 1 item1, you get five item3 for 10 % off."
Cart: 2x item1, 2x item2, 6x item3
Expected result: 2x item1 full price, 2x item2 discounted, 5x item3 discounted, 1x item3 full price

* Fix discount calculation bug

* Update src/pretix/base/models/discount.py

Co-authored-by: Raphael Michel <michel@rami.io>

* Update src/pretix/base/models/discount.py

Co-authored-by: Richard Schreiber <schreiber@rami.io>

---------

Co-authored-by: Raphael Michel <michel@rami.io>
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-05-02 18:21:56 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d2b96b2425 Bump @babel/core from 7.24.3 to 7.24.5 in /src/pretix/static/npm_dir (#4125)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.24.3 to 7.24.5.
- [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.24.5/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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-02 18:21:30 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
04d4c4f8f1 Bump @babel/preset-env from 7.24.3 to 7.24.5 in /src/pretix/static/npm_dir (#4124)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.24.3 to 7.24.5.
- [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.24.5/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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-02 16:31:55 +02:00
Raphaël DeuxandRaphael Michel f4da94cbcd Translations: Update French
Currently translated at 96.7% (5424 of 5607 strings)

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

powered by weblate
2024-05-02 16:31:45 +02:00
Serhii HorichenkoandRaphael Michel 97e3d5387f Translations: Update Ukrainian
Currently translated at 98.2% (225 of 229 strings)

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

powered by weblate
2024-05-02 16:31:45 +02:00
Serhii HorichenkoandRaphael Michel 8fc07523a9 Translations: Update Ukrainian
Currently translated at 62.8% (3526 of 5607 strings)

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

powered by weblate
2024-05-02 16:31:45 +02:00
NikolaiandRaphael Michel f18b0ae187 Translations: Update Danish
Currently translated at 31.1% (1744 of 5607 strings)

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

powered by weblate
2024-05-02 16:31:45 +02:00
Raphael Michel f7e16f56ac Revert "Install argon2"
This reverts commit 3f4e869cea.
2024-05-02 16:27:46 +02:00
Raphael Michel 3f4e869cea Install argon2 2024-05-02 16:27:37 +02:00
Martin Gross 8c2a1d58f4 Templates: Add Herma 4515 (Acetatesilk) to repository 2024-05-02 13:57:46 +02:00
Raphael Michel 0b05eb34f4 Fix style of buttons in alerts 2024-04-30 12:44:41 +02:00
Raphael Michel be48c5f94c Bump version to 2024.5.0.dev0 2024-04-30 11:11:13 +02:00
Raphael MichelandGitHub cebb6d3b43 Introduce locking to prevent duplicate invoices (Z#23150548) (#4067)
* Introduce locking to prevent duplicate invoices

This is not a perfect solution as it does not handle all code paths to
create invoices, but it handles all that seem likely to be triggered
concurrently

* Review note
2024-04-30 10:43:13 +02:00
0de96ed066 Add links to invalid inputs on error alert (Z#23149061) (#4114)
* Add links to invalid inputs on error alert

* add errors in sub-forms to message, fix issues with multi-checkboxes labels and inputs

* add scrollTarget.scrollIntoView

* add missing semi-colon

* improve comment

* add style for links in alert-danger

* fix link color for all alert-boxes

* Update src/pretix/static/pretixcontrol/js/ui/main.js

---------

Co-authored-by: Raphael Michel <michel@rami.io>
2024-04-30 10:18:32 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a9d506b1fa Update pytest-xdist requirement from ==3.5.* to ==3.6.* (#4118)
Updates the requirements on [pytest-xdist](https://github.com/pytest-dev/pytest-xdist) to permit the latest version.
- [Release notes](https://github.com/pytest-dev/pytest-xdist/releases)
- [Changelog](https://github.com/pytest-dev/pytest-xdist/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-xdist/compare/v3.5.0...v3.6.1)

---
updated-dependencies:
- dependency-name: pytest-xdist
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-30 10:14:54 +02:00
Serhii HorichenkoandRaphael Michel 7a01057429 Translations: Update Ukrainian
Currently translated at 89.9% (206 of 229 strings)

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

powered by weblate
2024-04-30 10:14:00 +02:00
Serhii HorichenkoandRaphael Michel 64e1a602d6 Translations: Update Ukrainian
Currently translated at 62.9% (3527 of 5607 strings)

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

powered by weblate
2024-04-30 10:14:00 +02:00
NikolaiandRaphael Michel fe060c387a Translations: Update Danish
Currently translated at 31.0% (1741 of 5607 strings)

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

powered by weblate
2024-04-30 10:14:00 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1dba4c7cc9 Update pytest requirement from ==8.1.* to ==8.2.* (#4119)
Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.1.0.dev0...8.2.0)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-29 21:02:07 +02:00
Richard SchreiberandGitHub 20b2a3d2aa Control: add link to orders for each subevent in list of subevents (Z#23129436) (#3566) 2024-04-29 18:44:42 +02:00
044f0c5480 Fix N+1 query in event calendar found by sentry (#4104)
* Fix N+1 query in event calendar found by sentry

* isort

---------

Co-authored-by: Mira Weller <weller@rami.io>
2024-04-29 18:41:50 +02:00
4d394f9e8a Answer file export: Allow to filter by subevent (Z#23150581) (#4066)
* Answer file export: Allow to filter by subevent (Z#23150581)

* Update src/pretix/base/exporters/answers.py

Co-authored-by: Mira <weller@rami.io>

* Fix isort

---------

Co-authored-by: Mira <weller@rami.io>
2024-04-29 18:12:58 +02:00
Raphael MichelandGitHub 247c4c6c9c Do not remove unavailable addons when changing order (Z#23150855) (#4086) 2024-04-29 18:11:20 +02:00
Raphael MichelandGitHub 11a038feb3 Allow secret generators to access order datetime (#4110) 2024-04-26 15:09:01 +02:00
Raphael MichelandGitHub 9d57ea8534 API: Do not write log entry for events when no changes are made (#4090) 2024-04-26 13:56:46 +02:00
292 changed files with 137596 additions and 117808 deletions
+16 -16
View File
@@ -1,29 +1,30 @@
before_script:
tests: tests:
image:
name: pretix/ci-image
stage: test stage: test
before_script:
- pip install -U pip uv
- uv pip install --system -U wheel setuptools
script: script:
- virtualenv env - uv pip install --system -e ".[dev]"
- source env/bin/activate
- pip install -U pip wheel setuptools
- XDG_CACHE_HOME=/cache pip3 install -e ".[dev]"
- cd src - cd src
- python manage.py check - python manage.py check
- make all compress - make all compress
- py.test --reruns 3 -n 3 tests - PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg py.test --reruns 3 -n 3 tests --maxfail=100
tags:
- python3
except: except:
- pypi - pypi
pypi: pypi:
stage: release stage: release
image:
name: pretix/ci-image
before_script:
- cat $PYPIRC > ~/.pypirc
- pip install -U pip uv
- uv pip install --system -U wheel setuptools twine build pretix-plugin-build check-manifest
script: script:
- cp /keys/.pypirc ~/.pypirc - uv pip install --system -e ".[dev]"
- virtualenv env
- source env/bin/activate
- pip install -U pip wheel setuptools check-manifest twine
- XDG_CACHE_HOME=/cache pip3 install -e ".[dev]"
- python setup.py sdist - python setup.py sdist
- pip install dist/pretix-*.tar.gz - uv pip install --system dist/pretix-*.tar.gz
- python -m pretix migrate - python -m pretix migrate
- python -m pretix check - python -m pretix check
- cd src - cd src
@@ -33,13 +34,12 @@ pypi:
- python -m build - python -m build
- twine check dist/* - twine check dist/*
- twine upload dist/* - twine upload dist/*
tags:
- python3
only: only:
- pypi - pypi
artifacts: artifacts:
paths: paths:
- src/dist/ - src/dist/
stages: stages:
- test - test
- build - build
+1 -1
View File
@@ -47,7 +47,7 @@ if [ "$1" == "taskworker" ]; then
fi fi
if [ "$1" == "upgrade" ]; then if [ "$1" == "upgrade" ]; then
exec python3 -m pretix updatestyles exec python3 -m pretix updateassets
fi fi
exec python3 -m pretix "$@" exec python3 -m pretix "$@"
+7 -2
View File
@@ -3,11 +3,16 @@
.. _`community`: .. _`community`:
Community install guides Community install guides
================================ ========================
.. warning:: The guides are maintained by the community and not by the pretix core team. If you encounter any issues with the guides, please report them to the maintainers of the guides. The pretix core team can not provide support for installs using these guides. .. warning:: The guides are maintained by the community and not by the pretix core team. If you encounter any issues with the guides, please report them to the maintainers of the guides. The pretix core team can not provide support for installs using these guides.
Kubernetes Kubernetes
------------ ----------
- Helm Chart by techwolf12 - A Helm chart for deploying pretix on Kubernetes. The chart documentation is available on `ArtifactHub <https://artifacthub.io/packages/helm/techwolf12/pretix>`_ and the source code is available on `GitHub <https://github.com/Techwolf12/charts/tree/main/pretix-helm>`_. - Helm Chart by techwolf12 - A Helm chart for deploying pretix on Kubernetes. The chart documentation is available on `ArtifactHub <https://artifacthub.io/packages/helm/techwolf12/pretix>`_ and the source code is available on `GitHub <https://github.com/Techwolf12/charts/tree/main/pretix-helm>`_.
Docker
------
- `docker compose setup <https://github.com/ZPascal/pretix-docker-compose>`_ by ZPascal
+1 -1
View File
@@ -19,7 +19,7 @@ You can use ``pip`` to update pretix directly to the development branch. Then, u
(venv)$ pip3 install -U "git+https://github.com/pretix/pretix.git#egg=pretix" (venv)$ pip3 install -U "git+https://github.com/pretix/pretix.git#egg=pretix"
(venv)$ python -m pretix migrate (venv)$ python -m pretix migrate
(venv)$ python -m pretix rebuild (venv)$ python -m pretix rebuild
(venv)$ python -m pretix updatestyles (venv)$ python -m pretix updateassets
# systemctl restart pretix-web pretix-worker # systemctl restart pretix-web pretix-worker
Docker installation Docker installation
+2 -2
View File
@@ -285,7 +285,7 @@ To upgrade to a new pretix release, pull the latest code changes and run the fol
(venv)$ pip3 install -U --upgrade-strategy eager pretix gunicorn (venv)$ pip3 install -U --upgrade-strategy eager pretix gunicorn
(venv)$ python -m pretix migrate (venv)$ python -m pretix migrate
(venv)$ python -m pretix rebuild (venv)$ python -m pretix rebuild
(venv)$ python -m pretix updatestyles (venv)$ python -m pretix updateassets
# systemctl restart pretix-web pretix-worker # systemctl restart pretix-web pretix-worker
Make sure to also read :ref:`update_notes` and the release notes of the version you are updating to. Pay special Make sure to also read :ref:`update_notes` and the release notes of the version you are updating to. Pay special
@@ -325,7 +325,7 @@ Then, proceed like after any plugin installation::
(venv)$ python -m pretix migrate (venv)$ python -m pretix migrate
(venv)$ python -m pretix rebuild (venv)$ python -m pretix rebuild
(venv)$ python -m pretix updatestyles (venv)$ python -m pretix updateassets
# systemctl restart pretix-web pretix-worker # systemctl restart pretix-web pretix-worker
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-22-04 .. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-22-04
+2 -2
View File
@@ -103,10 +103,10 @@ pretix_celery_tasks_queued_count
pretix_celery_tasks_queued_age_seconds pretix_celery_tasks_queued_age_seconds
The age of the longest-waiting in the worker queue in seconds, labeled with ``queue``. The age of the longest-waiting in the worker queue in seconds, labeled with ``queue``.
pretix_successful_logins pretix_logins_successful
Counter. The number of successful backend logins. Counter. The number of successful backend logins.
pretix_failed_logins pretix_logins_failed
Counter. The number of failed backend logins, labeled with ``reason``. Counter. The number of failed backend logins, labeled with ``reason``.
.. _metric types: https://prometheus.io/docs/concepts/metric_types/ .. _metric types: https://prometheus.io/docs/concepts/metric_types/
+1 -1
View File
@@ -35,7 +35,7 @@ Frontend
-------- --------
.. automodule:: pretix.presale.signals .. automodule:: pretix.presale.signals
:members: html_head, html_footer, footer_link, global_footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header :members: html_head, html_footer, footer_link, global_footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header
.. automodule:: pretix.presale.signals .. automodule:: pretix.presale.signals
+1
View File
@@ -19,3 +19,4 @@ Contents:
permissions permissions
logging logging
locking locking
timemachine
+10 -1
View File
@@ -15,7 +15,7 @@ includes serializers for serializing the following types:
* Built-in types: ``int``, ``float``, ``decimal.Decimal``, ``dict``, ``list``, ``bool`` * Built-in types: ``int``, ``float``, ``decimal.Decimal``, ``dict``, ``list``, ``bool``
* ``datetime.date``, ``datetime.datetime``, ``datetime.time`` * ``datetime.date``, ``datetime.datetime``, ``datetime.time``
* ``LazyI18nString`` * ``LazyI18nString``
* References to Django ``File`` objects that are already stored in a storage backend * References to Django ``File`` objects that are already stored in a storage backend [#f1]_
* References to model instances * References to model instances
In code, we recommend to always use the ``.get()`` method on the settings object to access a value, but for In code, we recommend to always use the ``.get()`` method on the settings object to access a value, but for
@@ -55,6 +55,9 @@ You can simply use it like this:
"preserve his reservation."), "preserve his reservation."),
) )
.. _settings-defaults-in-plugins:
Defaults in plugins Defaults in plugins
------------------- -------------------
@@ -70,3 +73,9 @@ Make sure that you include this code in a module that is imported at app loading
.. _django-hierarkey: https://github.com/raphaelm/django-hierarkey .. _django-hierarkey: https://github.com/raphaelm/django-hierarkey
.. _documentation: https://django-hierarkey.readthedocs.io/en/latest/ .. _documentation: https://django-hierarkey.readthedocs.io/en/latest/
.. rubric:: Footnotes
.. [#f1] If you store ``File`` instances in per-event settings, make sure to always register them with ``add_default``
as described above in :ref:`settings-defaults-in-plugins`. Otherwise, the file won't get copied properly if the
user copies the settings of an existing event to a new one.
@@ -0,0 +1,32 @@
Time machine mode
=================
In test mode, pretix provides a "time machine" feature which allows event organizers
to test their shop as if it were a different date and time. To enable this feature, they can
click on the "time machine"-link in the test mode warning box on the event page.
Internally, this time machine mode is implemented by calling our custom :py:meth:`time_machine_now()`
function instead of :py:meth:`django.utils.timezone.now()` in all places where the fake time should be
taken into account. If you add code that uses the current date and time for checking whether some
product can be bought, you should use :py:meth:`time_machine_now`.
.. autofunction:: pretix.base.timemachine.time_machine_now
Background tasks
----------------
The time machine datetime is passed through the request flow via a thread-local variable (ContextVar).
Therefore, if you call a background task in the order process, where time_machine_now should be
respected, you need to pass it through manually as shown in the example below:
.. code-block:: python
@app.task()
def my_task(self, override_now_dt: datetime=None) -> None:
with time_machine_now_assigned(override_now_dt):
# ...do something that uses time_machine_now()
my_task.apply_async(kwargs={'override_now_dt': time_machine_now(default=None)})
.. autofunction:: pretix.base.timemachine.time_machine_now_assigned
@@ -90,6 +90,10 @@ as its first argument and can be used like this::
<a href="{% eventurl request.event "presale:event.checkout" step="payment" %}">Pay</a> <a href="{% eventurl request.event "presale:event.checkout" step="payment" %}">Pay</a>
<a href="{% abseventurl request.event "presale:event.checkout" step="payment" %}">Pay</a> <a href="{% abseventurl request.event "presale:event.checkout" step="payment" %}">Pay</a>
To generate absolute URLs on the main domain, you can use the ``absurl`` template tag::
{% load eventurl %}
<a href="{% absmainurl "control:event.settings" organizer=request.event.organizer.slug event=request.event.slug %}">Event settings</a>
Implementation details Implementation details
---------------------- ----------------------
+10
View File
@@ -211,5 +211,15 @@ with the documentation a lot, you might find it useful to use sphinx-autobuild::
Then, go to http://localhost:8081 for a version of the documentation that automatically re-builds Then, go to http://localhost:8081 for a version of the documentation that automatically re-builds
whenever you change a source file. whenever you change a source file.
Working with frontend assets
----------------------------
To update the frontend styles of shops with a custom styling, run the following commands inside
your virtual environment.::
python -m pretix collectstatic --noinput
python -m pretix updateassets
.. _Django's documentation: https://docs.djangoproject.com/en/1.11/ref/django-admin/#runserver .. _Django's documentation: https://docs.djangoproject.com/en/1.11/ref/django-admin/#runserver
.. _pretixdroid: https://github.com/pretix/pretixdroid .. _pretixdroid: https://github.com/pretix/pretixdroid
+2 -2
View File
@@ -31,7 +31,7 @@ pretix/
Additional code implementing our customized :ref:`URL handling <urlconf>`. Additional code implementing our customized :ref:`URL handling <urlconf>`.
static/ static/
Contains all static files (CSS/SASS, JavaScript, images) of pretix' core Contains all static files (CSS/SASS, JavaScript, images) of pretix' core.
We use libsass as a preprocessor for CSS. Our own sass code is built in the same We use libsass as a preprocessor for CSS. Our own sass code is built in the same
step as Bootstrap and FontAwesome, so their mixins etc. are fully available. step as Bootstrap and FontAwesome, so their mixins etc. are fully available.
@@ -41,6 +41,6 @@ pretix/
tests/ tests/
This is the root directory for all test codes. It includes subdirectories ``api``, ``base``, This is the root directory for all test codes. It includes subdirectories ``api``, ``base``,
``control``, ``presale``, ``helpers`, ``multidomain`` and ``plugins`` to mirror the structure ``control``, ``presale``, ``helpers``, ``multidomain`` and ``plugins`` to mirror the structure
of the pretix source code as well as ``testdummy``, which is a pretix plugin used during of the pretix source code as well as ``testdummy``, which is a pretix plugin used during
testing. testing.
+18 -5
View File
@@ -45,6 +45,8 @@ allow_voucher_access boolean Enables access
lead_scanning_scope_by_device string Enables lead scanning to be handled as one lead per attendee lead_scanning_scope_by_device string Enables lead scanning to be handled as one lead per attendee
per scanning device, instead of only per exhibitor. per scanning device, instead of only per exhibitor.
comment string Internal comment, not shown to exhibitor comment string Internal comment, not shown to exhibitor
exhibitor_tags list of strings Internal tags to categorize exhibitors, not shown to exhibitor.
The tags need to be created through the web interface currently.
===================================== ========================== ======================================================= ===================================== ========================== =======================================================
You can also access the scanned leads through the API which contains the following public fields: You can also access the scanned leads through the API which contains the following public fields:
@@ -119,7 +121,8 @@ Endpoints
"allow_lead_scanning": true, "allow_lead_scanning": true,
"allow_lead_access": true, "allow_lead_access": true,
"allow_voucher_access": true, "allow_voucher_access": true,
"comment": "" "comment": "",
"exhibitor_tags": []
} }
] ]
} }
@@ -173,7 +176,8 @@ Endpoints
"allow_lead_scanning": true, "allow_lead_scanning": true,
"allow_lead_access": true, "allow_lead_access": true,
"allow_voucher_access": true, "allow_voucher_access": true,
"comment": "" "comment": "",
"exhibitor_tags": []
} }
:param organizer: The ``slug`` field of the organizer to fetch :param organizer: The ``slug`` field of the organizer to fetch
@@ -374,7 +378,10 @@ Endpoints
"allow_lead_scanning": true, "allow_lead_scanning": true,
"allow_lead_access": true, "allow_lead_access": true,
"allow_voucher_access": true, "allow_voucher_access": true,
"comment": "" "comment": "",
"exhibitor_tags": [
"Gold Sponsor"
]
} }
**Example response**: **Example response**:
@@ -407,7 +414,10 @@ Endpoints
"allow_lead_scanning": true, "allow_lead_scanning": true,
"allow_lead_access": true, "allow_lead_access": true,
"allow_voucher_access": true, "allow_voucher_access": true,
"comment": "" "comment": "",
"exhibitor_tags": [
"Gold Sponsor"
]
} }
:param organizer: The ``slug`` field of the organizer to create new exhibitor for :param organizer: The ``slug`` field of the organizer to create new exhibitor for
@@ -468,7 +478,10 @@ Endpoints
"allow_lead_scanning": true, "allow_lead_scanning": true,
"allow_lead_access": true, "allow_lead_access": true,
"allow_voucher_access": true, "allow_voucher_access": true,
"comment": "" "comment": "",
"exhibitor_tags": [
"Gold Sponsor"
]
} }
:param organizer: The ``slug`` field of the organizer to modify :param organizer: The ``slug`` field of the organizer to modify
+3 -3
View File
@@ -339,9 +339,9 @@ Currently, the following attributes are understood by pretix itself:
``data-attendee-name``, which will pre-fill the last part of the name, whatever that is. ``data-attendee-name``, which will pre-fill the last part of the name, whatever that is.
* ``data-invoice-address-FIELD`` will pre-fill the corresponding field of the invoice address. Possible values for * ``data-invoice-address-FIELD`` will pre-fill the corresponding field of the invoice address. Possible values for
``FIELD`` are ``company``, ``street``, ``zipcode``, ``city`` and ``country``, as well as fields specified by the ``FIELD`` are ``company``, ``street``, ``zipcode``, ``city``, ``country``, ``internal-reference``, ``vat-id``, and
naming scheme such as ``name-title`` or ``name-given-name`` (see above). ``country`` expects a two-character ``custom-field``, as well as fields specified by the naming scheme such as ``name-title`` or ``name-given-name``
country code. (see above). ``country`` expects a two-character country code.
* If ``data-fix="true"`` is given, the user will not be able to change the other given values later. This currently * If ``data-fix="true"`` is given, the user will not be able to change the other given values later. This currently
only works for the order email address as well as the invoice address. Attendee-level fields and questions can only works for the order email address as well as the invoice address. Attendee-level fields and questions can
+14 -14
View File
@@ -36,15 +36,15 @@ dependencies = [
"css-inline==0.14.*", "css-inline==0.14.*",
"defusedcsv>=1.1.0", "defusedcsv>=1.1.0",
"dj-static", "dj-static",
"Django==4.2.*", "Django[argon2]==4.2.*",
"django-bootstrap3==24.2", "django-bootstrap3==24.2",
"django-compressor==4.4", "django-compressor==4.5",
"django-countries==7.6.*", "django-countries==7.6.*",
"django-filter==24.2", "django-filter==24.2",
"django-formset-js-improved==0.5.0.3", "django-formset-js-improved==0.5.0.3",
"django-formtools==2.5.1", "django-formtools==2.5.1",
"django-hierarkey==1.1.*", "django-hierarkey==1.2.*",
"django-hijack==3.4.*", "django-hijack==3.5.*",
"django-i18nfield==1.9.*,>=1.9.4", "django-i18nfield==1.9.*,>=1.9.4",
"django-libsass==0.9", "django-libsass==0.9",
"django-localflavor==4.0", "django-localflavor==4.0",
@@ -59,7 +59,7 @@ dependencies = [
"dnspython==2.6.*", "dnspython==2.6.*",
"drf_ujson2==1.7.*", "drf_ujson2==1.7.*",
"geoip2==4.*", "geoip2==4.*",
"importlib_metadata==7.*", # Polyfill, we can probably drop this once we require Python 3.10+ "importlib_metadata==8.*", # Polyfill, we can probably drop this once we require Python 3.10+
"isoweek", "isoweek",
"jsonschema", "jsonschema",
"kombu==5.3.*", "kombu==5.3.*",
@@ -77,12 +77,12 @@ dependencies = [
"phonenumberslite==8.13.*", "phonenumberslite==8.13.*",
"Pillow==10.3.*", "Pillow==10.3.*",
"pretix-plugin-build", "pretix-plugin-build",
"protobuf==5.26.*", "protobuf==5.27.*",
"psycopg2-binary", "psycopg2-binary",
"pycountry", "pycountry",
"pycparser==2.22", "pycparser==2.22",
"pycryptodome==3.20.*", "pycryptodome==3.20.*",
"pypdf==3.9.*", "pypdf==4.2.*",
"python-bidi==0.4.*", # Support for Arabic in reportlab "python-bidi==0.4.*", # Support for Arabic in reportlab
"python-dateutil==2.9.*", "python-dateutil==2.9.*",
"pytz", "pytz",
@@ -92,7 +92,7 @@ dependencies = [
"redis==5.0.*", "redis==5.0.*",
"reportlab==4.2.*", "reportlab==4.2.*",
"requests==2.31.*", "requests==2.31.*",
"sentry-sdk==1.45.*", "sentry-sdk==2.5.*",
"sepaxml==2.6.*", "sepaxml==2.6.*",
"slimit", "slimit",
"static3==0.7.*", "static3==0.7.*",
@@ -103,7 +103,7 @@ dependencies = [
"ua-parser==0.18.*", "ua-parser==0.18.*",
"vat_moss_forked==2020.3.20.0.11.0", "vat_moss_forked==2020.3.20.0.11.0",
"vobject==0.9.*", "vobject==0.9.*",
"webauthn==2.1.*", "webauthn==2.2.*",
"zeep==4.2.*" "zeep==4.2.*"
] ]
@@ -113,11 +113,11 @@ dev = [
"aiohttp==3.9.*", "aiohttp==3.9.*",
"coverage", "coverage",
"coveralls", "coveralls",
"fakeredis==2.22.*", "fakeredis==2.23.*",
"flake8==7.0.*", "flake8==7.1.*",
"freezegun", "freezegun",
"isort==5.13.*", "isort==5.13.*",
"pep8-naming==0.13.*", "pep8-naming==0.14.*",
"potypo", "potypo",
"pytest-asyncio", "pytest-asyncio",
"pytest-cache", "pytest-cache",
@@ -126,8 +126,8 @@ dev = [
"pytest-mock==3.14.*", "pytest-mock==3.14.*",
"pytest-rerunfailures==14.*", "pytest-rerunfailures==14.*",
"pytest-sugar", "pytest-sugar",
"pytest-xdist==3.5.*", "pytest-xdist==3.6.*",
"pytest==8.1.*", "pytest==8.2.*",
"responses", "responses",
] ]
+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 # 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/>. # <https://www.gnu.org/licenses/>.
# #
__version__ = "2024.4.0" __version__ = "2024.7.0.dev0"
+2
View File
@@ -79,6 +79,7 @@ ALL_LANGUAGES = [
('de', _('German')), ('de', _('German')),
('de-informal', _('German (informal)')), ('de-informal', _('German (informal)')),
('ar', _('Arabic')), ('ar', _('Arabic')),
('ca', _('Catalan')),
('zh-hans', _('Chinese (simplified)')), ('zh-hans', _('Chinese (simplified)')),
('zh-hant', _('Chinese (traditional)')), ('zh-hant', _('Chinese (traditional)')),
('cs', _('Czech')), ('cs', _('Czech')),
@@ -98,6 +99,7 @@ ALL_LANGUAGES = [
('pt-br', _('Portuguese (Brazil)')), ('pt-br', _('Portuguese (Brazil)')),
('ro', _('Romanian')), ('ro', _('Romanian')),
('ru', _('Russian')), ('ru', _('Russian')),
('sk', _('Slovak')),
('es', _('Spanish')), ('es', _('Spanish')),
('tr', _('Turkish')), ('tr', _('Turkish')),
('uk', _('Ukrainian')), ('uk', _('Ukrainian')),
+3 -1
View File
@@ -684,8 +684,9 @@ class EventSettingsSerializer(SettingsSerializer):
'locales', 'locales',
'locale', 'locale',
'region', 'region',
'last_order_modification_date', 'allow_modifications',
'allow_modifications_after_checkin', 'allow_modifications_after_checkin',
'last_order_modification_date',
'show_quota_left', 'show_quota_left',
'waiting_list_enabled', 'waiting_list_enabled',
'waiting_list_auto_disable', 'waiting_list_auto_disable',
@@ -735,6 +736,7 @@ class EventSettingsSerializer(SettingsSerializer):
'payment_term_accept_late', 'payment_term_accept_late',
'payment_explanation', 'payment_explanation',
'payment_pending_hidden', 'payment_pending_hidden',
'payment_giftcard__enabled',
'mail_days_order_expire_warning', 'mail_days_order_expire_warning',
'ticket_download', 'ticket_download',
'ticket_download_date', 'ticket_download_date',
+4 -1
View File
@@ -564,6 +564,8 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
attendee_name = AttendeeNameField(source='*') attendee_name = AttendeeNameField(source='*')
attendee_name_parts = AttendeeNamePartsField(source='*') attendee_name_parts = AttendeeNamePartsField(source='*')
order__status = serializers.SlugRelatedField(read_only=True, slug_field='status', source='order') order__status = serializers.SlugRelatedField(read_only=True, slug_field='status', source='order')
order__valid_if_pending = serializers.SlugRelatedField(read_only=True, slug_field='valid_if_pending', source='order')
order__require_approval = serializers.SlugRelatedField(read_only=True, slug_field='require_approval', source='order')
class Meta: class Meta:
model = OrderPosition model = OrderPosition
@@ -571,7 +573,8 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
'company', 'street', 'zipcode', 'city', 'country', 'state', 'company', 'street', 'zipcode', 'city', 'country', 'state',
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins', 'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'require_attention', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'require_attention',
'order__status', 'valid_from', 'valid_until', 'blocked') 'order__status', 'order__valid_if_pending', 'order__require_approval', 'valid_from', 'valid_until',
'blocked')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
+1
View File
@@ -89,6 +89,7 @@ class SettingsSerializer(serializers.Serializer):
except OSError: # pragma: no cover except OSError: # pragma: no cover
logger.error('Deleting file %s failed.' % fname.name) logger.error('Deleting file %s failed.' % fname.name)
instance.delete(attr) instance.delete(attr)
self.changed_data.append(attr)
else: else:
# file is unchanged # file is unchanged
continue continue
+14 -9
View File
@@ -57,10 +57,8 @@ from pretix.base.models import (
) )
from pretix.base.models.event import SubEvent from pretix.base.models.event import SubEvent
from pretix.base.services.quotas import QuotaAvailability from pretix.base.services.quotas import QuotaAvailability
from pretix.base.settings import SETTINGS_AFFECTING_CSS
from pretix.helpers.dicts import merge_dicts from pretix.helpers.dicts import merge_dicts
from pretix.helpers.i18n import i18ncomp from pretix.helpers.i18n import i18ncomp
from pretix.presale.style import regenerate_css
from pretix.presale.views.organizer import filter_qs_by_attr from pretix.presale.views.organizer import filter_qs_by_attr
with scopes_disabled(): with scopes_disabled():
@@ -190,7 +188,10 @@ class EventViewSet(viewsets.ModelViewSet):
serializer = self.get_serializer(page, many=True) serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data) return self.get_paginated_response(serializer.data)
@transaction.atomic()
def perform_update(self, serializer): def perform_update(self, serializer):
original_data = self.get_serializer(instance=serializer.instance).data
current_live_value = serializer.instance.live current_live_value = serializer.instance.live
updated_live_value = serializer.validated_data.get('live', None) updated_live_value = serializer.validated_data.get('live', None)
current_plugins_value = serializer.instance.get_plugins() current_plugins_value = serializer.instance.get_plugins()
@@ -198,6 +199,11 @@ class EventViewSet(viewsets.ModelViewSet):
super().perform_update(serializer) super().perform_update(serializer)
if serializer.data == original_data:
# Performance optimization: If nothing was changed, we do not need to save or log anything.
# This costs us a few cycles on save, but avoids thousands of lines in our log.
return
if updated_live_value is not None and updated_live_value != current_live_value: if updated_live_value is not None and updated_live_value != current_live_value:
log_action = 'pretix.event.live.activated' if updated_live_value else 'pretix.event.live.deactivated' log_action = 'pretix.event.live.activated' if updated_live_value else 'pretix.event.live.deactivated'
serializer.instance.log_action( serializer.instance.log_action(
@@ -622,13 +628,12 @@ class EventSettingsView(views.APIView):
s.is_valid(raise_exception=True) s.is_valid(raise_exception=True)
with transaction.atomic(): with transaction.atomic():
s.save() s.save()
self.request.event.log_action( if s.changed_data:
'pretix.event.settings', user=self.request.user, auth=self.request.auth, data={ self.request.event.log_action(
k: v for k, v in s.validated_data.items() 'pretix.event.settings', user=self.request.user, auth=self.request.auth, data={
} k: v for k, v in s.validated_data.items()
) }
if any(p in s.changed_data for p in SETTINGS_AFFECTING_CSS): )
regenerate_css.apply_async(args=(request.event.pk,))
s = EventSettingsSerializer( s = EventSettingsSerializer(
instance=request.event.settings, event=request.event, context={ instance=request.event.settings, event=request.event, context={
'request': request 'request': request
+6 -1
View File
@@ -97,6 +97,7 @@ from pretix.base.signals import (
) )
from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.money import money_filter
from pretix.control.signals import order_search_filter_q from pretix.control.signals import order_search_filter_q
from pretix.helpers import OF_SELF
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -575,8 +576,10 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
return self.retrieve(request, [], **kwargs) return self.retrieve(request, [], **kwargs)
@action(detail=True, methods=['POST']) @action(detail=True, methods=['POST'])
@transaction.atomic()
def create_invoice(self, request, **kwargs): def create_invoice(self, request, **kwargs):
order = self.get_object() order = self.get_object()
order = Order.objects.select_for_update(of=OF_SELF).get(pk=order.pk)
has_inv = order.invoices.exists() and not ( has_inv = order.invoices.exists() and not (
order.status in (Order.STATUS_PAID, Order.STATUS_PENDING) order.status in (Order.STATUS_PAID, Order.STATUS_PENDING)
and order.invoices.filter(is_cancellation=True).count() >= order.invoices.filter(is_cancellation=False).count() and order.invoices.filter(is_cancellation=True).count() >= order.invoices.filter(is_cancellation=False).count()
@@ -1905,6 +1908,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
return Response(status=204) return Response(status=204)
@action(detail=True, methods=['POST']) @action(detail=True, methods=['POST'])
@transaction.atomic()
def reissue(self, request, **kwargs): def reissue(self, request, **kwargs):
inv = self.get_object() inv = self.get_object()
if inv.canceled: if inv.canceled:
@@ -1912,9 +1916,10 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
elif inv.shredded: elif inv.shredded:
raise PermissionDenied('The invoice file is no longer stored on the server.') raise PermissionDenied('The invoice file is no longer stored on the server.')
else: else:
order = Order.objects.select_for_update(of=OF_SELF).get(pk=inv.order_id)
c = generate_cancellation(inv) c = generate_cancellation(inv)
if inv.order.status != Order.STATUS_CANCELED: if inv.order.status != Order.STATUS_CANCELED:
inv = generate_invoice(inv.order) inv = generate_invoice(order)
else: else:
inv = c inv = c
inv.order.log_action( inv.order.log_action(
-4
View File
@@ -51,10 +51,8 @@ from pretix.base.models import (
MembershipType, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite, MembershipType, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite,
User, User,
) )
from pretix.base.settings import SETTINGS_AFFECTING_CSS
from pretix.helpers import OF_SELF from pretix.helpers import OF_SELF
from pretix.helpers.dicts import merge_dicts from pretix.helpers.dicts import merge_dicts
from pretix.presale.style import regenerate_organizer_css
class OrganizerViewSet(viewsets.ReadOnlyModelViewSet): class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
@@ -504,8 +502,6 @@ class OrganizerSettingsView(views.APIView):
k: v for k, v in s.validated_data.items() k: v for k, v in s.validated_data.items()
} }
) )
if any(p in s.changed_data for p in SETTINGS_AFFECTING_CSS):
regenerate_organizer_css.apply_async(args=(request.organizer.pk,))
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={ s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={
'request': request 'request': request
}) })
+24 -1
View File
@@ -39,10 +39,12 @@ from zipfile import ZipFile
from django import forms from django import forms
from django.dispatch import receiver from django.dispatch import receiver
from django.urls import reverse
from django.utils.translation import gettext_lazy as _, pgettext_lazy from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.models import QuestionAnswer from pretix.base.models import QuestionAnswer
from ...control.forms.widgets import Select2
from ..exporter import BaseExporter from ..exporter import BaseExporter
from ..signals import register_data_exporters from ..signals import register_data_exporters
@@ -56,7 +58,7 @@ class AnswerFilesExporter(BaseExporter):
@property @property
def export_form_fields(self): def export_form_fields(self):
return OrderedDict( d = OrderedDict(
[ [
('questions', ('questions',
forms.ModelMultipleChoiceField( forms.ModelMultipleChoiceField(
@@ -69,11 +71,32 @@ class AnswerFilesExporter(BaseExporter):
)), )),
] ]
) )
if self.event.has_subevents:
d['subevent'] = forms.ModelChoiceField(
label=pgettext_lazy('subevent', 'Date'),
queryset=self.event.subevents.all(),
required=False,
empty_label=pgettext_lazy('subevent', 'All dates')
)
d['subevent'].widget = Select2(
attrs={
'data-model-select2': 'event',
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
}),
'data-placeholder': pgettext_lazy('subevent', 'All dates')
}
)
d['subevent'].widget.choices = d['subevent'].choices
return d
def render(self, form_data: dict): def render(self, form_data: dict):
qs = QuestionAnswer.objects.filter( qs = QuestionAnswer.objects.filter(
orderposition__order__event=self.event, orderposition__order__event=self.event,
).select_related('orderposition', 'orderposition__order', 'question') ).select_related('orderposition', 'orderposition__order', 'question')
if form_data.get('subevent'):
qs = qs.filter(orderposition__subevent=form_data.get('subevent'))
if form_data.get('questions'): if form_data.get('questions'):
qs = qs.filter(question__in=form_data['questions']) qs = qs.filter(question__in=form_data['questions'])
with tempfile.TemporaryDirectory() as d: with tempfile.TemporaryDirectory() as d:
+30 -5
View File
@@ -32,11 +32,12 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # 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. # License for the specific language governing permissions and limitations under the License.
from collections import OrderedDict from collections import OrderedDict, defaultdict
from decimal import Decimal from decimal import Decimal
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from django import forms from django import forms
from django.conf import settings
from django.db.models import ( from django.db.models import (
Case, CharField, Count, DateTimeField, F, IntegerField, Max, Min, OuterRef, Case, CharField, Count, DateTimeField, F, IntegerField, Max, Min, OuterRef,
Q, Subquery, Sum, When, Q, Subquery, Sum, When,
@@ -54,7 +55,7 @@ from openpyxl.comments import Comment
from openpyxl.styles import Font, PatternFill from openpyxl.styles import Font, PatternFill
from pretix.base.models import ( from pretix.base.models import (
GiftCard, GiftCardTransaction, Invoice, InvoiceAddress, Order, Checkin, GiftCard, GiftCardTransaction, Invoice, InvoiceAddress, Order,
OrderPosition, Question, OrderPosition, Question,
) )
from pretix.base.models.orders import ( from pretix.base.models.orders import (
@@ -541,6 +542,22 @@ class OrderListExporter(MultiSheetListExporter):
).order_by() ).order_by()
qs = base_qs.annotate( qs = base_qs.annotate(
payment_providers=Subquery(p_providers, output_field=CharField()), payment_providers=Subquery(p_providers, output_field=CharField()),
checked_in_lists=Subquery(
Checkin.objects.filter(
successful=True,
type=Checkin.TYPE_ENTRY,
position=OuterRef("pk"),
).order_by().values("position").annotate(
c=GroupConcat(
"list__name",
# These appear not to work properly on SQLite. Well, we don't support SQLite outside testing
# anyways.
ordered='sqlite' not in settings.DATABASES['default']['ENGINE'],
distinct='sqlite' not in settings.DATABASES['default']['ENGINE'],
delimiter=", "
)
).values("c")
),
).select_related( ).select_related(
'order', 'order__invoice_address', 'order__customer', 'item', 'variation', 'order', 'order__invoice_address', 'order__customer', 'item', 'variation',
'voucher', 'tax_rule' 'voucher', 'tax_rule'
@@ -605,10 +622,9 @@ class OrderListExporter(MultiSheetListExporter):
] ]
questions = list(Question.objects.filter(event__in=self.events)) questions = list(Question.objects.filter(event__in=self.events))
options = {} options = defaultdict(list)
for q in questions: for q in questions:
if q.type == Question.TYPE_CHOICE_MULTIPLE: if q.type == Question.TYPE_CHOICE_MULTIPLE:
options[q.pk] = []
if form_data['group_multiple_choice']: if form_data['group_multiple_choice']:
for o in q.options.all(): for o in q.options.all():
options[q.pk].append(o) options[q.pk].append(o)
@@ -618,6 +634,9 @@ class OrderListExporter(MultiSheetListExporter):
headers.append(str(q.question) + ' ' + str(o.answer)) headers.append(str(q.question) + ' ' + str(o.answer))
options[q.pk].append(o) options[q.pk].append(o)
else: else:
if q.type == Question.TYPE_CHOICE:
for o in q.options.all():
options[q.pk].append(o)
headers.append(str(q.question)) headers.append(str(q.question))
headers += [ headers += [
_('Company'), _('Company'),
@@ -636,6 +655,7 @@ class OrderListExporter(MultiSheetListExporter):
_('Sales channel'), _('Order locale'), _('Sales channel'), _('Order locale'),
_('E-mail address verified'), _('E-mail address verified'),
_('External customer ID'), _('External customer ID'),
_('Check-in lists'),
_('Payment providers'), _('Payment providers'),
] ]
@@ -727,7 +747,7 @@ class OrderListExporter(MultiSheetListExporter):
for a in op.answers.all(): for a in op.answers.all():
# We do not want to localize Date, Time and Datetime question answers, as those can lead # We do not want to localize Date, Time and Datetime question answers, as those can lead
# to difficulties parsing the data (for example 2019-02-01 may become Février, 2019 01 in French). # to difficulties parsing the data (for example 2019-02-01 may become Février, 2019 01 in French).
if a.question.type == Question.TYPE_CHOICE_MULTIPLE: if a.question.type in (Question.TYPE_CHOICE_MULTIPLE, Question.TYPE_CHOICE):
acache[a.question_id] = set(o.pk for o in a.options.all()) acache[a.question_id] = set(o.pk for o in a.options.all())
elif a.question.type in Question.UNLOCALIZED_TYPES: elif a.question.type in Question.UNLOCALIZED_TYPES:
acache[a.question_id] = a.answer acache[a.question_id] = a.answer
@@ -740,6 +760,10 @@ class OrderListExporter(MultiSheetListExporter):
else: else:
for o in options[q.pk]: for o in options[q.pk]:
row.append(_('Yes') if o.pk in acache.get(q.pk, set()) else _('No')) row.append(_('Yes') if o.pk in acache.get(q.pk, set()) else _('No'))
elif q.type == Question.TYPE_CHOICE:
# Join is only necessary if the question type was modified but also keeps the code simpler here
# as we'd otherwise need some [0] and existance checks
row.append(", ".join(str(o.answer) for o in options[q.pk] if o.pk in acache.get(q.pk, set())))
else: else:
row.append(acache.get(q.pk, '')) row.append(acache.get(q.pk, ''))
@@ -770,6 +794,7 @@ class OrderListExporter(MultiSheetListExporter):
_('Yes') if order.email_known_to_work else _('No'), _('Yes') if order.email_known_to_work else _('No'),
str(order.customer.external_identifier) if order.customer and order.customer.external_identifier else '', str(order.customer.external_identifier) if order.customer and order.customer.external_identifier else '',
] ]
row.append(op.checked_in_lists or "")
row.append(', '.join([ row.append(', '.join([
str(self.providers.get(p, p)) for p in sorted(set((op.payment_providers or '').split(','))) str(self.providers.get(p, p)) for p in sorted(set((op.payment_providers or '').split(',')))
if p and p != 'free' if p and p != 'free'
+7 -6
View File
@@ -57,7 +57,7 @@ from django.forms.widgets import FILE_INPUT_CONTRADICTION
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.html import escape from django.utils.html import escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.timezone import get_current_timezone, now from django.utils.timezone import get_current_timezone
from django.utils.translation import gettext_lazy as _, pgettext_lazy from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_countries import countries from django_countries import countries
from django_countries.fields import Country, CountryField from django_countries.fields import Country, CountryField
@@ -86,6 +86,7 @@ from pretix.base.settings import (
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS,
) )
from pretix.base.templatetags.rich_text import rich_text from pretix.base.templatetags.rich_text import rich_text
from pretix.base.timemachine import time_machine_now
from pretix.control.forms import ( from pretix.control.forms import (
ExtFileField, ExtValidationMixin, SizeValidationMixin, SplitDateTimeField, ExtFileField, ExtValidationMixin, SizeValidationMixin, SplitDateTimeField,
) )
@@ -606,13 +607,13 @@ class BaseQuestionsForm(forms.Form):
if cartpos and item.validity_mode == Item.VALIDITY_MODE_DYNAMIC and item.validity_dynamic_start_choice: if cartpos and item.validity_mode == Item.VALIDITY_MODE_DYNAMIC and item.validity_dynamic_start_choice:
if item.validity_dynamic_start_choice_day_limit: if item.validity_dynamic_start_choice_day_limit:
max_date = now().astimezone(event.timezone) + timedelta(days=item.validity_dynamic_start_choice_day_limit) max_date = time_machine_now().astimezone(event.timezone) + timedelta(days=item.validity_dynamic_start_choice_day_limit)
else: else:
max_date = None max_date = None
min_date = now() min_date = time_machine_now()
initial = None initial = None
if (item.require_membership or (pos.variation and pos.variation.require_membership)) and pos.used_membership: if (item.require_membership or (pos.variation and pos.variation.require_membership)) and pos.used_membership:
if pos.used_membership.date_start >= now(): if pos.used_membership.date_start >= time_machine_now():
initial = min_date = pos.used_membership.date_start initial = min_date = pos.used_membership.date_start
max_date = min(max_date, pos.used_membership.date_end) if max_date else pos.used_membership.date_end max_date = min(max_date, pos.used_membership.date_end) if max_date else pos.used_membership.date_end
if item.validity_dynamic_duration_months or item.validity_dynamic_duration_days: if item.validity_dynamic_duration_months or item.validity_dynamic_duration_days:
@@ -1034,7 +1035,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
self.all_optional = kwargs.pop('all_optional', False) self.all_optional = kwargs.pop('all_optional', False)
kwargs.setdefault('initial', {}) kwargs.setdefault('initial', {})
if not kwargs.get('instance') or not kwargs['instance'].country: if (not kwargs.get('instance') or not kwargs['instance'].country) and not kwargs["initial"].get("country"):
kwargs['initial']['country'] = guess_country_from_request(self.request, self.event) kwargs['initial']['country'] = guess_country_from_request(self.request, self.event)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -1170,7 +1171,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
self.instance.vat_id_validated = False self.instance.vat_id_validated = False
messages.warning(self.request, e.message) messages.warning(self.request, e.message)
else: else:
raise ValidationError(e.message) raise ValidationError({"vat_id": e.message})
except VATIDTemporaryError as e: except VATIDTemporaryError as e:
self.instance.vat_id_validated = False self.instance.vat_id_validated = False
if self.request and self.vat_warning: if self.request and self.vat_warning:
+15 -16
View File
@@ -32,13 +32,13 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # 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. # License for the specific language governing permissions and limitations under the License.
import re
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import BaseValidator from django.core.validators import BaseValidator
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from i18nfield.strings import LazyI18nString from i18nfield.strings import LazyI18nString
from pretix.helpers.format import format_map
class PlaceholderValidator(BaseValidator): class PlaceholderValidator(BaseValidator):
""" """
@@ -47,6 +47,12 @@ class PlaceholderValidator(BaseValidator):
which are not presented in taken list. which are not presented in taken list.
""" """
error_message = _(
'There is an error with your placeholder syntax. Please check that the opening "{" and closing "}" curly '
'brackets on your placeholders match up. '
'Please note: to use literal "{" or "}", you need to double them as "{{" and "}}".'
)
def __init__(self, limit_value): def __init__(self, limit_value):
super().__init__(limit_value) super().__init__(limit_value)
self.limit_value = limit_value self.limit_value = limit_value
@@ -57,22 +63,15 @@ class PlaceholderValidator(BaseValidator):
self.__call__(v) self.__call__(v)
return return
if value.count('{') != value.count('}'): try:
format_map(value, {key.strip('{}'): "" for key in self.limit_value}, raise_on_missing=True)
except ValueError:
raise ValidationError(self.error_message, code='invalid_placeholder_syntax')
except KeyError as e:
raise ValidationError( raise ValidationError(
_('Invalid placeholder syntax: You used a different number of "{" than of "}".'), _('Invalid placeholder: {%(value)s}'),
code='invalid_placeholder_syntax',
)
data_placeholders = list(re.findall(r'({[^}]*})', value, re.X))
invalid_placeholders = []
for placeholder in data_placeholders:
if placeholder not in self.limit_value:
invalid_placeholders.append(placeholder)
if invalid_placeholders:
raise ValidationError(
_('Invalid placeholder(s): %(value)s'),
code='invalid_placeholders', code='invalid_placeholders',
params={'value': ", ".join(invalid_placeholders,)}) params={'value': e.args[0]})
def clean(self, x): def clean(self, x):
return x return x
@@ -0,0 +1,24 @@
# Generated by Django 4.2.11 on 2024-05-16 11:07
from django.db import migrations, models
import pretix.base.models.orders
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0263_auto_20240409_0732"),
]
operations = [
migrations.AddField(
model_name="order",
name="internal_secret",
field=models.CharField(
default=None,
max_length=32,
null=True,
),
),
]
+30 -3
View File
@@ -19,6 +19,7 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # 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/>. # <https://www.gnu.org/licenses/>.
# #
import datetime
from collections import defaultdict from collections import defaultdict
import pycountry import pycountry
@@ -26,6 +27,7 @@ from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import EmailValidator from django.core.validators import EmailValidator
from django.db.models import Q from django.db.models import Q
from django.utils import formats
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import ( from django.utils.translation import (
gettext as _, gettext_lazy, pgettext, pgettext_lazy, gettext as _, gettext_lazy, pgettext, pgettext_lazy,
@@ -509,6 +511,30 @@ class ValidUntil(DatetimeColumnMixin, ImportColumn):
position.valid_until = value position.valid_until = value
class Expires(DatetimeColumnMixin, ImportColumn):
identifier = 'expires'
verbose_name = gettext_lazy('Expiry date')
def clean(self, value, previous_values):
if not value:
return
input_formats = formats.get_format('DATE_INPUT_FORMATS', use_l10n=True)
for format in input_formats:
try:
d = datetime.datetime.strptime(value, format)
d = d.replace(tzinfo=self.timezone, hour=23, minute=59, second=59)
return d
except (ValueError, TypeError):
pass
else:
return super().clean(value, previous_values) # parse date
def assign(self, value, order, position, invoice_address, **kwargs):
if value:
order.expires = value
class Saleschannel(ImportColumn): class Saleschannel(ImportColumn):
identifier = 'sales_channel' identifier = 'sales_channel'
verbose_name = gettext_lazy('Sales channel') verbose_name = gettext_lazy('Sales channel')
@@ -702,12 +728,13 @@ def get_order_import_columns(event):
AttendeeState(event), AttendeeState(event),
Price(event), Price(event),
Secret(event), Secret(event),
Locale(event),
Saleschannel(event),
SeatColumn(event), SeatColumn(event),
Comment(event),
ValidFrom(event), ValidFrom(event),
ValidUntil(event), ValidUntil(event),
Locale(event),
Saleschannel(event),
Expires(event),
Comment(event),
] ]
for q in event.questions.prefetch_related('options').exclude(type=Question.TYPE_FILE): for q in event.questions.prefetch_related('options').exclude(type=Question.TYPE_FILE):
default.append(QuestionColumn(event, q)) default.append(QuestionColumn(event, q))
+1 -3
View File
@@ -44,8 +44,6 @@ class CodeColumn(ImportColumn):
super().__init__(*args) super().__init__(*args)
def clean(self, value, previous_values): def clean(self, value, previous_values):
if not value:
raise ValidationError(_('A voucher cannot be created without a code.'))
if value: if value:
MinLengthValidator(5)(value) MinLengthValidator(5)(value)
if value and (value in self._cached or Voucher.objects.filter(event=self.event, code=value).exists()): if value and (value in self._cached or Voucher.objects.filter(event=self.event, code=value).exists()):
@@ -77,7 +75,7 @@ class MaxUsagesColumn(IntegerColumnMixin, ImportColumn):
] ]
def clean(self, value, previous_values): def clean(self, value, previous_values):
if value is None: if value is None and previous_values.get("code"):
raise ValidationError(_('The maximum number of usages must be set.')) raise ValidationError(_('The maximum number of usages must be set.'))
return super().clean(value, previous_values) return super().clean(value, previous_values)
+7 -3
View File
@@ -418,18 +418,22 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
else: else:
return set() return set()
def has_event_permission(self, organizer, event, perm_name=None, request=None) -> bool: def has_event_permission(self, organizer, event, perm_name=None, request=None, session_key=None) -> bool:
""" """
Checks if this user is part of any team that grants access of type ``perm_name`` Checks if this user is part of any team that grants access of type ``perm_name``
to the event ``event``. to the event ``event``.
Either ``request`` or ``session_key`` are required to detect staff sessions properly.
:param organizer: The organizer of the event :param organizer: The organizer of the event
:param event: The event to check :param event: The event to check
:param perm_name: The permission, e.g. ``can_change_teams`` :param perm_name: The permission, e.g. ``can_change_teams``
:param request: The current request (optional). Required to detect staff sessions properly. :param request: The current request (optional)
:param session_key: The current session key (optional)
:return: bool :return: bool
""" """
if request and self.has_active_staff_session(request.session.session_key): assert not (session_key and request)
if (session_key or request) and self.has_active_staff_session(session_key or request.session.session_key):
return True return True
teams = self._get_teams_for_event(organizer, event) teams = self._get_teams_for_event(organizer, event)
if teams: if teams:
+2 -1
View File
@@ -23,6 +23,7 @@
from collections import defaultdict from collections import defaultdict
from decimal import Decimal from decimal import Decimal
from itertools import groupby from itertools import groupby
from math import ceil
from typing import Dict, Optional, Tuple from typing import Dict, Optional, Tuple
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@@ -272,7 +273,7 @@ class Discount(LoggedModel):
# Prevent over-consuming of items, i.e. if our discount is "buy 2, get 1 free", we only # Prevent over-consuming of items, i.e. if our discount is "buy 2, get 1 free", we only
# want to match multiples of 3 # want to match multiples of 3
n_groups = min(len(condition_idx_group) // self.condition_min_count, len(benefit_idx_group)) n_groups = min(len(condition_idx_group) // self.condition_min_count, ceil(len(benefit_idx_group) / self.benefit_only_apply_to_cheapest_n_matches))
consume_idx = condition_idx_group[:n_groups * self.condition_min_count] consume_idx = condition_idx_group[:n_groups * self.condition_min_count]
benefit_idx = benefit_idx_group[:n_groups * self.benefit_only_apply_to_cheapest_n_matches] benefit_idx = benefit_idx_group[:n_groups * self.benefit_only_apply_to_cheapest_n_matches]
else: else:
+18 -31
View File
@@ -45,6 +45,7 @@ from zoneinfo import ZoneInfo
import pytz_deprecation_shim import pytz_deprecation_shim
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.files import File
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.core.mail import get_connection from django.core.mail import get_connection
from django.core.validators import ( from django.core.validators import (
@@ -67,6 +68,7 @@ from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.models.base import LoggedModel from pretix.base.models.base import LoggedModel
from pretix.base.models.fields import MultiStringField from pretix.base.models.fields import MultiStringField
from pretix.base.reldate import RelativeDateWrapper from pretix.base.reldate import RelativeDateWrapper
from pretix.base.timemachine import time_machine_now
from pretix.base.validators import EventSlugBanlistValidator from pretix.base.validators import EventSlugBanlistValidator
from pretix.helpers.database import GroupConcat from pretix.helpers.database import GroupConcat
from pretix.helpers.daterange import daterange from pretix.helpers.daterange import daterange
@@ -234,7 +236,7 @@ class EventMixin:
if not self.settings.waiting_list_enabled: if not self.settings.waiting_list_enabled:
return False return False
if self.settings.waiting_list_auto_disable: if self.settings.waiting_list_auto_disable:
return self.settings.waiting_list_auto_disable.datetime(self) > now() return self.settings.waiting_list_auto_disable.datetime(self) > time_machine_now()
return True return True
@property @property
@@ -243,11 +245,11 @@ class EventMixin:
Is true, when ``presale_end`` is set and in the past. Is true, when ``presale_end`` is set and in the past.
""" """
if self.effective_presale_end: if self.effective_presale_end:
return now() > self.effective_presale_end return time_machine_now() > self.effective_presale_end
elif self.date_to: elif self.date_to:
return now() > self.date_to return time_machine_now() > self.date_to
else: else:
return now().astimezone(self.timezone).date() > self.date_from.astimezone(self.timezone).date() return time_machine_now().astimezone(self.timezone).date() > self.date_from.astimezone(self.timezone).date()
@property @property
def effective_presale_start(self): def effective_presale_start(self):
@@ -267,7 +269,7 @@ class EventMixin:
Is true, when ``presale_end`` is not set or in the future and ``presale_start`` is not Is true, when ``presale_end`` is not set or in the future and ``presale_start`` is not
set or in the past. set or in the past.
""" """
if self.effective_presale_start and now() < self.effective_presale_start: if self.effective_presale_start and time_machine_now() < self.effective_presale_start:
return False return False
return not self.presale_has_ended return not self.presale_has_ended
@@ -315,11 +317,11 @@ class EventMixin:
q_variation = ( q_variation = (
Q(active=True) Q(active=True)
& Q(sales_channels__contains=channel) & Q(sales_channels__contains=channel)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now())) & Q(Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now())) & Q(Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()))
& Q(item__active=True) & Q(item__active=True)
& Q(Q(item__available_from__isnull=True) | Q(item__available_from__lte=now())) & Q(Q(item__available_from__isnull=True) | Q(item__available_from__lte=time_machine_now()))
& Q(Q(item__available_until__isnull=True) | Q(item__available_until__gte=now())) & Q(Q(item__available_until__isnull=True) | Q(item__available_until__gte=time_machine_now()))
& Q(Q(item__category__isnull=True) | Q(item__category__is_addon=False)) & Q(Q(item__category__isnull=True) | Q(item__category__is_addon=False))
& Q(item__sales_channels__contains=channel) & Q(item__sales_channels__contains=channel)
& Q(item__require_bundling=False) & Q(item__require_bundling=False)
@@ -694,7 +696,7 @@ class Event(EventMixin, LoggedModel):
@property @property
def presale_has_ended(self): def presale_has_ended(self):
if self.has_subevents: if self.has_subevents:
return self.presale_end and now() > self.presale_end return self.presale_end and time_machine_now() > self.presale_end
else: else:
return super().presale_has_ended return super().presale_has_ended
@@ -787,8 +789,6 @@ class Event(EventMixin, LoggedModel):
), tz) ), tz)
def copy_data_from(self, other, skip_meta_data=False): def copy_data_from(self, other, skip_meta_data=False):
from pretix.presale.style import regenerate_css
from ..signals import event_copy_data from ..signals import event_copy_data
from . import ( from . import (
Discount, Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, Discount, Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue,
@@ -1009,10 +1009,10 @@ class Event(EventMixin, LoggedModel):
s.product = item_map[s.product_id] s.product = item_map[s.product_id]
s.save(force_insert=True) s.save(force_insert=True)
has_custom_style = other.settings.presale_css_file or other.settings.presale_widget_css_file
skip_settings = ( skip_settings = (
'ticket_secrets_pretix_sig1_pubkey', 'ticket_secrets_pretix_sig1_pubkey',
'ticket_secrets_pretix_sig1_privkey', 'ticket_secrets_pretix_sig1_privkey',
# no longer used, but we still don't need to copy them
'presale_css_file', 'presale_css_file',
'presale_css_checksum', 'presale_css_checksum',
'presale_widget_css_file', 'presale_widget_css_file',
@@ -1025,7 +1025,7 @@ class Event(EventMixin, LoggedModel):
s.object = self s.object = self
s.pk = None s.pk = None
if s.value.startswith('file://'): if s.value.startswith('file://') and settings_hierarkey.get_declared_type(s.key) == File:
fi = default_storage.open(s.value[len('file://'):], 'rb') fi = default_storage.open(s.value[len('file://'):], 'rb')
nonce = get_random_string(length=8) nonce = get_random_string(length=8)
fname_base = clean_filename(os.path.basename(s.value)) fname_base = clean_filename(os.path.basename(s.value))
@@ -1055,9 +1055,6 @@ class Event(EventMixin, LoggedModel):
question_map=question_map, checkin_list_map=checkin_list_map, quota_map=quota_map, question_map=question_map, checkin_list_map=checkin_list_map, quota_map=quota_map,
) )
if has_custom_style:
regenerate_css.apply_async(args=(self.pk,))
def get_payment_providers(self, cached=False) -> dict: def get_payment_providers(self, cached=False) -> dict:
""" """
Returns a dictionary of initialized payment providers mapped by their identifiers. Returns a dictionary of initialized payment providers mapped by their identifiers.
@@ -1187,8 +1184,8 @@ class Event(EventMixin, LoggedModel):
) )
).filter( ).filter(
Q(active=True) & Q(is_public=True) & ( Q(active=True) & Q(is_public=True) & (
Q(Q(date_to__isnull=True) & Q(date_from__gte=now() - timedelta(hours=24))) Q(Q(date_to__isnull=True) & Q(date_from__gte=time_machine_now() - timedelta(hours=24)))
| Q(date_to__gte=now() - timedelta(hours=24)) | Q(date_to__gte=time_machine_now() - timedelta(hours=24))
) )
) # order_by doesn't make sense with I18nField ) # order_by doesn't make sense with I18nField
if ordering in ("date_ascending", "date_descending"): if ordering in ("date_ascending", "date_descending"):
@@ -1335,18 +1332,12 @@ class Event(EventMixin, LoggedModel):
def enable_plugin(self, module, allow_restricted=frozenset()): def enable_plugin(self, module, allow_restricted=frozenset()):
plugins_active = self.get_plugins() plugins_active = self.get_plugins()
from pretix.presale.style import regenerate_css
if module not in plugins_active: if module not in plugins_active:
plugins_active.append(module) plugins_active.append(module)
self.set_active_plugins(plugins_active, allow_restricted=allow_restricted) self.set_active_plugins(plugins_active, allow_restricted=allow_restricted)
regenerate_css.apply_async(args=(self.pk,))
def disable_plugin(self, module): def disable_plugin(self, module):
plugins_active = self.get_plugins() plugins_active = self.get_plugins()
from pretix.presale.style import regenerate_css
if module in plugins_active: if module in plugins_active:
plugins_active.remove(module) plugins_active.remove(module)
self.set_active_plugins(plugins_active) self.set_active_plugins(plugins_active)
@@ -1355,8 +1346,6 @@ class Event(EventMixin, LoggedModel):
if module in plugins_available and hasattr(plugins_available[module].app, 'uninstalled'): if module in plugins_available and hasattr(plugins_available[module].app, 'uninstalled'):
getattr(plugins_available[module].app, 'uninstalled')(self) getattr(plugins_available[module].app, 'uninstalled')(self)
regenerate_css.apply_async(args=(self.pk,))
@staticmethod @staticmethod
def clean_has_subevents(event, has_subevents): def clean_has_subevents(event, has_subevents):
if event is not None and event.has_subevents is not None: if event is not None and event.has_subevents is not None:
@@ -1466,8 +1455,6 @@ class SubEvent(EventMixin, LoggedModel):
seating_plan = models.ForeignKey('SeatingPlan', on_delete=models.PROTECT, null=True, blank=True, seating_plan = models.ForeignKey('SeatingPlan', on_delete=models.PROTECT, null=True, blank=True,
related_name='subevents', verbose_name=_('Seating plan')) related_name='subevents', verbose_name=_('Seating plan'))
items = models.ManyToManyField('Item', through='SubEventItem')
variations = models.ManyToManyField('ItemVariation', through='SubEventItemVariation')
comment = models.TextField( comment = models.TextField(
verbose_name=_("Internal comment"), verbose_name=_("Internal comment"),
null=True, blank=True null=True, blank=True
@@ -1508,7 +1495,7 @@ class SubEvent(EventMixin, LoggedModel):
disabled_items=Coalesce( disabled_items=Coalesce(
Subquery( Subquery(
SubEventItem.objects.filter( SubEventItem.objects.filter(
Q(disabled=True) | Q(available_from__gt=now()) | Q(available_until__lt=now()), Q(disabled=True) | Q(available_from__gt=time_machine_now()) | Q(available_until__lt=time_machine_now()),
subevent=OuterRef('pk'), subevent=OuterRef('pk'),
).order_by().values('subevent').annotate(items=GroupConcat('item_id', delimiter=',')).values('items'), ).order_by().values('subevent').annotate(items=GroupConcat('item_id', delimiter=',')).values('items'),
output_field=models.TextField(), output_field=models.TextField(),
@@ -1519,7 +1506,7 @@ class SubEvent(EventMixin, LoggedModel):
disabled_vars=Coalesce( disabled_vars=Coalesce(
Subquery( Subquery(
SubEventItemVariation.objects.filter( SubEventItemVariation.objects.filter(
Q(disabled=True) | Q(available_from__gt=now()) | Q(available_until__lt=now()), Q(disabled=True) | Q(available_from__gt=time_machine_now()) | Q(available_until__lt=time_machine_now()),
subevent=OuterRef('pk'), subevent=OuterRef('pk'),
).order_by().values('subevent').annotate(items=GroupConcat('variation_id', delimiter=',')).values('items'), ).order_by().values('subevent').annotate(items=GroupConcat('variation_id', delimiter=',')).values('items'),
output_field=models.TextField(), output_field=models.TextField(),
+29 -16
View File
@@ -55,7 +55,7 @@ from django.db.models import Q
from django.utils import formats from django.utils import formats
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import is_naive, make_aware, now from django.utils.timezone import is_naive, make_aware
from django.utils.translation import gettext_lazy as _, pgettext_lazy from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_countries.fields import Country from django_countries.fields import Country
from django_scopes import ScopedManager from django_scopes import ScopedManager
@@ -65,6 +65,7 @@ from pretix.base.models import fields
from pretix.base.models.base import LoggedModel from pretix.base.models.base import LoggedModel
from pretix.base.models.fields import MultiStringField from pretix.base.models.fields import MultiStringField
from pretix.base.models.tax import TaxedPrice from pretix.base.models.tax import TaxedPrice
from pretix.base.timemachine import time_machine_now
from ...helpers.images import ImageSizeValidator from ...helpers.images import ImageSizeValidator
from ..media import MEDIA_TYPES from ..media import MEDIA_TYPES
@@ -121,6 +122,16 @@ class ItemCategory(LoggedModel):
return _('{category} (Add-On products)').format(category=str(name)) return _('{category} (Add-On products)').format(category=str(name))
return str(name) return str(name)
def get_category_type_display(self):
if self.is_addon:
return _('Add-On products')
else:
return None
@property
def category_type(self):
return 'addon' if self.is_addon else 'normal'
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
super().delete(*args, **kwargs) super().delete(*args, **kwargs)
if self.event: if self.event:
@@ -192,7 +203,7 @@ class SubEventItem(models.Model):
self.subevent.event.cache.clear() self.subevent.event.cache.clear()
def is_available(self, now_dt: datetime=None) -> bool: def is_available(self, now_dt: datetime=None) -> bool:
now_dt = now_dt or now() now_dt = now_dt or time_machine_now()
if self.disabled: if self.disabled:
return False return False
if self.available_from and self.available_from > now_dt: if self.available_from and self.available_from > now_dt:
@@ -248,7 +259,7 @@ class SubEventItemVariation(models.Model):
self.subevent.event.cache.clear() self.subevent.event.cache.clear()
def is_available(self, now_dt: datetime=None) -> bool: def is_available(self, now_dt: datetime=None) -> bool:
now_dt = now_dt or now() now_dt = now_dt or time_machine_now()
if self.disabled: if self.disabled:
return False return False
if self.available_from and self.available_from > now_dt: if self.available_from and self.available_from > now_dt:
@@ -263,8 +274,8 @@ def filter_available(qs, channel='web', voucher=None, allow_addons=False):
# IMPORTANT: If this is updated, also update the ItemVariation query # IMPORTANT: If this is updated, also update the ItemVariation query
# in models/event.py: EventMixin.annotated() # in models/event.py: EventMixin.annotated()
Q(active=True) Q(active=True)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()) | Q(available_from_mode='info')) & Q(Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()) | Q(available_from_mode='info'))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()) | Q(available_until_mode='info')) & Q(Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()) | Q(available_until_mode='info'))
& Q(sales_channels__contains=channel) & Q(require_bundling=False) & Q(sales_channels__contains=channel) & Q(require_bundling=False)
) )
if not allow_addons: if not allow_addons:
@@ -443,7 +454,8 @@ class Item(LoggedModel):
free_price_suggestion = models.DecimalField( free_price_suggestion = models.DecimalField(
verbose_name=_("Suggested price"), verbose_name=_("Suggested price"),
help_text=_("This price will be used as the default value of the input field. The user can choose a lower " help_text=_("This price will be used as the default value of the input field. The user can choose a lower "
"value, but not lower than the price this product would have without the free price option."), "value, but not lower than the price this product would have without the free price option. This "
"will be ignored if a voucher is used that lowers the price."),
max_digits=13, decimal_places=2, null=True, blank=True, max_digits=13, decimal_places=2, null=True, blank=True,
) )
tax_rule = models.ForeignKey( tax_rule = models.ForeignKey(
@@ -782,7 +794,7 @@ class Item(LoggedModel):
return t return t
def is_available_by_time(self, now_dt: datetime=None) -> bool: def is_available_by_time(self, now_dt: datetime=None) -> bool:
now_dt = now_dt or now() now_dt = now_dt or time_machine_now()
if self.available_from and self.available_from > now_dt: if self.available_from and self.available_from > now_dt:
return False return False
if self.available_until and self.available_until < now_dt: if self.available_until and self.available_until < now_dt:
@@ -794,13 +806,13 @@ class Item(LoggedModel):
Returns whether this item is available according to its ``active`` flag Returns whether this item is available according to its ``active`` flag
and its ``available_from`` and ``available_until`` fields and its ``available_from`` and ``available_until`` fields
""" """
now_dt = now_dt or now() now_dt = now_dt or time_machine_now()
if not self.active or not self.is_available_by_time(now_dt): if not self.active or not self.is_available_by_time(now_dt):
return False return False
return True return True
def unavailability_reason(self, now_dt: datetime=None, has_voucher=False, subevent=None) -> Optional[str]: def unavailability_reason(self, now_dt: datetime=None, has_voucher=False, subevent=None) -> Optional[str]:
now_dt = now_dt or now() now_dt = now_dt or time_machine_now()
subevent_item = subevent and subevent.item_overrides.get(self.pk) subevent_item = subevent and subevent.item_overrides.get(self.pk)
if not self.active: if not self.active:
return 'active' return 'active'
@@ -957,11 +969,11 @@ class Item(LoggedModel):
return self.validity_fixed_from, self.validity_fixed_until return self.validity_fixed_from, self.validity_fixed_until
elif self.validity_mode == Item.VALIDITY_MODE_DYNAMIC: elif self.validity_mode == Item.VALIDITY_MODE_DYNAMIC:
tz = override_tz or self.event.timezone tz = override_tz or self.event.timezone
requested_start = requested_start or now() requested_start = requested_start or time_machine_now()
if enforce_start_limit and not self.validity_dynamic_start_choice: if enforce_start_limit and not self.validity_dynamic_start_choice:
requested_start = now() requested_start = time_machine_now()
if enforce_start_limit and self.validity_dynamic_start_choice_day_limit is not None: if enforce_start_limit and self.validity_dynamic_start_choice_day_limit is not None:
requested_start = min(requested_start, now() + timedelta(days=self.validity_dynamic_start_choice_day_limit)) requested_start = min(requested_start, time_machine_now() + timedelta(days=self.validity_dynamic_start_choice_day_limit))
valid_until = requested_start.astimezone(tz) valid_until = requested_start.astimezone(tz)
@@ -1085,7 +1097,8 @@ class ItemVariation(models.Model):
free_price_suggestion = models.DecimalField( free_price_suggestion = models.DecimalField(
verbose_name=_("Suggested price"), verbose_name=_("Suggested price"),
help_text=_("This price will be used as the default value of the input field. The user can choose a lower " help_text=_("This price will be used as the default value of the input field. The user can choose a lower "
"value, but not lower than the price this product would have without the free price option."), "value, but not lower than the price this product would have without the free price option. This "
"will be ignored if a voucher is used that lowers the price."),
max_digits=13, decimal_places=2, null=True, blank=True, max_digits=13, decimal_places=2, null=True, blank=True,
) )
require_approval = models.BooleanField( require_approval = models.BooleanField(
@@ -1290,7 +1303,7 @@ class ItemVariation(models.Model):
return ItemVariation.objects.filter(item=self.item).count() == 1 return ItemVariation.objects.filter(item=self.item).count() == 1
def is_available_by_time(self, now_dt: datetime=None) -> bool: def is_available_by_time(self, now_dt: datetime=None) -> bool:
now_dt = now_dt or now() now_dt = now_dt or time_machine_now()
if self.available_from and self.available_from > now_dt: if self.available_from and self.available_from > now_dt:
return False return False
if self.available_until and self.available_until < now_dt: if self.available_until and self.available_until < now_dt:
@@ -1302,13 +1315,13 @@ class ItemVariation(models.Model):
Returns whether this item is available according to its ``active`` flag Returns whether this item is available according to its ``active`` flag
and its ``available_from`` and ``available_until`` fields and its ``available_from`` and ``available_until`` fields
""" """
now_dt = now_dt or now() now_dt = now_dt or time_machine_now()
if not self.active or not self.is_available_by_time(now_dt): if not self.active or not self.is_available_by_time(now_dt):
return False return False
return True return True
def unavailability_reason(self, now_dt: datetime=None, has_voucher=False, subevent=None) -> Optional[str]: def unavailability_reason(self, now_dt: datetime=None, has_voucher=False, subevent=None) -> Optional[str]:
now_dt = now_dt or now() now_dt = now_dt or time_machine_now()
subevent_var = subevent and subevent.var_overrides.get(self.pk) subevent_var = subevent and subevent.var_overrides.get(self.pk)
if not self.active: if not self.active:
return 'active' return 'active'
+3 -3
View File
@@ -23,7 +23,6 @@ from django.db import models
from django.db.models import Count, OuterRef, Subquery, Value from django.db.models import Count, OuterRef, Subquery, Value
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_scopes import ScopedManager, scopes_disabled from django_scopes import ScopedManager, scopes_disabled
from i18nfield.fields import I18nCharField from i18nfield.fields import I18nCharField
@@ -31,6 +30,7 @@ from i18nfield.fields import I18nCharField
from pretix.base.models import Customer from pretix.base.models import Customer
from pretix.base.models.base import LoggedModel from pretix.base.models.base import LoggedModel
from pretix.base.models.organizer import Organizer from pretix.base.models.organizer import Organizer
from pretix.base.timemachine import time_machine_now
from pretix.helpers.names import build_name from pretix.helpers.names import build_name
@@ -165,13 +165,13 @@ class Membership(models.Model):
def is_valid(self, ev=None, ticket_valid_from=None, valid_from_not_chosen=False): def is_valid(self, ev=None, ticket_valid_from=None, valid_from_not_chosen=False):
if valid_from_not_chosen: if valid_from_not_chosen:
return not self.canceled and self.date_end >= now() return not self.canceled and self.date_end >= time_machine_now()
elif ticket_valid_from: elif ticket_valid_from:
dt = ticket_valid_from dt = ticket_valid_from
elif ev: elif ev:
dt = ev.date_from dt = ev.date_from
else: else:
dt = now() dt = time_machine_now()
return not self.canceled and dt >= self.date_start and dt <= self.date_end return not self.canceled and dt >= self.date_start and dt <= self.date_end
+87 -12
View File
@@ -35,6 +35,7 @@
import copy import copy
import hashlib import hashlib
import hmac
import json import json
import logging import logging
import operator import operator
@@ -59,7 +60,7 @@ from django.db.models.functions import Coalesce, Greatest
from django.db.models.signals import post_delete from django.db.models.signals import post_delete
from django.dispatch import receiver from django.dispatch import receiver
from django.urls import reverse from django.urls import reverse
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string, salted_hmac
from django.utils.encoding import escape_uri_path from django.utils.encoding import escape_uri_path
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.functional import cached_property from django.utils.functional import cached_property
@@ -80,6 +81,7 @@ from pretix.base.models import Customer, User
from pretix.base.reldate import RelativeDateWrapper from pretix.base.reldate import RelativeDateWrapper
from pretix.base.settings import PERSON_NAME_SCHEMES from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import allow_ticket_download, order_gracefully_delete from pretix.base.signals import allow_ticket_download, order_gracefully_delete
from pretix.base.timemachine import time_machine_now
from ...helpers import OF_SELF from ...helpers import OF_SELF
from ...helpers.countries import CachedCountries, FastCountryField from ...helpers.countries import CachedCountries, FastCountryField
@@ -104,6 +106,34 @@ def generate_position_secret():
raise TypeError("Function no longer exists, use secret generators") raise TypeError("Function no longer exists, use secret generators")
class OrderQuerySet(models.QuerySet):
def get_with_secret_check(self, code, received_secret, tag, secret_length=64):
dummy = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"[:secret_length]
try:
order = self.get(code=code)
except Order.DoesNotExist:
# Do a hash comparison as well to harden against timing attacks
hmac.compare_digest(
salted_hmac(key_salt=b"", value=tag, algorithm="sha256",
secret=dummy).hexdigest()[:secret_length],
received_secret[:secret_length]
)
raise Order.DoesNotExist
if not hmac.compare_digest(
order.tagged_secret(tag, secret_length) if tag else order.secret,
received_secret[:secret_length].lower() if tag else received_secret.lower()
) and not (
# TODO: remove this clause after a while (compatibility with old secrets currently in flight)
tag and hmac.compare_digest(
hashlib.sha1(order.secret.lower().encode()).hexdigest(),
received_secret.lower()
)
):
raise Order.DoesNotExist
return order
class Order(LockModel, LoggedModel): class Order(LockModel, LoggedModel):
""" """
An order is created when a user clicks 'buy' on his cart. It holds An order is created when a user clicks 'buy' on his cart. It holds
@@ -222,6 +252,7 @@ class Order(LockModel, LoggedModel):
verbose_name=_('Locale') verbose_name=_('Locale')
) )
secret = models.CharField(max_length=32, default=generate_secret) secret = models.CharField(max_length=32, default=generate_secret)
internal_secret = models.CharField(null=True, blank=True, max_length=32, default=generate_secret)
datetime = models.DateTimeField( datetime = models.DateTimeField(
verbose_name=_("Date"), db_index=False verbose_name=_("Date"), db_index=False
) )
@@ -284,7 +315,7 @@ class Order(LockModel, LoggedModel):
default=False, default=False,
) )
objects = ScopedManager(organizer='event__organizer') objects = ScopedManager(OrderQuerySet.as_manager().__class__, organizer='event__organizer')
class Meta: class Meta:
verbose_name = _("Order") verbose_name = _("Order")
@@ -681,7 +712,7 @@ class Order(LockModel, LoggedModel):
for op in positions: for op in positions:
if op.issued_gift_cards.all(): if op.issued_gift_cards.all():
return False return False
if self.user_change_deadline and now() > self.user_change_deadline: if self.user_change_deadline and time_machine_now() > self.user_change_deadline:
return False return False
return ( return (
@@ -713,7 +744,7 @@ class Order(LockModel, LoggedModel):
return False return False
if op.granted_memberships.with_usages().filter(usages__gt=0): if op.granted_memberships.with_usages().filter(usages__gt=0):
return False return False
if self.user_cancel_deadline and now() > self.user_cancel_deadline: if self.user_cancel_deadline and time_machine_now() > self.user_cancel_deadline:
return False return False
if self.status == Order.STATUS_PAID: if self.status == Order.STATUS_PAID:
@@ -850,8 +881,11 @@ class Order(LockModel, LoggedModel):
if self.status not in (Order.STATUS_PENDING, Order.STATUS_PAID, Order.STATUS_EXPIRED): if self.status not in (Order.STATUS_PENDING, Order.STATUS_PAID, Order.STATUS_EXPIRED):
return False return False
if self.event.settings.allow_modifications not in ("order", "attendee"):
return False
modify_deadline = self.modify_deadline modify_deadline = self.modify_deadline
if modify_deadline is not None and now() > modify_deadline: if modify_deadline is not None and time_machine_now() > modify_deadline:
return False return False
positions = list( positions = list(
@@ -903,7 +937,7 @@ class Order(LockModel, LoggedModel):
return self.event.settings.ticket_download and ( return self.event.settings.ticket_download and (
self.event.settings.ticket_download_date is None self.event.settings.ticket_download_date is None
or self.ticket_download_date is None or self.ticket_download_date is None
or now() > self.ticket_download_date or time_machine_now() > self.ticket_download_date
) and ( ) and (
self.status == Order.STATUS_PAID self.status == Order.STATUS_PAID
or ( or (
@@ -975,7 +1009,7 @@ class Order(LockModel, LoggedModel):
return error_messages['require_approval'] return error_messages['require_approval']
term_last = self.payment_term_last term_last = self.payment_term_last
if term_last and not ignore_date: if term_last and not ignore_date:
if now() > term_last: if time_machine_now() > term_last:
return error_messages['late_lastdate'] return error_messages['late_lastdate']
if self.status == self.STATUS_PENDING: if self.status == self.STATUS_PENDING:
@@ -998,7 +1032,7 @@ class Order(LockModel, LoggedModel):
'voucher_budget': _('The voucher "{voucher}" no longer has sufficient budget.'), 'voucher_budget': _('The voucher "{voucher}" no longer has sufficient budget.'),
'voucher_usages': _('The voucher "{voucher}" has been used in the meantime.'), 'voucher_usages': _('The voucher "{voucher}" has been used in the meantime.'),
} }
now_dt = now_dt or now() now_dt = now_dt or time_machine_now()
positions = list(self.positions.all().select_related('item', 'variation', 'seat', 'voucher')) positions = list(self.positions.all().select_related('item', 'variation', 'seat', 'voucher'))
quota_cache = {} quota_cache = {}
v_budget = {} v_budget = {}
@@ -1219,6 +1253,10 @@ class Order(LockModel, LoggedModel):
_transactions_mark_order_clean(self.pk) _transactions_mark_order_clean(self.pk)
return create return create
def tagged_secret(self, tag, secret_length=64):
return salted_hmac(value=tag, key_salt=b"", algorithm="sha256",
secret=self.internal_secret or self.secret).hexdigest()[:secret_length]
def answerfile_name(instance, filename: str) -> str: def answerfile_name(instance, filename: str) -> str:
secret = get_random_string(length=32, allowed_chars=string.ascii_letters + string.digits) secret = get_random_string(length=32, allowed_chars=string.ascii_letters + string.digits)
@@ -2513,6 +2551,43 @@ class OrderPosition(AbstractPosition):
reasons[b] = b reasons[b] = b
return reasons return reasons
@property
def can_modify_answers(self) -> bool:
"""
``True`` if the user can change the question answers / attendee names that are
related to the position. This checks order status and modification deadlines. It also
returns ``False`` if there are no questions that can be answered.
"""
from .checkin import Checkin
if self.order.status not in (Order.STATUS_PENDING, Order.STATUS_PAID, Order.STATUS_EXPIRED):
return False
if self.event.settings.allow_modifications != "attendee":
return False
modify_deadline = self.order.modify_deadline
if modify_deadline is not None and now() > modify_deadline:
return False
positions = list(
self.order.positions.all().annotate(
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk'), list__consider_tickets_used=True))
).select_related('item').prefetch_related('item__questions')
)
if not self.event.settings.allow_modifications_after_checkin:
for cp in positions:
if cp.has_checkin:
return False
ask_names = self.event.settings.get('attendee_names_asked', as_type=bool)
for cp in positions:
if cp.pk == self.pk or cp.addon_to_id == self.pk:
if (cp.item.ask_attendee_data and ask_names) or cp.item.questions.all():
return True
return False # nothing there to modify
@classmethod @classmethod
def transform_cart_positions(cls, cp: List, order) -> list: def transform_cart_positions(cls, cp: List, order) -> list:
from . import Voucher from . import Voucher
@@ -2535,9 +2610,9 @@ class OrderPosition(AbstractPosition):
if cartpos.item.validity_mode: if cartpos.item.validity_mode:
valid_from, valid_until = cartpos.item.compute_validity( valid_from, valid_until = cartpos.item.compute_validity(
requested_start=( requested_start=(
max(cartpos.requested_valid_from, now()) max(cartpos.requested_valid_from, time_machine_now())
if cartpos.requested_valid_from and cartpos.item.validity_dynamic_start_choice if cartpos.requested_valid_from and cartpos.item.validity_dynamic_start_choice
else now() else time_machine_now()
), ),
enforce_start_limit=True, enforce_start_limit=True,
override_tz=order.event.timezone, override_tz=order.event.timezone,
@@ -3063,9 +3138,9 @@ class CartPosition(AbstractPosition):
def predicted_validity(self): def predicted_validity(self):
return self.item.compute_validity( return self.item.compute_validity(
requested_start=( requested_start=(
max(self.requested_valid_from, now()) max(self.requested_valid_from, time_machine_now())
if self.requested_valid_from and self.item.validity_dynamic_start_choice if self.requested_valid_from and self.item.validity_dynamic_start_choice
else now() else time_machine_now()
), ),
override_tz=self.event.timezone, override_tz=self.event.timezone,
) )
+6 -5
View File
@@ -370,10 +370,11 @@ class Voucher(LoggedModel):
'redeemed': redeemed 'redeemed': redeemed
} }
) )
if data.get('max_usages', 1) < data.get('min_usages', 1): if data.get('min_usages') is not None:
raise ValidationError( if data.get('max_usages', 1) < data.get('min_usages', 1):
_('The maximum number of usages may not be lower than the minimum number of usages.'), raise ValidationError(
) _('The maximum number of usages may not be lower than the minimum number of usages.'),
)
@staticmethod @staticmethod
def clean_subevent(data, event): def clean_subevent(data, event):
@@ -459,7 +460,7 @@ class Voucher(LoggedModel):
new_quotas = set( new_quotas = set(
Quota.objects.filter( Quota.objects.filter(
pk__in=Quota.variations.through.objects.filter( pk__in=Quota.variations.through.objects.filter(
itemvariation__item=old_instance.item, itemvariation__item=item,
quota__subevent=data.get('subevent'), quota__subevent=data.get('subevent'),
).values('quota_id') ).values('quota_id')
) )
+4 -3
View File
@@ -67,6 +67,7 @@ from pretix.base.settings import SettingsSandbox
from pretix.base.signals import register_payment_providers from pretix.base.signals import register_payment_providers
from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.rich_text import rich_text from pretix.base.templatetags.rich_text import rich_text
from pretix.base.timemachine import time_machine_now
from pretix.helpers import OF_SELF from pretix.helpers import OF_SELF
from pretix.helpers.countries import CachedCountries from pretix.helpers.countries import CachedCountries
from pretix.helpers.format import format_map from pretix.helpers.format import format_map
@@ -1441,7 +1442,7 @@ class GiftCardPayment(BasePaymentProvider):
if not gc.testmode and self.event.testmode: if not gc.testmode and self.event.testmode:
messages.error(request, _("Only test gift cards can be used in test mode.")) messages.error(request, _("Only test gift cards can be used in test mode."))
return return
if gc.expires and gc.expires < now(): if gc.expires and gc.expires < time_machine_now():
messages.error(request, _("This gift card is no longer valid.")) messages.error(request, _("This gift card is no longer valid."))
return return
if gc.value <= Decimal("0.00"): if gc.value <= Decimal("0.00"):
@@ -1491,7 +1492,7 @@ class GiftCardPayment(BasePaymentProvider):
if not gc.testmode and payment.order.testmode: if not gc.testmode and payment.order.testmode:
messages.error(request, _("Only test gift cards can be used in test mode.")) messages.error(request, _("Only test gift cards can be used in test mode."))
return return
if gc.expires and gc.expires < now(): if gc.expires and gc.expires < time_machine_now():
messages.error(request, _("This gift card is no longer valid.")) messages.error(request, _("This gift card is no longer valid."))
return return
if gc.value <= Decimal("0.00"): if gc.value <= Decimal("0.00"):
@@ -1539,7 +1540,7 @@ class GiftCardPayment(BasePaymentProvider):
raise PaymentException(_("This gift card can only be used in test mode.")) raise PaymentException(_("This gift card can only be used in test mode."))
if not gc.testmode and payment.order.testmode: if not gc.testmode and payment.order.testmode:
raise PaymentException(_("Only test gift cards can be used in test mode.")) raise PaymentException(_("Only test gift cards can be used in test mode."))
if gc.expires and gc.expires < now(): if gc.expires and gc.expires < time_machine_now():
raise PaymentException(_("This gift card is no longer valid.")) raise PaymentException(_("This gift card is no longer valid."))
trans = gc.transactions.create( trans = gc.transactions.create(
+144 -84
View File
@@ -62,8 +62,7 @@ from django.utils.html import conditional_escape
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext from django.utils.translation import gettext_lazy as _, pgettext
from i18nfield.strings import LazyI18nString from i18nfield.strings import LazyI18nString
from pypdf import PdfReader, PdfWriter, Transformation from pypdf import PdfReader, PdfWriter
from pypdf.generic import RectangleObject
from reportlab.graphics import renderPDF from reportlab.graphics import renderPDF
from reportlab.graphics.barcode.qr import QrCodeWidget from reportlab.graphics.barcode.qr import QrCodeWidget
from reportlab.graphics.shapes import Drawing from reportlab.graphics.shapes import Drawing
@@ -408,6 +407,30 @@ DEFAULT_VARIABLES = OrderedDict((
"TIME_FORMAT" "TIME_FORMAT"
) )
}), }),
("purchase_date", {
"label": _("Purchase date"),
"editor_sample": _("2017-05-31"),
"evaluate": lambda op, order, ev: date_format(
order.datetime.astimezone(ev.timezone),
"SHORT_DATE_FORMAT"
)
}),
("purchase_datetime", {
"label": _("Purchase date and time"),
"editor_sample": _("2017-05-31 19:00"),
"evaluate": lambda op, order, ev: date_format(
order.datetime.astimezone(ev.timezone),
"SHORT_DATETIME_FORMAT"
)
}),
("purchase_time", {
"label": _("Purchase time"),
"editor_sample": _("19:00"),
"evaluate": lambda op, order, ev: date_format(
order.datetime.astimezone(ev.timezone),
"TIME_FORMAT"
)
}),
("valid_from_date", { ("valid_from_date", {
"label": _("Validity start date"), "label": _("Validity start date"),
"editor_sample": _("2017-05-31"), "editor_sample": _("2017-05-31"),
@@ -1045,56 +1068,81 @@ class Renderer:
canvas.showPage() canvas.showPage()
def render_background(self, buffer, title=_('Ticket')): def render_background(self, buffer, title=_('Ticket')):
buffer.seek(0)
fg_pdf = PdfReader(buffer)
if settings.PDFTK: if settings.PDFTK:
buffer.seek(0)
with tempfile.TemporaryDirectory() as d: with tempfile.TemporaryDirectory() as d:
with open(os.path.join(d, 'back.pdf'), 'wb') as f: fg_filename = os.path.join(d, 'fg.pdf')
f.write(self.bg_bytes) bg_filename = os.path.join(d, 'bg.pdf')
with open(os.path.join(d, 'front.pdf'), 'wb') as f: out_filename = os.path.join(d, 'out.pdf')
buffer.seek(0)
with open(fg_filename, 'wb') as f:
f.write(buffer.read()) f.write(buffer.read())
subprocess.run([ # pdf_header is a string like "%pdf-X.X"
settings.PDFTK, if float(self.bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
os.path.join(d, 'front.pdf'), # To fix issues with pdftk and background-PDF using pdf-version greater
'multibackground', # than foreground-PDF, we stamp front onto back instead.
os.path.join(d, 'back.pdf'), # Just changing PDF-version in fg.pdf to match the version of
'output', # bg.pdf as we do with pypdf, does not work with pdftk.
os.path.join(d, 'out.pdf'), #
'compress' # Make sure that bg.pdf matches the number of pages of fg.pdf
], check=True) # note: self.bg_pdf is a PdfReader(), not a PdfWriter()
with open(os.path.join(d, 'out.pdf'), 'rb') as f: fg_num_pages = fg_pdf.get_num_pages()
bg_num_pages = self.bg_pdf.get_num_pages()
bg_pdf_to_merge = PdfWriter()
bg_pdf_to_merge.append(
self.bg_pdf,
pages=(0, min(bg_num_pages, fg_num_pages)),
import_outline=False,
excluded_fields=("/Annots", "/B")
)
if fg_num_pages > bg_num_pages:
# repeat last page in bg_pdf to match fg_pdf
bg_pdf_to_merge.append(
bg_pdf_to_merge,
pages=[bg_num_pages - 1] * (fg_num_pages - bg_num_pages),
import_outline=False,
excluded_fields=("/Annots", "/B")
)
bg_pdf_to_merge.write(bg_filename)
pdftk_cmd = [
settings.PDFTK,
bg_filename,
'multistamp',
fg_filename
]
else:
with open(bg_filename, 'wb') as f:
f.write(self.bg_bytes)
pdftk_cmd = [
settings.PDFTK,
fg_filename,
'multibackground',
bg_filename
]
pdftk_cmd.extend(('output', out_filename, 'compress'))
subprocess.run(pdftk_cmd, check=True)
with open(out_filename, 'rb') as f:
return BytesIO(f.read()) return BytesIO(f.read())
else: else:
buffer.seek(0)
new_pdf = PdfReader(buffer)
output = PdfWriter() output = PdfWriter()
for i, page in enumerate(new_pdf.pages): for i, page in enumerate(fg_pdf.pages):
bg_page = copy.deepcopy(self.bg_pdf.pages[i]) bg_page = self.bg_pdf.pages[i]
bg_rotation = bg_page.get('/Rotate') if bg_page.rotation != 0:
if bg_rotation: bg_page.transfer_rotation_to_content()
# /Rotate is clockwise, transformation.rotate is counter-clockwise page.merge_page(bg_page, over=False)
t = Transformation().rotate(bg_rotation) output.add_page(page)
w = float(page.mediabox.getWidth())
h = float(page.mediabox.getHeight()) # pdf_header is a string like "%pdf-X.X"
if bg_rotation in (90, 270): if float(self.bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
# offset due to rotation base output.pdf_header = self.bg_pdf.pdf_header
if bg_rotation == 90:
t = t.translate(h, 0)
else:
t = t.translate(0, w)
# rotate mediabox as well
page.mediabox = RectangleObject((
page.mediabox.left.as_numeric(),
page.mediabox.bottom.as_numeric(),
page.mediabox.top.as_numeric(),
page.mediabox.right.as_numeric(),
))
page.trimbox = page.mediabox
elif bg_rotation == 180:
t = t.translate(w, h)
page.add_transformation(t)
bg_page.merge_page(page)
output.add_page(bg_page)
output.add_metadata({ output.add_metadata({
'/Title': str(title), '/Title': str(title),
@@ -1106,54 +1154,66 @@ class Renderer:
return outbuffer return outbuffer
def merge_background(fg_pdf, bg_pdf, out_file, compress): def merge_background(fg_pdf: PdfWriter, bg_pdf: PdfWriter, out_file, compress):
if settings.PDFTK: if settings.PDFTK:
with tempfile.TemporaryDirectory() as d: with tempfile.TemporaryDirectory() as d:
fg_filename = os.path.join(d, 'fg.pdf') fg_filename = os.path.join(d, 'fg.pdf')
bg_filename = os.path.join(d, 'bg.pdf') bg_filename = os.path.join(d, 'bg.pdf')
fg_pdf.write(fg_filename)
bg_pdf.write(bg_filename) # pdf_header is a string like "%pdf-X.X"
pdftk_cmd = [ if float(bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
settings.PDFTK, # To fix issues with pdftk and background-PDF using pdf-version greater
fg_filename, # than foreground-PDF, we stamp front onto back instead.
'multibackground', # Just changing PDF-version in fg.pdf to match the version of
bg_filename, # bg.pdf as we do with pypdf, does not work with pdftk.
'output',
'-', # Make sure that bg.pdf matches the number of pages of fg.pdf
] fg_num_pages = fg_pdf.get_num_pages()
bg_num_pages = bg_pdf.get_num_pages()
if fg_num_pages > bg_num_pages:
# repeat last page in bg_pdf to match fg_pdf
bg_pdf.append(
bg_pdf,
pages=[bg_num_pages - 1] * (fg_num_pages - bg_num_pages),
import_outline=False,
excluded_fields=("/Annots", "/B")
)
bg_pdf.write(bg_filename)
pdftk_cmd = [
settings.PDFTK,
bg_filename,
'multistamp',
fg_filename,
]
else:
pdftk_cmd = [
settings.PDFTK,
fg_filename,
'multibackground',
bg_filename
]
pdftk_cmd.extend(('output', '-'))
if compress: if compress:
pdftk_cmd.append('compress') pdftk_cmd.append('compress')
fg_pdf.write(fg_filename)
bg_pdf.write(bg_filename)
subprocess.run(pdftk_cmd, check=True, stdout=out_file) subprocess.run(pdftk_cmd, check=True, stdout=out_file)
else: else:
output = PdfWriter()
for i, page in enumerate(fg_pdf.pages): for i, page in enumerate(fg_pdf.pages):
bg_page = copy.deepcopy(bg_pdf.pages[i]) bg_page = bg_pdf.pages[i]
bg_rotation = bg_page.get('/Rotate') if bg_page.rotation != 0:
if bg_rotation: bg_page.transfer_rotation_to_content()
# /Rotate is clockwise, transformation.rotate is counter-clockwise page.merge_page(bg_page, over=False)
t = Transformation().rotate(bg_rotation)
w = float(page.mediabox.getWidth()) # pdf_header is a string like "%pdf-X.X"
h = float(page.mediabox.getHeight()) if float(bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
if bg_rotation in (90, 270): fg_pdf.pdf_header = bg_pdf.pdf_header
# offset due to rotation base
if bg_rotation == 90: fg_pdf.write(out_file)
t = t.translate(h, 0)
else:
t = t.translate(0, w)
# rotate mediabox as well
page.mediabox = RectangleObject((
page.mediabox.left.as_numeric(),
page.mediabox.bottom.as_numeric(),
page.mediabox.top.as_numeric(),
page.mediabox.right.as_numeric(),
))
page.trimbox = page.mediabox
elif bg_rotation == 180:
t = t.translate(w, h)
page.add_transformation(t)
bg_page.merge_page(page)
output.add_page(bg_page)
output.write(out_file)
@deconstructible @deconstructible
+2
View File
@@ -257,6 +257,8 @@ def assign_ticket_secret(event, position, force_invalidate_if_revokation_list_us
kwargs['valid_from'] = position.valid_from kwargs['valid_from'] = position.valid_from
if 'valid_until' in params: if 'valid_until' in params:
kwargs['valid_until'] = position.valid_until kwargs['valid_until'] = position.valid_until
if 'order_datetime' in params:
kwargs['order_datetime'] = position.order.datetime
secret = gen.generate_secret( secret = gen.generate_secret(
item=position.item, item=position.item,
variation=position.variation, variation=position.variation,
+28 -27
View File
@@ -74,6 +74,7 @@ from pretix.base.services.tasks import ProfiledEventTask
from pretix.base.settings import PERSON_NAME_SCHEMES, LazyI18nStringList from pretix.base.settings import PERSON_NAME_SCHEMES, LazyI18nStringList
from pretix.base.signals import validate_cart_addons from pretix.base.signals import validate_cart_addons
from pretix.base.templatetags.rich_text import rich_text from pretix.base.templatetags.rich_text import rich_text
from pretix.base.timemachine import time_machine_now, time_machine_now_assigned
from pretix.celery_app import app from pretix.celery_app import app
from pretix.presale.signals import ( from pretix.presale.signals import (
checkout_confirm_messages, fee_calculation_for_cart, checkout_confirm_messages, fee_calculation_for_cart,
@@ -278,7 +279,7 @@ class CartManager:
sales_channel='web'): sales_channel='web'):
self.event = event self.event = event
self.cart_id = cart_id self.cart_id = cart_id
self.now_dt = now() self.real_now_dt = now()
self._operations = [] self._operations = []
self._quota_diff = Counter() self._quota_diff = Counter()
self._voucher_use_diff = Counter() self._voucher_use_diff = Counter()
@@ -305,10 +306,10 @@ class CartManager:
return self._seated_cache[item, subevent] return self._seated_cache[item, subevent]
def _calculate_expiry(self): def _calculate_expiry(self):
self._expiry = self.now_dt + timedelta(minutes=self.event.settings.get('reservation_time', as_type=int)) self._expiry = self.real_now_dt + timedelta(minutes=self.event.settings.get('reservation_time', as_type=int))
def _check_presale_dates(self): def _check_presale_dates(self):
if self.event.presale_start and self.now_dt < self.event.presale_start: if self.event.presale_start and time_machine_now(self.real_now_dt) < self.event.presale_start:
raise CartError(error_messages['not_started']) raise CartError(error_messages['not_started'])
if self.event.presale_has_ended: if self.event.presale_has_ended:
raise CartError(error_messages['ended']) raise CartError(error_messages['ended'])
@@ -319,13 +320,13 @@ class CartManager:
tlv.datetime(self.event).date(), tlv.datetime(self.event).date(),
time(hour=23, minute=59, second=59) time(hour=23, minute=59, second=59)
), self.event.timezone) ), self.event.timezone)
if term_last < self.now_dt: if term_last < time_machine_now(self.real_now_dt):
raise CartError(error_messages['payment_ended']) raise CartError(error_messages['payment_ended'])
def _extend_expiry_of_valid_existing_positions(self): def _extend_expiry_of_valid_existing_positions(self):
# Extend this user's cart session to ensure all items in the cart expire at the same time # Extend this user's cart session to ensure all items in the cart expire at the same time
# We can extend the reservation of items which are not yet expired without risk # We can extend the reservation of items which are not yet expired without risk
self.positions.filter(expires__gt=self.now_dt).update(expires=self._expiry) self.positions.filter(expires__gt=self.real_now_dt).update(expires=self._expiry)
def _delete_out_of_timeframe(self): def _delete_out_of_timeframe(self):
err = None err = None
@@ -333,12 +334,12 @@ class CartManager:
if not cp.pk: if not cp.pk:
continue continue
if cp.subevent and cp.subevent.presale_start and self.now_dt < cp.subevent.presale_start: if cp.subevent and cp.subevent.presale_start and time_machine_now(self.real_now_dt) < cp.subevent.presale_start:
err = error_messages['some_subevent_not_started'] err = error_messages['some_subevent_not_started']
cp.addons.all().delete() cp.addons.all().delete()
cp.delete() cp.delete()
if cp.subevent and cp.subevent.presale_end and self.now_dt > cp.subevent.presale_end: if cp.subevent and cp.subevent.presale_end and time_machine_now(self.real_now_dt) > cp.subevent.presale_end:
err = error_messages['some_subevent_ended'] err = error_messages['some_subevent_ended']
cp.addons.all().delete() cp.addons.all().delete()
cp.delete() cp.delete()
@@ -350,7 +351,7 @@ class CartManager:
tlv.datetime(cp.subevent).date(), tlv.datetime(cp.subevent).date(),
time(hour=23, minute=59, second=59) time(hour=23, minute=59, second=59)
), self.event.timezone) ), self.event.timezone)
if term_last < self.now_dt: if term_last < time_machine_now(self.real_now_dt):
err = error_messages['some_subevent_ended'] err = error_messages['some_subevent_ended']
cp.addons.all().delete() cp.addons.all().delete()
cp.delete() cp.delete()
@@ -449,7 +450,7 @@ class CartManager:
if op.subevent and not op.subevent.active: if op.subevent and not op.subevent.active:
raise CartError(error_messages['inactive_subevent']) raise CartError(error_messages['inactive_subevent'])
if op.subevent and op.subevent.presale_start and self.now_dt < op.subevent.presale_start: if op.subevent and op.subevent.presale_start and time_machine_now(self.real_now_dt) < op.subevent.presale_start:
raise CartError(error_messages['not_started']) raise CartError(error_messages['not_started'])
if op.subevent and op.subevent.presale_has_ended: if op.subevent and op.subevent.presale_has_ended:
@@ -472,7 +473,7 @@ class CartManager:
tlv.datetime(op.subevent).date(), tlv.datetime(op.subevent).date(),
time(hour=23, minute=59, second=59) time(hour=23, minute=59, second=59)
), self.event.timezone) ), self.event.timezone)
if term_last < self.now_dt: if term_last < time_machine_now(self.real_now_dt):
raise CartError(error_messages['payment_ended']) raise CartError(error_messages['payment_ended'])
if isinstance(op, self.AddOperation): if isinstance(op, self.AddOperation):
@@ -509,7 +510,7 @@ class CartManager:
) )
if not self.event.settings.seating_choice: if not self.event.settings.seating_choice:
requires_seat = Value(0, output_field=IntegerField()) requires_seat = Value(0, output_field=IntegerField())
expired = self.positions.filter(expires__lte=self.now_dt).select_related( expired = self.positions.filter(expires__lte=self.real_now_dt).select_related(
'item', 'variation', 'voucher', 'addon_to', 'addon_to__item' 'item', 'variation', 'voucher', 'addon_to', 'addon_to__item'
).annotate( ).annotate(
requires_seat=requires_seat requires_seat=requires_seat
@@ -690,7 +691,7 @@ class CartManager:
# than either of the possible default assumptions. # than either of the possible default assumptions.
predicted_redeemed_after = ( predicted_redeemed_after = (
voucher.redeemed + voucher.redeemed +
CartPosition.objects.filter(voucher=voucher, expires__gte=self.now_dt).count() + CartPosition.objects.filter(voucher=voucher, expires__gte=self.real_now_dt).count() +
self._voucher_use_diff[voucher] + self._voucher_use_diff[voucher] +
voucher_use_diff[voucher] voucher_use_diff[voucher]
) )
@@ -982,7 +983,7 @@ class CartManager:
current_num = len(current_addons[cp].get(k, [])) current_num = len(current_addons[cp].get(k, []))
if input_num < current_num: if input_num < current_num:
for a in current_addons[cp][k][:current_num - input_num]: for a in current_addons[cp][k][:current_num - input_num]:
if a.expires > self.now_dt: if a.expires > self.real_now_dt:
quotas = list(a.quotas) quotas = list(a.quotas)
for quota in quotas: for quota in quotas:
@@ -996,7 +997,7 @@ class CartManager:
def _get_voucher_availability(self): def _get_voucher_availability(self):
vouchers_ok, self._voucher_depend_on_cart = _get_voucher_availability( vouchers_ok, self._voucher_depend_on_cart = _get_voucher_availability(
self.event, self._voucher_use_diff, self.now_dt, self.event, self._voucher_use_diff, self.real_now_dt,
exclude_position_ids=[ exclude_position_ids=[
op.position.id for op in self._operations if isinstance(op, self.ExtendOperation) op.position.id for op in self._operations if isinstance(op, self.ExtendOperation)
] ]
@@ -1101,7 +1102,7 @@ class CartManager:
shared_lock_objects=[self.event] shared_lock_objects=[self.event]
) )
vouchers_ok = self._get_voucher_availability() vouchers_ok = self._get_voucher_availability()
quotas_ok = _get_quota_availability(self._quota_diff, self.now_dt) quotas_ok = _get_quota_availability(self._quota_diff, self.real_now_dt)
err = None err = None
new_cart_positions = [] new_cart_positions = []
deleted_positions = set() deleted_positions = set()
@@ -1118,7 +1119,7 @@ class CartManager:
for iop, op in enumerate(self._operations): for iop, op in enumerate(self._operations):
if isinstance(op, self.RemoveOperation): if isinstance(op, self.RemoveOperation):
if op.position.expires > self.now_dt: if op.position.expires > self.real_now_dt:
for q in op.position.quotas: for q in op.position.quotas:
quotas_ok[q] += 1 quotas_ok[q] += 1
addons = op.position.addons.all() addons = op.position.addons.all()
@@ -1395,7 +1396,7 @@ class CartManager:
err = self.extend_expired_positions() or err err = self.extend_expired_positions() or err
err = err or self._check_min_per_voucher() err = err or self._check_min_per_voucher()
self.now_dt = now() self.real_now_dt = now()
self._extend_expiry_of_valid_existing_positions() self._extend_expiry_of_valid_existing_positions()
err = self._perform_operations() or err err = self._perform_operations() or err
@@ -1487,7 +1488,7 @@ def get_fees(event, request, total, invoice_address, payments, positions):
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,)) @app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, locale='en', def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, locale='en',
invoice_address: int=None, widget_data=None, sales_channel='web') -> None: invoice_address: int=None, widget_data=None, sales_channel='web', override_now_dt: datetime=None) -> None:
""" """
Adds a list of items to a user's cart. Adds a list of items to a user's cart.
:param event: The event ID in question :param event: The event ID in question
@@ -1495,7 +1496,7 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
:param cart_id: Session ID of a guest :param cart_id: Session ID of a guest
:raises CartError: On any error that occurred :raises CartError: On any error that occurred
""" """
with language(locale): with language(locale), time_machine_now_assigned(override_now_dt):
ia = False ia = False
if invoice_address: if invoice_address:
try: try:
@@ -1517,14 +1518,14 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,)) @app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='en', sales_channel='web') -> None: def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
""" """
Removes a list of items from a user's cart. Removes a list of items from a user's cart.
:param event: The event ID in question :param event: The event ID in question
:param voucher: A voucher code :param voucher: A voucher code
:param session: Session ID of a guest :param session: Session ID of a guest
""" """
with language(locale): with language(locale), time_machine_now_assigned(override_now_dt):
try: try:
try: try:
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel) cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
@@ -1537,14 +1538,14 @@ def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='e
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,)) @app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def remove_cart_position(self, event: Event, position: int, cart_id: str=None, locale='en', sales_channel='web') -> None: def remove_cart_position(self, event: Event, position: int, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
""" """
Removes a list of items from a user's cart. Removes a list of items from a user's cart.
:param event: The event ID in question :param event: The event ID in question
:param position: A cart position ID :param position: A cart position ID
:param session: Session ID of a guest :param session: Session ID of a guest
""" """
with language(locale): with language(locale), time_machine_now_assigned(override_now_dt):
try: try:
try: try:
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel) cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
@@ -1557,13 +1558,13 @@ def remove_cart_position(self, event: Event, position: int, cart_id: str=None, l
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,)) @app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel='web') -> None: def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
""" """
Removes a list of items from a user's cart. Removes a list of items from a user's cart.
:param event: The event ID in question :param event: The event ID in question
:param session: Session ID of a guest :param session: Session ID of a guest
""" """
with language(locale): with language(locale), time_machine_now_assigned(override_now_dt):
try: try:
try: try:
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel) cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
@@ -1577,14 +1578,14 @@ def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,)) @app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def set_cart_addons(self, event: Event, addons: List[dict], cart_id: str=None, locale='en', def set_cart_addons(self, event: Event, addons: List[dict], cart_id: str=None, locale='en',
invoice_address: int=None, sales_channel='web') -> None: invoice_address: int=None, sales_channel='web', override_now_dt: datetime=None) -> None:
""" """
Removes a list of items from a user's cart. Removes a list of items from a user's cart.
:param event: The event ID in question :param event: The event ID in question
:param addons: A list of dicts with the keys addon_to, item, variation :param addons: A list of dicts with the keys addon_to, item, variation
:param session: Session ID of a guest :param session: Session ID of a guest
""" """
with language(locale): with language(locale), time_machine_now_assigned(override_now_dt):
ia = False ia = False
if invoice_address: if invoice_address:
try: try:
+2 -2
View File
@@ -25,13 +25,13 @@ from typing import List, Optional
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from pretix.base.models import ( from pretix.base.models import (
AbstractPosition, CartPosition, Customer, Event, Item, Membership, Order, AbstractPosition, CartPosition, Customer, Event, Item, Membership, Order,
OrderPosition, SubEvent, OrderPosition, SubEvent,
) )
from pretix.base.timemachine import time_machine_now
from pretix.helpers import OF_SELF from pretix.helpers import OF_SELF
@@ -48,7 +48,7 @@ def membership_validity(item: Item, subevent: Optional[SubEvent], event: Event):
else: else:
# Always start at start of day # Always start at start of day
date_start = now().astimezone(tz).replace(hour=0, minute=0, second=0, microsecond=0) date_start = time_machine_now().astimezone(tz).replace(hour=0, minute=0, second=0, microsecond=0)
date_end = date_start date_end = date_start
if item.grant_membership_duration_months: if item.grant_membership_duration_months:
+2
View File
@@ -213,6 +213,8 @@ def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user
voucher = Voucher(event=event) voucher = Voucher(event=event)
vouchers.append(voucher) vouchers.append(voucher)
if not record.get("code"):
raise ValidationError(_('A voucher cannot be created without a code.'))
Voucher.clean_item_properties( Voucher.clean_item_properties(
record, record,
event, event,
+36 -18
View File
@@ -99,9 +99,10 @@ from pretix.base.services.quotas import QuotaAvailability
from pretix.base.services.tasks import ProfiledEventTask, ProfiledTask from pretix.base.services.tasks import ProfiledEventTask, ProfiledTask
from pretix.base.signals import ( from pretix.base.signals import (
order_approved, order_canceled, order_changed, order_denied, order_expired, order_approved, order_canceled, order_changed, order_denied, order_expired,
order_fee_calculation, order_paid, order_placed, order_split, order_fee_calculation, order_paid, order_placed, order_reactivated,
order_valid_if_pending, periodic_task, validate_order, order_split, order_valid_if_pending, periodic_task, validate_order,
) )
from pretix.base.timemachine import time_machine_now, time_machine_now_assigned
from pretix.celery_app import app from pretix.celery_app import app
from pretix.helpers import OF_SELF from pretix.helpers import OF_SELF
from pretix.helpers.models import modelcopy from pretix.helpers.models import modelcopy
@@ -253,7 +254,7 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
else: else:
raise OrderError(is_available) raise OrderError(is_available)
order_approved.send(order.event, order=order) order_reactivated.send(order.event, order=order)
if order.status == Order.STATUS_PAID: if order.status == Order.STATUS_PAID:
order_paid.send(order.event, order=order) order_paid.send(order.event, order=order)
@@ -468,10 +469,10 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
order_denied.send(order.event, order=order) order_denied.send(order.event, order=order)
if send_mail: 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): with language(order.locale, order.event.settings.region):
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)
try: try:
order.send_mail( order.send_mail(
email_subject, email_template, email_context, email_subject, email_template, email_context,
@@ -648,10 +649,11 @@ def _check_date(event: Event, now_dt: datetime):
raise OrderError(error_messages['ended']) raise OrderError(error_messages['ended'])
def _check_positions(event: Event, now_dt: datetime, positions: List[CartPosition], address: InvoiceAddress=None, def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: datetime, positions: List[CartPosition],
address: InvoiceAddress = None,
sales_channel='web', customer=None): sales_channel='web', customer=None):
err = None err = None
_check_date(event, now_dt) _check_date(event, time_machine_now_dt)
products_seen = Counter() products_seen = Counter()
q_avail = Counter() q_avail = Counter()
@@ -729,7 +731,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
delete(cp) delete(cp)
continue continue
if cp.subevent and cp.subevent.presale_start and now_dt < cp.subevent.presale_start: if cp.subevent and cp.subevent.presale_start and time_machine_now_dt < cp.subevent.presale_start:
err = err or error_messages['some_subevent_not_started'] err = err or error_messages['some_subevent_not_started']
delete(cp) delete(cp)
break break
@@ -741,7 +743,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
tlv.datetime(cp.subevent).date(), tlv.datetime(cp.subevent).date(),
time(hour=23, minute=59, second=59) time(hour=23, minute=59, second=59)
), event.timezone) ), event.timezone)
if term_last < now_dt: if term_last < time_machine_now_dt:
err = err or error_messages['some_subevent_ended'] err = err or error_messages['some_subevent_ended']
delete(cp) delete(cp)
break break
@@ -787,19 +789,19 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
delete(cp) delete(cp)
continue continue
if cp.subevent and cp.item.pk in cp.subevent.item_overrides and not cp.subevent.item_overrides[cp.item.pk].is_available(now_dt): if cp.subevent and cp.item.pk in cp.subevent.item_overrides and not cp.subevent.item_overrides[cp.item.pk].is_available(time_machine_now_dt):
err = err or error_messages['unavailable'] err = err or error_messages['unavailable']
delete(cp) delete(cp)
continue continue
if cp.subevent and cp.variation and cp.variation.pk in cp.subevent.var_overrides and \ if cp.subevent and cp.variation and cp.variation.pk in cp.subevent.var_overrides and \
not cp.subevent.var_overrides[cp.variation.pk].is_available(now_dt): not cp.subevent.var_overrides[cp.variation.pk].is_available(time_machine_now_dt):
err = err or error_messages['unavailable'] err = err or error_messages['unavailable']
delete(cp) delete(cp)
continue continue
if cp.voucher: if cp.voucher:
if cp.voucher.valid_until and cp.voucher.valid_until < now_dt: if cp.voucher.valid_until and cp.voucher.valid_until < time_machine_now_dt:
err = err or error_messages['voucher_expired'] err = err or error_messages['voucher_expired']
delete(cp) delete(cp)
continue continue
@@ -1163,7 +1165,8 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
warnings = [] warnings = []
any_payment_failed = False any_payment_failed = False
now_dt = now() real_now_dt = now()
time_machine_now_dt = time_machine_now(real_now_dt)
err_out = None err_out = None
with transaction.atomic(durable=True): with transaction.atomic(durable=True):
positions = list( positions = list(
@@ -1175,14 +1178,15 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
if len(position_ids) != len(positions): if len(position_ids) != len(positions):
raise OrderError(error_messages['internal']) raise OrderError(error_messages['internal'])
try: try:
_check_positions(event, now_dt, positions, address=addr, sales_channel=sales_channel, customer=customer) _check_positions(event, real_now_dt, time_machine_now_dt, positions,
address=addr, sales_channel=sales_channel, customer=customer)
except OrderError as e: except OrderError as e:
err_out = e # Don't raise directly to make sure transaction is committed, as it might have deleted things err_out = e # Don't raise directly to make sure transaction is committed, as it might have deleted things
else: else:
if 'sleep-after-quota-check' in debugflags_var.get(): if 'sleep-after-quota-check' in debugflags_var.get():
sleep(2) sleep(2)
order, payment_objs = _create_order(event, email, positions, now_dt, payment_requests, order, payment_objs = _create_order(event, email, positions, real_now_dt, payment_requests,
locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel, locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel,
shown_total=shown_total, customer=customer, valid_if_pending=valid_if_pending) shown_total=shown_total, customer=customer, valid_if_pending=valid_if_pending)
@@ -2013,6 +2017,20 @@ class OrderChangeManager:
for a in current_addons[cp][k][:current_num - input_num]: for a in current_addons[cp][k][:current_num - input_num]:
if a.canceled: if a.canceled:
continue continue
is_unavailable = (
# If an item is no longer available due to time, it should usually also be no longer
# user-removable, because e.g. the stock has already been ordered.
# We always pass has_voucher=True because if a product now requires a voucher, it usually does
# not mean it should be unremovable for others.
# This also prevents accidental removal through the UI because a hidden product will no longer
# be part of the input.
(a.variation and a.variation.unavailability_reason(has_voucher=True, subevent=a.subevent))
or (a.variation and self.order.sales_channel not in a.variation.sales_channels)
or a.item.unavailability_reason(has_voucher=True, subevent=a.subevent)
or self.order.sales_channel not in item.sales_channels
)
if is_unavailable:
continue
if a.checkins.filter(list__consider_tickets_used=True).exists(): if a.checkins.filter(list__consider_tickets_used=True).exists():
raise OrderError( raise OrderError(
error_messages['addon_already_checked_in'] % { error_messages['addon_already_checked_in'] % {
@@ -2835,8 +2853,8 @@ class OrderChangeManager:
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,)) @app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
def perform_order(self, event: Event, payments: List[dict], positions: List[str], def perform_order(self, event: Event, payments: List[dict], positions: List[str],
email: str=None, locale: str=None, address: int=None, meta_info: dict=None, email: str=None, locale: str=None, address: int=None, meta_info: dict=None,
sales_channel: str='web', shown_total=None, customer=None): sales_channel: str='web', shown_total=None, customer=None, override_now_dt: datetime=None):
with language(locale): with language(locale), time_machine_now_assigned(override_now_dt):
try: try:
try: try:
return _perform_order(event, payments, positions, email, locale, address, meta_info, return _perform_order(event, payments, positions, email, locale, address, meta_info,
+34
View File
@@ -337,6 +337,40 @@ def base_placeholders(sender, **kwargs):
} }
), ),
), ),
SimpleFunctionalTextPlaceholder(
'url_info_change', ['position', 'event'], lambda position, event: build_absolute_uri(
event,
'presale:event.order.position.modify', kwargs={
'order': position.order.code,
'secret': position.web_secret,
'position': position.positionid
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.position.modify', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
'position': '123',
}
),
),
SimpleFunctionalTextPlaceholder(
'url_products_change', ['position', 'event'], lambda position, event: build_absolute_uri(
event,
'presale:event.order.position.change', kwargs={
'order': position.order.code,
'secret': position.web_secret,
'position': position.positionid
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.position.change', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
'position': '123'
}
),
),
SimpleFunctionalTextPlaceholder( SimpleFunctionalTextPlaceholder(
'order_modification_deadline_date_and_time', ['order', 'event'], 'order_modification_deadline_date_and_time', ['order', 'event'],
lambda order, event: lambda order, event:
+3 -3
View File
@@ -25,7 +25,6 @@ from typing import List, Optional, Tuple
from django import forms from django import forms
from django.db.models import Q from django.db.models import Q
from django.utils.timezone import now
from pretix.base.decimal import round_decimal from pretix.base.decimal import round_decimal
from pretix.base.models import ( from pretix.base.models import (
@@ -33,6 +32,7 @@ from pretix.base.models import (
) )
from pretix.base.models.event import Event, SubEvent from pretix.base.models.event import Event, SubEvent
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
from pretix.base.timemachine import time_machine_now
def get_price(item: Item, variation: ItemVariation = None, def get_price(item: Item, variation: ItemVariation = None,
@@ -167,8 +167,8 @@ def apply_discounts(event: Event, sales_channel: str,
new_prices = {} new_prices = {}
discount_qs = event.discounts.filter( discount_qs = event.discounts.filter(
Q(available_from__isnull=True) | Q(available_from__lte=now()), Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()),
Q(available_until__isnull=True) | Q(available_until__gte=now()), Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()),
sales_channels__contains=sales_channel, sales_channels__contains=sales_channel,
active=True, active=True,
).prefetch_related('condition_limit_products', 'benefit_limit_products').order_by('position', 'pk') ).prefetch_related('condition_limit_products', 'benefit_limit_products').order_by('position', 'pk')
+2
View File
@@ -29,6 +29,7 @@ from django.conf import settings
from django.db import models from django.db import models
from django.db.models import ( from django.db.models import (
Case, Count, F, Func, Max, OuterRef, Q, Subquery, Sum, Value, When, Case, Count, F, Func, Max, OuterRef, Q, Subquery, Sum, Value, When,
prefetch_related_objects,
) )
from django.utils.timezone import now from django.utils.timezone import now
@@ -446,6 +447,7 @@ class QuotaAvailability:
self.results[q] = Quota.AVAILABILITY_RESERVED, 0 self.results[q] = Quota.AVAILABILITY_RESERVED, 0
def _compute_waitinglist(self, quotas, q_items, q_vars, size_left): def _compute_waitinglist(self, quotas, q_items, q_vars, size_left):
prefetch_related_objects(quotas, "event", "event__organizer")
quotas = [ quotas = [
q for q in quotas q for q in quotas
if not q.event.settings.waiting_list_auto_disable or q.event.settings.waiting_list_auto_disable.datetime(q.subevent or q.event) > now() if not q.event.settings.waiting_list_auto_disable or q.event.settings.waiting_list_auto_disable.datetime(q.subevent or q.event) > now()
+4 -1
View File
@@ -62,7 +62,10 @@ class VATIDTemporaryError(VATIDError):
def _validate_vat_id_NO(vat_id, country_code): def _validate_vat_id_NO(vat_id, country_code):
# Inspired by vat_moss library # Inspired by vat_moss library
vat_id = vat_moss.id.normalize(vat_id) try:
vat_id = vat_moss.id.normalize(vat_id)
except ValueError:
raise VATIDFinalError(error_messages['invalid'])
if not vat_id or len(vat_id) < 3 or not re.match('^\\d{9}MVA$', vat_id[2:]): if not vat_id or len(vat_id) < 3 or not re.match('^\\d{9}MVA$', vat_id[2:]):
raise VATIDFinalError(error_messages['invalid']) raise VATIDFinalError(error_messages['invalid'])
+30 -20
View File
@@ -20,6 +20,7 @@
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
import sys import sys
from collections import defaultdict
from datetime import timedelta from datetime import timedelta
from django.db import transaction from django.db import transaction
@@ -49,19 +50,28 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
quota_cache = {} quota_cache = {}
gone = set() gone = set()
seats_available = {} _seats_available_cache = {}
seats_used = defaultdict(int)
for m in SeatCategoryMapping.objects.filter(event=event).select_related('subevent'): seated_product_set = set(
SeatCategoryMapping.objects.filter(event=event).values_list('product_id', 'subevent_id')
)
def _seats_available(item, subevent):
# See comment in WaitingListEntry.send_voucher() for rationale # See comment in WaitingListEntry.send_voucher() for rationale
num_free_seets_for_product = (m.subevent or event).free_seats().filter(product_id=m.product_id).count() subevent_id = subevent.pk if subevent else None
num_valid_vouchers_for_product = event.vouchers.filter( if (item.pk, subevent_id) not in _seats_available_cache:
Q(valid_until__isnull=True) | Q(valid_until__gte=now()), num_free_seats_for_product = (subevent or event).free_seats().filter(product_id=item.pk).count()
block_quota=True, num_valid_vouchers_for_product = event.vouchers.filter(
item_id=m.product_id, Q(valid_until__isnull=True) | Q(valid_until__gte=now()),
subevent_id=m.subevent_id, block_quota=True,
waitinglistentries__isnull=False item_id=item.pk,
).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0 subevent_id=subevent_id,
seats_available[(m.product_id, m.subevent_id)] = num_free_seets_for_product - num_valid_vouchers_for_product waitinglistentries__isnull=False
).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0
_seats_available_cache[item.pk, subevent_id] = num_free_seats_for_product - num_valid_vouchers_for_product
return _seats_available_cache[item.pk, subevent_id] - seats_used[item.pk, subevent_id]
prefetch_related_objects( prefetch_related_objects(
[event.organizer], [event.organizer],
@@ -103,7 +113,7 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
lock_objects(quotas, shared_lock_objects=[event]) lock_objects(quotas, shared_lock_objects=[event])
for wle in qs: for wle in qs:
if (wle.item, wle.variation, wle.subevent) in gone: if (wle.item_id, wle.variation_id, wle.subevent_id) in gone:
continue continue
ev = (wle.subevent or event) ev = (wle.subevent or event)
if not ev.presale_is_running or (wle.subevent and not wle.subevent.active): if not ev.presale_is_running or (wle.subevent and not wle.subevent.active):
@@ -111,15 +121,15 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
if wle.subevent and not wle.subevent.presale_is_running: if wle.subevent and not wle.subevent.presale_is_running:
continue continue
if event.settings.waiting_list_auto_disable and event.settings.waiting_list_auto_disable.datetime(wle.subevent or event) <= now(): if event.settings.waiting_list_auto_disable and event.settings.waiting_list_auto_disable.datetime(wle.subevent or event) <= now():
gone.add((wle.item, wle.variation, wle.subevent)) gone.add((wle.item_id, wle.variation_id, wle.subevent_id))
continue continue
if not wle.item.is_available(): if not wle.item.is_available():
gone.add((wle.item, wle.variation, wle.subevent)) gone.add((wle.item_id, wle.variation_id, wle.subevent_id))
continue continue
if (wle.item_id, wle.subevent_id) in seats_available: if (wle.item_id, wle.subevent_id) in seated_product_set:
if seats_available[wle.item_id, wle.subevent_id] < 1: if _seats_available(wle.item, wle.subevent) < 1:
gone.add((wle.item, wle.variation, wle.subevent)) gone.add((wle.item_id, wle.variation_id, wle.subevent_id))
continue continue
availability = ( availability = (
@@ -141,10 +151,10 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
quota_cache[q.pk][1] - 1 if quota_cache[q.pk][1] is not None else sys.maxsize quota_cache[q.pk][1] - 1 if quota_cache[q.pk][1] is not None else sys.maxsize
) )
if (wle.item_id, wle.subevent_id) in seats_available: if (wle.item_id, wle.subevent_id) in seated_product_set:
seats_available[wle.item_id, wle.subevent_id] -= 1 seats_used[wle.item_id, wle.subevent_id] += 1
else: else:
gone.add((wle.item, wle.variation, wle.subevent)) gone.add((wle.item_id, wle.variation_id, wle.subevent_id))
return sent return sent
+26 -21
View File
@@ -971,7 +971,8 @@ DEFAULTS = {
}, },
'payment_giftcard__enabled': { 'payment_giftcard__enabled': {
'default': 'True', 'default': 'True',
'type': bool 'type': bool,
'serializer_class': serializers.BooleanField,
}, },
'payment_giftcard_public_name': { 'payment_giftcard_public_name': {
'default': LazyI18nString.from_gettext(gettext_noop('Gift card')), 'default': LazyI18nString.from_gettext(gettext_noop('Gift card')),
@@ -1653,6 +1654,28 @@ DEFAULTS = {
"calendar.") "calendar.")
) )
}, },
'allow_modifications': {
'default': 'order',
'type': str,
'form_class': forms.ChoiceField,
'serializer_class': serializers.ChoiceField,
'serializer_kwargs': dict(
choices=(
('no', _('No modifications after order was submitted')),
('order', _('Only the person who ordered can make changes')),
('attendee', _('Both the attendee and the person who ordered can make changes')),
)
),
'form_kwargs': dict(
label=_("Allow customers to modify their information"),
widget=forms.RadioSelect,
choices=(
('no', _('No modifications after order was submitted')),
('order', _('Only the person who ordered can make changes')),
('attendee', _('Both the attendee and the person who ordered can make changes')),
)
),
},
'allow_modifications_after_checkin': { 'allow_modifications_after_checkin': {
'default': 'False', 'default': 'False',
'type': bool, 'type': bool,
@@ -1660,6 +1683,8 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Allow customers to modify their information after they checked in."), label=_("Allow customers to modify their information after they checked in."),
help_text=_("By default, no more modifications are possible for an order as soon as one of the tickets "
"in the order has been checked in.")
) )
}, },
'last_order_modification_date': { 'last_order_modification_date': {
@@ -2826,22 +2851,6 @@ Your {organizer} team""")) # noqa: W291
**primary_font_kwargs() **primary_font_kwargs()
), ),
}, },
'presale_css_file': {
'default': None,
'type': str
},
'presale_css_checksum': {
'default': None,
'type': str
},
'presale_widget_css_file': {
'default': None,
'type': str
},
'presale_widget_css_checksum': {
'default': None,
'type': str
},
'logo_image': { 'logo_image': {
'default': None, 'default': None,
'type': File, 'type': File,
@@ -3371,10 +3380,6 @@ Your {organizer} team""")) # noqa: W291
'type': str, 'type': str,
} }
} }
SETTINGS_AFFECTING_CSS = {
'primary_color', 'theme_color_success', 'theme_color_danger', 'primary_font',
'theme_color_background', 'theme_round_borders'
}
PERSON_NAME_TITLE_GROUPS = OrderedDict([ PERSON_NAME_TITLE_GROUPS = OrderedDict([
('english_common', (_('Most common English titles'), ( ('english_common', (_('Most common English titles'), (
'Mr', 'Mr',
+86
View File
@@ -0,0 +1,86 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import contextvars
from contextlib import contextmanager
from dateutil.parser import parse
from django.utils.timezone import now
timemachine_now_var = contextvars.ContextVar('timemachine_now', default=None)
@contextmanager
def time_machine_now_assigned_from_request(request):
if hasattr(request, 'event') and f'timemachine_now_dt:{request.event.pk}' in request.session and \
request.event.testmode and has_time_machine_permission(request, request.event):
request.now_dt = parse(request.session[f'timemachine_now_dt:{request.event.pk}'])
request.now_dt_is_fake = True
else:
request.now_dt = now()
request.now_dt_is_fake = False
try:
timemachine_now_var.set(request.now_dt if request.now_dt_is_fake else None)
yield
finally:
timemachine_now_var.set(None)
def time_machine_now(default=False):
"""
Return the datetime to use as current datetime for checking order restrictions in event
index and checkout flow.
:param default: Value to return if time machine mode is disabled. By default the current datetime is used.
"""
if default is False:
default = now()
return timemachine_now_var.get() or default
@contextmanager
def time_machine_now_assigned(now_dt):
"""
Use this context manager to assign current datetime for time machine mode. Useful e.g. for background tasks.
:param now_dt: The datetime value to assign. May be `None` to disable time machine.
"""
try:
timemachine_now_var.set(now_dt)
yield
finally:
timemachine_now_var.set(None)
def has_time_machine_permission(request, event):
permission = 'can_change_event_settings'
return (
request.user.is_authenticated and
request.user.has_event_permission(request.organizer, request.event, permission, request=request)
) or (
getattr(request, 'event_access_user', None) and
request.event_access_user.is_authenticated and
request.event_access_user.has_event_permission(request.organizer, request.event, permission,
session_key=request.event_access_parent_session_key)
)
+4 -6
View File
@@ -46,11 +46,11 @@ from pretix.base.settings import GlobalSettingsObject
from pretix.control.navigation import ( from pretix.control.navigation import (
get_event_navigation, get_global_navigation, get_organizer_navigation, get_event_navigation, get_global_navigation, get_organizer_navigation,
) )
from pretix.helpers.i18n import (
from ..helpers.i18n import (
get_javascript_format, get_javascript_output_format, get_moment_locale, get_javascript_format, get_javascript_output_format, get_moment_locale,
) )
from ..multidomain.urlreverse import get_event_domain from pretix.multidomain.urlreverse import get_event_domain
from .signals import html_head, nav_topbar from .signals import html_head, nav_topbar
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
@@ -106,7 +106,7 @@ def _default_context(request):
else: else:
ctx['complain_testmode_orders'] = False ctx['complain_testmode_orders'] = False
if not request.event.live and ctx['has_domain']: if (request.event.testmode or not request.event.live) and ctx['has_domain']:
child_sess = request.session.get('child_session_{}'.format(request.event.pk)) child_sess = request.session.get('child_session_{}'.format(request.event.pk))
s = SessionStore() s = SessionStore()
if not child_sess or not s.exists(child_sess): if not child_sess or not s.exists(child_sess):
@@ -114,10 +114,8 @@ def _default_context(request):
s.create() s.create()
ctx['new_session'] = s.session_key ctx['new_session'] = s.session_key
request.session['child_session_{}'.format(request.event.pk)] = s.session_key request.session['child_session_{}'.format(request.event.pk)] = s.session_key
request.session['event_access'] = True
else: else:
ctx['new_session'] = child_sess ctx['new_session'] = child_sess
request.session['event_access'] = True
if request.GET.get('subevent', ''): if request.GET.get('subevent', ''):
# Do not use .get() for lazy evaluation # Do not use .get() for lazy evaluation
+1
View File
@@ -580,6 +580,7 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
'banner_text', 'banner_text',
'banner_text_bottom', 'banner_text_bottom',
'order_email_asked_twice', 'order_email_asked_twice',
'allow_modifications',
'last_order_modification_date', 'last_order_modification_date',
'allow_modifications_after_checkin', 'allow_modifications_after_checkin',
'checkout_show_copy_answers_button', 'checkout_show_copy_answers_button',
+16
View File
@@ -698,6 +698,14 @@ class ItemUpdateForm(I18nModelForm):
'tax_rule', 'tax_rule',
_("Gift card products should use a tax rule with a rate of 0 percent since sales tax will be applied when the gift card is redeemed.") _("Gift card products should use a tax rule with a rate of 0 percent since sales tax will be applied when the gift card is redeemed.")
) )
if d.get('validity_mode'):
self.add_error(
'validity_mode',
_(
"Do not set a specific validity for gift card products as it will not restrict the validity "
"of the gift card. A validity of gift cards can be set in your organizer settings."
)
)
if d.get('admission'): if d.get('admission'):
self.add_error( self.add_error(
'admission', 'admission',
@@ -736,6 +744,14 @@ class ItemUpdateForm(I18nModelForm):
_("The start of validity must be before the end of validity.") _("The start of validity must be before the end of validity.")
) )
if d.get('validity_mode') == Item.VALIDITY_MODE_DYNAMIC:
if not any(d.get(f'validity_dynamic_duration_{k}') for k in ('months', 'days', 'hours', 'minutes')):
self.add_error(
'validity_dynamic_duration_months',
_("You have selected dynamic validity but have not entered a time period. This would render "
"the tickets unusable.")
)
Item.clean_media_settings(self.event, d.get('media_policy'), d.get('media_type'), d.get('issue_giftcard')) Item.clean_media_settings(self.event, d.get('media_policy'), d.get('media_type'), d.get('issue_giftcard'))
return d return d
+8 -8
View File
@@ -90,8 +90,10 @@ class VoucherForm(I18nModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
instance = kwargs.get('instance') instance = kwargs.get('instance')
initial = kwargs.get('initial') initial = kwargs.get('initial')
self.initial_instance_data = None
if instance: if instance:
self.initial_instance_data = modelcopy(instance) if instance.pk:
self.initial_instance_data = modelcopy(instance)
try: try:
if instance.variation: if instance.variation:
initial['itemvar'] = '%d-%d' % (instance.item.pk, instance.variation.pk) initial['itemvar'] = '%d-%d' % (instance.item.pk, instance.variation.pk)
@@ -101,8 +103,6 @@ class VoucherForm(I18nModelForm):
initial['itemvar'] = 'q-%d' % instance.quota.pk initial['itemvar'] = 'q-%d' % instance.quota.pk
except Item.DoesNotExist: except Item.DoesNotExist:
pass pass
else:
self.initial_instance_data = None
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if instance.event.has_subevents: if instance.event.has_subevents:
@@ -234,8 +234,8 @@ class VoucherForm(I18nModelForm):
) )
if check_quota: if check_quota:
Voucher.clean_quota_check( Voucher.clean_quota_check(
data, cnt, self.initial_instance_data, self.instance.event, data, cnt, self.initial_instance_data,
self.instance.quota, self.instance.item, self.instance.variation self.instance.event, self.instance.quota, self.instance.item, self.instance.variation
) )
Voucher.clean_voucher_code(data, self.instance.event, self.instance.pk) Voucher.clean_voucher_code(data, self.instance.event, self.instance.pk)
if 'seat' in self.fields and data.get('seat'): if 'seat' in self.fields and data.get('seat'):
@@ -366,14 +366,14 @@ class VoucherBulkForm(VoucherForm):
raise ValidationError(_('CSV input contains an unknown field with the header "{header}".').format(header=unknown_fields[0])) raise ValidationError(_('CSV input contains an unknown field with the header "{header}".').format(header=unknown_fields[0]))
for i, row in enumerate(reader): for i, row in enumerate(reader):
try: try:
EmailValidator()(row['email']) EmailValidator()(row['email'].strip())
except ValidationError as err: except ValidationError as err:
raise ValidationError(_('{value} is not a valid email address.').format(value=row['email'])) from err raise ValidationError(_('{value} is not a valid email address.').format(value=row['email'].strip())) from err
try: try:
res.append(self.Recipient( res.append(self.Recipient(
name=row.get('name', ''), name=row.get('name', ''),
email=row['email'].strip(), email=row['email'].strip(),
number=int(row.get('number', 1)), number=int(row.get('number', 1) or ""),
tag=row.get('tag', None) tag=row.get('tag', None)
)) ))
except ValueError as err: except ValueError as err:
+5
View File
@@ -444,6 +444,11 @@ def get_global_navigation(request):
'url': reverse('control:global.license'), 'url': reverse('control:global.license'),
'active': (url.url_name == 'global.license'), 'active': (url.url_name == 'global.license'),
}, },
{
'label': _('System report'),
'url': reverse('control:global.sysreport'),
'active': (url.url_name == 'global.sysreport'),
},
] ]
}) })
+307
View File
@@ -0,0 +1,307 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import os
import platform
import sys
import zoneinfo
from datetime import datetime, timedelta
from django.conf import settings
from django.db.models import Count, Exists, F, Min, OuterRef, Q, Sum
from django.utils.formats import date_format
from django.utils.timezone import now
from reportlab.lib import pagesizes
from reportlab.lib.enums import TA_CENTER
from reportlab.lib.units import mm
from reportlab.platypus import Paragraph, Spacer, Table, TableStyle
from pretix import __version__
from pretix.base.models import Order, OrderPayment, Transaction
from pretix.base.plugins import get_all_plugins
from pretix.base.templatetags.money import money_filter
from pretix.plugins.reports.exporters import ReportlabExportMixin
from pretix.settings import DATA_DIR
class SysReport(ReportlabExportMixin):
@property
def pagesize(self):
return pagesizes.portrait(pagesizes.A4)
def __init__(self, start_month, tzname):
self.tzname = tzname
self.tz = zoneinfo.ZoneInfo(tzname)
self.start_month = start_month
def page_header(self, canvas, doc):
pass
def page_footer(self, canvas, doc):
from reportlab.lib.units import mm
canvas.setFont("OpenSans", 8)
canvas.drawString(15 * mm, 10 * mm, "Page %d" % doc.page)
canvas.drawRightString(
self.pagesize[0] - doc.rightMargin,
10 * mm,
"Created: %s"
% date_format(now().astimezone(self.tz), "SHORT_DATETIME_FORMAT"),
)
def render(self):
return "sysreport.pdf", "application/pdf", self.create({})
def get_story(self, doc, form_data):
headlinestyle = self.get_style()
headlinestyle.fontSize = 15
subheadlinestyle = self.get_style()
subheadlinestyle.fontSize = 13
style_small = self.get_style()
style_small.fontSize = 6
story = [
Paragraph("System report", headlinestyle),
Spacer(1, 5 * mm),
Paragraph("Usage", subheadlinestyle),
Spacer(1, 5 * mm),
self._usage_table(),
Spacer(1, 5 * mm),
Paragraph("Installed versions", subheadlinestyle),
Spacer(1, 5 * mm),
self._tech_table(),
Spacer(1, 5 * mm),
Paragraph("Plugins", subheadlinestyle),
Spacer(1, 5 * mm),
Paragraph(self._get_plugin_versions(), style_small),
Spacer(1, 5 * mm),
Paragraph("Custom templates", subheadlinestyle),
Spacer(1, 5 * mm),
Paragraph(self._get_custom_templates(), style_small),
Spacer(1, 5 * mm),
]
return story
def _tech_table(self):
style = self.get_style()
style.fontSize = 8
style_small = self.get_style()
style_small.fontSize = 6
w = self.pagesize[0] - 30 * mm
colwidths = [
a * w
for a in (
0.2,
0.8,
)
]
tstyledata = [
("VALIGN", (0, 0), (-1, -1), "TOP"),
("LEFTPADDING", (0, 0), (0, -1), 0),
("RIGHTPADDING", (-1, 0), (-1, -1), 0),
]
tdata = [
[Paragraph("Site URL:", style), Paragraph(settings.SITE_URL, style)],
[Paragraph("pretix version:", style), Paragraph(__version__, style)],
[Paragraph("Python version:", style), Paragraph(sys.version, style)],
[Paragraph("Platform:", style), Paragraph(platform.platform(), style)],
[
Paragraph("Database engine:", style),
Paragraph(settings.DATABASES["default"]["ENGINE"], style),
],
]
table = Table(tdata, colWidths=colwidths, repeatRows=0)
table.setStyle(TableStyle(tstyledata))
return table
def _usage_table(self):
style = self.get_style()
style.fontSize = 8
style_small = self.get_style()
style_small.fontSize = 6
style_small.leading = 8
style_small.alignment = TA_CENTER
style_small_head = self.get_style()
style_small_head.fontSize = 6
style_small_head.leading = 8
style_small_head.alignment = TA_CENTER
style_small_head.fontName = "OpenSansBd"
w = self.pagesize[0] - 30 * mm
successful = (
Q(status=Order.STATUS_PAID)
| Q(valid_if_pending=True, status=Order.STATUS_PENDING)
| Q(
Exists(
OrderPayment.objects.filter(
order_id=OuterRef("pk"),
state__in=(
OrderPayment.PAYMENT_STATE_CONFIRMED,
OrderPayment.PAYMENT_STATE_REFUNDED,
),
)
),
)
)
orders_q = Order.objects.filter(
successful,
testmode=False,
)
orders_testmode_q = Order.objects.filter(
testmode=True,
)
orders_unconfirmed_q = Order.objects.filter(
~successful,
testmode=False,
)
revenue_q = Transaction.objects.filter(
Exists(
OrderPayment.objects.filter(
order_id=OuterRef("order_id"),
state__in=(
OrderPayment.PAYMENT_STATE_CONFIRMED,
OrderPayment.PAYMENT_STATE_REFUNDED,
),
)
),
order__testmode=False,
)
currencies = sorted(
list(
set(
Transaction.objects.annotate(c=F("order__event__currency"))
.values_list("c", flat=True)
.distinct()
)
)
)
year_first = orders_q.aggregate(m=Min("datetime__year"))["m"]
if not year_first:
year_first = now().year
elif datetime.now().month - 1 <= self.start_month:
year_first -= 1
year_last = now().year
tdata = [
[
Paragraph(l, style_small_head)
for l in (
"Time frame",
"Currency",
"Successful orders",
"Net revenue",
"Testmode orders",
"Unsucessful orders",
"Positions",
"Gross revenue",
)
]
]
for year in range(year_first, year_last + 1):
for i, c in enumerate(currencies):
first_day = datetime(
year, self.start_month, 1, 0, 0, 0, 0, tzinfo=self.tz
)
after_day = datetime(
year + 1, self.start_month, 1, 0, 0, 0, 0, tzinfo=self.tz
)
orders_count = (
orders_q.filter(
datetime__gte=first_day, datetime__lt=after_day
).aggregate(c=Count("*"))["c"]
or 0
)
testmode_count = (
orders_testmode_q.filter(
datetime__gte=first_day, datetime__lt=after_day
).aggregate(c=Count("*"))["c"]
or 0
)
unconfirmed_count = (
orders_unconfirmed_q.filter(
datetime__gte=first_day, datetime__lt=after_day
).aggregate(c=Count("*"))["c"]
or 0
)
revenue_data = revenue_q.filter(
datetime__gte=first_day, datetime__lt=after_day, order__event__currency=c
).aggregate(
c=Sum("count"),
s_net=Sum(F("price") - F("tax_value")),
s_gross=Sum(F("price")),
)
tdata.append(
(
Paragraph(
date_format(first_day, "M Y")
+ " "
+ date_format(after_day - timedelta(days=1), "M Y"),
style_small,
),
Paragraph(c, style_small),
Paragraph(str(orders_count), style_small) if i == 0 else "",
Paragraph(money_filter(revenue_data.get("s_net") or 0, c), style_small),
Paragraph(str(testmode_count), style_small) if i == 0 else "",
Paragraph(str(unconfirmed_count), style_small) if i == 0 else "",
Paragraph(str(revenue_data.get("c") or 0), style_small),
Paragraph(money_filter(revenue_data.get("s_gross") or 0, c), style_small),
)
)
colwidths = [a * w for a in (0.18,) + (0.82 / 7,) * 7]
tstyledata = [
("VALIGN", (0, 0), (-1, -1), "TOP"),
("LEFTPADDING", (0, 0), (0, -1), 0),
("RIGHTPADDING", (-1, 0), (-1, -1), 0),
("TOPPADDING", (0, 0), (-1, -1), 0),
("BOTTOMPADDING", (0, 0), (-1, -1), 1),
]
table = Table(tdata, colWidths=colwidths, repeatRows=0)
table.setStyle(TableStyle(tstyledata))
return table
def _get_plugin_versions(self):
lines = []
for p in get_all_plugins():
lines.append(f"{p.name} {p.version}")
return ", ".join(lines)
def _get_custom_templates(self):
lines = []
for dirpath, dirnames, filenames in os.walk(
os.path.join(DATA_DIR, "templates")
):
for f in filenames:
lines.append(f"{dirpath}/{f}")
d = "<br/>".join(lines[:50])
if len(lines) > 50:
d += "<br/>..."
if not d:
return ""
return d
@@ -327,8 +327,7 @@
{% endblocktrans %} {% endblocktrans %}
{% blocktrans trimmed %} {% blocktrans trimmed %}
Internet Explorer is an old browser that does not support lots of recent web-based Internet Explorer is an old browser that does not support lots of recent web-based
technologies. While some features might already not work properly, we plan on no longer technologies and is no longer supported by this website.
supporting Internet Explorer in our administrative backend in the next months.
{% endblocktrans %} {% endblocktrans %}
{% blocktrans trimmed %} {% blocktrans trimmed %}
We kindly ask you to move to one of our supported browsers, such as Microsoft Edge, We kindly ask you to move to one of our supported browsers, such as Microsoft Edge,
@@ -46,7 +46,7 @@
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
<div class="test-right"> <div class="text-right">
<button type="submit" class="btn btn-primary btn-lg btn-save" disabled> <button type="submit" class="btn btn-primary btn-lg btn-save" disabled>
{% trans "Go live" %} {% trans "Go live" %}
</button> </button>
@@ -82,10 +82,10 @@
<p> <p>
{% trans "Your shop is currently in test mode. All orders are not persistent and can be deleted at any point." %} {% trans "Your shop is currently in test mode. All orders are not persistent and can be deleted at any point." %}
</p> </p>
<div class="form-inline"> <div class="checkbox">
<label class="checkbox"> <label>
<input type="checkbox" name="delete" value="yes" /> <input type="checkbox" name="delete" value="yes" />
{% trans "Permanently delete all orders created in test mode" %} <b>{% trans "Permanently delete all orders created in test mode" %}</b>
</label> </label>
</div> </div>
<div class="text-right"> <div class="text-right">
@@ -113,10 +113,17 @@
</div> </div>
{% bootstrap_field sform.attendee_data_explanation_text layout="control" %} {% bootstrap_field sform.attendee_data_explanation_text layout="control" %}
<h4>{% trans "Other settings" %}</h4> <h4>{% trans "Form settings" %}</h4>
{% bootstrap_field sform.name_scheme layout="control" %} {% bootstrap_field sform.name_scheme layout="control" %}
{% bootstrap_field sform.name_scheme_titles layout="control" %} {% bootstrap_field sform.name_scheme_titles layout="control" %}
{% bootstrap_field sform.checkout_show_copy_answers_button layout="control" %} {% bootstrap_field sform.checkout_show_copy_answers_button layout="control" %}
<h4>{% trans "Changes to existing orders" %}</h4>
{% bootstrap_field sform.allow_modifications layout="control" %}
<div data-display-dependency='#id_settings-allow_modifications_0' data-inverse>
{% bootstrap_field sform.last_order_modification_date layout="control" %}
{% bootstrap_field sform.allow_modifications_after_checkin layout="control" %}
</div>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "Texts" %}</legend> <legend>{% trans "Texts" %}</legend>
@@ -225,21 +232,47 @@
{% bootstrap_field sform.presale_start_show_date layout="control" %} {% bootstrap_field sform.presale_start_show_date layout="control" %}
{% bootstrap_field form.presale_end layout="control" %} {% bootstrap_field form.presale_end layout="control" %}
{% bootstrap_field sform.show_items_outside_presale_period layout="control" %} {% bootstrap_field sform.show_items_outside_presale_period layout="control" %}
{% bootstrap_field sform.last_order_modification_date layout="control" %}
{% bootstrap_field sform.allow_modifications_after_checkin layout="control" %}
{% bootstrap_field sform.show_checkin_number_user layout="control" %}
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "Display" %}</legend> <legend>{% trans "Display" %}</legend>
<h4>{% trans "Date and time" %}</h4>
{% bootstrap_field sform.show_dates_on_frontpage layout="control" %} {% bootstrap_field sform.show_dates_on_frontpage layout="control" %}
{% bootstrap_field sform.show_date_to layout="control" %} {% bootstrap_field sform.show_date_to layout="control" %}
{% bootstrap_field sform.show_times layout="control" %} {% bootstrap_field sform.show_times layout="control" %}
<h4>{% trans "Product list" %}</h4>
{% bootstrap_field sform.show_quota_left layout="control" %} {% bootstrap_field sform.show_quota_left layout="control" %}
{% bootstrap_field sform.display_net_prices layout="control" %} {% bootstrap_field sform.display_net_prices layout="control" %}
{% bootstrap_field sform.hide_prices_from_attendees layout="control" %}
{% bootstrap_field sform.show_variations_expanded layout="control" %} {% bootstrap_field sform.show_variations_expanded layout="control" %}
{% bootstrap_field sform.hide_sold_out layout="control" %} {% bootstrap_field sform.hide_sold_out layout="control" %}
<h4>{% trans "Calendar and list views" context "subevents" %}</h4>
{% if sform.frontpage_subevent_ordering %}
{% bootstrap_field sform.frontpage_subevent_ordering layout="control" %}
{% endif %}
{% if sform.event_list_type %}
{% bootstrap_field sform.event_list_type layout="control" %}
{% endif %}
{% if sform.event_list_available_only %}
{% bootstrap_field sform.event_list_available_only layout="control" %}
{% endif %}
{% if sform.event_list_filters %}
{% bootstrap_field sform.event_list_filters layout="control" %}
{% endif %}
{% if sform.event_calendar_future_only %}
{% bootstrap_field sform.event_calendar_future_only layout="control" %}
{% endif %}
{% bootstrap_field sform.low_availability_percentage layout="control" addon_after="%" %}
<h4>{% trans "Order details" %}</h4>
{% bootstrap_field sform.hide_prices_from_attendees layout="control" %}
{% bootstrap_field sform.show_checkin_number_user layout="control" %}
<h4>{% trans "Other settings" %}</h4>
{% url "control:organizer.edit" organizer=request.organizer.slug as org_url %}
{% propagated request.event org_url "meta_noindex" %}
{% bootstrap_field sform.meta_noindex layout="control" %}
{% endpropagated %}
<div class="form-group"> <div class="form-group">
<label class="col-md-3 control-label"> <label class="col-md-3 control-label">
{% trans "Footer links" %}<br> {% trans "Footer links" %}<br>
@@ -306,28 +339,6 @@
</div> </div>
</div> </div>
</div> </div>
{% if sform.frontpage_subevent_ordering %}
{% bootstrap_field sform.frontpage_subevent_ordering layout="control" %}
{% endif %}
{% if sform.event_list_type %}
{% bootstrap_field sform.event_list_type layout="control" %}
{% endif %}
{% if sform.event_list_available_only %}
{% bootstrap_field sform.event_list_available_only layout="control" %}
{% endif %}
{% if sform.event_list_filters %}
{% bootstrap_field sform.event_list_filters layout="control" %}
{% endif %}
{% if sform.event_calendar_future_only %}
{% bootstrap_field sform.event_calendar_future_only layout="control" %}
{% endif %}
{% bootstrap_field sform.low_availability_percentage layout="control" addon_after="%" %}
{% url "control:organizer.edit" organizer=request.organizer.slug as org_url %}
{% propagated request.event org_url "meta_noindex" %}
{% bootstrap_field sform.meta_noindex layout="control" %}
{% endpropagated %}
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "Cart" %}</legend> <legend>{% trans "Cart" %}</legend>
@@ -0,0 +1,9 @@
{% load eventurl %}
{% load static %}
<form action="{% eventurl request.event "presale:event.auth" %}{% if request.GET.next %}?next={{ request.GET.next }}{% endif %}" method="post">
<input type="hidden" value="{{ new_session }}" name="session">
<button type="submit">
Continue
</button>
</form>
<script src="{% static "pretixcontrol/js/send_form.js" %}"></script>
@@ -0,0 +1,35 @@
{% extends "pretixcontrol/global_settings_base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inner %}
<p>
{% trans "If you have a pretix Enterprise license, this report must be submitted to pretix support when your license renews. It may also be requested by pretix support to aid debugging of problems." %}
{% trans "It serves two purposes: Collecting useful information that might help with debugging problems in your pretix installation, and verifying that your usage of pretix is in compliance with the Enterprise license you purchased." %}
</p>
<form method="post">
{% csrf_token %}
<p>
<label>
{% trans "First month of license term:" %}
<select name="month" class="form-control">
<option value="1">{% trans "January" %}</option>
<option value="2">{% trans "February" %}</option>
<option value="3">{% trans "March" %}</option>
<option value="4">{% trans "April" %}</option>
<option value="5">{% trans "May" %}</option>
<option value="6">{% trans "June" %}</option>
<option value="7">{% trans "July" %}</option>
<option value="8">{% trans "August" %}</option>
<option value="9">{% trans "September" %}</option>
<option value="10">{% trans "October" %}</option>
<option value="11">{% trans "November" %}</option>
<option value="11">{% trans "December" %}</option>
</select>
</label>
</p>
<button type="submit" class="btn btn-primary btn-lg">
{% trans "Generate report" %}
</button>
</form>
{% endblock %}
@@ -32,7 +32,6 @@
<tr> <tr>
<th>{% trans "Product categories" %}</th> <th>{% trans "Product categories" %}</th>
<th class="action-col-2"></th> <th class="action-col-2"></th>
<th class="action-col-2"></th>
</tr> </tr>
</thead> </thead>
<tbody data-dnd-url="{% url "control:event.items.categories.reorder" organizer=request.event.organizer.slug event=request.event.slug %}"> <tbody data-dnd-url="{% url "control:event.items.categories.reorder" organizer=request.event.organizer.slug event=request.event.slug %}">
@@ -41,18 +40,16 @@
<td> <td>
<strong><a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}">{{ c.internal_name|default:c.name }}</a></strong> <strong><a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}">{{ c.internal_name|default:c.name }}</a></strong>
</td> </td>
<td>
<button formaction="{% url "control:event.items.categories.up" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 and not page_obj.has_previous %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button formaction="{% url "control:event.items.categories.down" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container"></span>
</td>
<td class="text-right flip"> <td class="text-right flip">
<a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a> <button title="{% trans "Move up" %}" formaction="{% url "control:event.items.categories.up" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 and not page_obj.has_previous %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button title="{% trans "Move down" %}" formaction="{% url "control:event.items.categories.down" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
<a title="{% trans "Edit" %}" href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.items.categories.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ c.id }}" <a href="{% url "control:event.items.categories.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ c.id }}"
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip"> class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
<span class="fa fa-copy"></span> <span class="fa fa-copy"></span>
</a> </a>
<a href="{% url "control:event.items.categories.delete" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a> <a title="{% trans "Delete" %}" href="{% url "control:event.items.categories.delete" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@@ -56,8 +56,7 @@
<th>{% trans "Internal name" %}</th> <th>{% trans "Internal name" %}</th>
<th></th> <th></th>
<th></th> <th></th>
<th>{% trans "Products" %}</th> <th colspan="2">{% trans "Products" %}</th>
<th class="action-col-2"></th>
<th class="action-col-2"></th> <th class="action-col-2"></th>
</tr> </tr>
</thead> </thead>
@@ -102,9 +101,10 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
</td> </td>
<td> <td {% if d.benefit_same_products %}colspan="2"{% endif %}>
{% if not d.benefit_same_products %}{% trans "Condition:" %}{% endif %}
{% if d.condition_all_products %} {% if d.condition_all_products %}
<em>{% trans "All" %}</em> <ul><li><em>{% trans "All" %}</em></li></ul>
{% else %} {% else %}
<ul> <ul>
{% for item in d.condition_limit_products.all %} {% for item in d.condition_limit_products.all %}
@@ -115,18 +115,28 @@
</ul> </ul>
{% endif %} {% endif %}
</td> </td>
<td> {% if not d.benefit_same_products %}
<td>
{% trans "Applies to:" %}
<ul>
{% for item in d.benefit_limit_products.all %}
<li>
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a>
</li>
{% endfor %}
</ul>
</td>
{% endif %}
<td class="text-right flip">
<button formaction="{% url "control:event.items.discounts.up" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}" <button formaction="{% url "control:event.items.discounts.up" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
class="btn btn-default btn-sm sortable-up" class="btn btn-default btn-sm sortable-up" title="{% trans "Move up" %}"
{% if forloop.counter0 == 0 and not page_obj.has_previous %} {% if forloop.counter0 == 0 and not page_obj.has_previous %}
disabled{% endif %}><i class="fa fa-arrow-up"></i></button> disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button formaction="{% url "control:event.items.discounts.down" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}" <button formaction="{% url "control:event.items.discounts.down" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
class="btn btn-default btn-sm sortable-down" class="btn btn-default btn-sm sortable-down" title="{% trans "Move down" %}"
{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}> {% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}>
<i class="fa fa-arrow-down"></i></button> <i class="fa fa-arrow-down"></i></button>
<span class="dnd-container"></span> <span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
</td>
<td class="text-right flip">
<a href="{% url "control:event.items.discounts.edit" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}" <a href="{% url "control:event.items.discounts.edit" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a> class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.items.discounts.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ d.id }}" <a href="{% url "control:event.items.discounts.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ d.id }}"
@@ -1,13 +1,15 @@
{% extends "pretixcontrol/items/base.html" %} {% extends "pretixcontrol/items/base.html" %}
{% load i18n %} {% load i18n %}
{% load money %}
{% block title %}{% trans "Products" %}{% endblock %} {% block title %}{% trans "Products" %}{% endblock %}
{% block inside %} {% block inside %}
{% blocktrans asvar s_taxes %}taxes{% endblocktrans %}
<h1>{% trans "Products" %}</h1> <h1>{% trans "Products" %}</h1>
<p> <p>
{% blocktrans trimmed %} {% blocktrans trimmed %}
Below, you find a list of all available products. You can click on a product name to inspect and change Below, you find a list of all available products. You can click on a product name to inspect and change
product details. You can also use the buttons on the right to change the order of products within a product details. You can also use the buttons on the right to change the order of products or move
give category. products to a different category.
{% endblocktrans %} {% endblocktrans %}
</p> </p>
{% if items|length == 0 %} {% if items|length == 0 %}
@@ -29,7 +31,7 @@
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-condensed table-hover"> <table class="table table-condensed table-hover table-items">
<thead> <thead>
<tr> <tr>
<th>{% trans "Product name" %}</th> <th>{% trans "Product name" %}</th>
@@ -37,16 +39,24 @@
<th class="iconcol"></th> <th class="iconcol"></th>
<th class="iconcol"></th> <th class="iconcol"></th>
<th class="iconcol"></th> <th class="iconcol"></th>
<th>{% trans "Category" %}</th> <th class="text-right flip">{% trans "Default price" %}</th>
<th class="action-col-2"><span class="sr-only">Move</span></th>
<th class="action-col-2"><span class="sr-only">Edit</span></th> <th class="action-col-2"><span class="sr-only">Edit</span></th>
</tr> </tr>
</thead> </thead>
{% regroup items by category as cat_list %}
{% for c in cat_list %} {% for c, items in cat_list %}
<tbody data-dnd-url="{% url "control:event.items.reorder" organizer=request.event.organizer.slug event=request.event.slug %}"> {% if c %}
{% for i in c.list %} <tbody>
{% if forloop.counter0 == 0 and i.category %}<tr class="sortable-disabled"><th colspan="8" scope="colgroup" class="text-muted">{{ i.category }}</th></tr>{% endif %} <tr class="sortable-disabled"><th colspan="9" scope="colgroup" class="text-muted">
{{ c.internal_name|default:c.name }}{% if c.category_type != "normal" %} <span class="font-normal">({{ c.get_category_type_display }})</span>{% endif %}
<a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" title="{% trans "Edit" %}"><span class="fa fa-edit fa-fw"></span></a>
</th></tr>
</tbody>
{% endif %}
<tbody data-dnd-url="{% url "control:event.items.reorder" organizer=request.event.organizer.slug event=request.event.slug category=c.id|default:0 %}"
data-dnd-group="items">
{% for i in items %}
{% if forloop.counter0 == 0 and i.category %}{% endif %}
<tr data-dnd-id="{{ i.id }}" {% if not i.active %}class="row-muted"{% endif %}> <tr data-dnd-id="{{ i.id }}" {% if not i.active %}class="row-muted"{% endif %}>
<td><strong> <td><strong>
{% if not i.active %}<strike>{% endif %} {% if not i.active %}<strike>{% endif %}
@@ -92,15 +102,15 @@
</td> </td>
<td> <td>
{% if i.var_count %} {% if i.var_count %}
<span class="fa fa-th-large fa-fw text-muted" data-toggle="tooltip" title="{% trans "Product with variations" %}"></span> <span class="fa fa-bars fa-fw text-muted" data-toggle="tooltip" title="{% trans "Product with variations" %}"></span>
{% endif %} {% endif %}
</td> </td>
<td> <td>
{% if i.category.is_addon %} {% if i.category.is_addon %}
<span class="fa fa-puzzle-piece fa-fw text-muted" data-toggle="tooltip" <span class="fa fa-plus-square fa-fw text-muted" data-toggle="tooltip"
title="{% trans "Only available as an add-on product" %}"></span> title="{% trans "Only available as an add-on product" %}"></span>
{% elif i.require_bundling %} {% elif i.require_bundling %}
<span class="fa fa-puzzle-piece fa-fw text-muted" data-toggle="tooltip" <span class="fa fa-plus-square fa-fw text-muted" data-toggle="tooltip"
title="{% trans "Only available as part of a bundle" %}"></span> title="{% trans "Only available as part of a bundle" %}"></span>
{% elif i.hide_without_voucher %} {% elif i.hide_without_voucher %}
<span class="fa fa-tags fa-fw text-muted" data-toggle="tooltip" <span class="fa fa-tags fa-fw text-muted" data-toggle="tooltip"
@@ -110,16 +120,29 @@
title="{% trans "Can only be bought using a voucher" %}"></span> title="{% trans "Can only be bought using a voucher" %}"></span>
{% endif %} {% endif %}
</td> </td>
<td>{% if i.category %}{{ i.category.name }}{% endif %}</td> <td class="text-right flip">
<td> {% if i.free_price %}
<button formaction="{% url "control:event.items.up" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button> <span class="fa fa-edit fa-fw text-muted" data-toggle="tooltip" title="{% trans "Free price input" %}">
<button formaction="{% url "control:event.items.down" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button> </span>
<span class="dnd-container"></span> {% endif %}
{{ i.default_price|money:request.event.currency }}
{% if i.original_price %}<strike class="text-muted">{{ i.original_price|money:request.event.currency }}</strike>{% endif %}
{% if i.tax_rule and i.default_price %}
<br/>
<small class="text-muted">
{% blocktrans trimmed with rate=i.tax_rule.rate|floatformat:-2 taxname=i.tax_rule.name|default:s_taxes %}
incl. {{ rate }}% {{ taxname }}
{% endblocktrans %}
</small>
{% endif %}
</td> </td>
<td class="text-right flip col-actions"> <td class="text-right flip col-actions">
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a> <button title="{% trans "Move up" %}" formaction="{% url "control:event.items.up" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button title="{% trans "Move down" %}" formaction="{% url "control:event.items.down" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm" title="{% trans "Edit" %}"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.items.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ i.id }}" class="btn btn-default btn-sm" title="{% trans "Clone" %}" data-toggle="tooltip"><i class="fa fa-copy"></i></a> <a href="{% url "control:event.items.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ i.id }}" class="btn btn-default btn-sm" title="{% trans "Clone" %}" data-toggle="tooltip"><i class="fa fa-copy"></i></a>
<a href="{% url "control:event.items.delete" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a> <a href="{% url "control:event.items.delete" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-danger btn-sm" title="{% trans "Delete" %}"><i class="fa fa-trash"></i></a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@@ -436,7 +436,7 @@
{% endif %} {% endif %}
{% if line.used_membership %} {% if line.used_membership %}
<br /><span class="fa fa-id-card fa-fw" aria-hidden="true"></span> <br /><span class="fa fa-id-card fa-fw" aria-hidden="true"></span>
<a href="{% url "control:organizer.customer.membership.edit" organizer=request.organizer.slug customer=order.customer.identifier id=line.used_membership.pk %}"> <a href="{% url "control:organizer.customer.membership.edit" organizer=request.organizer.slug customer=line.used_membership.customer.identifier id=line.used_membership.pk %}">
{{ line.used_membership }} {{ line.used_membership }}
</a> </a>
{% endif %} {% endif %}
@@ -192,6 +192,7 @@
<input type="hidden" name="start-action" value="{{ start_form.cleaned_data.action }}"> <input type="hidden" name="start-action" value="{{ start_form.cleaned_data.action }}">
<input type="hidden" name="start-mode" value="{{ start_form.cleaned_data.mode }}"> <input type="hidden" name="start-mode" value="{{ start_form.cleaned_data.mode }}">
<input type="hidden" name="start-partial_amount" value="{{ partial_amount }}"> <input type="hidden" name="start-partial_amount" value="{{ partial_amount }}">
<input type="hidden" name="last_known_refund_id" value="{{ last_known_refund_id }}">
<div class="form-group"> <div class="form-group">
<label class="control-label" for="id_comment">{% trans "Refund reason" %}</label> <label class="control-label" for="id_comment">{% trans "Refund reason" %}</label>
@@ -51,9 +51,9 @@
{% endif %} {% endif %}
</td> </td>
<td> <td>
<button formaction="{% url "control:organizer.property.up" organizer=request.organizer.slug property=p.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 and not page_obj.has_previous %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button> <button title="{% trans "Move up" %}" formaction="{% url "control:organizer.property.up" organizer=request.organizer.slug property=p.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 and not page_obj.has_previous %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button formaction="{% url "control:organizer.property.down" organizer=request.organizer.slug property=p.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button> <button title="{% trans "Move down" %}" formaction="{% url "control:organizer.property.down" organizer=request.organizer.slug property=p.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container"></span> <span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
</td> </td>
<td class="text-right flip"> <td class="text-right flip">
<a href="{% url "control:organizer.property.edit" organizer=request.organizer.slug property=p.id %}" <a href="{% url "control:organizer.property.edit" organizer=request.organizer.slug property=p.id %}"
@@ -173,6 +173,8 @@
{% endif %} {% endif %}
</td> </td>
<td class="text-right flip"> <td class="text-right flip">
<a href="{% url "control:event.orders" organizer=request.event.organizer.slug event=request.event.slug %}?subevent={{ s.id }}" class="btn btn-default btn-sm" title="{% trans "Show orders" %}"><i class="fa fa-shopping-cart" aria-hidden="true"></i></a>
<a href="{% url "control:event.subevent" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}?returnto={{ request.GET.urlencode|urlencode }}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a> <a href="{% url "control:event.subevent" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}?returnto={{ request.GET.urlencode|urlencode }}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<div class="btn-group {% if forloop.revcounter0 < 2 %}dropup{% endif %}"> <div class="btn-group {% if forloop.revcounter0 < 2 %}dropup{% endif %}">
<button type="button" class="btn btn-default btn-sm dropdown-toggle" <button type="button" class="btn btn-default btn-sm dropdown-toggle"
@@ -48,6 +48,12 @@
class="form-control" class="form-control"
id="id_url" readonly> id="id_url" readonly>
<div class="input-group-btn"> <div class="input-group-btn">
<button type="button" class="btn btn-default btn-clipboard" data-clipboard-target="#id_url">
<i class="fa fa-clipboard" aria-hidden="true"></i>
<span class="sr-only">
{% trans "Copy" %}
</span>
</button>
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" title="{% trans "Create QR code" %}" aria-haspopup="true" aria-expanded="false"> <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" title="{% trans "Create QR code" %}" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-qrcode" aria-hidden="true"></i> <i class="fa fa-qrcode" aria-hidden="true"></i>
</button> </button>
+3 -1
View File
@@ -56,6 +56,7 @@ urlpatterns = [
re_path(r'^global/settings/$', global_settings.GlobalSettingsView.as_view(), name='global.settings'), re_path(r'^global/settings/$', global_settings.GlobalSettingsView.as_view(), name='global.settings'),
re_path(r'^global/update/$', global_settings.UpdateCheckView.as_view(), name='global.update'), re_path(r'^global/update/$', global_settings.UpdateCheckView.as_view(), name='global.update'),
re_path(r'^global/license/$', global_settings.LicenseCheckView.as_view(), name='global.license'), re_path(r'^global/license/$', global_settings.LicenseCheckView.as_view(), name='global.license'),
re_path(r'^global/sysreport/$', global_settings.SysReportView.as_view(), name='global.sysreport'),
re_path(r'^global/message/$', global_settings.MessageView.as_view(), name='global.message'), re_path(r'^global/message/$', global_settings.MessageView.as_view(), name='global.message'),
re_path(r'^logdetail/$', global_settings.LogDetailView.as_view(), name='global.logdetail'), re_path(r'^logdetail/$', global_settings.LogDetailView.as_view(), name='global.logdetail'),
re_path(r'^logdetail/payment/$', global_settings.PaymentDetailView.as_view(), name='global.paymentdetail'), re_path(r'^logdetail/payment/$', global_settings.PaymentDetailView.as_view(), name='global.paymentdetail'),
@@ -247,6 +248,7 @@ urlpatterns = [
re_path(r'^widgets.json$', dashboards.event_index_widgets_lazy, name='event.index.widgets'), re_path(r'^widgets.json$', dashboards.event_index_widgets_lazy, name='event.index.widgets'),
re_path(r'^logs/embed$', dashboards.event_index_log_lazy, name='event.index.logs'), re_path(r'^logs/embed$', dashboards.event_index_log_lazy, name='event.index.logs'),
re_path(r'^live/$', event.EventLive.as_view(), name='event.live'), re_path(r'^live/$', event.EventLive.as_view(), name='event.live'),
re_path(r'^transfer_session/$', event.EventTransferSession.as_view(), name='event.transfer_session'),
re_path(r'^logs/$', event.EventLog.as_view(), name='event.log'), re_path(r'^logs/$', event.EventLog.as_view(), name='event.log'),
re_path(r'^delete/$', event.EventDelete.as_view(), name='event.delete'), re_path(r'^delete/$', event.EventDelete.as_view(), name='event.delete'),
re_path(r'^comment/$', event.EventComment.as_view(), re_path(r'^comment/$', event.EventComment.as_view(),
@@ -291,7 +293,7 @@ urlpatterns = [
re_path(r'^items/(?P<item>\d+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'), re_path(r'^items/(?P<item>\d+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'),
re_path(r'^items/(?P<item>\d+)/up$', item.item_move_up, name='event.items.up'), re_path(r'^items/(?P<item>\d+)/up$', item.item_move_up, name='event.items.up'),
re_path(r'^items/(?P<item>\d+)/down$', item.item_move_down, name='event.items.down'), re_path(r'^items/(?P<item>\d+)/down$', item.item_move_down, name='event.items.down'),
re_path(r'^items/reorder$', item.reorder_items, name='event.items.reorder'), re_path(r'^items/reorder/(?P<category>\d+)/$', item.reorder_items, name='event.items.reorder'),
re_path(r'^items/(?P<item>\d+)/delete$', item.ItemDelete.as_view(), name='event.items.delete'), re_path(r'^items/(?P<item>\d+)/delete$', item.ItemDelete.as_view(), name='event.items.delete'),
re_path(r'^items/typeahead/meta/$', typeahead.item_meta_values, name='event.items.meta.typeahead'), re_path(r'^items/typeahead/meta/$', typeahead.item_meta_values, name='event.items.meta.typeahead'),
re_path(r'^items/select2$', typeahead.items_select2, name='event.items.select2'), re_path(r'^items/select2$', typeahead.items_select2, name='event.items.select2'),
+25 -24
View File
@@ -73,6 +73,7 @@ from i18nfield.utils import I18nJSONEncoder
from pretix.base.channels import get_all_sales_channels from pretix.base.channels import get_all_sales_channels
from pretix.base.email import get_available_placeholders from pretix.base.email import get_available_placeholders
from pretix.base.forms import PlaceholderValidator
from pretix.base.models import Event, LogEntry, Order, TaxRule, Voucher from pretix.base.models import Event, LogEntry, Order, TaxRule, Voucher
from pretix.base.models.event import EventMetaValue from pretix.base.models.event import EventMetaValue
from pretix.base.services import tickets from pretix.base.services import tickets
@@ -93,13 +94,12 @@ from pretix.control.views.user import RecentAuthenticationRequiredMixin
from pretix.helpers.database import rolledback_transaction from pretix.helpers.database import rolledback_transaction
from pretix.multidomain.urlreverse import build_absolute_uri, get_event_domain from pretix.multidomain.urlreverse import build_absolute_uri, get_event_domain
from pretix.plugins.stripe.payment import StripeSettingsHolder from pretix.plugins.stripe.payment import StripeSettingsHolder
from pretix.presale.style import regenerate_css
from ...base.i18n import language from ...base.i18n import language
from ...base.models.items import ( from ...base.models.items import (
Item, ItemCategory, ItemMetaProperty, Question, Quota, Item, ItemCategory, ItemMetaProperty, Question, Quota,
) )
from ...base.settings import SETTINGS_AFFECTING_CSS, LazyI18nStringList from ...base.settings import LazyI18nStringList
from ...helpers.compat import CompatDeleteView from ...helpers.compat import CompatDeleteView
from ...helpers.format import format_map from ...helpers.format import format_map
from ..logdisplay import OVERVIEW_BANLIST from ..logdisplay import OVERVIEW_BANLIST
@@ -201,19 +201,17 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
def form_valid(self, form): def form_valid(self, form):
self._save_decoupled(self.sform) self._save_decoupled(self.sform)
self.sform.save() self.sform.save()
self.object.cache.clear()
self.save_meta() self.save_meta()
self.save_item_meta_property_formset(self.object) self.save_item_meta_property_formset(self.object)
self.save_confirm_texts_formset(self.object) self.save_confirm_texts_formset(self.object)
self.save_footer_links_formset(self.object) self.save_footer_links_formset(self.object)
change_css = False
if self.sform.has_changed() or self.confirm_texts_formset.has_changed(): if self.sform.has_changed() or self.confirm_texts_formset.has_changed():
data = {k: self.request.event.settings.get(k) for k in self.sform.changed_data} data = {k: self.request.event.settings.get(k) for k in self.sform.changed_data}
if self.confirm_texts_formset.has_changed(): if self.confirm_texts_formset.has_changed():
data.update(confirm_texts=self.confirm_texts_formset.cleaned_data) data.update(confirm_texts=self.confirm_texts_formset.cleaned_data)
self.request.event.log_action('pretix.event.settings', user=self.request.user, data=data) self.request.event.log_action('pretix.event.settings', user=self.request.user, data=data)
if any(p in self.sform.changed_data for p in SETTINGS_AFFECTING_CSS):
change_css = True
if self.footer_links_formset.has_changed(): if self.footer_links_formset.has_changed():
self.request.event.log_action('pretix.event.footerlinks.changed', user=self.request.user, data={ self.request.event.log_action('pretix.event.footerlinks.changed', user=self.request.user, data={
'data': self.footer_links_formset.cleaned_data 'data': self.footer_links_formset.cleaned_data
@@ -227,13 +225,7 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
}) })
tickets.invalidate_cache.apply_async(kwargs={'event': self.request.event.pk}) tickets.invalidate_cache.apply_async(kwargs={'event': self.request.event.pk})
if change_css: messages.success(self.request, _('Your changes have been saved.'))
regenerate_css.apply_async(args=(self.request.event.pk,))
messages.success(self.request, _('Your changes have been saved. Please note that it can '
'take a short period of time until your changes become '
'active.'))
else:
messages.success(self.request, _('Your changes have been saved.'))
return super().form_valid(form) return super().form_valid(form)
def get_success_url(self) -> str: def get_success_url(self) -> str:
@@ -713,11 +705,6 @@ class MailSettingsSetup(EventPermissionRequiredMixin, MailSettingsSetupView):
class MailSettingsPreview(EventPermissionRequiredMixin, View): class MailSettingsPreview(EventPermissionRequiredMixin, View):
permission = 'can_change_event_settings' permission = 'can_change_event_settings'
# return the origin text if key is missing in dict
class SafeDict(dict):
def __missing__(self, key):
return '{' + key + '}'
# create index-language mapping # create index-language mapping
@cached_property @cached_property
def supported_locale(self): def supported_locale(self):
@@ -742,7 +729,7 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
_('This value will be replaced based on dynamic parameters.'), _('This value will be replaced based on dynamic parameters.'),
s s
) )
return self.SafeDict(ctx) return ctx
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
preview_item = request.POST.get('item', '') preview_item = request.POST.get('item', '')
@@ -758,12 +745,21 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
idx = matched.group('idx') idx = matched.group('idx')
if idx in self.supported_locale: if idx in self.supported_locale:
with language(self.supported_locale[idx], self.request.event.settings.region): with language(self.supported_locale[idx], self.request.event.settings.region):
if k.startswith('mail_subject_'): try:
msgs[self.supported_locale[idx]] = format_map(bleach.clean(v), self.placeholders(preview_item)) if k.startswith('mail_subject_'):
else: msgs[self.supported_locale[idx]] = format_map(
msgs[self.supported_locale[idx]] = markdown_compile_email( bleach.clean(v), self.placeholders(preview_item), raise_on_missing=True
format_map(v, self.placeholders(preview_item)) )
) else:
msgs[self.supported_locale[idx]] = markdown_compile_email(
format_map(v, self.placeholders(preview_item), raise_on_missing=True)
)
except ValueError:
msgs[self.supported_locale[idx]] = '<div class="alert alert-danger">{}</div>'.format(
PlaceholderValidator.error_message)
except KeyError as e:
msgs[self.supported_locale[idx]] = '<div class="alert alert-danger">{}</div>'.format(
_('Invalid placeholder: {%(value)s}') % {'value': e.args[0]})
return JsonResponse({ return JsonResponse({
'item': preview_item, 'item': preview_item,
@@ -1017,6 +1013,11 @@ class EventLive(EventPermissionRequiredMixin, TemplateView):
}) })
class EventTransferSession(EventPermissionRequiredMixin, TemplateView):
permission = 'can_change_event_settings'
template_name = 'pretixcontrol/event/transfer_session.html'
class EventDelete(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixin, FormView): class EventDelete(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixin, FormView):
permission = 'can_change_event_settings' permission = 'can_change_event_settings'
template_name = 'pretixcontrol/event/delete.html' template_name = 'pretixcontrol/event/delete.html'
+26 -1
View File
@@ -32,14 +32,16 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # 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. # License for the specific language governing permissions and limitations under the License.
import importlib_metadata as metadata import importlib_metadata as metadata
from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.http import JsonResponse from django.http import HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404, redirect, reverse from django.shortcuts import get_object_or_404, redirect, reverse
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views import View from django.views import View
from django.views.generic import FormView, TemplateView from django.views.generic import FormView, TemplateView
from pretix.base.i18n import language
from pretix.base.models import LogEntry, OrderPayment, OrderRefund from pretix.base.models import LogEntry, OrderPayment, OrderRefund
from pretix.base.services.update_check import check_result_table, update_check from pretix.base.services.update_check import check_result_table, update_check
from pretix.base.settings import GlobalSettingsObject from pretix.base.settings import GlobalSettingsObject
@@ -49,6 +51,7 @@ from pretix.control.forms.global_settings import (
from pretix.control.permissions import ( from pretix.control.permissions import (
AdministratorPermissionRequiredMixin, StaffMemberRequiredMixin, AdministratorPermissionRequiredMixin, StaffMemberRequiredMixin,
) )
from pretix.control.sysreport import SysReport
class GlobalSettingsView(AdministratorPermissionRequiredMixin, FormView): class GlobalSettingsView(AdministratorPermissionRequiredMixin, FormView):
@@ -262,3 +265,25 @@ class LicenseCheckView(StaffMemberRequiredMixin, FormView):
)) ))
return res return res
class SysReportView(AdministratorPermissionRequiredMixin, TemplateView):
template_name = 'pretixcontrol/global_sysreport.html'
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
with language("en"):
try:
month = int(request.POST.get("month"))
except ValueError:
return super().get(request, *args, **kwargs)
if month < 1 or month > 12:
return super().get(request, *args, **kwargs)
name, mime, data = SysReport(month, settings.TIME_ZONE).render()
resp = HttpResponse(data)
resp['Content-Type'] = mime
resp['Content-Disposition'] = 'inline; filename="{}"'.format(name)
resp._csp_ignore = True
return resp
+14 -11
View File
@@ -35,6 +35,7 @@
import json import json
from collections import OrderedDict, namedtuple from collections import OrderedDict, namedtuple
from itertools import groupby
from json.decoder import JSONDecodeError from json.decoder import JSONDecodeError
from django.contrib import messages from django.contrib import messages
@@ -113,6 +114,8 @@ class ItemList(ListView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
ctx['sales_channels'] = get_all_sales_channels() ctx['sales_channels'] = get_all_sales_channels()
items_by_category = {cat: list(items) for cat, items in groupby(ctx['items'], lambda item: item.category)}
ctx['cat_list'] = [(cat, items_by_category.get(cat, [])) for cat in [None, *self.request.event.categories.all()]]
return ctx return ctx
@@ -169,7 +172,7 @@ def item_move_down(request, organizer, event, item):
@transaction.atomic @transaction.atomic
@event_permission_required("can_change_items") @event_permission_required("can_change_items")
@require_http_methods(["POST"]) @require_http_methods(["POST"])
def reorder_items(request, organizer, event): def reorder_items(request, organizer, event, category):
try: try:
ids = json.loads(request.body.decode('utf-8'))['ids'] ids = json.loads(request.body.decode('utf-8'))['ids']
except (JSONDecodeError, KeyError, ValueError): except (JSONDecodeError, KeyError, ValueError):
@@ -180,23 +183,21 @@ def reorder_items(request, organizer, event):
if len(input_items) != len(ids): if len(input_items) != len(ids):
raise Http404(_("Some of the provided object ids are invalid.")) raise Http404(_("Some of the provided object ids are invalid."))
item_categories = {i.category_id for i in input_items} if int(category):
if len(item_categories) > 1: target_category = request.event.categories.get(id=category)
raise Http404(_("You cannot reorder items spanning different categories.")) else:
target_category = None
# get first and only category
item_category = next(iter(item_categories))
if len(input_items) != request.event.items.filter(category=item_category).count():
raise Http404(_("Not all objects have been selected."))
for i in input_items: for i in input_items:
pos = ids.index(str(i.pk)) pos = ids.index(str(i.pk))
if pos != i.position: # Save unneccessary UPDATE queries if pos != i.position or target_category != i.category: # Save unneccessary UPDATE queries
i.position = pos i.position = pos
i.save(update_fields=['position']) i.category = target_category
i.save(update_fields=['position', 'category_id'])
i.log_action( i.log_action(
'pretix.event.item.reordered', user=request.user, data={ 'pretix.event.item.reordered', user=request.user, data={
'position': i, 'position': i,
'category': target_category and target_category.pk,
} }
) )
@@ -1323,6 +1324,8 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
def plugin_forms(self): def plugin_forms(self):
forms = [] forms = []
for rec, resp in item_forms.send(sender=self.request.event, item=self.item, request=self.request): for rec, resp in item_forms.send(sender=self.request.event, item=self.item, request=self.request):
if not resp:
continue
if isinstance(resp, (list, tuple)): if isinstance(resp, (list, tuple)):
forms.extend(resp) forms.extend(resp)
else: else:
+296 -276
View File
@@ -49,7 +49,7 @@ from django.core.exceptions import PermissionDenied, ValidationError
from django.core.files import File from django.core.files import File
from django.db import transaction from django.db import transaction
from django.db.models import ( from django.db.models import (
Count, Exists, F, IntegerField, OuterRef, Prefetch, ProtectedError, Q, Count, Exists, F, IntegerField, Max, OuterRef, Prefetch, ProtectedError, Q,
QuerySet, Subquery, Sum, QuerySet, Subquery, Sum,
) )
from django.forms import formset_factory from django.forms import formset_factory
@@ -129,6 +129,7 @@ from pretix.control.forms.rrule import RRuleForm
from pretix.control.permissions import EventPermissionRequiredMixin from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.signals import order_search_forms from pretix.control.signals import order_search_forms
from pretix.control.views import PaginationMixin from pretix.control.views import PaginationMixin
from pretix.helpers import OF_SELF
from pretix.helpers.compat import CompatDeleteView from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.format import format_map from pretix.helpers.format import format_map
from pretix.helpers.safedownload import check_token from pretix.helpers.safedownload import check_token
@@ -1103,249 +1104,9 @@ class OrderRefundView(OrderView):
p.propose_refund = proposals.get(p, 0) p.propose_refund = proposals.get(p, 0)
if 'perform' in self.request.POST: if 'perform' in self.request.POST:
refund_selected = Decimal('0.00') r = self.perform_refund(comment, full_refund, payments)
refunds = [] if r:
return r
is_valid = True
manual_value = self.request.POST.get('refund-manual', '0') or '0'
manual_value = formats.sanitize_separators(manual_value)
try:
manual_value = Decimal(manual_value)
except (DecimalException, TypeError):
messages.error(self.request, _('You entered an invalid number.'))
is_valid = False
else:
refund_selected += manual_value
if manual_value:
refunds.append(OrderRefund(
order=self.order,
payment=None,
source=OrderRefund.REFUND_SOURCE_ADMIN,
state=(
OrderRefund.REFUND_STATE_DONE
if self.request.POST.get('manual_state') == 'done'
else OrderRefund.REFUND_STATE_CREATED
),
execution_date=(
now()
if self.request.POST.get('manual_state') == 'done'
else None
),
amount=manual_value,
comment=comment,
provider='manual'
))
giftcard_value = self.request.POST.get('refund-new-giftcard', '0') or '0'
giftcard_value = formats.sanitize_separators(giftcard_value)
try:
giftcard_value = Decimal(giftcard_value)
except (DecimalException, TypeError):
messages.error(self.request, _('You entered an invalid number.'))
is_valid = False
else:
if giftcard_value:
refund_selected += giftcard_value
if self.request.POST.get('giftcard-expires'):
try:
expires = forms.DateField().to_python(self.request.POST.get('giftcard-expires'))
expires = make_aware(datetime.combine(
expires,
time(hour=23, minute=59, second=59)
), self.request.event.timezone)
except ValidationError as e:
messages.error(self.request, e.message)
is_valid = False
else:
expires = None
giftcard = self.request.organizer.issued_gift_cards.create(
expires=expires,
currency=self.request.event.currency,
testmode=self.order.testmode
)
giftcard.log_action('pretix.giftcards.created', user=self.request.user, data={})
refunds.append(OrderRefund(
order=self.order,
payment=None,
source=OrderRefund.REFUND_SOURCE_ADMIN,
state=OrderRefund.REFUND_STATE_CREATED,
execution_date=now(),
amount=giftcard_value,
provider='giftcard',
comment=comment,
info=json.dumps({
'gift_card': giftcard.pk
})
))
offsetting_value = self.request.POST.get('refund-offsetting', '0') or '0'
offsetting_value = formats.sanitize_separators(offsetting_value)
try:
offsetting_value = Decimal(offsetting_value)
except (DecimalException, TypeError):
messages.error(self.request, _('You entered an invalid number.'))
is_valid = False
else:
if offsetting_value:
refund_selected += offsetting_value
try:
order = Order.objects.get(code=self.request.POST.get('order-offsetting'),
event__organizer=self.request.organizer)
except Order.DoesNotExist:
messages.error(self.request, _('You entered an order that could not be found.'))
is_valid = False
else:
if order.event.currency != self.request.event.currency:
messages.error(self.request, _('You entered an order in an event with a different currency.'))
is_valid = False
refunds.append(OrderRefund(
order=self.order,
payment=None,
source=OrderRefund.REFUND_SOURCE_ADMIN,
state=OrderRefund.REFUND_STATE_DONE,
execution_date=now(),
amount=offsetting_value,
provider='offsetting',
comment=comment,
info=json.dumps({
'orders': [order.code]
})
))
for identifier, prov in self.request.event.get_payment_providers().items():
prof_value = self.request.POST.get(f'newrefund-{identifier}', '0') or '0'
prof_value = formats.sanitize_separators(prof_value)
try:
prof_value = Decimal(prof_value)
except (DecimalException, TypeError):
messages.error(self.request, _('You entered an invalid number.'))
is_valid = False
continue
if prof_value > Decimal('0.00'):
try:
refund = prov.new_refund_control_form_process(self.request, prof_value, self.order)
except ValidationError as e:
for err in e:
messages.error(self.request, err)
is_valid = False
continue
if refund:
refund_selected += refund.amount
refund.comment = comment
refund.source = OrderRefund.REFUND_SOURCE_ADMIN
refunds.append(refund)
for p in payments:
value = self.request.POST.get('refund-{}'.format(p.pk), '0') or '0'
value = formats.sanitize_separators(value)
try:
value = Decimal(value)
except (DecimalException, TypeError):
messages.error(self.request, _('You entered an invalid number.'))
is_valid = False
else:
if value == 0:
continue
elif value > p.available_amount:
messages.error(self.request, _('You can not refund more than the amount of a '
'payment that is not yet refunded.'))
is_valid = False
break
elif value != p.amount and not p.partial_refund_possible:
messages.error(self.request, _('You selected a partial refund for a payment method that '
'only supports full refunds.'))
is_valid = False
break
elif (p.partial_refund_possible or p.full_refund_possible) and value > 0:
refund_selected += value
refunds.append(OrderRefund(
order=self.order,
payment=p,
source=OrderRefund.REFUND_SOURCE_ADMIN,
state=OrderRefund.REFUND_STATE_CREATED,
amount=value,
comment=comment,
provider=p.provider
))
any_success = False
if refund_selected == full_refund and is_valid:
for r in refunds:
r.save()
self.order.log_action('pretix.event.order.refund.created', {
'local_id': r.local_id,
'provider': r.provider,
}, user=self.request.user)
if r.provider != "manual":
try:
r.payment_provider.execute_refund(r)
except PaymentException as e:
r.state = OrderRefund.REFUND_STATE_FAILED
r.save()
messages.error(self.request, _('One of the refunds failed to be processed. You should '
'retry to refund in a different way. The error message '
'was: {}').format(str(e)))
else:
any_success = True
if r.state == OrderRefund.REFUND_STATE_DONE:
messages.success(self.request, _('A refund of {} has been processed.').format(
money_filter(r.amount, self.request.event.currency)
))
elif r.state == OrderRefund.REFUND_STATE_CREATED:
messages.info(self.request, _('A refund of {} has been saved, but not yet '
'fully executed. You can mark it as complete '
'below.').format(
money_filter(r.amount, self.request.event.currency)
))
else:
any_success = True
if r.state == OrderRefund.REFUND_STATE_DONE:
self.order.log_action('pretix.event.order.refund.done', {
'local_id': r.local_id,
'provider': r.provider,
}, user=self.request.user)
if any_success:
if self.start_form.cleaned_data.get('action') == 'mark_refunded':
if self.order.cancel_allowed():
mark_order_refunded(self.order, user=self.request.user)
elif self.start_form.cleaned_data.get('action') == 'mark_pending':
if not (self.order.status == Order.STATUS_PAID and self.order.pending_sum <= 0):
self.order.status = Order.STATUS_PENDING
self.order.set_expires(
now(),
self.order.event.subevents.filter(
id__in=self.order.positions.values_list('subevent_id', flat=True))
)
self.order.save(update_fields=['status', 'expires'])
if giftcard_value and self.order.email:
messages.success(self.request, _('A new gift card was created. You can now send the user their '
'gift card code.'))
with language(self.order.locale, self.request.event.settings.region):
return redirect(reverse('control:event.order.sendmail', kwargs={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug,
'code': self.order.code
}) + '?' + urlencode({
'subject': gettext('Your gift card code'),
'message': gettext(
'Hello,\n\nwe have refunded you {amount} for your order.\n\nYou can use the gift '
'card code {giftcard} to pay for future ticket purchases in our shop.\n\n'
'Your {event} team'
).format(
event="{event}",
amount=money_filter(giftcard_value, self.request.event.currency),
giftcard=giftcard.secret,
)
}))
return redirect(self.get_order_url())
else:
messages.error(self.request, _('The refunds you selected do not match the selected total refund '
'amount.'))
new_refunds = [] new_refunds = []
for identifier, prov in self.request.event.get_payment_providers().items(): for identifier, prov in self.request.event.get_payment_providers().items():
@@ -1375,9 +1136,264 @@ class OrderRefundView(OrderView):
self.request.POST.get('start-partial_amount') if self.request.method == 'POST' self.request.POST.get('start-partial_amount') if self.request.method == 'POST'
else self.request.GET.get('start-partial_amount') else self.request.GET.get('start-partial_amount')
), ),
'start_form': self.start_form 'start_form': self.start_form,
'last_known_refund_id': self.order.refunds.aggregate(m=Max("id"))["m"] or 0,
}) })
@transaction.atomic()
def perform_refund(self, comment, full_refund, payments):
order = Order.objects.select_for_update(of=OF_SELF).get(pk=self.order.pk)
if self.request.POST.get("last_known_refund_id", "0") != str(self.order.refunds.aggregate(m=Max("id"))["m"] or 0):
messages.error(self.request, _('The refund was prevented due to a refund already being processed at the '
'same time. Please have a look at the order details and check if your '
'refund is still necessary.'))
return redirect(self.get_order_url())
refund_selected = Decimal('0.00')
refunds = []
is_valid = True
manual_value = self.request.POST.get('refund-manual', '0') or '0'
manual_value = formats.sanitize_separators(manual_value)
try:
manual_value = Decimal(manual_value)
except (DecimalException, TypeError):
messages.error(self.request, _('You entered an invalid number.'))
is_valid = False
else:
refund_selected += manual_value
if manual_value:
refunds.append(OrderRefund(
order=order,
payment=None,
source=OrderRefund.REFUND_SOURCE_ADMIN,
state=(
OrderRefund.REFUND_STATE_DONE
if self.request.POST.get('manual_state') == 'done'
else OrderRefund.REFUND_STATE_CREATED
),
execution_date=(
now()
if self.request.POST.get('manual_state') == 'done'
else None
),
amount=manual_value,
comment=comment,
provider='manual'
))
giftcard_value = self.request.POST.get('refund-new-giftcard', '0') or '0'
giftcard_value = formats.sanitize_separators(giftcard_value)
try:
giftcard_value = Decimal(giftcard_value)
except (DecimalException, TypeError):
messages.error(self.request, _('You entered an invalid number.'))
is_valid = False
else:
if giftcard_value:
refund_selected += giftcard_value
if self.request.POST.get('giftcard-expires'):
try:
expires = forms.DateField().to_python(self.request.POST.get('giftcard-expires'))
expires = make_aware(datetime.combine(
expires,
time(hour=23, minute=59, second=59)
), self.request.event.timezone)
except ValidationError as e:
messages.error(self.request, e.message)
is_valid = False
else:
expires = None
giftcard = self.request.organizer.issued_gift_cards.create(
expires=expires,
currency=self.request.event.currency,
testmode=order.testmode
)
giftcard.log_action('pretix.giftcards.created', user=self.request.user, data={})
refunds.append(OrderRefund(
order=order,
payment=None,
source=OrderRefund.REFUND_SOURCE_ADMIN,
state=OrderRefund.REFUND_STATE_CREATED,
execution_date=now(),
amount=giftcard_value,
provider='giftcard',
comment=comment,
info=json.dumps({
'gift_card': giftcard.pk
})
))
offsetting_value = self.request.POST.get('refund-offsetting', '0') or '0'
offsetting_value = formats.sanitize_separators(offsetting_value)
try:
offsetting_value = Decimal(offsetting_value)
except (DecimalException, TypeError):
messages.error(self.request, _('You entered an invalid number.'))
is_valid = False
else:
if offsetting_value:
refund_selected += offsetting_value
try:
offset_order = Order.objects.get(code=self.request.POST.get('order-offsetting'),
event__organizer=self.request.organizer)
except Order.DoesNotExist:
messages.error(self.request, _('You entered an order that could not be found.'))
is_valid = False
else:
if offset_order.event.currency != self.request.event.currency:
messages.error(self.request, _('You entered an order in an event with a different currency.'))
is_valid = False
refunds.append(OrderRefund(
order=order,
payment=None,
source=OrderRefund.REFUND_SOURCE_ADMIN,
state=OrderRefund.REFUND_STATE_DONE,
execution_date=now(),
amount=offsetting_value,
provider='offsetting',
comment=comment,
info=json.dumps({
'orders': [offset_order.code]
})
))
for identifier, prov in self.request.event.get_payment_providers().items():
prof_value = self.request.POST.get(f'newrefund-{identifier}', '0') or '0'
prof_value = formats.sanitize_separators(prof_value)
try:
prof_value = Decimal(prof_value)
except (DecimalException, TypeError):
messages.error(self.request, _('You entered an invalid number.'))
is_valid = False
continue
if prof_value > Decimal('0.00'):
try:
refund = prov.new_refund_control_form_process(self.request, prof_value, order)
except ValidationError as e:
for err in e:
messages.error(self.request, err)
is_valid = False
continue
if refund:
refund_selected += refund.amount
refund.comment = comment
refund.source = OrderRefund.REFUND_SOURCE_ADMIN
refunds.append(refund)
for p in payments:
value = self.request.POST.get('refund-{}'.format(p.pk), '0') or '0'
value = formats.sanitize_separators(value)
try:
value = Decimal(value)
except (DecimalException, TypeError):
messages.error(self.request, _('You entered an invalid number.'))
is_valid = False
else:
if value == 0:
continue
elif value > p.available_amount:
messages.error(self.request, _('You can not refund more than the amount of a '
'payment that is not yet refunded.'))
is_valid = False
break
elif value != p.amount and not p.partial_refund_possible:
messages.error(self.request, _('You selected a partial refund for a payment method that '
'only supports full refunds.'))
is_valid = False
break
elif (p.partial_refund_possible or p.full_refund_possible) and value > 0:
refund_selected += value
refunds.append(OrderRefund(
order=order,
payment=p,
source=OrderRefund.REFUND_SOURCE_ADMIN,
state=OrderRefund.REFUND_STATE_CREATED,
amount=value,
comment=comment,
provider=p.provider
))
any_success = False
if refund_selected == full_refund and is_valid:
for r in refunds:
r.save()
order.log_action('pretix.event.order.refund.created', {
'local_id': r.local_id,
'provider': r.provider,
}, user=self.request.user)
if r.provider != "manual":
try:
r.payment_provider.execute_refund(r)
except PaymentException as e:
r.state = OrderRefund.REFUND_STATE_FAILED
r.save()
messages.error(self.request, _('One of the refunds failed to be processed. You should '
'retry to refund in a different way. The error message '
'was: {}').format(str(e)))
else:
any_success = True
if r.state == OrderRefund.REFUND_STATE_DONE:
messages.success(self.request, _('A refund of {} has been processed.').format(
money_filter(r.amount, self.request.event.currency)
))
elif r.state == OrderRefund.REFUND_STATE_CREATED:
messages.info(self.request, _('A refund of {} has been saved, but not yet '
'fully executed. You can mark it as complete '
'below.').format(
money_filter(r.amount, self.request.event.currency)
))
else:
any_success = True
if r.state == OrderRefund.REFUND_STATE_DONE:
order.log_action('pretix.event.order.refund.done', {
'local_id': r.local_id,
'provider': r.provider,
}, user=self.request.user)
if any_success:
if self.start_form.cleaned_data.get('action') == 'mark_refunded':
if order.cancel_allowed():
mark_order_refunded(order, user=self.request.user)
elif self.start_form.cleaned_data.get('action') == 'mark_pending':
if not (order.status == Order.STATUS_PAID and self.order.pending_sum <= 0):
order.status = Order.STATUS_PENDING
order.set_expires(
now(),
order.event.subevents.filter(
id__in=order.positions.values_list('subevent_id', flat=True))
)
order.save(update_fields=['status', 'expires'])
if giftcard_value and order.email:
messages.success(self.request, _('A new gift card was created. You can now send the user their '
'gift card code.'))
with language(order.locale, self.request.event.settings.region):
return redirect(reverse('control:event.order.sendmail', kwargs={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug,
'code': order.code
}) + '?' + urlencode({
'subject': gettext('Your gift card code'),
'message': gettext(
'Hello,\n\nwe have refunded you {amount} for your order.\n\nYou can use the gift '
'card code {giftcard} to pay for future ticket purchases in our shop.\n\n'
'Your {event} team'
).format(
event="{event}",
amount=money_filter(giftcard_value, self.request.event.currency),
giftcard=giftcard.secret,
)
}))
return redirect(self.get_order_url())
else:
messages.error(self.request, _('The refunds you selected do not match the selected total refund '
'amount.'))
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
if self.start_form.is_valid(): if self.start_form.is_valid():
return self.choose_form() return self.choose_form()
@@ -1562,20 +1578,22 @@ class OrderInvoiceCreate(OrderView):
permission = 'can_change_orders' permission = 'can_change_orders'
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
has_inv = self.order.invoices.exists() and not ( with transaction.atomic():
self.order.status in (Order.STATUS_PAID, Order.STATUS_PENDING) order = Order.objects.select_for_update(of=OF_SELF).get(pk=self.order.pk)
and self.order.invoices.filter(is_cancellation=True).count() >= self.order.invoices.filter(is_cancellation=False).count() has_inv = order.invoices.exists() and not (
) order.status in (Order.STATUS_PAID, Order.STATUS_PENDING)
if self.request.event.settings.get('invoice_generate') not in ('admin', 'user', 'paid', 'True') or not invoice_qualified(self.order): and order.invoices.filter(is_cancellation=True).count() >= order.invoices.filter(is_cancellation=False).count()
messages.error(self.request, _('You cannot generate an invoice for this order.')) )
elif has_inv: if self.request.event.settings.get('invoice_generate') not in ('admin', 'user', 'paid', 'True') or not invoice_qualified(order):
messages.error(self.request, _('An invoice for this order already exists.')) messages.error(self.request, _('You cannot generate an invoice for this order.'))
else: elif has_inv:
inv = generate_invoice(self.order) messages.error(self.request, _('An invoice for this order already exists.'))
self.order.log_action('pretix.event.order.invoice.generated', user=self.request.user, data={ else:
'invoice': inv.pk inv = generate_invoice(order)
}) order.log_action('pretix.event.order.invoice.generated', user=self.request.user, data={
messages.success(self.request, _('The invoice has been generated.')) 'invoice': inv.pk
})
messages.success(self.request, _('The invoice has been generated.'))
return redirect(self.get_order_url()) return redirect(self.get_order_url())
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
@@ -1657,25 +1675,27 @@ class OrderInvoiceReissue(OrderView):
permission = 'can_change_orders' permission = 'can_change_orders'
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
try: with transaction.atomic():
inv = self.order.invoices.get(pk=kwargs.get('id')) order = Order.objects.select_for_update(of=OF_SELF).get(pk=self.order.pk)
except Invoice.DoesNotExist: try:
messages.error(self.request, _('Unknown invoice.')) inv = order.invoices.get(pk=kwargs.get('id'))
else: except Invoice.DoesNotExist:
if inv.canceled: messages.error(self.request, _('Unknown invoice.'))
messages.error(self.request, _('The invoice has already been canceled.'))
elif inv.shredded:
messages.error(self.request, _('The invoice has been cleaned of personal data.'))
else: else:
c = generate_cancellation(inv) if inv.canceled:
if self.order.status not in (Order.STATUS_CANCELED, Order.STATUS_EXPIRED): messages.error(self.request, _('The invoice has already been canceled.'))
inv = generate_invoice(self.order) elif inv.shredded:
messages.error(self.request, _('The invoice has been cleaned of personal data.'))
else: else:
inv = c c = generate_cancellation(inv)
self.order.log_action('pretix.event.order.invoice.reissued', user=self.request.user, data={ if order.status not in (Order.STATUS_CANCELED, Order.STATUS_EXPIRED):
'invoice': inv.pk inv = generate_invoice(order)
}) else:
messages.success(self.request, _('The invoice has been reissued.')) inv = c
order.log_action('pretix.event.order.invoice.reissued', user=self.request.user, data={
'invoice': inv.pk
})
messages.success(self.request, _('The invoice has been reissued.'))
return redirect(self.get_order_url()) return redirect(self.get_order_url())
def get(self, *args, **kwargs): # NOQA def get(self, *args, **kwargs): # NOQA
+2 -12
View File
@@ -91,7 +91,6 @@ from pretix.base.models.organizer import TeamAPIToken
from pretix.base.payment import PaymentException from pretix.base.payment import PaymentException
from pretix.base.services.export import multiexport, scheduled_organizer_export from pretix.base.services.export import multiexport, scheduled_organizer_export
from pretix.base.services.mail import SendMailException, mail from pretix.base.services.mail import SendMailException, mail
from pretix.base.settings import SETTINGS_AFFECTING_CSS
from pretix.base.signals import register_multievent_data_exporters from pretix.base.signals import register_multievent_data_exporters
from pretix.base.templatetags.rich_text import markdown_compile_email from pretix.base.templatetags.rich_text import markdown_compile_email
from pretix.base.views.tasks import AsyncAction from pretix.base.views.tasks import AsyncAction
@@ -127,7 +126,6 @@ from pretix.helpers.format import format_map
from pretix.helpers.urls import build_absolute_uri as build_global_uri from pretix.helpers.urls import build_absolute_uri as build_global_uri
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.presale.forms.customer import TokenGenerator from pretix.presale.forms.customer import TokenGenerator
from pretix.presale.style import regenerate_organizer_css
class OrganizerList(PaginationMixin, ListView): class OrganizerList(PaginationMixin, ListView):
@@ -466,7 +464,7 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
def form_valid(self, form): def form_valid(self, form):
self.sform.save() self.sform.save()
self.save_footer_links_formset(self.object) self.save_footer_links_formset(self.object)
change_css = False self.object.cache.clear()
if self.sform.has_changed(): if self.sform.has_changed():
self.request.organizer.log_action( self.request.organizer.log_action(
'pretix.organizer.settings', 'pretix.organizer.settings',
@@ -478,8 +476,6 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
for k in self.sform.changed_data for k in self.sform.changed_data
} }
) )
if any(p in self.sform.changed_data for p in SETTINGS_AFFECTING_CSS):
change_css = True
if self.footer_links_formset.has_changed(): if self.footer_links_formset.has_changed():
self.request.organizer.log_action('pretix.organizer.footerlinks.changed', user=self.request.user, data={ self.request.organizer.log_action('pretix.organizer.footerlinks.changed', user=self.request.user, data={
'data': self.footer_links_formset.cleaned_data 'data': self.footer_links_formset.cleaned_data
@@ -491,13 +487,7 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
data={k: form.cleaned_data.get(k) for k in form.changed_data} data={k: form.cleaned_data.get(k) for k in form.changed_data}
) )
if change_css: messages.success(self.request, _('Your changes have been saved.'))
regenerate_organizer_css.apply_async(args=(self.request.organizer.pk,))
messages.success(self.request, _('Your changes have been saved. Please note that it can '
'take a short period of time until your changes become '
'active.'))
else:
messages.success(self.request, _('Your changes have been saved.'))
return super().form_valid(form) return super().form_valid(form)
def get_form_kwargs(self): def get_form_kwargs(self):
+11 -12
View File
@@ -64,7 +64,8 @@ def casual_reads():
class GroupConcat(Aggregate): class GroupConcat(Aggregate):
function = 'group_concat' function = 'group_concat'
template = '%(function)s(%(field)s, "%(separator)s")' template = '%(function)s(%(distinct)s%(field)s, "%(separator)s")'
allow_distinct = True
def __init__(self, *expressions, ordered=False, **extra): def __init__(self, *expressions, ordered=False, **extra):
self.ordered = ordered self.ordered = ordered
@@ -73,19 +74,17 @@ class GroupConcat(Aggregate):
extra.update({'separator': ','}) extra.update({'separator': ','})
super().__init__(*expressions, **extra) super().__init__(*expressions, **extra)
def as_postgresql(self, compiler, connection): def as_postgresql(self, compiler, connection, **extra_context):
if self.ordered: if self.ordered:
return super().as_sql( template = "%(function)s(%(distinct)s%(field)s::text, '%(separator)s' ORDER BY %(field)s::text ASC)"
compiler, connection,
function='string_agg',
template="%(function)s(%(field)s::text, '%(separator)s' ORDER BY %(field)s ASC)",
)
else: else:
return super().as_sql( template = "%(function)s(%(distinct)s%(field)s::text, '%(separator)s')"
compiler, connection, return super().as_sql(
function='string_agg', compiler, connection,
template="%(function)s(%(field)s::text, '%(separator)s')", function='string_agg',
) template=template,
**extra_context,
)
class ReplicaRouter: class ReplicaRouter:
+6 -8
View File
@@ -30,17 +30,15 @@ class SafeFormatter(Formatter):
Customized version of ``str.format`` that (a) behaves just like ``str.format_map`` and Customized version of ``str.format`` that (a) behaves just like ``str.format_map`` and
(b) does not allow any unwanted shenanigans like attribute access or format specifiers. (b) does not allow any unwanted shenanigans like attribute access or format specifiers.
""" """
def __init__(self, context): def __init__(self, context, raise_on_missing=False):
self.context = context self.context = context
self.raise_on_missing = raise_on_missing
def get_field(self, field_name, args, kwargs): def get_field(self, field_name, args, kwargs):
if '.' in field_name or '[' in field_name: return self.get_value(field_name, args, kwargs), field_name
logger.warning(f'Ignored invalid field name "{field_name}"')
return ('{' + str(field_name) + '}', field_name)
return super().get_field(field_name, args, kwargs)
def get_value(self, key, args, kwargs): def get_value(self, key, args, kwargs):
if key not in self.context: if not self.raise_on_missing and key not in self.context:
return '{' + str(key) + '}' return '{' + str(key) + '}'
return self.context[key] return self.context[key]
@@ -49,7 +47,7 @@ class SafeFormatter(Formatter):
return super().format_field(value, '') return super().format_field(value, '')
def format_map(template, context): def format_map(template, context, raise_on_missing=False):
if not isinstance(template, str): if not isinstance(template, str):
template = str(template) template = str(template)
return SafeFormatter(context).format(template) return SafeFormatter(context, raise_on_missing).format(template)
+6 -2
View File
@@ -72,9 +72,13 @@ def remove_invalid_excel_chars(val):
return val return val
def SafeCell(*args, value=None, **kwargs): def SafeCell(worksheet, row=None, column=None, value=None, **kwargs):
value = remove_invalid_excel_chars(value) value = remove_invalid_excel_chars(value)
c = Cell(*args, value=value, **kwargs) if not column:
column = 1
if not row:
row = 1
c = Cell(worksheet, row=row, column=column, value=value, **kwargs)
if c.data_type == TYPE_FORMULA: if c.data_type == TYPE_FORMULA:
c.data_type = TYPE_STRING c.data_type = TYPE_STRING
return c return c
+7 -1
View File
@@ -180,7 +180,11 @@ def create_thumbnail(source, size, formats=None):
except: except:
raise ThumbnailError('Could not load image') raise ThumbnailError('Could not load image')
frames = [resize_image(frame, size) for frame in ImageSequence.Iterator(image)] frames = []
durations = []
for f in ImageSequence.Iterator(image):
durations.append(f.info.get("duration", 1000))
frames.append(resize_image(f, size))
image_out = frames[0] image_out = frames[0]
save_kwargs = {} save_kwargs = {}
source_ext = os.path.splitext(source_name)[1].lower() source_ext = os.path.splitext(source_name)[1].lower()
@@ -198,6 +202,8 @@ def create_thumbnail(source, size, formats=None):
'loop': image.info.get('loop', 0), 'loop': image.info.get('loop', 0),
'save_all': True, 'save_all': True,
} }
if len(frames) > 1 and 'duration' in image.info:
save_kwargs['duration'] = durations
else: else:
target_ext = 'png' target_ext = 'png'
quality = None quality = None
File diff suppressed because it is too large Load Diff
+44 -36
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-23 12:41+0000\n" "POT-Creation-Date: 2024-06-24 08:20+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -130,7 +130,7 @@ msgid "Mercado Pago"
msgstr "" msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
msgid "Continue" msgid "Continue"
msgstr "" msgstr ""
@@ -235,97 +235,105 @@ msgid "Canceled"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
msgid "Redeemed" msgid "Confirmed"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
msgid "Cancel" msgid "Approval pending"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
msgid "Redeemed"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58 msgid "Cancel"
msgid "Ticket not paid"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
msgid "This ticket is not yet paid. Do you want to continue anyways?"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
msgid "Additional information required" #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
msgid "Ticket not paid"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
msgid "Valid ticket" msgid "This ticket is not yet paid. Do you want to continue anyways?"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
msgid "Exit recorded" msgid "Additional information required"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
msgid "Ticket already used" msgid "Valid ticket"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
msgid "Information required" msgid "Exit recorded"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
msgid "Unknown ticket" msgid "Ticket already used"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
msgid "Ticket type not allowed here" msgid "Information required"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
msgid "Unknown ticket"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
msgid "Entry not allowed" msgid "Ticket type not allowed here"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
msgid "Ticket code revoked/changed"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
msgid "Ticket blocked" msgid "Entry not allowed"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
msgid "Ticket not valid at this time" msgid "Ticket code revoked/changed"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
msgid "Order canceled" msgid "Ticket blocked"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
msgid "Ticket code is ambiguous on list" msgid "Ticket not valid at this time"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
msgid "Order not approved" msgid "Order canceled"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
msgid "Checked-in Tickets" msgid "Ticket code is ambiguous on list"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
msgid "Valid Tickets" msgid "Order not approved"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
msgid "Currently inside" msgid "Checked-in Tickets"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
msgid "Valid Tickets"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70
msgid "Currently inside"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:71
#: pretix/static/pretixcontrol/js/ui/question.js:137 #: pretix/static/pretixcontrol/js/ui/question.js:137
#: pretix/static/pretixpresale/js/ui/questions.js:270 #: pretix/static/pretixpresale/js/ui/questions.js:270
msgid "Yes" msgid "Yes"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:72
#: pretix/static/pretixcontrol/js/ui/question.js:138 #: pretix/static/pretixcontrol/js/ui/question.js:138
#: pretix/static/pretixpresale/js/ui/questions.js:270 #: pretix/static/pretixpresale/js/ui/questions.js:270
msgid "No" msgid "No"
@@ -627,23 +635,23 @@ msgstr ""
msgid "Selected only" msgid "Selected only"
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:794 #: pretix/static/pretixcontrol/js/ui/main.js:828
msgid "Enter page number between 1 and %(max)s." msgid "Enter page number between 1 and %(max)s."
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:797 #: pretix/static/pretixcontrol/js/ui/main.js:831
msgid "Invalid page number." msgid "Invalid page number."
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:955 #: pretix/static/pretixcontrol/js/ui/main.js:989
msgid "Use a different name internally" msgid "Use a different name internally"
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:995 #: pretix/static/pretixcontrol/js/ui/main.js:1029
msgid "Click to close" msgid "Click to close"
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1070 #: pretix/static/pretixcontrol/js/ui/main.js:1104
msgid "You have unsaved changes!" msgid "You have unsaved changes!"
msgstr "" msgstr ""
File diff suppressed because it is too large Load Diff
+38 -30
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-23 12:41+0000\n" "POT-Creation-Date: 2024-06-24 08:20+0000\n"
"PO-Revision-Date: 2021-09-15 11:22+0000\n" "PO-Revision-Date: 2021-09-15 11:22+0000\n"
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n" "Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/" "Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -134,7 +134,7 @@ msgid "Mercado Pago"
msgstr "" msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
msgid "Continue" msgid "Continue"
msgstr "المتابعة" msgstr "المتابعة"
@@ -239,105 +239,113 @@ msgid "Canceled"
msgstr "ملغاة" msgstr "ملغاة"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
msgid "Confirmed"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
msgid "Approval pending"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
msgid "Redeemed" msgid "Redeemed"
msgstr "مستخدم" msgstr "مستخدم"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
msgid "Cancel" msgid "Cancel"
msgstr "قم بالإلغاء" msgstr "قم بالإلغاء"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
msgid "Ticket not paid" msgid "Ticket not paid"
msgstr "لم يتم دفع قيمة التذكرة" msgstr "لم يتم دفع قيمة التذكرة"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
msgid "This ticket is not yet paid. Do you want to continue anyways?" msgid "This ticket is not yet paid. Do you want to continue anyways?"
msgstr "لم يتم دفع قيمة التذكرة بعد، هل تريد المتابعة على أي حال؟" msgstr "لم يتم دفع قيمة التذكرة بعد، هل تريد المتابعة على أي حال؟"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
msgid "Additional information required" msgid "Additional information required"
msgstr "مطلوب معلومات إضافية" msgstr "مطلوب معلومات إضافية"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
msgid "Valid ticket" msgid "Valid ticket"
msgstr "تذكرة سارية المفعول" msgstr "تذكرة سارية المفعول"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
msgid "Exit recorded" msgid "Exit recorded"
msgstr "تم تسجيل الخروج" msgstr "تم تسجيل الخروج"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
msgid "Ticket already used" msgid "Ticket already used"
msgstr "تم استخدام التذكرة مسبقا" msgstr "تم استخدام التذكرة مسبقا"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
msgid "Information required" msgid "Information required"
msgstr "معلومات مطلوبة" msgstr "معلومات مطلوبة"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
#, fuzzy #, fuzzy
#| msgid "Unknown error." #| msgid "Unknown error."
msgid "Unknown ticket" msgid "Unknown ticket"
msgstr "خطأ غير معروف." msgstr "خطأ غير معروف."
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
#, fuzzy #, fuzzy
#| msgid "Entry not allowed" #| msgid "Entry not allowed"
msgid "Ticket type not allowed here" msgid "Ticket type not allowed here"
msgstr "إدخال غير مسموح" msgstr "إدخال غير مسموح"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
msgid "Entry not allowed" msgid "Entry not allowed"
msgstr "إدخال غير مسموح" msgstr "إدخال غير مسموح"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
msgid "Ticket code revoked/changed" msgid "Ticket code revoked/changed"
msgstr "تم إلغاء رمز التذكرة أو تبديله" msgstr "تم إلغاء رمز التذكرة أو تبديله"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
#, fuzzy #, fuzzy
#| msgid "Ticket not paid" #| msgid "Ticket not paid"
msgid "Ticket blocked" msgid "Ticket blocked"
msgstr "لم يتم دفع قيمة التذكرة" msgstr "لم يتم دفع قيمة التذكرة"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
#, fuzzy #, fuzzy
#| msgid "Ticket not paid" #| msgid "Ticket not paid"
msgid "Ticket not valid at this time" msgid "Ticket not valid at this time"
msgstr "لم يتم دفع قيمة التذكرة" msgstr "لم يتم دفع قيمة التذكرة"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
msgid "Order canceled" msgid "Order canceled"
msgstr "تم إلغاء الطلب" msgstr "تم إلغاء الطلب"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
msgid "Ticket code is ambiguous on list" msgid "Ticket code is ambiguous on list"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
msgid "Order not approved" msgid "Order not approved"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
msgid "Checked-in Tickets" msgid "Checked-in Tickets"
msgstr "تذاكر الدخول" msgstr "تذاكر الدخول"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
msgid "Valid Tickets" msgid "Valid Tickets"
msgstr "تذاكر سارية المفعول" msgstr "تذاكر سارية المفعول"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70
msgid "Currently inside" msgid "Currently inside"
msgstr "حاليا بالداخل" msgstr "حاليا بالداخل"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:71
#: pretix/static/pretixcontrol/js/ui/question.js:137 #: pretix/static/pretixcontrol/js/ui/question.js:137
#: pretix/static/pretixpresale/js/ui/questions.js:270 #: pretix/static/pretixpresale/js/ui/questions.js:270
msgid "Yes" msgid "Yes"
msgstr "نعم" msgstr "نعم"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:72
#: pretix/static/pretixcontrol/js/ui/question.js:138 #: pretix/static/pretixcontrol/js/ui/question.js:138
#: pretix/static/pretixpresale/js/ui/questions.js:270 #: pretix/static/pretixpresale/js/ui/questions.js:270
msgid "No" msgid "No"
@@ -657,23 +665,23 @@ msgstr "لا شيء"
msgid "Selected only" msgid "Selected only"
msgstr "المختارة فقط" msgstr "المختارة فقط"
#: pretix/static/pretixcontrol/js/ui/main.js:794 #: pretix/static/pretixcontrol/js/ui/main.js:828
msgid "Enter page number between 1 and %(max)s." msgid "Enter page number between 1 and %(max)s."
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:797 #: pretix/static/pretixcontrol/js/ui/main.js:831
msgid "Invalid page number." msgid "Invalid page number."
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:955 #: pretix/static/pretixcontrol/js/ui/main.js:989
msgid "Use a different name internally" msgid "Use a different name internally"
msgstr "قم باستخدم اسم مختلف داخليا" msgstr "قم باستخدم اسم مختلف داخليا"
#: pretix/static/pretixcontrol/js/ui/main.js:995 #: pretix/static/pretixcontrol/js/ui/main.js:1029
msgid "Click to close" msgid "Click to close"
msgstr "اضغط لاغلاق الصفحة" msgstr "اضغط لاغلاق الصفحة"
#: pretix/static/pretixcontrol/js/ui/main.js:1070 #: pretix/static/pretixcontrol/js/ui/main.js:1104
msgid "You have unsaved changes!" msgid "You have unsaved changes!"
msgstr "لم تقم بحفظ التعديلات!" msgstr "لم تقم بحفظ التعديلات!"
File diff suppressed because it is too large Load Diff
+44 -36
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-23 12:41+0000\n" "POT-Creation-Date: 2024-06-24 08:20+0000\n"
"PO-Revision-Date: 2020-12-19 07:00+0000\n" "PO-Revision-Date: 2020-12-19 07:00+0000\n"
"Last-Translator: albert <albert.serra.monner@gmail.com>\n" "Last-Translator: albert <albert.serra.monner@gmail.com>\n"
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-" "Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -131,7 +131,7 @@ msgid "Mercado Pago"
msgstr "" msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
msgid "Continue" msgid "Continue"
msgstr "" msgstr ""
@@ -236,97 +236,105 @@ msgid "Canceled"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
msgid "Redeemed" msgid "Confirmed"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
msgid "Cancel" msgid "Approval pending"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
msgid "Redeemed"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58 msgid "Cancel"
msgid "Ticket not paid"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
msgid "This ticket is not yet paid. Do you want to continue anyways?"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
msgid "Additional information required" #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
msgid "Ticket not paid"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
msgid "Valid ticket" msgid "This ticket is not yet paid. Do you want to continue anyways?"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
msgid "Exit recorded" msgid "Additional information required"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
msgid "Ticket already used" msgid "Valid ticket"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
msgid "Information required" msgid "Exit recorded"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
msgid "Unknown ticket" msgid "Ticket already used"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
msgid "Ticket type not allowed here" msgid "Information required"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
msgid "Unknown ticket"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
msgid "Entry not allowed" msgid "Ticket type not allowed here"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
msgid "Ticket code revoked/changed"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
msgid "Ticket blocked" msgid "Entry not allowed"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
msgid "Ticket not valid at this time" msgid "Ticket code revoked/changed"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
msgid "Order canceled" msgid "Ticket blocked"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
msgid "Ticket code is ambiguous on list" msgid "Ticket not valid at this time"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
msgid "Order not approved" msgid "Order canceled"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
msgid "Checked-in Tickets" msgid "Ticket code is ambiguous on list"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
msgid "Valid Tickets" msgid "Order not approved"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
msgid "Currently inside" msgid "Checked-in Tickets"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
msgid "Valid Tickets"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70
msgid "Currently inside"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:71
#: pretix/static/pretixcontrol/js/ui/question.js:137 #: pretix/static/pretixcontrol/js/ui/question.js:137
#: pretix/static/pretixpresale/js/ui/questions.js:270 #: pretix/static/pretixpresale/js/ui/questions.js:270
msgid "Yes" msgid "Yes"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:72
#: pretix/static/pretixcontrol/js/ui/question.js:138 #: pretix/static/pretixcontrol/js/ui/question.js:138
#: pretix/static/pretixpresale/js/ui/questions.js:270 #: pretix/static/pretixpresale/js/ui/questions.js:270
msgid "No" msgid "No"
@@ -628,23 +636,23 @@ msgstr ""
msgid "Selected only" msgid "Selected only"
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:794 #: pretix/static/pretixcontrol/js/ui/main.js:828
msgid "Enter page number between 1 and %(max)s." msgid "Enter page number between 1 and %(max)s."
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:797 #: pretix/static/pretixcontrol/js/ui/main.js:831
msgid "Invalid page number." msgid "Invalid page number."
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:955 #: pretix/static/pretixcontrol/js/ui/main.js:989
msgid "Use a different name internally" msgid "Use a different name internally"
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:995 #: pretix/static/pretixcontrol/js/ui/main.js:1029
msgid "Click to close" msgid "Click to close"
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1070 #: pretix/static/pretixcontrol/js/ui/main.js:1104
msgid "You have unsaved changes!" msgid "You have unsaved changes!"
msgstr "" msgstr ""
File diff suppressed because it is too large Load Diff
+38 -30
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-23 12:41+0000\n" "POT-Creation-Date: 2024-06-24 08:20+0000\n"
"PO-Revision-Date: 2023-09-15 06:00+0000\n" "PO-Revision-Date: 2023-09-15 06:00+0000\n"
"Last-Translator: Michael <michael.happl@gmx.at>\n" "Last-Translator: Michael <michael.happl@gmx.at>\n"
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/" "Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -131,7 +131,7 @@ msgid "Mercado Pago"
msgstr "Mercado Pago" msgstr "Mercado Pago"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
msgid "Continue" msgid "Continue"
msgstr "Pokračovat" msgstr "Pokračovat"
@@ -236,97 +236,105 @@ msgid "Canceled"
msgstr "Zrušeno" msgstr "Zrušeno"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
msgid "Confirmed"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
msgid "Approval pending"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
msgid "Redeemed" msgid "Redeemed"
msgstr "Uplatněno" msgstr "Uplatněno"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
msgid "Cancel" msgid "Cancel"
msgstr "Zrušit" msgstr "Zrušit"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
msgid "Ticket not paid" msgid "Ticket not paid"
msgstr "Vstupenka není zaplacena" msgstr "Vstupenka není zaplacena"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
msgid "This ticket is not yet paid. Do you want to continue anyways?" msgid "This ticket is not yet paid. Do you want to continue anyways?"
msgstr "Vstupenka nebyla zaplacena. Chcete i přesto pokračovat?" msgstr "Vstupenka nebyla zaplacena. Chcete i přesto pokračovat?"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
msgid "Additional information required" msgid "Additional information required"
msgstr "Potřebné další informace" msgstr "Potřebné další informace"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
msgid "Valid ticket" msgid "Valid ticket"
msgstr "Platná vstupenka" msgstr "Platná vstupenka"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
msgid "Exit recorded" msgid "Exit recorded"
msgstr "Opustit nahrávané" msgstr "Opustit nahrávané"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
msgid "Ticket already used" msgid "Ticket already used"
msgstr "Vstupenka již byla použita" msgstr "Vstupenka již byla použita"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
msgid "Information required" msgid "Information required"
msgstr "Informace vyžadována" msgstr "Informace vyžadována"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
msgid "Unknown ticket" msgid "Unknown ticket"
msgstr "Neznámá vstupenka" msgstr "Neznámá vstupenka"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
msgid "Ticket type not allowed here" msgid "Ticket type not allowed here"
msgstr "Typ vstupenky zde není povolen" msgstr "Typ vstupenky zde není povolen"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
msgid "Entry not allowed" msgid "Entry not allowed"
msgstr "Vstup není povolen" msgstr "Vstup není povolen"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
msgid "Ticket code revoked/changed" msgid "Ticket code revoked/changed"
msgstr "Kód vstupenky změněn" msgstr "Kód vstupenky změněn"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
msgid "Ticket blocked" msgid "Ticket blocked"
msgstr "Vstupenka zablokována" msgstr "Vstupenka zablokována"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
msgid "Ticket not valid at this time" msgid "Ticket not valid at this time"
msgstr "Vstupenka je v tuto chvíli neplatná" msgstr "Vstupenka je v tuto chvíli neplatná"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
msgid "Order canceled" msgid "Order canceled"
msgstr "Objednávka zrušena" msgstr "Objednávka zrušena"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
msgid "Ticket code is ambiguous on list" msgid "Ticket code is ambiguous on list"
msgstr "Kód vstupenky je v seznamu nejednoznačný" msgstr "Kód vstupenky je v seznamu nejednoznačný"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
msgid "Order not approved" msgid "Order not approved"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
msgid "Checked-in Tickets" msgid "Checked-in Tickets"
msgstr "Vyřízené vstupenky" msgstr "Vyřízené vstupenky"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
msgid "Valid Tickets" msgid "Valid Tickets"
msgstr "Platné vstupenky" msgstr "Platné vstupenky"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70
msgid "Currently inside" msgid "Currently inside"
msgstr "Aktuálně uvnitř" msgstr "Aktuálně uvnitř"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:71
#: pretix/static/pretixcontrol/js/ui/question.js:137 #: pretix/static/pretixcontrol/js/ui/question.js:137
#: pretix/static/pretixpresale/js/ui/questions.js:270 #: pretix/static/pretixpresale/js/ui/questions.js:270
msgid "Yes" msgid "Yes"
msgstr "Ano" msgstr "Ano"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:72
#: pretix/static/pretixcontrol/js/ui/question.js:138 #: pretix/static/pretixcontrol/js/ui/question.js:138
#: pretix/static/pretixpresale/js/ui/questions.js:270 #: pretix/static/pretixpresale/js/ui/questions.js:270
msgid "No" msgid "No"
@@ -651,23 +659,23 @@ msgstr "Žádný"
msgid "Selected only" msgid "Selected only"
msgstr "Pouze vybrané" msgstr "Pouze vybrané"
#: pretix/static/pretixcontrol/js/ui/main.js:794 #: pretix/static/pretixcontrol/js/ui/main.js:828
msgid "Enter page number between 1 and %(max)s." msgid "Enter page number between 1 and %(max)s."
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:797 #: pretix/static/pretixcontrol/js/ui/main.js:831
msgid "Invalid page number." msgid "Invalid page number."
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:955 #: pretix/static/pretixcontrol/js/ui/main.js:989
msgid "Use a different name internally" msgid "Use a different name internally"
msgstr "Interně používat jiný název" msgstr "Interně používat jiný název"
#: pretix/static/pretixcontrol/js/ui/main.js:995 #: pretix/static/pretixcontrol/js/ui/main.js:1029
msgid "Click to close" msgid "Click to close"
msgstr "Kliknutím zavřete" msgstr "Kliknutím zavřete"
#: pretix/static/pretixcontrol/js/ui/main.js:1070 #: pretix/static/pretixcontrol/js/ui/main.js:1104
msgid "You have unsaved changes!" msgid "You have unsaved changes!"
msgstr "Máte neuložené změny!" msgstr "Máte neuložené změny!"
File diff suppressed because it is too large Load Diff
+44 -36
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-23 12:41+0000\n" "POT-Creation-Date: 2024-06-24 08:20+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -131,7 +131,7 @@ msgid "Mercado Pago"
msgstr "" msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
msgid "Continue" msgid "Continue"
msgstr "" msgstr ""
@@ -236,97 +236,105 @@ msgid "Canceled"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
msgid "Redeemed" msgid "Confirmed"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
msgid "Cancel" msgid "Approval pending"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
msgid "Redeemed"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58 msgid "Cancel"
msgid "Ticket not paid"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
msgid "This ticket is not yet paid. Do you want to continue anyways?"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
msgid "Additional information required" #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
msgid "Ticket not paid"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
msgid "Valid ticket" msgid "This ticket is not yet paid. Do you want to continue anyways?"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
msgid "Exit recorded" msgid "Additional information required"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
msgid "Ticket already used" msgid "Valid ticket"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
msgid "Information required" msgid "Exit recorded"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
msgid "Unknown ticket" msgid "Ticket already used"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
msgid "Ticket type not allowed here" msgid "Information required"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
msgid "Unknown ticket"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
msgid "Entry not allowed" msgid "Ticket type not allowed here"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
msgid "Ticket code revoked/changed"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
msgid "Ticket blocked" msgid "Entry not allowed"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
msgid "Ticket not valid at this time" msgid "Ticket code revoked/changed"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
msgid "Order canceled" msgid "Ticket blocked"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
msgid "Ticket code is ambiguous on list" msgid "Ticket not valid at this time"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
msgid "Order not approved" msgid "Order canceled"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
msgid "Checked-in Tickets" msgid "Ticket code is ambiguous on list"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
msgid "Valid Tickets" msgid "Order not approved"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
msgid "Currently inside" msgid "Checked-in Tickets"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
msgid "Valid Tickets"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70
msgid "Currently inside"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:71
#: pretix/static/pretixcontrol/js/ui/question.js:137 #: pretix/static/pretixcontrol/js/ui/question.js:137
#: pretix/static/pretixpresale/js/ui/questions.js:270 #: pretix/static/pretixpresale/js/ui/questions.js:270
msgid "Yes" msgid "Yes"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:72
#: pretix/static/pretixcontrol/js/ui/question.js:138 #: pretix/static/pretixcontrol/js/ui/question.js:138
#: pretix/static/pretixpresale/js/ui/questions.js:270 #: pretix/static/pretixpresale/js/ui/questions.js:270
msgid "No" msgid "No"
@@ -628,23 +636,23 @@ msgstr ""
msgid "Selected only" msgid "Selected only"
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:794 #: pretix/static/pretixcontrol/js/ui/main.js:828
msgid "Enter page number between 1 and %(max)s." msgid "Enter page number between 1 and %(max)s."
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:797 #: pretix/static/pretixcontrol/js/ui/main.js:831
msgid "Invalid page number." msgid "Invalid page number."
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:955 #: pretix/static/pretixcontrol/js/ui/main.js:989
msgid "Use a different name internally" msgid "Use a different name internally"
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:995 #: pretix/static/pretixcontrol/js/ui/main.js:1029
msgid "Click to close" msgid "Click to close"
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1070 #: pretix/static/pretixcontrol/js/ui/main.js:1104
msgid "You have unsaved changes!" msgid "You have unsaved changes!"
msgstr "" msgstr ""
File diff suppressed because it is too large Load Diff
+41 -37
View File
@@ -6,9 +6,9 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-23 12:41+0000\n" "POT-Creation-Date: 2024-06-24 08:20+0000\n"
"PO-Revision-Date: 2022-12-01 17:00+0000\n" "PO-Revision-Date: 2024-05-30 17:00+0000\n"
"Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n" "Last-Translator: Nikolai <nikolai@lengefeldt.de>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/" "Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
"da/>\n" "da/>\n"
"Language: da\n" "Language: da\n"
@@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.14.2\n" "X-Generator: Weblate 5.5.5\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -132,7 +132,7 @@ msgid "Mercado Pago"
msgstr "" msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
#, fuzzy #, fuzzy
#| msgctxt "widget" #| msgctxt "widget"
#| msgid "Continue" #| msgid "Continue"
@@ -240,102 +240,110 @@ msgid "Canceled"
msgstr "Annulleret" msgstr "Annulleret"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
msgid "Confirmed"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
msgid "Approval pending"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
#, fuzzy #, fuzzy
#| msgctxt "widget" #| msgctxt "widget"
#| msgid "Redeem" #| msgid "Redeem"
msgid "Redeemed" msgid "Redeemed"
msgstr "Indløs" msgstr "Indløs"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
msgid "Ticket not paid" msgid "Ticket not paid"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
msgid "This ticket is not yet paid. Do you want to continue anyways?" msgid "This ticket is not yet paid. Do you want to continue anyways?"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
msgid "Additional information required" msgid "Additional information required"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
msgid "Valid ticket" msgid "Valid ticket"
msgstr "Gyldig billet" msgstr "Gyldig billet"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
msgid "Exit recorded" msgid "Exit recorded"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
msgid "Ticket already used" msgid "Ticket already used"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
msgid "Information required" msgid "Information required"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
msgid "Unknown ticket" msgid "Unknown ticket"
msgstr "Ukendt billet" msgstr "Ukendt billet"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
msgid "Ticket type not allowed here" msgid "Ticket type not allowed here"
msgstr "Billettype er ikke tilladt her" msgstr "Billettype er ikke tilladt her"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
msgid "Entry not allowed" msgid "Entry not allowed"
msgstr "Adgang ikke tilladt" msgstr "Adgang ikke tilladt"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
msgid "Ticket code revoked/changed" msgid "Ticket code revoked/changed"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
msgid "Ticket blocked" msgid "Ticket blocked"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
msgid "Ticket not valid at this time" msgid "Ticket not valid at this time"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
msgid "Order canceled" msgid "Order canceled"
msgstr "Bestilling annulleret" msgstr "Bestilling annulleret"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
msgid "Ticket code is ambiguous on list" msgid "Ticket code is ambiguous on list"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
msgid "Order not approved" msgid "Order not approved"
msgstr "" msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
#, fuzzy #, fuzzy
#| msgid "Check-in QR" #| msgid "Check-in QR"
msgid "Checked-in Tickets" msgid "Checked-in Tickets"
msgstr "Check-in QR" msgstr "Check-in QR"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
msgid "Valid Tickets" msgid "Valid Tickets"
msgstr "Gyldige billetter" msgstr "Gyldige billetter"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70
msgid "Currently inside" msgid "Currently inside"
msgstr "Inde i øjeblikket" msgstr "Inde i øjeblikket"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:71
#: pretix/static/pretixcontrol/js/ui/question.js:137 #: pretix/static/pretixcontrol/js/ui/question.js:137
#: pretix/static/pretixpresale/js/ui/questions.js:270 #: pretix/static/pretixpresale/js/ui/questions.js:270
msgid "Yes" msgid "Yes"
msgstr "Ja" msgstr "Ja"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:72
#: pretix/static/pretixcontrol/js/ui/question.js:138 #: pretix/static/pretixcontrol/js/ui/question.js:138
#: pretix/static/pretixpresale/js/ui/questions.js:270 #: pretix/static/pretixpresale/js/ui/questions.js:270
msgid "No" msgid "No"
@@ -671,23 +679,23 @@ msgstr "Ingen"
msgid "Selected only" msgid "Selected only"
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:794 #: pretix/static/pretixcontrol/js/ui/main.js:828
msgid "Enter page number between 1 and %(max)s." msgid "Enter page number between 1 and %(max)s."
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:797 #: pretix/static/pretixcontrol/js/ui/main.js:831
msgid "Invalid page number." msgid "Invalid page number."
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:955 #: pretix/static/pretixcontrol/js/ui/main.js:989
msgid "Use a different name internally" msgid "Use a different name internally"
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:995 #: pretix/static/pretixcontrol/js/ui/main.js:1029
msgid "Click to close" msgid "Click to close"
msgstr "Klik for at lukke" msgstr "Klik for at lukke"
#: pretix/static/pretixcontrol/js/ui/main.js:1070 #: pretix/static/pretixcontrol/js/ui/main.js:1104
msgid "You have unsaved changes!" msgid "You have unsaved changes!"
msgstr "Du har ændringer, der ikke er gemt!" msgstr "Du har ændringer, der ikke er gemt!"
@@ -724,10 +732,6 @@ msgid "Cart expired"
msgstr "Kurv udløbet" msgstr "Kurv udløbet"
#: pretix/static/pretixpresale/js/ui/cart.js:50 #: pretix/static/pretixpresale/js/ui/cart.js:50
#, fuzzy
#| msgid "The items in your cart are reserved for you for one minute."
#| msgid_plural ""
#| "The items in your cart are reserved for you for {num} minutes."
msgid "The items in your cart are reserved for you for one minute." msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes." 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[0] "Varerne i din kurv er reserveret for dig i et minut."
File diff suppressed because it is too large Load Diff
+44 -36
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-23 12:41+0000\n" "POT-Creation-Date: 2024-06-24 08:20+0000\n"
"PO-Revision-Date: 2024-03-15 18:00+0000\n" "PO-Revision-Date: 2024-06-20 15:00+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n" "Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/" "Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
"de/>\n" "de/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.4.2\n" "X-Generator: Weblate 5.5.5\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -131,7 +131,7 @@ msgid "Mercado Pago"
msgstr "Mercado Pago" msgstr "Mercado Pago"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
msgid "Continue" msgid "Continue"
msgstr "Fortfahren" msgstr "Fortfahren"
@@ -236,97 +236,105 @@ msgid "Canceled"
msgstr "storniert" msgstr "storniert"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
msgid "Confirmed"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
msgid "Approval pending"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
msgid "Redeemed" msgid "Redeemed"
msgstr "Eingelöst" msgstr "Eingelöst"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
msgid "Cancel" msgid "Cancel"
msgstr "Abbrechen" msgstr "Abbrechen"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
msgid "Ticket not paid" msgid "Ticket not paid"
msgstr "Ticket nicht bezahlt" msgstr "Ticket nicht bezahlt"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
msgid "This ticket is not yet paid. Do you want to continue anyways?" msgid "This ticket is not yet paid. Do you want to continue anyways?"
msgstr "Dieses Ticket ist noch nicht bezahlt. Möchten Sie dennoch fortfahren?" msgstr "Dieses Ticket ist noch nicht bezahlt. Möchten Sie dennoch fortfahren?"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
msgid "Additional information required" msgid "Additional information required"
msgstr "Zusätzliche Informationen benötigt" msgstr "Zusätzliche Informationen benötigt"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
msgid "Valid ticket" msgid "Valid ticket"
msgstr "Gültiges Ticket" msgstr "Gültiges Ticket"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
msgid "Exit recorded" msgid "Exit recorded"
msgstr "Ausgang gespeichert" msgstr "Ausgang gespeichert"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
msgid "Ticket already used" msgid "Ticket already used"
msgstr "Ticket bereits eingelöst" msgstr "Ticket bereits eingelöst"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
msgid "Information required" msgid "Information required"
msgstr "Infos benötigt" msgstr "Infos benötigt"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
msgid "Unknown ticket" msgid "Unknown ticket"
msgstr "Unbekanntes Ticket" msgstr "Unbekanntes Ticket"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
msgid "Ticket type not allowed here" msgid "Ticket type not allowed here"
msgstr "Ticketart hier nicht erlaubt" msgstr "Ticketart hier nicht erlaubt"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
msgid "Entry not allowed" msgid "Entry not allowed"
msgstr "Eingang nicht erlaubt" msgstr "Eingang nicht erlaubt"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
msgid "Ticket code revoked/changed" msgid "Ticket code revoked/changed"
msgstr "Ticket-Code gesperrt/geändert" msgstr "Ticket-Code gesperrt/geändert"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
msgid "Ticket blocked" msgid "Ticket blocked"
msgstr "Ticket gesperrt" msgstr "Ticket gesperrt"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
msgid "Ticket not valid at this time" msgid "Ticket not valid at this time"
msgstr "Ticket aktuell nicht gültig" msgstr "Ticket aktuell nicht gültig"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
msgid "Order canceled" msgid "Order canceled"
msgstr "Bestellung storniert" msgstr "Bestellung storniert"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
msgid "Ticket code is ambiguous on list" msgid "Ticket code is ambiguous on list"
msgstr "Ticket-Code ist nicht eindeutig auf der Liste" msgstr "Ticket-Code ist nicht eindeutig auf der Liste"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
msgid "Order not approved" msgid "Order not approved"
msgstr "Bestellung nicht freigegeben" msgstr "Bestellung nicht freigegeben"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
msgid "Checked-in Tickets" msgid "Checked-in Tickets"
msgstr "Eingecheckte Tickets" msgstr "Eingecheckte Tickets"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
msgid "Valid Tickets" msgid "Valid Tickets"
msgstr "Gültige Tickets" msgstr "Gültige Tickets"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70
msgid "Currently inside" msgid "Currently inside"
msgstr "Derzeit anwesend" msgstr "Derzeit anwesend"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:71
#: pretix/static/pretixcontrol/js/ui/question.js:137 #: pretix/static/pretixcontrol/js/ui/question.js:137
#: pretix/static/pretixpresale/js/ui/questions.js:270 #: pretix/static/pretixpresale/js/ui/questions.js:270
msgid "Yes" msgid "Yes"
msgstr "Ja" msgstr "Ja"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:72
#: pretix/static/pretixcontrol/js/ui/question.js:138 #: pretix/static/pretixcontrol/js/ui/question.js:138
#: pretix/static/pretixpresale/js/ui/questions.js:270 #: pretix/static/pretixpresale/js/ui/questions.js:270
msgid "No" msgid "No"
@@ -435,7 +443,7 @@ msgstr "ist nach"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:40 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:40
msgid "=" msgid "="
msgstr "" msgstr "="
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:99 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:99
msgid "Product" msgid "Product"
@@ -459,7 +467,7 @@ msgstr "Aktueller Tag der Woche (1 = Montag, 7 = Sonntag)"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:119 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:119
msgid "Current entry status" msgid "Current entry status"
msgstr "" msgstr "Aktueller Zutrittsstatus"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:123 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:123
msgid "Number of previous entries" msgid "Number of previous entries"
@@ -544,12 +552,12 @@ msgstr "Duplizieren"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:193 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:193
msgctxt "entry_status" msgctxt "entry_status"
msgid "present" msgid "present"
msgstr "" msgstr "anwesend"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:194 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:194
msgctxt "entry_status" msgctxt "entry_status"
msgid "absent" msgid "absent"
msgstr "" msgstr "abwesend"
#: pretix/static/pretixcontrol/js/ui/editor.js:72 #: pretix/static/pretixcontrol/js/ui/editor.js:72
msgid "Check-in QR" msgid "Check-in QR"
@@ -649,23 +657,23 @@ msgstr "Keine"
msgid "Selected only" msgid "Selected only"
msgstr "Nur ausgewählte" msgstr "Nur ausgewählte"
#: pretix/static/pretixcontrol/js/ui/main.js:794 #: pretix/static/pretixcontrol/js/ui/main.js:828
msgid "Enter page number between 1 and %(max)s." msgid "Enter page number between 1 and %(max)s."
msgstr "Geben Sie eine Seitenzahl zwischen 1 und %(max)s ein." msgstr "Geben Sie eine Seitenzahl zwischen 1 und %(max)s ein."
#: pretix/static/pretixcontrol/js/ui/main.js:797 #: pretix/static/pretixcontrol/js/ui/main.js:831
msgid "Invalid page number." msgid "Invalid page number."
msgstr "Ungültige Seitenzahl." msgstr "Ungültige Seitenzahl."
#: pretix/static/pretixcontrol/js/ui/main.js:955 #: pretix/static/pretixcontrol/js/ui/main.js:989
msgid "Use a different name internally" msgid "Use a different name internally"
msgstr "Intern einen anderen Namen verwenden" msgstr "Intern einen anderen Namen verwenden"
#: pretix/static/pretixcontrol/js/ui/main.js:995 #: pretix/static/pretixcontrol/js/ui/main.js:1029
msgid "Click to close" msgid "Click to close"
msgstr "Klicken zum Schließen" msgstr "Klicken zum Schließen"
#: pretix/static/pretixcontrol/js/ui/main.js:1070 #: pretix/static/pretixcontrol/js/ui/main.js:1104
msgid "You have unsaved changes!" msgid "You have unsaved changes!"
msgstr "Sie haben ungespeicherte Änderungen!" msgstr "Sie haben ungespeicherte Änderungen!"
+3
View File
@@ -33,6 +33,7 @@ Backend
Badge Badge
Badges Badges
Bancontact Bancontact
BankID
Banking Banking
barcodes barcodes
Bcc Bcc
@@ -301,6 +302,7 @@ Strg
Stripe Stripe
Stripes Stripes
Strong Strong
Swish
systemweiten systemweiten
Tab Tab
tag tag
@@ -318,6 +320,7 @@ Tracking
transaktionale transaktionale
Trustly Trustly
Turnover Turnover
TWINT
txt txt
überbuchen überbuchen
überbucht überbucht
File diff suppressed because it is too large Load Diff
@@ -7,8 +7,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-23 12:41+0000\n" "POT-Creation-Date: 2024-06-24 08:20+0000\n"
"PO-Revision-Date: 2024-03-15 18:00+0000\n" "PO-Revision-Date: 2024-06-20 15:00+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n" "Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German (informal) <https://translate.pretix.eu/projects/" "Language-Team: German (informal) <https://translate.pretix.eu/projects/"
"pretix/pretix-js/de_Informal/>\n" "pretix/pretix-js/de_Informal/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.4.2\n" "X-Generator: Weblate 5.5.5\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -131,7 +131,7 @@ msgid "Mercado Pago"
msgstr "Mercado Pago" msgstr "Mercado Pago"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
msgid "Continue" msgid "Continue"
msgstr "Fortfahren" msgstr "Fortfahren"
@@ -236,97 +236,105 @@ msgid "Canceled"
msgstr "storniert" msgstr "storniert"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
msgid "Confirmed"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
msgid "Approval pending"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
msgid "Redeemed" msgid "Redeemed"
msgstr "Eingelöst" msgstr "Eingelöst"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
msgid "Cancel" msgid "Cancel"
msgstr "Abbrechen" msgstr "Abbrechen"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
msgid "Ticket not paid" msgid "Ticket not paid"
msgstr "Ticket nicht bezahlt" msgstr "Ticket nicht bezahlt"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
msgid "This ticket is not yet paid. Do you want to continue anyways?" msgid "This ticket is not yet paid. Do you want to continue anyways?"
msgstr "Dieses Ticket ist noch nicht bezahlt. Möchtest du dennoch fortfahren?" msgstr "Dieses Ticket ist noch nicht bezahlt. Möchtest du dennoch fortfahren?"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
msgid "Additional information required" msgid "Additional information required"
msgstr "Zusätzliche Informationen benötigt" msgstr "Zusätzliche Informationen benötigt"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
msgid "Valid ticket" msgid "Valid ticket"
msgstr "Gültiges Ticket" msgstr "Gültiges Ticket"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
msgid "Exit recorded" msgid "Exit recorded"
msgstr "Ausgang gespeichert" msgstr "Ausgang gespeichert"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
msgid "Ticket already used" msgid "Ticket already used"
msgstr "Ticket bereits eingelöst" msgstr "Ticket bereits eingelöst"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
msgid "Information required" msgid "Information required"
msgstr "Infos benötigt" msgstr "Infos benötigt"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
msgid "Unknown ticket" msgid "Unknown ticket"
msgstr "Unbekanntes Ticket" msgstr "Unbekanntes Ticket"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
msgid "Ticket type not allowed here" msgid "Ticket type not allowed here"
msgstr "Ticketart hier nicht erlaubt" msgstr "Ticketart hier nicht erlaubt"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
msgid "Entry not allowed" msgid "Entry not allowed"
msgstr "Eingang nicht erlaubt" msgstr "Eingang nicht erlaubt"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
msgid "Ticket code revoked/changed" msgid "Ticket code revoked/changed"
msgstr "Ticket-Code gesperrt/geändert" msgstr "Ticket-Code gesperrt/geändert"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
msgid "Ticket blocked" msgid "Ticket blocked"
msgstr "Ticket gesperrt" msgstr "Ticket gesperrt"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
msgid "Ticket not valid at this time" msgid "Ticket not valid at this time"
msgstr "Ticket aktuell nicht gültig" msgstr "Ticket aktuell nicht gültig"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
msgid "Order canceled" msgid "Order canceled"
msgstr "Bestellung storniert" msgstr "Bestellung storniert"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
msgid "Ticket code is ambiguous on list" msgid "Ticket code is ambiguous on list"
msgstr "Ticket-Code ist nicht eindeutig auf der Liste" msgstr "Ticket-Code ist nicht eindeutig auf der Liste"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
msgid "Order not approved" msgid "Order not approved"
msgstr "Bestellung nicht freigegeben" msgstr "Bestellung nicht freigegeben"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
msgid "Checked-in Tickets" msgid "Checked-in Tickets"
msgstr "Eingecheckte Tickets" msgstr "Eingecheckte Tickets"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
msgid "Valid Tickets" msgid "Valid Tickets"
msgstr "Gültige Tickets" msgstr "Gültige Tickets"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70
msgid "Currently inside" msgid "Currently inside"
msgstr "Derzeit anwesend" msgstr "Derzeit anwesend"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:71
#: pretix/static/pretixcontrol/js/ui/question.js:137 #: pretix/static/pretixcontrol/js/ui/question.js:137
#: pretix/static/pretixpresale/js/ui/questions.js:270 #: pretix/static/pretixpresale/js/ui/questions.js:270
msgid "Yes" msgid "Yes"
msgstr "Ja" msgstr "Ja"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:72
#: pretix/static/pretixcontrol/js/ui/question.js:138 #: pretix/static/pretixcontrol/js/ui/question.js:138
#: pretix/static/pretixpresale/js/ui/questions.js:270 #: pretix/static/pretixpresale/js/ui/questions.js:270
msgid "No" msgid "No"
@@ -434,7 +442,7 @@ msgstr "ist nach"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:40 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:40
msgid "=" msgid "="
msgstr "" msgstr "="
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:99 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:99
msgid "Product" msgid "Product"
@@ -458,7 +466,7 @@ msgstr "Aktueller Wochentag (1 = Montag, 7 = Sonntag)"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:119 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:119
msgid "Current entry status" msgid "Current entry status"
msgstr "" msgstr "Aktueller Zutrittsstatus"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:123 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:123
msgid "Number of previous entries" msgid "Number of previous entries"
@@ -543,12 +551,12 @@ msgstr "Duplizieren"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:193 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:193
msgctxt "entry_status" msgctxt "entry_status"
msgid "present" msgid "present"
msgstr "" msgstr "anwesend"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:194 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:194
msgctxt "entry_status" msgctxt "entry_status"
msgid "absent" msgid "absent"
msgstr "" msgstr "abwesend"
#: pretix/static/pretixcontrol/js/ui/editor.js:72 #: pretix/static/pretixcontrol/js/ui/editor.js:72
msgid "Check-in QR" msgid "Check-in QR"
@@ -648,23 +656,23 @@ msgstr "Keine"
msgid "Selected only" msgid "Selected only"
msgstr "Nur ausgewählte" msgstr "Nur ausgewählte"
#: pretix/static/pretixcontrol/js/ui/main.js:794 #: pretix/static/pretixcontrol/js/ui/main.js:828
msgid "Enter page number between 1 and %(max)s." msgid "Enter page number between 1 and %(max)s."
msgstr "Gib eine Seitenzahl zwischen 1 und %(max)s ein." msgstr "Gib eine Seitenzahl zwischen 1 und %(max)s ein."
#: pretix/static/pretixcontrol/js/ui/main.js:797 #: pretix/static/pretixcontrol/js/ui/main.js:831
msgid "Invalid page number." msgid "Invalid page number."
msgstr "Ungültige Seitenzahl." msgstr "Ungültige Seitenzahl."
#: pretix/static/pretixcontrol/js/ui/main.js:955 #: pretix/static/pretixcontrol/js/ui/main.js:989
msgid "Use a different name internally" msgid "Use a different name internally"
msgstr "Intern einen anderen Namen verwenden" msgstr "Intern einen anderen Namen verwenden"
#: pretix/static/pretixcontrol/js/ui/main.js:995 #: pretix/static/pretixcontrol/js/ui/main.js:1029
msgid "Click to close" msgid "Click to close"
msgstr "Klicken zum Schließen" msgstr "Klicken zum Schließen"
#: pretix/static/pretixcontrol/js/ui/main.js:1070 #: pretix/static/pretixcontrol/js/ui/main.js:1104
msgid "You have unsaved changes!" msgid "You have unsaved changes!"
msgstr "Du hast ungespeicherte Änderungen!" msgstr "Du hast ungespeicherte Änderungen!"
@@ -33,6 +33,7 @@ Backend
Badge Badge
Badges Badges
Bancontact Bancontact
BankID
Banking Banking
barcodes barcodes
Bcc Bcc
@@ -301,6 +302,7 @@ Strg
Stripe Stripe
Stripes Stripes
Strong Strong
Swish
systemweiten systemweiten
Tab Tab
tag tag
@@ -318,6 +320,7 @@ Tracking
transaktionale transaktionale
Trustly Trustly
Turnover Turnover
TWINT
txt txt
überbuchen überbuchen
überbucht überbucht
+2539 -2255
View File
File diff suppressed because it is too large Load Diff

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