Compare commits

...

257 Commits

Author SHA1 Message Date
Raphael Michel
18ce0b3446 Bump to 2023.7.2 2023-09-12 11:50:22 +02:00
Raphael Michel
8583bfb7d9 [SECURITY] Do not allow Pillow to parse EPS files 2023-09-12 11:50:20 +02:00
Raphael Michel
34d1d3fa6e Bump to 2023.7.1 2023-09-11 09:58:10 +02:00
Raphael Michel
ccdce2ccb8 Fix incorrect handling of boolean configuration flags 2023-09-11 09:57:37 +02:00
Raphael Michel
6d834762c4 Bump to 2023.7.0 2023-07-28 09:29:07 +02:00
Raphael Michel
4f1e9a31c6 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5400 of 5400 strings)

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

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

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

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

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

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

* Add a .

* Update doc/development/nfc/uid.rst

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

powered by weblate
2023-06-23 16:05:46 +02:00
Raphael Michel
206b57adfd Revert "Markdown: Allow to escape domain name"
This reverts commit b7f3f7a7a1.
2023-06-23 15:32:16 +02:00
Raphael Michel
b7f3f7a7a1 Markdown: Allow to escape domain name 2023-06-23 15:32:00 +02:00
Raphael Michel
34e7a0fc31 PDF renderer: Fix crash while embedding iamge (PRETIXEU-8MY) 2023-06-23 11:51:23 +02:00
Raphael Michel
cc7f249cb8 Fix crash if a tax rule on a fee prevents sale (PRETIXEU-8MZ) 2023-06-23 11:49:09 +02:00
Raphael Michel
147061eaa4 Fix issue in middleware after organizer deletion (PRETIXEU-8N3) 2023-06-23 11:25:55 +02:00
Raphael Michel
c16491889b CSS generation: Compress cached result with gzip to save redis memory 2023-06-22 12:35:34 +02:00
Raphael Michel
1eb1d8df5f Check-in export: Fix filter options 2023-06-22 09:04:05 +02:00
Raphael Michel
3f47cf785c Teams: Allow admin user to delete the last team 2023-06-21 16:51:53 +02:00
Raphael Michel
e8859cb2e2 Bank transfer: Fix reference missing for non-SEPA accounts 2023-06-21 15:25:04 +02:00
Raphael Michel
61ab6f729d Add webhooks for waiting list events (#3423)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-06-21 14:17:41 +02:00
Raphael Michel
79c9ba3cf3 Check-in list export: ALlow to filter by status (#3424) 2023-06-21 14:03:37 +02:00
Raphael Michel
1d86f7a0c3 Bank transfer: Do not use <pre> for bank details in emails (#3413) 2023-06-19 12:45:14 +02:00
Yucheng Lin
e259b3994a Translations: Update Chinese (Traditional)
Currently translated at 78.6% (4210 of 5353 strings)

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

powered by weblate
2023-06-19 11:42:11 +02:00
Ronan LE MEILLAT
18e97624fd Translations: Update French
Currently translated at 49.5% (2652 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 64.9% (137 of 211 strings)

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

powered by weblate

Translations: Update French

Currently translated at 64.9% (137 of 211 strings)

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

powered by weblate

Translations: Update French

Currently translated at 49.5% (2653 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 71.5% (151 of 211 strings)

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

powered by weblate

Translations: Update French

Currently translated at 71.5% (151 of 211 strings)

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

powered by weblate

Translations: Update French

Currently translated at 49.5% (2655 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 86.7% (183 of 211 strings)

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

powered by weblate

Translations: Update French

Currently translated at 86.7% (183 of 211 strings)

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

powered by weblate

Translations: Update French

Currently translated at 90.9% (192 of 211 strings)

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

powered by weblate

Translations: Update French

Currently translated at 90.9% (192 of 211 strings)

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

powered by weblate

Translations: Update French

Currently translated at 49.6% (2658 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 93.8% (198 of 211 strings)

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

powered by weblate

Translations: Update French

Currently translated at 93.8% (198 of 211 strings)

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

powered by weblate

Translations: Update French

Currently translated at 49.8% (2666 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 100.0% (211 of 211 strings)

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

powered by weblate

Translations: Update French

Currently translated at 50.4% (2699 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 52.3% (2800 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 52.5% (2811 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 52.5% (2811 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 52.5% (2811 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 52.5% (2811 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 54.5% (2920 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 55.3% (2963 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 57.4% (3077 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 57.9% (3102 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 57.9% (3102 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 60.2% (3225 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 61.0% (3269 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 61.2% (3281 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 61.9% (3316 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 62.6% (3353 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 63.6% (3405 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 63.8% (3420 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 66.7% (3572 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 69.1% (3703 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 71.2% (3812 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 71.9% (3851 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 72.5% (3882 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 73.7% (3946 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 73.7% (3947 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 90.3% (4839 of 5353 strings)

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

powered by weblate

Translations: Update French

Currently translated at 90.3% (4839 of 5353 strings)

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

powered by weblate
2023-06-19 11:42:11 +02:00
Raphael Michel
1c9a245231 Extend wordlist 2023-06-19 11:31:25 +02:00
Raphael Michel
b51ca58820 Add BaseExporter.available_for_user() 2023-06-16 17:35:36 +02:00
Raphael Michel
7a48cac862 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2023-06-16 16:35:40 +02:00
Raphael Michel
1bdcc4580e Quick setup: Fix translation of default values 2023-06-16 16:04:25 +02:00
Raphael Michel
dd10bdd433 Shredder: Fix redirect to broken page on error 2023-06-16 15:58:38 +02:00
Raphael Michel
f7a74c2e74 Simple email layout: Remove margin of last paragraph 2023-06-16 15:46:29 +02:00
Raphael Michel
4037e1886d Mail settings: Fix missing texts for preview 2023-06-16 15:42:21 +02:00
Raphael Michel
c4ae363fdb Use hard line breaks in all default email texts 2023-06-16 15:38:46 +02:00
Raphael Michel
3df64a46e7 Rich text: Support intentional newlines in emails 2023-06-16 15:16:20 +02:00
Raphael Michel
69502986ad Email renderers: Allow line breaks in <pre> 2023-06-16 15:05:59 +02:00
Raphael Michel
51ea63335c Email renderers: Unify some CSS details 2023-06-16 15:05:57 +02:00
Raphael Michel
dc76b554f8 Simple email layout: Add missing line 2023-06-16 14:57:05 +02:00
Raphael Michel
f8be8296dd Gift cards: Improved support for cross-organizer acceptance (#3311)
Co-authored-by: Martin Gross <martin@pc-coholic.de>
2023-06-15 14:17:40 +02:00
Yucheng Lin
b3c917925c Translations: Update Chinese (Traditional)
Currently translated at 78.1% (4156 of 5319 strings)

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

powered by weblate
2023-06-15 13:48:35 +02:00
Yucheng Lin
4954373a04 Translations: Update Chinese (Traditional)
Currently translated at 78.0% (4151 of 5319 strings)

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

powered by weblate
2023-06-15 13:48:35 +02:00
Yucheng Lin
5571ec3858 Translations: Update Chinese (Traditional)
Currently translated at 77.9% (4146 of 5319 strings)

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

powered by weblate
2023-06-15 13:48:35 +02:00
hmontheline
9ef3139905 Translations: Update Spanish
Currently translated at 55.4% (2951 of 5319 strings)

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

powered by weblate
2023-06-15 13:48:35 +02:00
Fabian
3139b9fe6f Docs: update requirements and links 2023-06-15 13:21:22 +02:00
Martin Gross
437d33ba79 Expose SubEvent-PK in SubEvent Overview List (#3410) 2023-06-15 10:57:53 +02:00
Raphael Michel
0a9890b1b0 Transaction list export: Add count * price column 2023-06-14 11:52:36 +02:00
Raphael Michel
1420ad43db Grammar fix in backend warning message 2023-06-13 22:06:18 +02:00
Raphael Michel
30da7a6429 Order expert search: Allow to filter by check-in/check-out 2023-06-13 21:56:47 +02:00
Raphael Michel
a2f3dcce02 Do not allow to generate invoice for expired or canceled order 2023-06-13 15:56:18 +02:00
Raphael Michel
41f5ca3f9d Translations: Update Chinese (Traditional)
Currently translated at 77.9% (4145 of 5319 strings)

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

powered by weblate
2023-06-13 15:07:37 +02:00
Yucheng Lin
817f1e0371 Translations: Update Chinese (Traditional)
Currently translated at 77.9% (4145 of 5319 strings)

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

powered by weblate
2023-06-13 15:07:37 +02:00
Martin Gross
35fc001768 Add binary_file to SettingsSandbox get() (#3407) 2023-06-13 14:58:36 +02:00
Raphael Michel
002416e435 Add check-in simulator (#3380) 2023-06-13 14:57:24 +02:00
dependabot[bot]
4917249bab Update requests requirement from ==2.30.* to ==2.31.* (#3399)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-12 10:29:29 +02:00
Martin Gross
afd2468375 Add ePayBL documentation (#3397) 2023-06-12 10:29:06 +02:00
Raphael Michel
54d06dd7f8 Customer accounts: Validate duplicate identifier 2023-06-12 10:23:22 +02:00
Raphael Michel
5e59844cf5 Fix incorrect directory check 2023-06-12 10:13:49 +02:00
Raphael Michel
0d2a981674 Add dependency on pretix-plugin-build to avoid trouble 2023-06-12 09:38:17 +02:00
Raphael Michel
943aeaa31f Do not run custom build commands on other packages 2023-06-12 09:34:56 +02:00
Raphael Michel
cfe0f67f0d API: Allow to run exporter without events 2023-06-09 16:01:47 +02:00
Raphael Michel
635bb94cc4 API: Add date range filters for events and subevents 2023-06-09 15:20:53 +02:00
Raphael Michel
cf732ce173 Event dashboard: Make comment text box larger 2023-06-09 13:33:47 +02:00
Richard Schreiber
74e9a4ad2d API: add log_action/webhook for confirmed payments (#3395) 2023-06-09 09:29:32 +02:00
Raphael Michel
570357e9be Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5319 of 5319 strings)

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

powered by weblate
2023-06-07 18:04:43 +02:00
Raphael Michel
473375d4ae Translations: Update German
Currently translated at 100.0% (5319 of 5319 strings)

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

powered by weblate
2023-06-07 18:04:43 +02:00
Raphael Michel
a78b698520 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2023-06-07 17:41:44 +02:00
Thomas Vranken
332c968294 Translations: Update Dutch
Currently translated at 85.4% (4539 of 5314 strings)

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

powered by weblate
2023-06-07 17:05:02 +02:00
Raphael Michel
ad12c344c5 Translations: Add Lithuanian 2023-06-07 17:05:02 +02:00
dependabot[bot]
91c0db1ac0 Update pyjwt requirement from ==2.6.* to ==2.7.* (#3394)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-07 17:04:43 +02:00
Raphael Michel
4d231b70aa Accounting report: Fix hardcoded currency 2023-06-07 17:03:45 +02:00
Raphael Michel
ab2f6f6bed Accounting report: Allow to split by subevent, introduce sum by event 2023-06-07 16:45:28 +02:00
Richard Schreiber
28458f7b85 Cart: fix single-select checkbox button initial checked-state 2023-06-07 14:30:16 +02:00
Raphael Michel
50ff968c17 Fix #3391 -- Don't crash on GeoIP lookup failure 2023-06-06 17:12:38 +02:00
Richard Schreiber
0b4064f14f Fix: use format_lazy for formatted translation in settings (#3390) 2023-06-06 14:56:30 +02:00
Richard Schreiber
1897bd4b26 Cart: make single-select checkbox look like a button 2023-06-06 08:53:35 +02:00
dependabot[bot]
fd6843822b Update pytest-xdist requirement from ==3.2.* to ==3.3.* (#3388)
Updates the requirements on [pytest-xdist](https://github.com/pytest-dev/pytest-xdist) to permit the latest version.
- [Changelog](https://github.com/pytest-dev/pytest-xdist/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-xdist/compare/v3.2.0...v3.3.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>
2023-06-05 22:33:09 +02:00
Raphael Michel
ee1644e037 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5314 of 5314 strings)

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

powered by weblate
2023-06-05 18:34:11 +02:00
Raphael Michel
a6c1486650 Translations: Update German
Currently translated at 100.0% (5314 of 5314 strings)

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

powered by weblate
2023-06-05 18:34:11 +02:00
Raphael Michel
f4b437e92b Remove MariaDB support (#3381) 2023-06-05 18:25:20 +02:00
Raphael Michel
446c55dc89 Silence deprecation warning caused by pycountry 2023-06-05 18:24:57 +02:00
Raphael Michel
0990eeeea0 Fix deprecation warning 2023-06-05 18:24:51 +02:00
Raphael Michel
591fe23a99 Invoices: Fix timezone when calculating date of cancellation 2023-06-05 15:49:39 +02:00
Raphael Michel
ad70765287 Fix event creation after Django 4.1 upgrade 2023-06-05 13:00:32 +02:00
Richard Schreiber
c59d29493c Checkout: Hide empty add-on forms and show seat above add-on form 2023-06-05 10:08:47 +02:00
Raphael Michel
bd32b33ba9 Bump Django to 4.1.* (#2989) 2023-06-05 09:56:31 +02:00
Raphael Michel
3a8556bb78 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5314 of 5314 strings)

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

powered by weblate
2023-06-05 09:35:44 +02:00
Raphael Michel
c972d24ce7 Translations: Update German
Currently translated at 100.0% (5314 of 5314 strings)

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

powered by weblate
2023-06-05 09:35:44 +02:00
Yucheng Lin
647e68ef01 Translations: Update Chinese (Traditional)
Currently translated at 63.7% (3388 of 5314 strings)

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

powered by weblate
2023-06-05 09:35:44 +02:00
Yucheng Lin
f439a591df Translations: Update Chinese (Traditional)
Currently translated at 63.6% (3385 of 5314 strings)

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

powered by weblate
2023-06-05 09:35:44 +02:00
Richard Schreiber
8f17b338d1 Replace deprecated pypdf.PdfMerger with pypdf.PdfWriter (#3383) 2023-06-05 09:32:03 +02:00
Raphael Michel
35350a13d6 Fix #3360 -- Allow to revoke devices before initialized 2023-06-04 18:06:00 +02:00
Raphael Michel
0d93f7f52f Fix crash in name rendering (PRETIXEU-8GS) 2023-06-03 21:49:14 +02:00
dependabot[bot]
170dcf93e7 Update pypdf requirement from ==3.8.* to ==3.9.* (#3377)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-02 21:26:17 +02:00
dependabot[bot]
9319202213 Bump @babel/core from 7.21.5 to 7.22.1 in /src/pretix/static/npm_dir (#3373)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-02 21:25:26 +02:00
dependabot[bot]
bfd0eee2c1 Update mt-940 requirement from ==4.23.* to ==4.30.* (#3345)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
2023-06-02 21:25:15 +02:00
dependabot[bot]
8570f53ed0 Update django-otp requirement from ==1.1.* to ==1.2.* (#3338)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-02 20:08:56 +02:00
Raphael Michel
f56f6dd628 Voucher: Add link to order in voucher history 2023-06-02 20:07:12 +02:00
Richard Schreiber
413fabd821 Product list: add border to disabled spinner buttons (#3359) 2023-06-02 20:04:42 +02:00
Julian Rother
9813e59210 API: Fix crash when creating addons with order change endpoint (#3363) 2023-06-02 20:00:40 +02:00
Richard Schreiber
d91d942eac Invoicing: Add order-code to organizer CC mail (Z#23123051) (#3370) 2023-06-02 19:59:31 +02:00
dependabot[bot]
22104f79bd Bump @rollup/plugin-node-resolve from 15.0.2 to 15.1.0 in /src/pretix/static/npm_dir (#3374)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-02 19:59:12 +02:00
dependabot[bot]
f289ad9e4f Bump @babel/preset-env from 7.21.5 to 7.22.4 in /src/pretix/static/npm_dir (#3375)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-02 19:59:00 +02:00
Raphael Michel
f81a734716 Translations: Update Chinese (Traditional)
Currently translated at 63.6% (3384 of 5314 strings)

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

powered by weblate
2023-06-02 19:55:04 +02:00
Raphael Michel
7a27a42e79 Translations: Update Chinese (Traditional)
Currently translated at 63.6% (3384 of 5314 strings)

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

powered by weblate
2023-06-02 19:55:04 +02:00
Yucheng Lin
65a2bab9bb Translations: Update Chinese (Traditional)
Currently translated at 63.6% (3384 of 5314 strings)

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

powered by weblate
2023-06-02 19:55:04 +02:00
Yucheng Lin
a26f46b619 Translations: Update Chinese (Traditional)
Currently translated at 63.2% (3359 of 5314 strings)

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

powered by weblate
2023-06-02 19:55:04 +02:00
Hans Fraiponts
5c37c85415 Translations: Update Dutch
Currently translated at 85.2% (4531 of 5314 strings)

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

powered by weblate
2023-06-02 19:55:04 +02:00
Yucheng Lin
8ddba36690 Translations: Update Chinese (Traditional)
Currently translated at 61.6% (3275 of 5314 strings)

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

powered by weblate
2023-06-02 19:55:04 +02:00
Thomas Vranken
f9bf05e09b Translations: Update Dutch
Currently translated at 85.2% (4530 of 5314 strings)

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

powered by weblate
2023-06-02 19:55:04 +02:00
Raphael Michel
8471422bba Fix grammer error in settings help text 2023-06-02 19:08:21 +02:00
Raphael Michel
ee9acebe03 Devices: Fix crash in form validation 2023-06-02 17:19:25 +02:00
Raphael Michel
35d2a73f75 Voucher creation: Fix crash in validation (PRETIXEU-8GF) 2023-06-02 17:19:25 +02:00
Richard Schreiber
eb3eca45b5 Checkout/Addon: fix spinner button class name 2023-06-01 16:12:54 +02:00
Martin Gross
f7816924b0 Add Chinese (Traditional) (zh_Hant) to list of available languages. 2023-05-31 13:06:31 +02:00
Raphael Michel
12c3fef390 Docs: Add missing navigation node 2023-05-31 12:58:54 +02:00
Raphael Michel
8e39aaa292 Bump version to 4.21.0.dev0 2023-05-31 12:45:24 +02:00
399 changed files with 224758 additions and 135991 deletions

View File

@@ -35,7 +35,7 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Install Dependencies
run: pip3 install -e ".[dev]" mysqlclient psycopg2-binary
run: pip3 install -e ".[dev]" psycopg2-binary
- name: Run isort
run: isort -c .
working-directory: ./src
@@ -55,7 +55,7 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Install Dependencies
run: pip3 install -e ".[dev]" mysqlclient psycopg2-binary
run: pip3 install -e ".[dev]" psycopg2-binary
- name: Run flake8
run: flake8 .
working-directory: ./src

View File

@@ -25,24 +25,14 @@ jobs:
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11"]
database: [sqlite, postgres, mysql]
database: [sqlite, postgres]
exclude:
- database: mysql
python-version: "3.9"
- database: mysql
python-version: "3.11"
- database: sqlite
python-version: "3.9"
- database: sqlite
python-version: "3.10"
steps:
- uses: actions/checkout@v2
- uses: getong/mariadb-action@v1.1
with:
mariadb version: '10.10'
mysql database: 'pretix'
mysql root password: ''
if: matrix.database == 'mysql'
- uses: harmon758/postgresql-action@v1
with:
postgresql version: '11'
@@ -61,9 +51,9 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Install system dependencies
run: sudo apt update && sudo apt install gettext mariadb-client
run: sudo apt update && sudo apt install gettext
- name: Install Python dependencies
run: pip3 install --ignore-requires-python -e ".[dev]" mysqlclient psycopg2-binary # We ignore that flake8 needs newer python as we don't run flake8 during tests
run: pip3 install --ignore-requires-python -e ".[dev]" psycopg2-binary # We ignore that flake8 needs newer python as we don't run flake8 during tests
- name: Run checks
run: python manage.py check
working-directory: ./src

View File

@@ -3,7 +3,6 @@ FROM python:3.11-bullseye
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
libmariadb-dev \
gettext \
git \
libffi-dev \
@@ -34,8 +33,7 @@ RUN apt-get update && \
mkdir /static && \
mkdir /etc/supervisord && \
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash - && \
apt-get install -y nodejs && \
curl -qL https://www.npmjs.com/install.sh | sh
apt-get install -y nodejs
ENV LC_ALL=C.UTF-8 \
@@ -58,7 +56,7 @@ RUN pip3 install -U \
wheel && \
cd /pretix && \
PRETIX_DOCKER_BUILD=TRUE pip3 install \
-e ".[memcached,mysql]" \
-e ".[memcached]" \
gunicorn django-extensions ipython && \
rm -rf ~/.cache/pip

View File

@@ -152,25 +152,26 @@ Example::
password=abcd
host=localhost
port=3306
sslmode=require
sslrootcert=/etc/pretix/postgresql-ca.crt
sslcert=/etc/pretix/postgresql-client-crt.crt
sslkey=/etc/pretix/postgresql-client-key.key
``backend``
One of ``mysql`` (deprecated), ``sqlite3`` and ``postgresql``.
One of ``sqlite3`` and ``postgresql``.
Default: ``sqlite3``.
If you use MySQL, be sure to create your database using
``CREATE DATABASE <dbname> CHARACTER SET utf8;``. Otherwise, Unicode
support will not properly work.
``name``
The database's name. Default: ``db.sqlite3``.
``user``, ``password``, ``host``, ``port``
Connection details for the database connection. Empty by default.
``galera``
(Deprecated) Indicates if the database backend is a MySQL/MariaDB Galera cluster and
turns on some optimizations/special case handlers. Default: ``False``
``sslmode``, ``sslrootcert``
Connection TLS details for the PostgreSQL database connection. Possible values of ``sslmode`` are ``disable``, ``allow``, ``prefer``, ``require``, ``verify-ca``, and ``verify-full``. ``sslrootcert`` should be the accessible path of the ca certificate. Both values are empty by default.
``sslcert``, ``sslkey``
Connection mTLS details for the PostgreSQL database connection. It's also necessary to specify ``sslmode`` and ``sslrootcert`` parameters, please check the correct values from the TLS part. ``sslcert`` should be the accessible path of the client certificate. ``sslkey`` should be the accessible path of the client key. All values are empty by default.
.. _`config-replica`:
Database replica settings
@@ -332,6 +333,10 @@ to speed up various operations::
["sentinel_host_3", 26379]
]
password=password
ssl_cert_reqs=required
ssl_ca_certs=/etc/pretix/redis-ca.pem
ssl_keyfile=/etc/pretix/redis-client-crt.pem
ssl_certfile=/etc/pretix/redis-client-key.key
``location``
The location of redis, as a URL of the form ``redis://[:password]@localhost:6379/0``
@@ -355,6 +360,22 @@ to speed up various operations::
If your redis setup doesn't require a password or you already specified it in the location you can omit this option.
If this is set it will be passed to redis as the connection option PASSWORD.
``ssl_cert_reqs``
If this is set it will be passed to redis as the connection option ``SSL_CERT_REQS``.
Possible values are ``none``, ``optional``, and ``required``.
``ssl_ca_certs``
If your redis setup doesn't require TLS you can omit this option.
If this is set it will be passed to redis as the connection option ``SSL_CA_CERTS``. Possible value is the ca path.
``ssl_keyfile``
If your redis setup doesn't require mTLS you can omit this option.
If this is set it will be passed to redis as the connection option ``SSL_KEYFILE``. Possible value is the keyfile path.
``ssl_certfile``
If your redis setup doesn't require mTLS you can omit this option.
If this is set it will be passed to redis as the connection option ``SSL_CERTFILE``. Possible value is the certfile path.
If redis is not configured, pretix will store sessions and locks in the database. If memcached
is configured, memcached will be used for caching instead of redis.
@@ -404,6 +425,8 @@ The two ``transport_options`` entries can be omitted in most cases.
If they are present they need to be a valid JSON dictionary.
For possible entries in that dictionary see the `Celery documentation`_.
It is possible the use Redis with TLS/mTLS for the broker or the backend. To do so, it is necessary to specify the TLS identifier ``rediss``, the ssl mode ``ssl_cert_reqs`` and optionally specify the CA (TLS) ``ssl_ca_certs``, cert ``ssl_certfile`` and key ``ssl_keyfile`` (mTLS) path as encoded string. the following uri describes the format and possible parameters ``rediss://0.0.0.0:6379/1?ssl_cert_reqs=required&ssl_ca_certs=%2Fetc%2Fpretix%2Fredis-ca.pem&ssl_certfile=%2Fetc%2Fpretix%2Fredis-client-crt.pem&ssl_keyfile=%2Fetc%2Fpretix%2Fredis-client-key.key``
To use redis with sentinels set the broker or backend to ``sentinel://sentinel_host_1:26379;sentinel_host_2:26379/0``
and the respective transport_options to ``{"master_name":"mymaster"}``.
If your redis instances behind the sentinel have a password use ``sentinel://:my_password@sentinel_host_1:26379;sentinel_host_2:26379/0``.

View File

@@ -26,7 +26,7 @@ installation guides):
* `Docker`_
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
* A `PostgreSQL`_ 9.6+ database server
* A `PostgreSQL`_ 11+ database server
* A `redis`_ server
We also recommend that you use a firewall, although this is not a pretix-specific recommendation. If you're new to
@@ -321,11 +321,11 @@ workers, e.g. ``docker run … taskworker -Q notifications --concurrency 32``.
.. _Docker: https://docs.docker.com/engine/installation/linux/debian/
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-16-04
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-22-04
.. _nginx: https://botleg.com/stories/https-with-lets-encrypt-and-nginx/
.. _Let's Encrypt: https://letsencrypt.org/
.. _pretix.eu: https://pretix.eu/
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-20-04
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-22-04
.. _redis: https://blog.programster.org/debian-8-install-redis-server/
.. _ufw: https://en.wikipedia.org/wiki/Uncomplicated_Firewall
.. _redis website: https://redis.io/topics/security

View File

@@ -68,7 +68,7 @@ generated key and installs the plugin from the URL we told you::
mkdir -p /etc/ssh && \
ssh-keyscan -t rsa -p 10022 code.rami.io >> /root/.ssh/known_hosts && \
echo StrictHostKeyChecking=no >> /root/.ssh/config && \
DJANGO_SETTINGS_MODULE=pretix.settings pip3 install -U "git+ssh://git@code.rami.io:10022/pretix/pretix-slack.git@stable#egg=pretix-slack" && \
DJANGO_SETTINGS_MODULE= pip3 install -U "git+ssh://git@code.rami.io:10022/pretix/pretix-slack.git@stable#egg=pretix-slack" && \
cd /pretix/src && \
sudo -u pretixuser make production
USER pretixuser

View File

@@ -16,14 +16,11 @@ To use pretix, you will need the following things:
* A periodic task runner, e.g. ``cron``
* **A database**. This needs to be a SQL-based that is supported by Django. We highly recommend to either
go for **PostgreSQL** or **MySQL/MariaDB**. If you do not provide one, pretix will run on SQLite, which is useful
go for **PostgreSQL**. If you do not provide one, pretix will run on SQLite, which is useful
for evaluation and development purposes.
.. warning:: Do not ever use SQLite in production. It will break.
.. warning:: We recommend **PostgreSQL**. If you go for MySQL, make sure you run **MySQL 5.7 or newer** or
**MariaDB 10.2.7 or newer**.
* A **reverse proxy**. pretix needs to deliver some static content to your users (e.g. CSS, images, ...). While pretix
is capable of doing this, having this handled by a proper web server like **nginx** or **Apache** will be much
faster. Also, you need a proxying web server in front to provide SSL encryption.

View File

@@ -21,6 +21,7 @@ Requirements
Please set up the following systems beforehand, we'll not explain them here in detail (but see these links for external
installation guides):
* A python 3.9+ installation
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
* A `PostgreSQL`_ 11+ database server
@@ -323,11 +324,11 @@ Then, proceed like after any plugin installation::
(venv)$ python -m pretix updatestyles
# 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-16-04
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-22-04
.. _nginx: https://botleg.com/stories/https-with-lets-encrypt-and-nginx/
.. _Let's Encrypt: https://letsencrypt.org/
.. _pretix.eu: https://pretix.eu/
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-20-04
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-22-04
.. _redis: https://blog.programster.org/debian-8-install-redis-server/
.. _ufw: https://en.wikipedia.org/wiki/Uncomplicated_Firewall
.. _strong encryption settings: https://mozilla.github.io/server-side-tls/ssl-config-generator/

View File

@@ -3,11 +3,11 @@
Migrating from MySQL/MariaDB to PostgreSQL
==========================================
Our recommended database for all production installations is PostgreSQL. Support for MySQL/MariaDB will be removed in
pretix 5.0.
Our recommended database for all production installations is PostgreSQL. Support for MySQL/MariaDB has been removed
in newer pretix releases.
In order to follow this guide, your pretix installation needs to be a version that fully supports MySQL/MariaDB. If you
already upgraded to pretix 5.0, downgrade back to the last 4.x release using ``pip``.
already upgraded to pretix 5.0 or later, downgrade back to the last 4.x release using ``pip``.
.. note:: We have tested this guide carefully, but we can't assume any liability for its correctness. The data loss
risk should be low as long as pretix is not running while you do the migration. If you are a pretix Enterprise

View File

@@ -32,10 +32,16 @@ as well as the type of underlying hardware. Example:
"token": "kpp4jn8g2ynzonp6",
"hardware_brand": "Samsung",
"hardware_model": "Galaxy S",
"os_name": "Android",
"os_version": "2.3.6",
"software_brand": "pretixdroid",
"software_version": "4.0.0"
"software_version": "4.0.0",
"rsa_pubkey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqh…nswIDAQAB\n-----END PUBLIC KEY-----\n"
}
The ``rsa_pubkey`` is optional any only required for certain fatures such as working with reusable
media and NFC cryptography.
Every initialization token can only be used once. On success, you will receive a response containing
information on your device as well as your API token:
@@ -98,6 +104,8 @@ following endpoint:
{
"hardware_brand": "Samsung",
"hardware_model": "Galaxy S",
"os_name": "Android",
"os_version": "2.3.6",
"software_brand": "pretixdroid",
"software_version": "4.1.0",
"info": {"arbitrary": "data"}
@@ -133,9 +141,29 @@ The response will look like this:
"id": 3,
"name": "South entrance"
}
}
},
"server": {
"version": {
"pretix": "3.6.0.dev0",
"pretix_numeric": 30060001000
}
},
"medium_key_sets": [
{
"public_id": 3456349,
"organizer": "foo",
"active": true,
"media_type": "nfc_mf0aes",
"uid_key": "base64-encoded-encrypted-key",
"diversification_key": "base64-encoded-encrypted-key",
}
]
}
``"medium_key_sets`` will always be empty if you did not set an ``rsa_pubkey``.
The individual keys in the key sets are encrypted with the device's ``rsa_pubkey``
using ``RSA/ECB/PKCS1Padding``.
Creating a new API key
----------------------

View File

@@ -24,6 +24,8 @@ all_events boolean Whether this de
limit_events list List of event slugs this device has access to
hardware_brand string Device hardware manufacturer (read-only)
hardware_model string Device hardware model (read-only)
os_name string Device operating system name (read-only)
os_version string Device operating system version (read-only)
software_brand string Device software product (read-only)
software_version string Device software version (read-only)
created datetime Creation time
@@ -76,6 +78,8 @@ Device endpoints
"security_profile": "full",
"hardware_brand": "Zebra",
"hardware_model": "TC25",
"os_name": "Android",
"os_version": "8.1.0",
"software_brand": "pretixSCAN",
"software_version": "1.5.1"
}
@@ -123,6 +127,8 @@ Device endpoints
"security_profile": "full",
"hardware_brand": "Zebra",
"hardware_model": "TC25",
"os_name": "Android",
"os_version": "8.1.0",
"software_brand": "pretixSCAN",
"software_version": "1.5.1"
}
@@ -173,6 +179,8 @@ Device endpoints
"initialized": null
"hardware_brand": null,
"hardware_model": null,
"os_name": null,
"os_version": null,
"software_brand": null,
"software_version": null
}

View File

@@ -70,6 +70,11 @@ Endpoints
The ``public_url`` field has been added.
.. versionchanged:: 5.0
The ``date_from_before``, ``date_from_after``, ``date_to_before``, and ``date_to_after`` query parameters have been
added.
.. http:get:: /api/v1/organizers/(organizer)/events/
Returns a list of all events within a given organizer the authenticated user/token has access to.
@@ -141,6 +146,10 @@ Endpoints
:query has_subevents: If set to ``true``/``false``, only events with a matching value of ``has_subevents`` are returned.
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned. Event series are never (always) returned.
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned. Event series are never (always) returned.
:query date_from_after: If set to a date and time, only events that start at or after the given time are returned.
:query date_from_before: If set to a date and time, only events that start at or before the given time are returned.
:query date_to_after: If set to a date and time, only events that have an end date and end at or after the given time are returned.
:query date_to_before: If set to a date and time, only events that have an end date and end at or before the given time are returned.
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned. Event series are never returned.
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``date_from`` and
``slug``. Keep in mind that ``date_from`` of event series does not really tell you anything.

View File

@@ -111,7 +111,7 @@ Listing available exporters
"input_parameters": [
{
"name": "events",
"required": true
"required": false
},
{
"name": "_format",

View File

@@ -18,6 +18,7 @@ at :ref:`plugin-docs`.
item_variations
item_bundles
item_add-ons
item_meta_properties
questions
question_options
quotas

View File

@@ -18,7 +18,8 @@ The reusable medium resource contains the following public fields:
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the medium
type string Type of medium, e.g. ``"barcode"`` or ``"nfc_uid"``.
type string Type of medium, e.g. ``"barcode"``, ``"nfc_uid"`` or ``"nfc_mf0aes"``.
organizer string Organizer slug of the organizer who "owns" this medium.
identifier string Unique identifier of the medium. The format depends on the ``type``.
active boolean Whether this medium may be used.
created datetime Date of creation
@@ -36,6 +37,7 @@ Existing media types are:
- ``barcode``
- ``nfc_uid``
- ``nfc_mf0aes``
Endpoints
---------
@@ -67,6 +69,7 @@ Endpoints
"results": [
{
"id": 1,
"organizer": "bigevents",
"identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z",
@@ -123,6 +126,7 @@ Endpoints
{
"id": 1,
"organizer": "bigevents",
"identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z",
@@ -152,6 +156,9 @@ Endpoints
Look up a new reusable medium by its identifier. In some cases, this might lead to the automatic creation of a new
medium behind the scenes.
This endpoint, and this endpoint only, might return media from a different organizer if there is a cross-acceptance
agreement. In this case, only linked gift cards will be returned, no order position or customer records,
**Example request**:
.. sourcecode:: http
@@ -176,6 +183,7 @@ Endpoints
{
"id": 1,
"organizer": "bigevents",
"identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z",
@@ -235,6 +243,7 @@ Endpoints
{
"id": 1,
"organizer": "bigevents",
"identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z",
@@ -291,6 +300,7 @@ Endpoints
{
"id": 1,
"organizer": "bigevents",
"identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z",

View File

@@ -18,8 +18,15 @@ subject multi-lingual string The subject of
template multi-lingual string The body of the email
all_products boolean If ``true``, the email is sent to buyers of all products
limit_products list of integers List of product IDs, if ``all_products`` is not set
include_pending boolean If ``true``, the email is sent to pending orders. If ``false``,
[**DEPRECATED**] include_pending boolean If ``true``, the email is sent to pending orders. If ``false``,
only paid orders are considered.
restrict_to_status list List of order states to restrict recipients to. Valid
entries are ``p`` for paid, ``e`` for expired, ``c`` for canceled,
``n__pending_approval`` for pending approval,
``n__not_pending_approval_and_not_valid_if_pending`` for payment pending,
``n__valid_if_pending`` for payment pending but already confirmed,
and ``n__pending_overdue`` for pending with payment overdue.
The default is ``["p", "n__valid_if_pending"]``.
date_is_absolute boolean If ``true``, the email is set at a specific point in time.
send_date datetime If ``date_is_absolute`` is set: Date and time to send the email.
send_offset_days integer If ``date_is_absolute`` is not set, this is the number of days
@@ -37,7 +44,10 @@ send_to string Can be ``"order
or ``"both"``.
date. Otherwise it is relative to the event start date.
===================================== ========================== =======================================================
.. versionchanged:: 2023.7
The ``include_pending`` field has been deprecated.
The ``restrict_to_status`` field has been added.
Endpoints
---------
@@ -74,7 +84,11 @@ Endpoints
"template": {"en": "Don't forget your tickets, download them at {url}"},
"all_products": true,
"limit_products": [],
"include_pending": false,
"restrict_to_status": [
"p",
"n__not_pending_approval_and_not_valid_if_pending",
"n__valid_if_pending"
],
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
@@ -120,7 +134,11 @@ Endpoints
"template": {"en": "Don't forget your tickets, download them at {url}"},
"all_products": true,
"limit_products": [],
"include_pending": false,
"restrict_to_status": [
"p",
"n__not_pending_approval_and_not_valid_if_pending",
"n__valid_if_pending"
],
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
@@ -157,7 +175,11 @@ Endpoints
"template": {"en": "Don't forget your tickets, download them at {url}"},
"all_products": true,
"limit_products": [],
"include_pending": false,
"restrict_to_status": [
"p",
"n__not_pending_approval_and_not_valid_if_pending",
"n__valid_if_pending"
],
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
@@ -182,7 +204,11 @@ Endpoints
"template": {"en": "Don't forget your tickets, download them at {url}"},
"all_products": true,
"limit_products": [],
"include_pending": false,
"restrict_to_status": [
"p",
"n__not_pending_approval_and_not_valid_if_pending",
"n__valid_if_pending"
],
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
@@ -235,7 +261,11 @@ Endpoints
"template": {"en": "Don't forget your tickets, download them at {url}"},
"all_products": true,
"limit_products": [],
"include_pending": false,
"restrict_to_status": [
"p",
"n__not_pending_approval_and_not_valid_if_pending",
"n__valid_if_pending"
],
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",

View File

@@ -63,6 +63,11 @@ last_modified datetime Last modificati
The ``search`` query parameter has been added to filter sub-events by their name or location in any language.
.. versionchanged:: 5.0
The ``date_from_before``, ``date_from_after``, ``date_to_before``, and ``date_to_after`` query parameters have been
added.
Endpoints
---------
@@ -130,6 +135,10 @@ Endpoints
:query active: If set to ``true``/``false``, only events with a matching value of ``active`` are returned.
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned.
:query date_from_after: If set to a date and time, only events that start at or after the given time are returned.
:query date_from_before: If set to a date and time, only events that start at or before the given time are returned.
:query date_to_after: If set to a date and time, only events that have an end date and end at or after the given time are returned.
:query date_to_before: If set to a date and time, only events that have an end date and end at or before the given time are returned.
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned.
:query search: Only return events matching a given search query.
:param organizer: The ``slug`` field of a valid organizer
@@ -458,6 +467,10 @@ Endpoints
:query event__live: If set to ``true``/``false``, only events with a matching value of ``live`` on the parent event are returned.
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned.
:query date_from_after: If set to a date and time, only events that start at or after the given time are returned.
:query date_from_before: If set to a date and time, only events that start at or before the given time are returned.
:query date_to_after: If set to a date and time, only events that have an end date and end at or after the given time are returned.
:query date_to_before: If set to a date and time, only events that have an end date and end at or before the given time are returned.
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned.
:query sales_channel: If set to a sales channel identifier, the response will only contain subevents from events available on this sales channel.
:param organizer: The ``slug`` field of a valid organizer

View File

@@ -20,11 +20,16 @@ internal_name string An optional nam
rate decimal (string) Tax rate in percent
price_includes_tax boolean If ``true`` (default), tax is assumed to be included in
the specified product price
eu_reverse_charge boolean If ``true``, EU reverse charge rules are applied
eu_reverse_charge boolean If ``true``, EU reverse charge rules are applied. Will
be ignored if custom rules are set.
home_country string Merchant country (required for reverse charge), can be
``null`` or empty string
keep_gross_if_rate_changes boolean If ``true``, changes of the tax rate based on custom
rules keep the gross price constant (default is ``false``)
custom_rules object Dynamic rules specification. Each list element
corresponds to one rule that will be processed in order.
The current version of the schema in use can be found
`here`_.
===================================== ========================== =======================================================
@@ -32,6 +37,10 @@ keep_gross_if_rate_changes boolean If ``true``, ch
The ``internal_name`` and ``keep_gross_if_rate_changes`` attributes have been added.
.. versionchanged:: 2023.6
The ``custom_rules`` attribute has been added.
Endpoints
---------
@@ -68,6 +77,7 @@ Endpoints
"price_includes_tax": true,
"eu_reverse_charge": false,
"keep_gross_if_rate_changes": false,
"custom_rules": null,
"home_country": "DE"
}
]
@@ -108,6 +118,7 @@ Endpoints
"price_includes_tax": true,
"eu_reverse_charge": false,
"keep_gross_if_rate_changes": false,
"custom_rules": null,
"home_country": "DE"
}
@@ -156,6 +167,7 @@ Endpoints
"price_includes_tax": true,
"eu_reverse_charge": false,
"keep_gross_if_rate_changes": false,
"custom_rules": null,
"home_country": "DE"
}
@@ -203,6 +215,7 @@ Endpoints
"price_includes_tax": true,
"eu_reverse_charge": false,
"keep_gross_if_rate_changes": false,
"custom_rules": null,
"home_country": "DE"
}
@@ -242,3 +255,5 @@ Endpoints
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it **or** this tax rule cannot be deleted since it is currently in use.
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/schema/tax-rules-custom.schema.json

View File

@@ -50,6 +50,10 @@ The following values for ``action_types`` are valid with pretix core:
* ``pretix.event.order.payment.confirmed``
* ``pretix.event.order.approved``
* ``pretix.event.order.denied``
* ``pretix.event.orders.waitinglist.added``
* ``pretix.event.orders.waitinglist.changed``
* ``pretix.event.orders.waitinglist.deleted``
* ``pretix.event.orders.waitinglist.voucher_assigned``
* ``pretix.event.checkin``
* ``pretix.event.checkin.reverted``
* ``pretix.event.added``

View File

@@ -18,13 +18,13 @@ If you want to add a custom view to the control area of an event, just register
.. code-block:: python
from django.conf.urls import url
from django.urls import re_path
from . import views
urlpatterns = [
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/mypluginname/',
views.admin_view, name='backend'),
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/mypluginname/',
views.admin_view, name='backend'),
]
It is required that your URL parameters are called ``organizer`` and ``event``. If you want to

View File

@@ -61,7 +61,7 @@ Backend
item_formsets, order_search_filter_q, order_search_forms
.. automodule:: pretix.base.signals
:members: logentry_display, logentry_object_link, requiredaction_display, timeline_events, orderposition_blocked_display
:members: logentry_display, logentry_object_link, requiredaction_display, timeline_events, orderposition_blocked_display, customer_created, customer_signed_in
Vouchers
""""""""

View File

@@ -70,6 +70,8 @@ The provider class
.. autoattribute:: settings_form_fields
.. autoattribute:: walletqueries
.. automethod:: settings_form_clean
.. automethod:: settings_content_render

View File

@@ -35,13 +35,13 @@ automatically and should be provided by any plugin that provides any view.
A very basic example that provides one view in the admin panel and one view in the frontend
could look like this::
from django.conf.urls import url
from django.urls import re_path
from . import views
urlpatterns = [
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/mypluginname/',
views.AdminView.as_view(), name='backend'),
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/mypluginname/',
views.AdminView.as_view(), name='backend'),
]
event_patterns = [

View File

@@ -12,3 +12,4 @@ Developer documentation
api/index
structure
translation/index
nfc/index

View File

@@ -0,0 +1,15 @@
NFC media
=========
pretix supports using NFC chips as "reusable media", for example to store gift cards or tickets.
Most of this implementation currently lives in our proprietary app pretixPOS, but in the future might also become part of our open-source pretixSCAN solution.
Either way, we want this to be an open ecosystem and therefore document the exact mechanisms in use on the following pages.
We support multiple implementations of NFC media, each documented on its own page:
.. toctree::
:maxdepth: 2
uid
mf0aes

View File

@@ -0,0 +1,113 @@
Mifare Ultralight AES
=====================
We offer an implementation that provides a higher security level than the UID-based approach and uses the `Mifare Ultralight AES`_ chip sold by NXP.
We believe the security model of this approach is adequate to the situation where this will usually be used and we'll outline known risks below.
If you want to dive deeper into the properties of the Mifare Ultralight AES chip, we recommend reading the `data sheet`_.
Random UIDs
-----------
Mifare Ultralight AES supports a feature that returns a randomized UID every time a non-authenticated user tries to
read the UID. This has a strong privacy benefit, since no unauthorized entity can use the NFC chips to track users.
On the other hand, this reduces interoperability of the system. For example, this prevents you from using the same NFC
chips for a different purpose where you only need the UID. This will also prevent your guests from reading their UID
themselves with their phones, which might be useful e.g. in debugging situations.
Since there's no one-size-fits-all choice here, you can enable or disable this feature in the pretix organizer
settings. If you change it, the change will apply to all newly encoded chips after the change.
Key management
--------------
For every organizer, the server will generate create a "key set", which consists of a publicly known ID (random 32-bit integer) and two 16-byte keys ("diversification key" and "UID key").
Using our :ref:`Device authentication mechanism <rest-deviceauth>`, an authorized device can submit a locally generated RSA public key to the server.
This key can no longer changed on the server once it is set, thus protecting against the attack scenario of a leaked device API token.
The server will then include key sets in the response to ``/api/v1/device/info``, encrypted with the device's RSA key.
This includes all key sets generated for the organizer the device belongs to, as well as all keys of organizers that have granted sufficient access to this organizer.
The device will decrypt the key sets using its RSA key and store the key sets locally.
.. warning:: The device **will** have access to the raw key sets. Therefore, there is a risk of leaked master keys if an
authorized device is stolen or abused. Our implementation in pretixPOS attempts to make this very hard on
modern, non-rooted Android devices by keeping them encrypted with the RSA key and only storing the RSA key
in the hardware-backed keystore of the device. A sufficiently motivated attacker, however, will likely still
be able to extract the keys from a stolen device.
Encoding a chip
---------------
When a new chip is encoded, the following steps will be taken:
- The UID of the chip is retrieved.
- A chip-specific key is generated using the mechanism documented in `AN10922`_ using the "diversification key" from the
organizer's key set as the CMAC key and the diversification input concatenated in the from of ``0x01 + UID + APPID + SYSTEMID``
with the following values:
- The UID of the chip as ``UID``
- ``"eu.pretix"`` (``0x65 0x75 0x2e 0x70 0x72 0x65 0x74 0x69 0x78``) as ``APPID``
- The ``public_id`` from the organizer's key set as a 4-byte big-endian value as ``SYSTEMID``
- The chip-specific key is written to the chip as the "data protection key" (config pages 0x30 to 0x33)
- The UID key from the organizer's key set is written to the chip as the "UID retrieval key" (config pages 0x34 to 0x37)
- The config page 0x29 is set like this:
- ``RID_ACT`` (random UID) to ``1`` or ``0`` based on the organizer's configuration
- ``SEC_MSG_ACT`` (secure messaging) to ``1``
- ``AUTH0`` (first page that needs authentication) to 0x04 (first non-UID page)
- The config page 0x2A is set like this:
- ``PROT`` to ``0`` (only write access restricted, not read access)
- ``AUTHLIM`` to ``256`` (maximum number of wrong authentications before "self-desctruction")
- Everything else to its default value (no lock bits are set)
- The ``public_id`` of the key set will be written to page 0x04 as a big-endian value
- The UID of the chip will be registered as a reusable medium on the server.
.. warning:: During encoding, the chip-specific key and the UID key are transmitted in plain text over the air. The
security model therefore relies on the encoding of chips being performed in a trusted physical environment
to prevent a nearby attacker from sniffing the keys with a strong antenna.
.. note:: If an attacker tries to authenticate with the chip 256 times using the wrong key, the chip will become
unusable. A chip may also become unusable if it is detached from the reader in the middle of the encoding
process (even though we've tried to implement it in a way that makes this unlikely).
Usage
-----
When a chip is presented to the NFC reader, the following steps will be taken:
- Command ``GET_VERSION`` is used to determine if it is a Mifare Ultralight AES chip (if not, abort).
- Page 0x04 is read. If it is all zeroes, the chip is considered un-encoded (abort). If it contains a value that
corresponds to the ``public_id`` of a known key set, this key set is used for all further operations. If it contains
a different value, we consider this chip to belong to a different organizer or not to a pretix system at all (abort).
- An authentication with the chip using the UID key is performed.
- The UID of the chip will be read.
- The chip-specific key will be derived using the mechanism described above in the encoding step.
- An authentication with the chip using the chip-specific key is performed. If this is fully successful, this step
proves that the chip knows the same chip-specific key as we do and is therefore an authentic chip encoded by us and
we can trust its UID value.
- The UID is transmitted to the server to fetch the correct medium.
During these steps, the keys are never transmitted in plain text and can thus not be sniffed by a nearby attacker
with a strong antenna.
.. _Mifare Ultralight AES: https://www.nxp.com/products/rfid-nfc/mifare-hf/mifare-ultralight/mifare-ultralight-aes-enhanced-security-for-limited-use-contactless-applications:MF0AESx20
.. _data sheet: https://www.nxp.com/docs/en/data-sheet/MF0AES(H)20.pdf
.. _AN10922: https://www.nxp.com/docs/en/application-note/AN10922.pdf

View File

@@ -0,0 +1,10 @@
UID-based
=========
With UID-based NFC, only the unique ID (UID) of the NFC chip is used for identification purposes.
This can be used with virtually all NFC chips that provide compatibility with the NFC reader in use, typically at least all chips that comply with ISO/IEC 14443-3A.
We make only one restriction: The UID may not start with ``08``, since that usually signifies a randomized UID that changes on every read (which would not be very useful).
.. warning:: The UID-based approach provides only a very low level of security. It is easy to clone a chip with the same
UID and impersonate someone else.

143
doc/plugins/epaybl.rst Normal file
View File

@@ -0,0 +1,143 @@
ePayBL
======
.. note::
Since ePayBL is only available to german federal, provincial and communal entities, the following page is also
only provided in german. Should you require assistance with ePayBL and do not speak this language, please feel free
reach out to support@pretix.eu.
Einführung
----------
.. note::
Sollten Sie lediglich schnell entscheiden wollen, welcher Kontierungsmodus in den Einstellungen des pretix
ePayBL-plugins gewählt werden soll, so springen Sie direkt zur Sektion :ref:`Kontierungsmodus`.
`ePayBL`_ - das ePayment-System von Bund und Länder - ist das am weitesten verbreitete Zahlungssystem für Bundes-, Länder-
sowie kommunale Aufgabenträger. Während es nur wie eines von vielen anderen Zahlungssystemen scheint, so bietet es
seinen Nutzern besondere Vorteile, wie die automatische Erfassung von Zahlungsbelegen, dem Übertragen von Buchungen in
Haushaltskassen/-systeme sowie die automatische Erfassung von Kontierungen und Steuermerkmalen.
Rein technisch gesehen ist ePayBL hierbei nicht ein eigenständiger Zahlungsdienstleister sondern nur ein eine Komponente
im komplexen System, dass die Zahlungsabwicklung für Kommunen und Behörden ist.
Im folgenden der schematische Aufbau einer Umgebung, in welcher ePayBL zum Einsatz kommt:
.. figure:: img/epaybl_flowchart.png
:class: screenshot
Quelle: Integrationshandbuch ePayBL-Konnektor, DResearch Digital Media Systems GmbH
In diesem Schaubild stellt pretix, bzw. die von Ihnen als Veranstalter angelegten Ticketshops, das Fachverfahren dar.
ePayBL stellt das Bindeglied zwischen den Fachverfahren, Haushaltssystemen und dem eigentlichen Zahlungsdienstleister,
dem sog. ZV-Provider dar. Dieser ZV-Provider ist die Stelle, welche die eigentlichen Kundengelder einzieht und an den
Händler auszahlt. Das Gros der Zahlungsdienstleister unterstützt pretix hierbei auch direkt; sprich: Sollten Sie die
Anbindung an Ihre Haushaltssysteme nicht benötigen, kann eine direkte Anbindung in der Regel ebenso - und dies bei meist
vermindertem Aufwand - vorgenommen werden.
In der Vergangenheit zeigte sich jedoch schnell, dass nicht jeder IT-Dienstleister immer sofort die neueste Version von
ePayBL seinen Nutzern angeboten hat. Die Gründe hierfür sind mannigfaltig: Von fest vorgegebenen Update-Zyklen bis hin
zu Systeme mit speziellen Anpassungen, kann leider nicht davon ausgegangen werden, dass alle ePayBL-Systeme exakt gleich
ansprechbar sind - auch wenn es sich dabei eigentlich um einen standardisierten Dienst handelt.
Aus diesem Grund gibt es mit dem ePayBL-Konnektor eine weitere Abstraktionsschicht welche optional zwischen den
Fachverfahren und dem ePayBL-Server sitzt. Dieser Konnektor wird so gepflegt, dass er zum einen eine dauerhaft
gleichartige Schnittstelle den Fachverfahren bietet aber gleichzeitig auch mit jeder Version des ePayBL-Servers
kommunizieren kann - egal wie neu oder alt, wie regulär oder angepasst diese ist.
Im Grunde müsste daher eigentlich immer gesagt werden, dass pretix eine Anbindung an den ePayBL-Konnektor bietet; nicht
an "ePayBL" oder den "ePayBL-Server". Diese Unterscheidung kann bei der Ersteinrichtung und Anforderung von Zugangsdaten
von Relevanz sein. Da in der Praxis jedoch beide Begriffe gleichbedeutend genutzt werden, wird im Folgenden auch nur von
einer ePayBL-Anbindung die Rede sein - auch wenn explizit der Konnektor gemeint ist.
.. _`Kontierungsmodus`:
Kontierungsmodus
----------------
ePayBL ist ein Produkt, welches für die Abwicklung von Online-Zahlungsvorgängen in der Verwaltung geschaffen wurde. Ein
Umfeld, in dem klar definiert ist, was ein Kunde gerade bezahlt und wohin das Geld genau fließt. Diese Annahmen lassen
sich in einem Ticketshop wie pretix jedoch nur teilweise genauso abbilden.
Die ePayBL-Integration für pretix bietet daher zwei unterschiedliche Modi an, wie Buchungen erfasst und an ePayBL und
damit auch an die dahinterliegenden Haushaltssysteme gemeldet werden können.
Kontierung pro Position/Artikel
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Dieser Modus versucht den klassischen, behördentypischen ePayBL-Zahlungsvorgang abzubilden: Jede einzelne Position, die
ein Kunde in den Warenkorb legt, wird auch genauso 1:1 an ePayBL und die Hintergrundsysteme übermittelt.
Hierbei muss zwingend auch für jede Position ein Kennzeichen für Haushaltsstelle und Objektnummer, sowie optional ein
Kontierungsobjekt (``HREF``; bspw. ``stsl=Steuerschlüssel;psp=gsb:Geschäftsbereich,auft:Innenauftrag,kst:Kostenstelle;``
) übermittelt werden.
Diese Daten sind vom Veranstalter entsprechend für jeden in der Veranstaltung angelegten Artikel innerhalb des Tabs
"Zusätzliche Einstellungen" der Produkteinstellungen zu hinterlegen.
Während diese Einstellung eine größtmögliche Menge an Kontierungsdaten überträgt und auch ein separates Verbuchen von
Leistungen auf unterschiedliche Haushaltsstellen erlaubt, so hat diese Option auch einen großen Nachteil: Der Kunde kann
nur eine Zahlung für seine Bestellung leisten.
Während sich dies nicht nach einem großen Problem anhört, so kann dies beim Kunden zu Frust führen. pretix bietet die
Option an, dass ein Veranstalter eine Bestellung jederzeit verändern kann: Ändern von Preisen von Positionen in einer
aufgegebenen Bestellung, Zubuchen und Entfernen von Bestellpositionen, etc. Hat der Kunde seine ursprüngliche Bestellung
jedoch schon bezahlt, kann pretix nicht mehr die komplette Bestellung mit den passenden Kontierungen übertragen - es
müsste nur ein Differenz-Abbild zwischen Ursprungsbestellung und aktueller Bestellung übertragen werden. Aber auch wenn
eine "Nachmeldung" möglich wäre, so wäre ein konkretes Auflösen für was jetzt genau gezahlt wird, nicht mehr möglich.
Daher gilt bei der Nutzung der Kontierung pro Position/Artikel: Der Kunde kann nur eine (erfolgreiche) Zahlung auf seine
Bestellung leisten.
Eine weitere Einschränkung dieses Modus ist, dass aktuell keine Gebühren-Positionen (Versandkosten, Zahlungs-, Storno-
oder Servicegebühren) in diesem Modus übertragen werden können. Bitte wenden Sie sich an uns, wenn Sie diese
Funktionalität benötigen.
Kontierung pro Zahlvorgang
^^^^^^^^^^^^^^^^^^^^^^^^^^
Dieser Modus verabschiedet sich vom behördlichen "Jede Position gehört genau zu einem Haushaltskonto und muss genau
zugeordnet werden". Stattdessen werden alle Bestellpositionen - inklusive eventuell definierter Gebühren - vermengt und
nur als ein großer Warenkorb, genauer gesagt: eine einzige Position an ePayBL sowie die Hintergrundsysteme gemeldet.
Während im "pro Postion/Artikel"-Modus jeder Artikel einzeln übermittelt wird und damit auch korrekt pro Artikel der
jeweilige Brutto- und Nettopreis, sowie der anfallende Steuerbetrag und ein Steuerkennzeichen (mit Hilfe des optionalen
``HREF``-Attributs) übermittelt werden, ist dies im "pro Zahlvorgang"-Modus nicht möglich.
Stattdessen übermittelt pretix nur einen Betrag für den gesamten Warenkorb: Bruttopreis == Nettopreis. Der Steuerbetrag
wird hierbei als 0 übermittelt.
Die Angabe einer Haushaltsstelle und Objektnummer, sowie optional der ``HREF``-Kontierungsinformationen ist jedoch
weiterhin notwendig - allerdings nicht mehr individuell für jeden Artikel/jede Position sondern nur für die gesamte
Bestellung. Diese Daten sind direkt in den ePayBL-Einstellungen der Veranstaltung unter Einstellungen -> Zahlung ->
ePayBL vorzunehmen
In der Praxis bedeutet dies, dass in einem angeschlossenen Haushaltssystem nicht nachvollzogen kann, welche Positionen
konkret erworben und bezahlt wurden - stattdessen kann nur der Fakt, dass etwas verkauft wurde erfasst werden.
Je nach Aufbau und Vorgaben der Finanzbuchhaltung kann dies jedoch ausreichend sein - wenn bspw. eine Ferienfahrt
angeboten wird und seitens der Haushaltssysteme nicht erfasst werden muss, wie viel vom Gesamtbetrag einer Bestellung
auf die Ferienfahrt an sich, auf einen Zubringerbus und einen Satz Bettwäsche entfallen ist, sondern (vereinfacht
gesagt) es ausreichend ist, dass "Eine Summe X für die Haushaltsstelle/Objektnummer geflossen ist".
Dieser Modus der Kontierung bietet Ihnen auch als Vorteil gegenüber dem vorhergehenden an, dass die Bestellungen der
Kunden jederzeit erweitert und verändert werden können - auch wenn die Ursprungsbestellung schon bezahlt wurde und nur
noch eine Differenz gezahlt wird.
Einschränkungen
---------------
Zum aktuellen Zeitpunkt erlaubt die pretix-Anbindung an ePayBL nicht das durchführen von Erstattungen von bereits
geleisteten Zahlungen. Der Prozess hierfür unterscheidet sich von Behörde zu Behörde und muss daher händisch
durchgeführt werden.
.. _ePayBL: https://www.epaybl.de/

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -18,6 +18,7 @@ If you want to **create** a plugin, please go to the
campaigns
certificates
digital
epaybl
exhibitors
shipping
imported_secrets

View File

@@ -22,7 +22,7 @@ classifiers = [
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Framework :: Django :: 3.2",
"Framework :: Django :: 4.1",
]
dependencies = [
@@ -30,13 +30,13 @@ dependencies = [
"babel",
"BeautifulSoup4==4.12.*",
"bleach==5.0.*",
"celery==5.2.*",
"celery==5.3.*",
"chardet==5.1.*",
"cryptography>=3.4.2",
"css-inline==0.8.*",
"defusedcsv>=1.1.0",
"dj-static",
"Django==3.2.*,>=3.2.18",
"Django==4.1.*",
"django-bootstrap3==23.1.*",
"django-compressor==4.3.*",
"django-countries==7.5.*",
@@ -49,9 +49,8 @@ dependencies = [
"django-libsass==0.9",
"django-localflavor==4.0",
"django-markup",
"django-mysql",
"django-oauth-toolkit==2.2.*",
"django-otp==1.1.*",
"django-otp==1.2.*",
"django-phonenumber-field==7.1.*",
"django-redis==5.2.*",
"django-scopes==2.0.*",
@@ -60,38 +59,40 @@ dependencies = [
"dnspython==2.3.*",
"drf_ujson2==1.7.*",
"geoip2==4.*",
"importlib_metadata==6.6.*", # Polyfill, we can probably drop this once we require Python 3.10+
"importlib_metadata==6.*", # Polyfill, we can probably drop this once we require Python 3.10+
"isoweek",
"jsonschema",
"kombu==5.2.*",
"kombu==5.3.*",
"libsass==0.22.*",
"lxml",
"markdown==3.4.3", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
"mt-940==4.23.*",
"mt-940==4.30.*",
"oauthlib==3.2.*",
"openpyxl==3.1.*",
"packaging",
"paypalrestsdk==1.13.*",
"paypal-checkout-serversdk==1.0.*",
"PyJWT==2.6.*",
"PyJWT==2.7.*",
"phonenumberslite==8.13.*",
"Pillow==9.5.*",
"pretix-plugin-build",
"protobuf==4.23.*",
"psycopg2-binary",
"pycountry",
"pycparser==2.21",
"pycryptodome==3.18.*",
"pypdf==3.8.*",
"pypdf==3.9.*",
"python-bidi==0.4.*", # Support for Arabic in reportlab
"python-dateutil==2.8.*",
"python-u2flib-server==4.*",
"pytz",
"pytz-deprecation-shim==0.1.*",
"pyuca",
"qrcode==7.4.*",
"redis==4.5.*,>=4.5.4",
"reportlab==4.0.*",
"requests==2.30.*",
"requests==2.31.*",
"sentry-sdk==1.15.*",
"sepaxml==2.6.*",
"slimit",
@@ -108,7 +109,6 @@ dependencies = [
[project.optional-dependencies]
memcached = ["pylibmc"]
mysql = ["mysqlclient"]
dev = [
"coverage",
"coveralls",
@@ -125,7 +125,7 @@ dev = [
"pytest-mock==3.10.*",
"pytest-rerunfailures==11.*",
"pytest-sugar",
"pytest-xdist==3.2.*",
"pytest-xdist==3.3.*",
"pytest==7.3.*",
"responses",
]

View File

@@ -29,7 +29,6 @@ sys.path.append(str(Path.cwd() / 'src'))
def _CustomBuild(*args, **kwargs):
print(sys.path)
from pretix._build import CustomBuild
return CustomBuild(*args, **kwargs)

View File

@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "4.20.0"
__version__ = "2023.7.2"

View File

@@ -30,7 +30,6 @@ from django.utils.translation import gettext_lazy as _ # NOQA
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
USE_I18N = True
USE_L10N = True
USE_TZ = True
INSTALLED_APPS = [
@@ -68,6 +67,7 @@ INSTALLED_APPS = [
'oauth2_provider',
'phonenumber_field',
'statici18n',
'django.forms', # after pretix.base for overrides
]
FORMAT_MODULE_PATH = [
@@ -80,6 +80,7 @@ ALL_LANGUAGES = [
('de-informal', _('German (informal)')),
('ar', _('Arabic')),
('zh-hans', _('Chinese (simplified)')),
('zh-hant', _('Chinese (traditional)')),
('cs', _('Czech')),
('da', _('Danish')),
('nl', _('Dutch')),
@@ -179,6 +180,8 @@ TEMPLATES = [
},
]
FORM_RENDERER = "django.forms.renderers.TemplatesSetting"
STATIC_ROOT = os.path.join(os.path.dirname(__file__), 'static.dist')
STATICFILES_FINDERS = (

View File

@@ -45,6 +45,10 @@ def npm_install():
class CustomBuild(build):
def run(self):
if "src" not in os.listdir(".") or "pretix" not in os.listdir("src"):
# Only run this command on the pretix module, not on other modules even if it's registered globally
# in some cases
return build.run(self)
if "PRETIX_DOCKER_BUILD" in os.environ:
return # this is a hack to allow calling this file early in our docker build to make use of caching
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pretix._build_settings")
@@ -68,6 +72,10 @@ class CustomBuild(build):
class CustomBuildExt(build_ext):
def run(self):
if "src" not in os.listdir(".") or "pretix" not in os.listdir("src"):
# Only run this command on the pretix module, not on other modules even if it's registered globally
# in some cases
return build_ext.run(self)
if "PRETIX_DOCKER_BUILD" in os.environ:
return # this is a hack to allow calling this file early in our docker build to make use of caching
npm_install()

View File

@@ -223,6 +223,7 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
('POST', 'api-v1:checkinrpc.redeem'),
('GET', 'api-v1:checkinrpc.search'),
('POST', 'api-v1:reusablemedium-lookup'),
('POST', 'api-v1:reusablemedium-list'),
)

View File

@@ -59,7 +59,7 @@ class IdempotencyMiddleware:
auth_hash = sha1(auth_hash_parts.encode()).hexdigest()
idempotency_key = request.headers.get('X-Idempotency-Key', '')
with transaction.atomic():
with transaction.atomic(durable=True):
call, created = ApiCall.objects.select_for_update(of=OF_SELF).get_or_create(
auth_hash=auth_hash,
idempotency_key=idempotency_key,
@@ -75,7 +75,7 @@ class IdempotencyMiddleware:
if created:
resp = self.get_response(request)
with transaction.atomic():
with transaction.atomic(durable=True):
if resp.status_code in (409, 429, 500, 503):
# This is the exception: These calls are *meant* to be retried!
call.delete()

View File

@@ -19,6 +19,8 @@
# 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 json
from rest_framework import serializers
@@ -46,3 +48,16 @@ class AsymmetricField(serializers.Field):
def run_validation(self, data=serializers.empty):
return self.write.run_validation(data)
class CompatibleJSONField(serializers.JSONField):
def to_internal_value(self, data):
try:
return json.dumps(data)
except (TypeError, ValueError):
self.fail('invalid')
def to_representation(self, value):
if value:
return json.loads(value)
return value

View File

@@ -46,6 +46,7 @@ from rest_framework import serializers
from rest_framework.fields import ChoiceField, Field
from rest_framework.relations import SlugRelatedField
from pretix.api.serializers import CompatibleJSONField
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.settings import SettingsSerializer
from pretix.base.models import Device, Event, TaxRule, TeamAPIToken
@@ -53,6 +54,7 @@ from pretix.base.models.event import SubEvent
from pretix.base.models.items import (
ItemMetaProperty, SubEventItem, SubEventItemVariation,
)
from pretix.base.models.tax import CustomRulesValidator
from pretix.base.services.seating import (
SeatProtected, generate_seats, validate_plan_change,
)
@@ -650,9 +652,16 @@ class SubEventSerializer(I18nAwareModelSerializer):
class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
custom_rules = CompatibleJSONField(
validators=[CustomRulesValidator()],
required=False,
allow_null=True,
)
class Meta:
model = TaxRule
fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country', 'internal_name', 'keep_gross_if_rate_changes')
fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country', 'internal_name',
'keep_gross_if_rate_changes', 'custom_rules')
class EventSettingsSerializer(SettingsSerializer):
@@ -719,6 +728,7 @@ class EventSettingsSerializer(SettingsSerializer):
'payment_term_minutes',
'payment_term_last',
'payment_term_expire_automatically',
'payment_term_expire_delay_days',
'payment_term_accept_late',
'payment_explanation',
'payment_pending_hidden',
@@ -807,6 +817,10 @@ class EventSettingsSerializer(SettingsSerializer):
'reusable_media_type_nfc_uid',
'reusable_media_type_nfc_uid_autocreate_giftcard',
'reusable_media_type_nfc_uid_autocreate_giftcard_currency',
'reusable_media_type_nfc_mf0aes',
'reusable_media_type_nfc_mf0aes_autocreate_giftcard',
'reusable_media_type_nfc_mf0aes_autocreate_giftcard_currency',
'reusable_media_type_nfc_mf0aes_random_uid',
]
readonly_fields = [
# These are read-only since they are currently only settable on organizers, not events
@@ -816,6 +830,10 @@ class EventSettingsSerializer(SettingsSerializer):
'reusable_media_type_nfc_uid',
'reusable_media_type_nfc_uid_autocreate_giftcard',
'reusable_media_type_nfc_uid_autocreate_giftcard_currency',
'reusable_media_type_nfc_mf0aes',
'reusable_media_type_nfc_mf0aes_autocreate_giftcard',
'reusable_media_type_nfc_mf0aes_autocreate_giftcard_currency',
'reusable_media_type_nfc_mf0aes_random_uid',
]
def __init__(self, *args, **kwargs):
@@ -884,6 +902,8 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
'name_scheme',
'reusable_media_type_barcode',
'reusable_media_type_nfc_uid',
'reusable_media_type_nfc_mf0aes',
'reusable_media_type_nfc_mf0aes_random_uid',
'system_question_order',
]

View File

@@ -93,7 +93,7 @@ class JobRunSerializer(serializers.Serializer):
if events is not None and not isinstance(ex, OrganizerLevelExportMixin):
self.fields["events"] = serializers.SlugRelatedField(
queryset=events,
required=True,
required=False,
allow_empty=False,
slug_field='slug',
many=True
@@ -156,8 +156,9 @@ class JobRunSerializer(serializers.Serializer):
def to_internal_value(self, data):
if isinstance(data, QueryDict):
data = data.copy()
for k, v in self.fields.items():
if isinstance(v, serializers.ManyRelatedField) and k not in data:
if isinstance(v, serializers.ManyRelatedField) and k not in data and k != "events":
data[k] = []
for fk in self.fields.keys():

View File

@@ -60,6 +60,8 @@ class NestedGiftCardSerializer(GiftCardSerializer):
class ReusableMediaSerializer(I18nAwareModelSerializer):
organizer = serializers.SlugRelatedField(slug_field='slug', read_only=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -111,6 +113,7 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
model = ReusableMedium
fields = (
'id',
'organizer',
'created',
'updated',
'type',

View File

@@ -19,7 +19,6 @@
# 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 json
import logging
import os
from collections import Counter, defaultdict
@@ -39,6 +38,7 @@ from rest_framework.exceptions import ValidationError
from rest_framework.relations import SlugRelatedField
from rest_framework.reverse import reverse
from pretix.api.serializers import CompatibleJSONField
from pretix.api.serializers.event import SubEventSerializer
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.item import (
@@ -535,8 +535,9 @@ class OrderPaymentTypeField(serializers.Field):
# TODO: Remove after pretix 2.2
def to_representation(self, instance: Order):
t = None
for p in instance.payments.all():
t = p.provider
if instance.pk:
for p in instance.payments.all():
t = p.provider
return t
@@ -544,10 +545,10 @@ class OrderPaymentDateField(serializers.DateField):
# TODO: Remove after pretix 2.2
def to_representation(self, instance: Order):
t = None
for p in instance.payments.all():
t = p.payment_date or t
if instance.pk:
for p in instance.payments.all():
t = p.payment_date or t
if t:
return super().to_representation(t.date())
@@ -895,19 +896,6 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
return data
class CompatibleJSONField(serializers.JSONField):
def to_internal_value(self, data):
try:
return json.dumps(data)
except (TypeError, ValueError):
self.fail('invalid')
def to_representation(self, value):
if value:
return json.loads(value)
return value
class WrappedList:
def __init__(self, data):
self._data = data
@@ -1363,6 +1351,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
answers.append(answ)
pos.answers = answers
pos.pseudonymization_id = "PREVIEW"
pos.checkins = []
pos_map[pos.positionid] = pos
else:
if pos.voucher:
@@ -1459,6 +1448,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
if simulate:
order.fees = fees
order.positions = pos_map.values()
order.payments = []
order.refunds = []
return order # ignore payments
else:
order.save(update_fields=['total'])

View File

@@ -70,6 +70,8 @@ class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerialize
def validate(self, data):
data = super().validate(data)
if 'order' in self.context:
data['order'] = self.context['order']
if data.get('addon_to'):
try:
data['addon_to'] = data['order'].positions.get(positionid=data['addon_to'])

View File

@@ -36,9 +36,9 @@ from pretix.api.serializers.settings import SettingsSerializer
from pretix.base.auth import get_auth_backends
from pretix.base.i18n import get_language_without_region
from pretix.base.models import (
Customer, Device, GiftCard, GiftCardTransaction, Membership,
MembershipType, OrderPosition, Organizer, ReusableMedium, SeatingPlan,
Team, TeamAPIToken, TeamInvite, User,
Customer, Device, GiftCard, GiftCardAcceptance, GiftCardTransaction,
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium,
SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
)
from pretix.base.models.seating import SeatingPlanLayoutValidator
from pretix.base.services.mail import SendMailException, mail
@@ -183,8 +183,11 @@ class GiftCardSerializer(I18nAwareModelSerializer):
qs = GiftCard.objects.filter(
secret=s
).filter(
Q(issuer=self.context["organizer"]) | Q(
issuer__gift_card_collector_acceptance__collector=self.context["organizer"])
Q(issuer=self.context["organizer"]) |
Q(issuer__in=GiftCardAcceptance.objects.filter(
acceptor=self.context["organizer"],
active=True,
).values_list('issuer', flat=True))
)
if self.instance:
qs = qs.exclude(pk=self.instance.pk)
@@ -248,6 +251,8 @@ class DeviceSerializer(serializers.ModelSerializer):
unique_serial = serializers.CharField(read_only=True)
hardware_brand = serializers.CharField(read_only=True)
hardware_model = serializers.CharField(read_only=True)
os_name = serializers.CharField(read_only=True)
os_version = serializers.CharField(read_only=True)
software_brand = serializers.CharField(read_only=True)
software_version = serializers.CharField(read_only=True)
created = serializers.DateTimeField(read_only=True)
@@ -260,7 +265,7 @@ class DeviceSerializer(serializers.ModelSerializer):
fields = (
'device_id', 'unique_serial', 'initialization_token', 'all_events', 'limit_events',
'revoked', 'name', 'created', 'initialized', 'hardware_brand', 'hardware_model',
'software_brand', 'software_version', 'security_profile'
'os_name', 'os_version', 'software_brand', 'software_version', 'security_profile'
)
@@ -387,6 +392,9 @@ class OrganizerSettingsSerializer(SettingsSerializer):
'reusable_media_type_nfc_uid',
'reusable_media_type_nfc_uid_autocreate_giftcard',
'reusable_media_type_nfc_uid_autocreate_giftcard_currency',
'reusable_media_type_nfc_mf0aes',
'reusable_media_type_nfc_mf0aes_autocreate_giftcard',
'reusable_media_type_nfc_mf0aes_autocreate_giftcard_currency',
]
def __init__(self, *args, **kwargs):

View File

@@ -35,8 +35,7 @@
import importlib
from django.apps import apps
from django.conf.urls import re_path
from django.urls import include
from django.urls import include, re_path
from rest_framework import routers
from pretix.api.views import cart

View File

@@ -396,7 +396,7 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force, checkin_type, ignore_unpaid, nonce,
untrusted_input, user, auth, expand, pdf_data, request, questions_supported, canceled_supported,
source_type='barcode', legacy_url_support=False):
source_type='barcode', legacy_url_support=False, simulate=False):
if not checkinlists:
raise ValidationError('No check-in list passed.')
@@ -433,6 +433,8 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
)
raw_barcode_for_checkin = None
from_revoked_secret = False
if simulate:
common_checkin_args['__fake_arg_to_prevent_this_from_being_saved'] = True
# 1. Gather a list of positions that could be the one we looking for, either from their ID, secret or
# parent secret
@@ -472,13 +474,14 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
revoked_matches = list(
RevokedTicketSecret.objects.filter(event_id__in=list_by_event.keys(), secret=raw_barcode))
if len(revoked_matches) == 0:
checkinlists[0].event.log_action('pretix.event.checkin.unknown', data={
'datetime': datetime,
'type': checkin_type,
'list': checkinlists[0].pk,
'barcode': raw_barcode,
'searched_lists': [cl.pk for cl in checkinlists]
}, user=user, auth=auth)
if not simulate:
checkinlists[0].event.log_action('pretix.event.checkin.unknown', data={
'datetime': datetime,
'type': checkin_type,
'list': checkinlists[0].pk,
'barcode': raw_barcode,
'searched_lists': [cl.pk for cl in checkinlists]
}, user=user, auth=auth)
for cl in checkinlists:
for k, s in cl.event.ticket_secret_generators.items():
@@ -492,12 +495,13 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
except:
pass
Checkin.objects.create(
position=None,
successful=False,
error_reason=Checkin.REASON_INVALID,
**common_checkin_args,
)
if not simulate:
Checkin.objects.create(
position=None,
successful=False,
error_reason=Checkin.REASON_INVALID,
**common_checkin_args,
)
if force and legacy_url_support and isinstance(auth, Device):
# There was a bug in libpretixsync: If you scanned a ticket in offline mode that was
@@ -539,19 +543,20 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
from_revoked_secret = True
else:
op = revoked_matches[0].position
op.order.log_action('pretix.event.checkin.revoked', data={
'datetime': datetime,
'type': checkin_type,
'list': list_by_event[revoked_matches[0].event_id].pk,
'barcode': raw_barcode
}, user=user, auth=auth)
common_checkin_args['list'] = list_by_event[revoked_matches[0].event_id]
Checkin.objects.create(
position=op,
successful=False,
error_reason=Checkin.REASON_REVOKED,
**common_checkin_args
)
if not simulate:
op.order.log_action('pretix.event.checkin.revoked', data={
'datetime': datetime,
'type': checkin_type,
'list': list_by_event[revoked_matches[0].event_id].pk,
'barcode': raw_barcode
}, user=user, auth=auth)
common_checkin_args['list'] = list_by_event[revoked_matches[0].event_id]
Checkin.objects.create(
position=op,
successful=False,
error_reason=Checkin.REASON_REVOKED,
**common_checkin_args
)
return Response({
'status': 'error',
'reason': Checkin.REASON_REVOKED,
@@ -588,24 +593,25 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
# We choose the first match (regardless of product) for the logging since it's most likely to be the
# base product according to our order_by above.
op = op_candidates[0]
op.order.log_action('pretix.event.checkin.denied', data={
'position': op.id,
'positionid': op.positionid,
'errorcode': Checkin.REASON_AMBIGUOUS,
'reason_explanation': None,
'force': force,
'datetime': datetime,
'type': checkin_type,
'list': list_by_event[op.order.event_id].pk,
}, user=user, auth=auth)
common_checkin_args['list'] = list_by_event[op.order.event_id]
Checkin.objects.create(
position=op,
successful=False,
error_reason=Checkin.REASON_AMBIGUOUS,
error_explanation=None,
**common_checkin_args,
)
if not simulate:
op.order.log_action('pretix.event.checkin.denied', data={
'position': op.id,
'positionid': op.positionid,
'errorcode': Checkin.REASON_AMBIGUOUS,
'reason_explanation': None,
'force': force,
'datetime': datetime,
'type': checkin_type,
'list': list_by_event[op.order.event_id].pk,
}, user=user, auth=auth)
common_checkin_args['list'] = list_by_event[op.order.event_id]
Checkin.objects.create(
position=op,
successful=False,
error_reason=Checkin.REASON_AMBIGUOUS,
error_explanation=None,
**common_checkin_args,
)
return Response({
'status': 'error',
'reason': Checkin.REASON_AMBIGUOUS,
@@ -652,6 +658,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
raw_barcode=raw_barcode_for_checkin,
raw_source_type=source_type,
from_revoked_secret=from_revoked_secret,
simulate=simulate,
)
except RequiredQuestionsError as e:
return Response({
@@ -664,23 +671,24 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
}, status=400)
except CheckInError as e:
op.order.log_action('pretix.event.checkin.denied', data={
'position': op.id,
'positionid': op.positionid,
'errorcode': e.code,
'reason_explanation': e.reason,
'force': force,
'datetime': datetime,
'type': checkin_type,
'list': list_by_event[op.order.event_id].pk,
}, user=user, auth=auth)
Checkin.objects.create(
position=op,
successful=False,
error_reason=e.code,
error_explanation=e.reason,
**common_checkin_args,
)
if not simulate:
op.order.log_action('pretix.event.checkin.denied', data={
'position': op.id,
'positionid': op.positionid,
'errorcode': e.code,
'reason_explanation': e.reason,
'force': force,
'datetime': datetime,
'type': checkin_type,
'list': list_by_event[op.order.event_id].pk,
}, user=user, auth=auth)
Checkin.objects.create(
position=op,
successful=False,
error_reason=e.code,
error_explanation=e.reason,
**common_checkin_args,
)
return Response({
'status': 'error',
'reason': e.code,

View File

@@ -19,8 +19,12 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import base64
import logging
from cryptography.hazmat.backends.openssl.backend import Backend
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import load_pem_public_key
from django.db.models import Exists, OuterRef, Q
from django.db.models.functions import Coalesce
from django.utils.timezone import now
@@ -34,6 +38,8 @@ from pretix.api.auth.device import DeviceTokenAuthentication
from pretix.api.views.version import numeric_version
from pretix.base.models import CheckinList, Device, SubEvent
from pretix.base.models.devices import Gate, generate_api_token
from pretix.base.models.media import MediumKeySet
from pretix.base.services.media import get_keysets_for_organizer
logger = logging.getLogger(__name__)
@@ -42,17 +48,73 @@ class InitializationRequestSerializer(serializers.Serializer):
token = serializers.CharField(max_length=190)
hardware_brand = serializers.CharField(max_length=190)
hardware_model = serializers.CharField(max_length=190)
os_name = serializers.CharField(max_length=190, required=False, allow_null=True)
os_version = serializers.CharField(max_length=190, required=False, allow_null=True)
software_brand = serializers.CharField(max_length=190)
software_version = serializers.CharField(max_length=190)
info = serializers.JSONField(required=False, allow_null=True)
rsa_pubkey = serializers.CharField(required=False, allow_null=True)
def validate(self, attrs):
if attrs.get('rsa_pubkey'):
try:
load_pem_public_key(
attrs['rsa_pubkey'].encode(), Backend()
)
except:
raise ValidationError({'rsa_pubkey': ['Not a valid public key.']})
return attrs
class UpdateRequestSerializer(serializers.Serializer):
hardware_brand = serializers.CharField(max_length=190)
hardware_model = serializers.CharField(max_length=190)
os_name = serializers.CharField(max_length=190, required=False, allow_null=True)
os_version = serializers.CharField(max_length=190, required=False, allow_null=True)
software_brand = serializers.CharField(max_length=190)
software_version = serializers.CharField(max_length=190)
info = serializers.JSONField(required=False, allow_null=True)
rsa_pubkey = serializers.CharField(required=False, allow_null=True)
def validate(self, attrs):
if attrs.get('rsa_pubkey'):
try:
load_pem_public_key(
attrs['rsa_pubkey'].encode(), Backend()
)
except:
raise ValidationError({'rsa_pubkey': ['Not a valid public key.']})
return attrs
class RSAEncryptedField(serializers.Field):
def to_representation(self, value):
public_key = load_pem_public_key(
self.context['device'].rsa_pubkey.encode(), Backend()
)
cipher_text = public_key.encrypt(
# RSA/ECB/PKCS1Padding
value,
padding.PKCS1v15()
)
return base64.b64encode(cipher_text).decode()
class MediumKeySetSerializer(serializers.ModelSerializer):
uid_key = RSAEncryptedField(read_only=True)
diversification_key = RSAEncryptedField(read_only=True)
organizer = serializers.SlugRelatedField(slug_field='slug', read_only=True)
class Meta:
model = MediumKeySet
fields = [
'public_id',
'organizer',
'active',
'media_type',
'uid_key',
'diversification_key',
]
class GateSerializer(serializers.ModelSerializer):
@@ -93,12 +155,19 @@ class InitializeView(APIView):
if device.initialized:
raise ValidationError({'token': ['This initialization token has already been used.']})
if device.revoked:
raise ValidationError({'token': ['This initialization token has been revoked.']})
device.initialized = now()
device.hardware_brand = serializer.validated_data.get('hardware_brand')
device.hardware_model = serializer.validated_data.get('hardware_model')
device.os_name = serializer.validated_data.get('os_name')
device.os_version = serializer.validated_data.get('os_version')
device.software_brand = serializer.validated_data.get('software_brand')
device.software_version = serializer.validated_data.get('software_version')
device.info = serializer.validated_data.get('info')
print(serializer.validated_data, request.data)
device.rsa_pubkey = serializer.validated_data.get('rsa_pubkey')
device.api_token = generate_api_token()
device.save()
@@ -117,8 +186,15 @@ class UpdateView(APIView):
device = request.auth
device.hardware_brand = serializer.validated_data.get('hardware_brand')
device.hardware_model = serializer.validated_data.get('hardware_model')
device.os_name = serializer.validated_data.get('os_name')
device.os_version = serializer.validated_data.get('os_version')
device.software_brand = serializer.validated_data.get('software_brand')
device.software_version = serializer.validated_data.get('software_version')
if serializer.validated_data.get('rsa_pubkey') and serializer.validated_data.get('rsa_pubkey') != device.rsa_pubkey:
if device.rsa_pubkey:
raise ValidationError({'rsa_pubkey': ['You cannot change the rsa_pubkey of the device once it is set.']})
else:
device.rsa_pubkey = serializer.validated_data.get('rsa_pubkey')
device.info = serializer.validated_data.get('info')
device.save()
device.log_action('pretix.device.updated', data=serializer.validated_data, auth=device)
@@ -166,8 +242,12 @@ class InfoView(APIView):
'pretix': __version__,
'pretix_numeric': numeric_version(__version__),
}
}
},
'medium_key_sets': MediumKeySetSerializer(
get_keysets_for_organizer(device.organizer),
many=True,
context={'device': request.auth}
).data if device.rsa_pubkey else []
})

View File

@@ -71,6 +71,8 @@ with scopes_disabled():
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
sales_channel = django_filters.rest_framework.CharFilter(method='sales_channel_qs')
search = django_filters.rest_framework.CharFilter(method='search_qs')
date_from = django_filters.rest_framework.IsoDateTimeFromToRangeFilter()
date_to = django_filters.rest_framework.IsoDateTimeFromToRangeFilter()
class Meta:
model = Event
@@ -336,6 +338,8 @@ with scopes_disabled():
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
sales_channel = django_filters.rest_framework.CharFilter(method='sales_channel_qs')
search = django_filters.rest_framework.CharFilter(method='search_qs')
date_from = django_filters.rest_framework.IsoDateTimeFromToRangeFilter()
date_to = django_filters.rest_framework.IsoDateTimeFromToRangeFilter()
class Meta:
model = SubEvent

View File

@@ -133,7 +133,12 @@ class EventExportersViewSet(ExportersMixin, viewsets.ViewSet):
def exporters(self):
exporters = []
responses = register_data_exporters.send(self.request.event)
for ex in sorted([response(self.request.event, self.request.organizer) for r, response in responses if response], key=lambda ex: str(ex.verbose_name)):
raw_exporters = [response(self.request.event, self.request.organizer) for r, response in responses if response]
raw_exporters = [
ex for ex in raw_exporters
if ex.available_for_user(self.request.user if self.request.user and self.request.user.is_authenticated else None)
]
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
ex._serializer = JobRunSerializer(exporter=ex)
exporters.append(ex)
return exporters
@@ -166,7 +171,7 @@ class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
if (
not isinstance(ex, OrganizerLevelExportMixin) or
perm_holder.has_organizer_permission(self.request.organizer, ex.organizer_required_permission, self.request)
)
) and ex.available_for_user(self.request.user if self.request.user and self.request.user.is_authenticated else None)
]
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
ex._serializer = JobRunSerializer(exporter=ex, events=events)

View File

@@ -39,7 +39,8 @@ from pretix.api.serializers.media import (
)
from pretix.base.media import MEDIA_TYPES
from pretix.base.models import (
Checkin, GiftCard, GiftCardTransaction, OrderPosition, ReusableMedium,
Checkin, GiftCard, GiftCardAcceptance, GiftCardTransaction, OrderPosition,
ReusableMedium,
)
from pretix.helpers import OF_SELF
from pretix.helpers.dicts import merge_dicts
@@ -103,6 +104,12 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
auth=self.request.auth,
data=merge_dicts(self.request.data, {'id': inst.pk})
)
mt = MEDIA_TYPES.get(serializer.validated_data["type"])
if mt:
m = mt.handle_new(self.request.organizer, inst, self.request.user, self.request.auth)
if m:
s = self.get_serializer(m)
return Response({"result": s.data})
@transaction.atomic()
def perform_update(self, serializer):
@@ -135,12 +142,28 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
s = self.get_serializer(m)
return Response({"result": s.data})
except ReusableMedium.DoesNotExist:
mt = MEDIA_TYPES.get(s.validated_data["type"])
if mt:
m = mt.handle_unknown(request.organizer, s.validated_data["identifier"], request.user, request.auth)
if m:
s = self.get_serializer(m)
return Response({"result": s.data})
try:
with scopes_disabled():
m = ReusableMedium.objects.get(
organizer__in=GiftCardAcceptance.objects.filter(
acceptor=request.organizer,
active=True,
reusable_media=True,
).values_list('issuer', flat=True),
type=s.validated_data["type"],
identifier=s.validated_data["identifier"],
)
m.linked_orderposition = None # not relevant for cross-organizer
m.customer = None # not relevant for cross-organizer
s = self.get_serializer(m)
return Response({"result": s.data})
except ReusableMedium.DoesNotExist:
mt = MEDIA_TYPES.get(s.validated_data["type"])
if mt:
m = mt.handle_unknown(request.organizer, s.validated_data["identifier"], request.user, request.auth)
if m:
s = self.get_serializer(m)
return Response({"result": s.data})
return Response({"result": None})

View File

@@ -23,9 +23,10 @@ import datetime
import mimetypes
import os
from decimal import Decimal
from zoneinfo import ZoneInfo
import django_filters
import pytz
from django.conf import settings
from django.db import transaction
from django.db.models import (
Exists, F, OuterRef, Prefetch, Q, Subquery, prefetch_related_objects,
@@ -612,7 +613,7 @@ class OrderViewSet(viewsets.ModelViewSet):
status=status.HTTP_400_BAD_REQUEST
)
tz = pytz.timezone(self.request.event.settings.timezone)
tz = ZoneInfo(self.request.event.settings.timezone)
new_date = make_aware(datetime.datetime.combine(
new_date,
datetime.time(hour=23, minute=59, second=59)
@@ -661,7 +662,16 @@ class OrderViewSet(viewsets.ModelViewSet):
with language(order.locale, self.request.event.settings.region):
payment = order.payments.last()
# OrderCreateSerializer creates at most one payment
if payment and payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED:
order.log_action(
'pretix.event.order.payment.confirmed', {
'local_id': payment.local_id,
'provider': payment.provider,
},
user=request.user if request.user.is_authenticated else None,
auth=request.auth,
)
order_placed.send(self.request.event, order=order)
if order.status == Order.STATUS_PAID:
order_paid.send(self.request.event, order=order)
@@ -936,6 +946,7 @@ with scopes_disabled():
| Q(addon_to__attendee_email__icontains=value)
| Q(order__code__istartswith=value)
| Q(order__invoice_address__name_cached__icontains=value)
| Q(order__invoice_address__company__icontains=value)
| Q(order__email__icontains=value)
| Q(pk__in=matching_media)
)
@@ -1181,7 +1192,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
ftype, ignored = mimetypes.guess_type(image_file.name)
extension = os.path.basename(image_file.name).split('.')[-1]
else:
img = Image.open(image_file)
img = Image.open(image_file, formats=settings.PILLOW_FORMATS_QUESTIONS_IMAGE)
ftype = Image.MIME[img.format]
extensions = {
'GIF': 'gif', 'TIFF': 'tif', 'BMP': 'bmp', 'JPEG': 'jpg', 'PNG': 'png'

View File

@@ -189,6 +189,19 @@ class ParametrizedOrderPositionWebhookEvent(ParametrizedOrderWebhookEvent):
return d
class ParametrizedWaitingListEntryWebhookEvent(ParametrizedWebhookEvent):
def build_payload(self, logentry: LogEntry):
# do not use content_object, this is also called in deletion
return {
'notification_id': logentry.pk,
'organizer': logentry.event.organizer.slug,
'event': logentry.event.slug,
'waitinglistentry': logentry.object_id,
'action': logentry.action_type,
}
@receiver(register_webhook_events, dispatch_uid="base_register_default_webhook_events")
def register_default_webhook_events(sender, **kwargs):
return (
@@ -321,6 +334,22 @@ def register_default_webhook_events(sender, **kwargs):
'pretix.event.testmode.deactivated',
_('Test-Mode of shop has been deactivated'),
),
ParametrizedWaitingListEntryWebhookEvent(
'pretix.event.orders.waitinglist.added',
_('Waiting list entry added'),
),
ParametrizedWaitingListEntryWebhookEvent(
'pretix.event.orders.waitinglist.changed',
_('Waiting list entry changed'),
),
ParametrizedWaitingListEntryWebhookEvent(
'pretix.event.orders.waitinglist.deleted',
_('Waiting list entry deleted'),
),
ParametrizedWaitingListEntryWebhookEvent(
'pretix.event.orders.waitinglist.voucher_assigned',
_('Waiting list entry received voucher'),
),
)

View File

@@ -37,8 +37,8 @@ import tempfile
from collections import OrderedDict, namedtuple
from decimal import Decimal
from typing import Optional, Tuple
from zoneinfo import ZoneInfo
import pytz
from defusedcsv import csv
from django import forms
from django.conf import settings
@@ -68,7 +68,7 @@ class BaseExporter:
self.events = event
self.event = None
e = self.events.first()
self.timezone = e.timezone if e else pytz.timezone(settings.TIME_ZONE)
self.timezone = e.timezone if e else ZoneInfo(settings.TIME_ZONE)
else:
self.events = Event.objects.filter(pk=event.pk)
self.timezone = event.timezone
@@ -140,7 +140,7 @@ class BaseExporter:
"""
return {}
def render(self, form_data: dict) -> Tuple[str, str, bytes]:
def render(self, form_data: dict) -> Tuple[str, str, Optional[bytes]]:
"""
Render the exported file and return a tuple consisting of a filename, a file type
and file content.
@@ -157,6 +157,13 @@ class BaseExporter:
"""
raise NotImplementedError() # NOQA
def available_for_user(self, user) -> bool:
"""
Allows to do additional checks whether an exporter is available based on the user who calls it. Note that
``user`` may be ``None`` e.g. during API usage.
"""
return True
class OrganizerLevelExportMixin:
@property

View File

@@ -34,8 +34,8 @@
from collections import OrderedDict
from decimal import Decimal
from zoneinfo import ZoneInfo
import pytz
from django import forms
from django.db.models import (
Case, CharField, Count, DateTimeField, F, IntegerField, Max, Min, OuterRef,
@@ -326,7 +326,7 @@ class OrderListExporter(MultiSheetListExporter):
yield self.ProgressSetTotal(total=qs.count())
for order in qs.order_by('datetime').iterator():
tz = pytz.timezone(self.event_object_cache[order.event_id].settings.timezone)
tz = ZoneInfo(self.event_object_cache[order.event_id].settings.timezone)
row = [
self.event_object_cache[order.event_id].slug,
@@ -459,7 +459,7 @@ class OrderListExporter(MultiSheetListExporter):
yield self.ProgressSetTotal(total=qs.count())
for op in qs.order_by('order__datetime').iterator():
order = op.order
tz = pytz.timezone(order.event.settings.timezone)
tz = ZoneInfo(order.event.settings.timezone)
row = [
self.event_object_cache[order.event_id].slug,
order.code,
@@ -631,7 +631,7 @@ class OrderListExporter(MultiSheetListExporter):
for op in ops:
order = op.order
tz = pytz.timezone(self.event_object_cache[order.event_id].settings.timezone)
tz = ZoneInfo(self.event_object_cache[order.event_id].settings.timezone)
row = [
self.event_object_cache[order.event_id].slug,
order.code,
@@ -850,6 +850,8 @@ class TransactionListExporter(ListExporter):
_('Tax rule ID'),
_('Tax rule'),
_('Tax value'),
_('Gross total'),
_('Tax total'),
]
if form_data.get('_format') == 'xlsx':
@@ -901,6 +903,8 @@ class TransactionListExporter(ListExporter):
t.tax_rule_id or '',
str(t.tax_rule.internal_name or t.tax_rule.name) if t.tax_rule_id else '',
t.tax_value,
t.price * t.count,
t.tax_value * t.count,
]
if form_data.get('_format') == 'xlsx':
@@ -1024,7 +1028,7 @@ class PaymentListExporter(ListExporter):
yield self.ProgressSetTotal(total=len(objs))
for obj in objs:
tz = pytz.timezone(obj.order.event.settings.timezone)
tz = ZoneInfo(obj.order.event.settings.timezone)
if isinstance(obj, OrderPayment) and obj.payment_date:
d2 = obj.payment_date.astimezone(tz).date().strftime('%Y-%m-%d')
elif isinstance(obj, OrderRefund) and obj.execution_date:
@@ -1143,7 +1147,7 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
def iterate_list(self, form_data):
qs = GiftCardTransaction.objects.filter(
card__issuer=self.organizer,
).order_by('datetime').select_related('card', 'order', 'order__event')
).order_by('datetime').select_related('card', 'order', 'order__event', 'acceptor')
if form_data.get('date_range'):
dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['date_range'], self.timezone)
@@ -1159,6 +1163,7 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
_('Amount'),
_('Currency'),
_('Order'),
_('Organizer'),
]
yield headers
@@ -1170,6 +1175,7 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
obj.value,
obj.card.currency,
obj.order.full_code if obj.order else None,
str(obj.acceptor or ""),
]
yield row
@@ -1203,7 +1209,7 @@ class GiftcardRedemptionListExporter(ListExporter):
yield headers
for obj in objs:
tz = pytz.timezone(obj.order.event.settings.timezone)
tz = ZoneInfo(obj.order.event.settings.timezone)
gc = GiftCard.objects.get(pk=obj.info_data.get('gift_card'))
row = [
obj.order.event.slug,

View File

@@ -20,8 +20,8 @@
# <https://www.gnu.org/licenses/>.
#
from collections import OrderedDict
from zoneinfo import ZoneInfo
import pytz
from django import forms
from django.db.models import F, Q
from django.dispatch import receiver
@@ -137,7 +137,7 @@ class WaitingListExporter(ListExporter):
# which event should be used to output dates in columns "Start date" and "End date"
event_for_date_columns = entry.subevent if entry.subevent else entry.event
tz = pytz.timezone(entry.event.settings.timezone)
tz = ZoneInfo(entry.event.settings.timezone)
datetime_format = '%Y-%m-%d %H:%M:%S'
row = [

View File

@@ -167,6 +167,7 @@ class SettingsForm(i18nfield.forms.I18nFormMixin, HierarkeyForm):
class PrefixForm(forms.Form):
prefix = forms.CharField(widget=forms.HiddenInput)
template_name = "django/forms/table.html"
class SafeSessionWizardView(SessionWizardView):

View File

@@ -38,10 +38,10 @@ import logging
from datetime import timedelta
from decimal import Decimal
from io import BytesIO
from zoneinfo import ZoneInfo
import dateutil.parser
import pycountry
import pytz
from django import forms
from django.conf import settings
from django.contrib import messages
@@ -61,6 +61,7 @@ from django.utils.timezone import get_current_timezone, now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_countries import countries
from django_countries.fields import Country, CountryField
from geoip2.errors import AddressNotFoundError
from phonenumber_field.formfields import PhoneNumberField
from phonenumber_field.phonenumber import PhoneNumber
from phonenumber_field.widgets import PhoneNumberPrefixWidget
@@ -356,9 +357,12 @@ class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
def guess_country_from_request(request, event):
if settings.HAS_GEOIP:
g = GeoIP2()
res = g.country(get_client_ip(request))
if res['country_code'] and len(res['country_code']) == 2:
return Country(res['country_code'])
try:
res = g.country(get_client_ip(request))
if res['country_code'] and len(res['country_code']) == 2:
return Country(res['country_code'])
except AddressNotFoundError:
pass
return guess_country(event)
@@ -496,14 +500,14 @@ class PortraitImageField(SizeValidationMixin, ExtValidationMixin, forms.FileFiel
file = BytesIO(data['content'])
try:
image = Image.open(file)
image = Image.open(file, formats=settings.PILLOW_FORMATS_QUESTIONS_IMAGE)
# verify() must be called immediately after the constructor.
image.verify()
# We want to do more than just verify(), so we need to re-open the file
if hasattr(file, 'seek'):
file.seek(0)
image = Image.open(file)
image = Image.open(file, formats=settings.PILLOW_FORMATS_QUESTIONS_IMAGE)
# load() is a potential DoS vector (see Django bug #18520), so we verify the size first
if image.width > 10_000 or image.height > 10_000:
@@ -562,7 +566,7 @@ class PortraitImageField(SizeValidationMixin, ExtValidationMixin, forms.FileFiel
return f
def __init__(self, *args, **kwargs):
kwargs.setdefault('ext_whitelist', (".png", ".jpg", ".jpeg", ".jfif", ".tif", ".tiff", ".bmp"))
kwargs.setdefault('ext_whitelist', settings.FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE)
kwargs.setdefault('max_size', settings.FILE_UPLOAD_MAX_SIZE_IMAGE)
super().__init__(*args, **kwargs)
@@ -733,7 +737,7 @@ class BaseQuestionsForm(forms.Form):
initial = answers[0]
else:
initial = None
tz = pytz.timezone(event.settings.timezone)
tz = ZoneInfo(event.settings.timezone)
help_text = rich_text(q.help_text)
label = escape(q.question) # django-bootstrap3 calls mark_safe
required = q.required and not self.all_optional
@@ -822,11 +826,7 @@ class BaseQuestionsForm(forms.Form):
help_text=help_text,
initial=initial.file if initial else None,
widget=UploadedFileWidget(position=pos, event=event, answer=initial),
ext_whitelist=(
".png", ".jpg", ".gif", ".jpeg", ".pdf", ".txt", ".docx", ".gif", ".svg",
".pptx", ".ppt", ".doc", ".xlsx", ".xls", ".jfif", ".heic", ".heif", ".pages",
".bmp", ".tif", ".tiff"
),
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_OTHER,
max_size=settings.FILE_UPLOAD_MAX_SIZE_OTHER,
)
elif q.type == Question.TYPE_DATE:

View File

@@ -0,0 +1,63 @@
#
# 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/>.
#
from bootstrap3.renderers import (
FieldRenderer as BaseFieldRenderer,
InlineFieldRenderer as BaseInlineFieldRenderer,
)
from django.forms import (
CheckboxInput, CheckboxSelectMultiple, ClearableFileInput, RadioSelect,
SelectDateWidget,
)
class FieldRenderer(BaseFieldRenderer):
# Local application of https://github.com/zostera/django-bootstrap3/pull/859
def post_widget_render(self, html):
if isinstance(self.widget, CheckboxSelectMultiple):
html = self.list_to_class(html, "checkbox")
elif isinstance(self.widget, RadioSelect):
html = self.list_to_class(html, "radio")
elif isinstance(self.widget, SelectDateWidget):
html = self.fix_date_select_input(html)
elif isinstance(self.widget, ClearableFileInput):
html = self.fix_clearable_file_input(html)
elif isinstance(self.widget, CheckboxInput):
html = self.put_inside_label(html)
return html
class InlineFieldRenderer(BaseInlineFieldRenderer):
# Local application of https://github.com/zostera/django-bootstrap3/pull/859
def post_widget_render(self, html):
if isinstance(self.widget, CheckboxSelectMultiple):
html = self.list_to_class(html, "checkbox")
elif isinstance(self.widget, RadioSelect):
html = self.list_to_class(html, "radio")
elif isinstance(self.widget, SelectDateWidget):
html = self.fix_date_select_input(html)
elif isinstance(self.widget, ClearableFileInput):
html = self.fix_clearable_file_input(html)
elif isinstance(self.widget, CheckboxInput):
html = self.put_inside_label(html)
return html

View File

@@ -24,7 +24,7 @@ Django, for theoretically very valid reasons, creates migrations for *every sing
we change on a model. Even the `help_text`! This makes sense, as we don't know if any
database backend unknown to us might actually use this information for its database schema.
However, pretix only supports PostgreSQL, MySQL, MariaDB and SQLite and we can be pretty
However, pretix only supports PostgreSQL and SQLite and we can be pretty
certain that some changes to models will never require a change to the database. In this case,
not creating a migration for certain changes will save us some performance while applying them
*and* allow for a cleaner git history. Win-win!

View File

@@ -22,7 +22,7 @@
import json
import sys
import pytz
import pytz_deprecation_shim
from django.core.management.base import BaseCommand
from django.utils.timezone import override
from django_scopes import scope
@@ -60,7 +60,7 @@ class Command(BaseCommand):
sys.exit(1)
locale = options.get("locale", None)
timezone = pytz.timezone(options['timezone']) if options.get('timezone') else None
timezone = pytz_deprecation_shim.timezone(options['timezone']) if options.get('timezone') else None
with scope(organizer=o):
if options['event_slug']:

View File

@@ -49,6 +49,9 @@ class BaseMediaType:
def handle_unknown(self, organizer, identifier, user, auth):
pass
def handle_new(self, organizer, medium, user, auth):
pass
def __str__(self):
return str(self.verbose_name)
@@ -108,9 +111,43 @@ class NfcUidMediaType(BaseMediaType):
return m
class NfcMf0aesMediaType(BaseMediaType):
identifier = 'nfc_mf0aes'
verbose_name = 'NFC Mifare Ultralight AES'
medium_created_by_server = False
supports_giftcard = True
supports_orderposition = False
def handle_new(self, organizer, medium, user, auth):
from pretix.base.models import GiftCard
if organizer.settings.get(f'reusable_media_type_{self.identifier}_autocreate_giftcard', as_type=bool):
with transaction.atomic():
gc = GiftCard.objects.create(
issuer=organizer,
expires=organizer.default_gift_card_expiry,
currency=organizer.settings.get(f'reusable_media_type_{self.identifier}_autocreate_giftcard_currency'),
)
medium.linked_giftcard = gc
medium.save()
medium.log_action(
'pretix.reusable_medium.linked_giftcard.changed',
user=user, auth=auth,
data={
'linked_giftcard': gc.pk
}
)
gc.log_action(
'pretix.giftcards.created',
user=user, auth=auth,
)
return medium
MEDIA_TYPES = {
m.identifier: m for m in [
BarcodePlainMediaType(),
NfcUidMediaType(),
NfcMf0aesMediaType(),
]
}

View File

@@ -21,12 +21,12 @@
#
from collections import OrderedDict
from urllib.parse import urlsplit
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
import pytz
from django.conf import settings
from django.http import Http404, HttpRequest, HttpResponse
from django.middleware.common import CommonMiddleware
from django.urls import get_script_prefix
from django.urls import get_script_prefix, resolve
from django.utils import timezone, translation
from django.utils.cache import patch_vary_headers
from django.utils.deprecation import MiddlewareMixin
@@ -98,9 +98,9 @@ class LocaleMiddleware(MiddlewareMixin):
tzname = request.user.timezone
if tzname:
try:
timezone.activate(pytz.timezone(tzname))
timezone.activate(ZoneInfo(tzname))
request.timezone = tzname
except pytz.UnknownTimeZoneError:
except ZoneInfoNotFoundError:
pass
else:
timezone.deactivate()
@@ -230,6 +230,8 @@ class SecurityMiddleware(MiddlewareMixin):
)
def process_response(self, request, resp):
url = resolve(request.path_info)
if settings.DEBUG and resp.status_code >= 400:
# Don't use CSP on debug error page as it breaks of Django's fancy error
# pages
@@ -249,20 +251,26 @@ class SecurityMiddleware(MiddlewareMixin):
h = {
'default-src': ["{static}"],
'script-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
'script-src': ['{static}'],
'object-src': ["'none'"],
'frame-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
'frame-src': ['{static}'],
'style-src': ["{static}", "{media}"],
'connect-src': ["{dynamic}", "{media}", "https://checkout.stripe.com"],
'img-src': ["{static}", "{media}", "data:", "https://*.stripe.com"] + img_src,
'connect-src': ["{dynamic}", "{media}"],
'img-src': ["{static}", "{media}", "data:"] + img_src,
'font-src': ["{static}"],
'media-src': ["{static}", "data:"],
# form-action is not only used to match on form actions, but also on URLs
# form-actions redirect to. In the context of e.g. payment providers or
# single-sign-on this can be nearly anything so we cannot really restrict
# single-sign-on this can be nearly anything, so we cannot really restrict
# this. However, we'll restrict it to HTTPS.
'form-action': ["{dynamic}", "https:"] + (['http:'] if settings.SITE_URL.startswith('http://') else []),
}
# Only include pay.google.com for wallet detection purposes on the Payment selection page
if (
url.url_name == "event.order.pay.change" or
(url.url_name == "event.checkout" and url.kwargs['step'] == "payment")
):
h['script-src'].append('https://pay.google.com')
if settings.LOG_CSP:
h['report-uri'] = ["/csp_report/"]
if 'Content-Security-Policy' in resp:

View File

@@ -2,6 +2,8 @@
# Generated by Django 1.10.4 on 2017-02-03 14:21
from __future__ import unicode_literals
from zoneinfo import ZoneInfo
import django.core.validators
import django.db.migrations.operations.special
import django.db.models.deletion
@@ -26,7 +28,7 @@ def forwards42(apps, schema_editor):
for s in EventSetting.objects.filter(key='timezone').values('object_id', 'value')
}
for order in Order.objects.all():
tz = pytz.timezone(etz.get(order.event_id, 'UTC'))
tz = ZoneInfo(etz.get(order.event_id, 'UTC'))
order.expires = order.expires.astimezone(tz).replace(hour=23, minute=59, second=59)
order.save()

View File

@@ -2,9 +2,9 @@
# Generated by Django 1.10.2 on 2016-10-19 17:57
from __future__ import unicode_literals
import pytz
from zoneinfo import ZoneInfo
from django.db import migrations
from django.utils import timezone
def forwards(apps, schema_editor):
@@ -15,7 +15,7 @@ def forwards(apps, schema_editor):
for s in EventSetting.objects.filter(key='timezone').values('object_id', 'value')
}
for order in Order.objects.all():
tz = pytz.timezone(etz.get(order.event_id, 'UTC'))
tz = ZoneInfo(etz.get(order.event_id, 'UTC'))
order.expires = order.expires.astimezone(tz).replace(hour=23, minute=59, second=59)
order.save()

View File

@@ -3,7 +3,6 @@
from django.core.exceptions import ImproperlyConfigured
from django.db import migrations, models
from django_mysql.checks import mysql_connections
def set_attendee_name_parts(apps, schema_editor):
@@ -24,40 +23,12 @@ def set_attendee_name_parts(apps, schema_editor):
ia.save(update_fields=['name_parts'])
def check_mysqlversion(apps, schema_editor):
errors = []
any_conn_works = False
conns = list(mysql_connections())
found = 'Unknown version'
for alias, conn in conns:
if hasattr(conn, 'mysql_is_mariadb') and conn.mysql_is_mariadb and hasattr(conn, 'mysql_version'):
if conn.mysql_version >= (10, 2, 7):
any_conn_works = True
else:
found = 'MariaDB ' + '.'.join(str(v) for v in conn.mysql_version)
elif hasattr(conn, 'mysql_version'):
if conn.mysql_version >= (5, 7):
any_conn_works = True
else:
found = 'MySQL ' + '.'.join(str(v) for v in conn.mysql_version)
if conns and not any_conn_works:
raise ImproperlyConfigured(
'As of pretix 2.2, you need MySQL 5.7+ or MariaDB 10.2.7+ to run pretix. However, we detected a '
'database connection to {}'.format(found)
)
return errors
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0101_auto_20181025_2255'),
]
operations = [
migrations.RunPython(
check_mysqlversion, migrations.RunPython.noop
),
migrations.RenameField(
model_name='cartposition',
old_name='attendee_name',

View File

@@ -1,8 +1,7 @@
# Generated by Django 3.2.4 on 2021-09-30 10:25
from datetime import datetime
from datetime import datetime, timezone
from django.db import migrations, models
from pytz import UTC
class Migration(migrations.Migration):
@@ -15,7 +14,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='invoice',
name='sent_to_customer',
field=models.DateTimeField(blank=True, null=True, default=UTC.localize(datetime(1970, 1, 1, 0, 0, 0, 0))),
field=models.DateTimeField(blank=True, null=True, default=datetime(1970, 1, 1, 0, 0, 0, 0, tzinfo=timezone.utc)),
preserve_default=False,
),
]

View File

@@ -50,6 +50,6 @@ class Migration(migrations.Migration):
],
options={
'unique_together': {('event', 'secret')},
} if 'mysql' not in settings.DATABASES['default']['ENGINE'] else {}
}
),
]

View File

@@ -0,0 +1,38 @@
# Generated by Django 3.2.18 on 2023-05-12 10:08
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0241_itemmetaproperties_required_values'),
]
operations = [
migrations.RenameField(
model_name='giftcardacceptance',
old_name='collector',
new_name='acceptor',
),
migrations.AddField(
model_name='giftcardacceptance',
name='active',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='giftcardacceptance',
name='reusable_media',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='giftcardacceptance',
name='issuer',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gift_card_acceptor_acceptance', to='pretixbase.organizer'),
),
migrations.AlterUniqueTogether(
name='giftcardacceptance',
unique_together={('issuer', 'acceptor')},
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 4.1.9 on 2023-06-26 10:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0242_auto_20230512_1008'),
]
operations = [
migrations.AddField(
model_name='device',
name='os_name',
field=models.CharField(max_length=190, null=True),
),
migrations.AddField(
model_name='device',
name='os_version',
field=models.CharField(max_length=190, null=True),
),
]

View File

@@ -0,0 +1,35 @@
# Generated by Django 3.2.18 on 2023-05-17 11:32
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0243_device_os_name_and_os_version'),
]
operations = [
migrations.AddField(
model_name='device',
name='rsa_pubkey',
field=models.TextField(null=True),
),
migrations.CreateModel(
name='MediumKeySet',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('public_id', models.BigIntegerField(unique=True)),
('media_type', models.CharField(max_length=100)),
('active', models.BooleanField(default=True)),
('uid_key', models.BinaryField()),
('diversification_key', models.BinaryField()),
('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='medium_key_sets', to='pretixbase.organizer')),
],
),
migrations.AddConstraint(
model_name='mediumkeyset',
constraint=models.UniqueConstraint(condition=models.Q(('active', True)), fields=('organizer', 'media_type'), name='keyset_unique_active'),
),
]

View File

@@ -178,7 +178,7 @@ class LoggedModel(models.Model, LoggingMixin):
return LogEntry.objects.filter(
content_type=self.logs_content_type, object_id=self.pk
).select_related('user', 'event', 'oauth_application', 'api_token', 'device')
).select_related('user', 'event', 'event__organizer', 'oauth_application', 'api_token', 'device')
class LockModel:

View File

@@ -121,14 +121,23 @@ class Customer(LoggedModel):
if self.email:
self.email = self.email.lower()
if 'update_fields' in kwargs and 'last_modified' not in kwargs['update_fields']:
kwargs['update_fields'] = list(kwargs['update_fields']) + ['last_modified']
kwargs['update_fields'] = {'last_modified'}.union(kwargs['update_fields'])
if not self.identifier:
self.assign_identifier()
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'identifier'}.union(kwargs['update_fields'])
if self.name_parts:
self.name_cached = self.name
name = self.name
if self.name_cached != name:
self.name_cached = name
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'name_cached'}.union(kwargs['update_fields'])
else:
self.name_cached = ""
self.name_parts = {}
if self.name_cached != "" or self.name_parts != {}:
self.name_cached = ""
self.name_parts = {}
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'name_cached', 'name_parts'}.union(kwargs['update_fields'])
super().save(**kwargs)
def anonymize(self):

View File

@@ -98,6 +98,8 @@ class Gate(LoggedModel):
if not Gate.objects.filter(organizer=self.organizer, identifier=code).exists():
self.identifier = code
break
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'identifier'}.union(kwargs['update_fields'])
return super().save(*args, **kwargs)
@@ -141,6 +143,14 @@ class Device(LoggedModel):
max_length=190,
null=True, blank=True
)
os_name = models.CharField(
max_length=190,
null=True, blank=True
)
os_version = models.CharField(
max_length=190,
null=True, blank=True
)
software_brand = models.CharField(
max_length=190,
null=True, blank=True
@@ -156,6 +166,10 @@ class Device(LoggedModel):
null=True,
blank=False
)
rsa_pubkey = models.TextField(
null=True,
blank=True,
)
info = models.JSONField(
null=True, blank=True,
)
@@ -173,6 +187,8 @@ class Device(LoggedModel):
def save(self, *args, **kwargs):
if not self.device_id:
self.device_id = (self.organizer.devices.aggregate(m=Max('device_id'))['m'] or 0) + 1
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'device_id'}.union(kwargs['update_fields'])
super().save(*args, **kwargs)
def permission_set(self) -> set:

View File

@@ -40,8 +40,9 @@ from collections import Counter, OrderedDict, defaultdict
from datetime import datetime, time, timedelta
from operator import attrgetter
from urllib.parse import urljoin
from zoneinfo import ZoneInfo
import pytz
import pytz_deprecation_shim
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.files.storage import default_storage
@@ -214,7 +215,7 @@ class EventMixin:
@property
def timezone(self):
return pytz.timezone(self.settings.timezone)
return pytz_deprecation_shim.timezone(self.settings.timezone)
@property
def effective_presale_end(self):
@@ -773,7 +774,7 @@ class Event(EventMixin, LoggedModel):
"""
The last datetime of payments for this event.
"""
tz = pytz.timezone(self.settings.timezone)
tz = ZoneInfo(self.settings.timezone)
return make_aware(datetime.combine(
self.settings.get('payment_term_last', as_type=RelativeDateWrapper).datetime(self).date(),
time(hour=23, minute=59, second=59)
@@ -1276,6 +1277,9 @@ class Event(EventMixin, LoggedModel):
return not self.orders.exists() and not self.invoices.exists()
def delete_sub_objects(self):
from .checkin import Checkin
Checkin.all.filter(successful=False, list__event=self).delete()
self.cartposition_set.filter(addon_to__isnull=False).delete()
self.cartposition_set.all().delete()
self.vouchers.all().delete()

View File

@@ -19,10 +19,11 @@
# 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 zoneinfo
from datetime import datetime, timedelta
import pytz
from dateutil.rrule import rrulestr
from dateutil.tz import datetime_exists
from django.conf import settings
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
@@ -108,12 +109,9 @@ class AbstractScheduledExport(LoggedModel):
self.schedule_next_run = None
return
try:
self.schedule_next_run = make_aware(datetime.combine(new_d.date(), self.schedule_rrule_time), tz)
except pytz.exceptions.AmbiguousTimeError:
self.schedule_next_run = make_aware(datetime.combine(new_d.date(), self.schedule_rrule_time), tz, is_dst=False)
except pytz.exceptions.NonExistentTimeError:
self.schedule_next_run = make_aware(datetime.combine(new_d.date(), self.schedule_rrule_time) + timedelta(hours=1), tz)
self.schedule_next_run = make_aware(datetime.combine(new_d.date(), self.schedule_rrule_time), tz)
if not datetime_exists(self.schedule_next_run):
self.schedule_next_run += timedelta(hours=1)
class ScheduledEventExport(AbstractScheduledExport):
@@ -136,4 +134,4 @@ class ScheduledOrganizerExport(AbstractScheduledExport):
@property
def tz(self):
return pytz.timezone(self.timezone)
return zoneinfo.ZoneInfo(self.timezone)

View File

@@ -46,14 +46,19 @@ def gen_giftcard_secret(length=8):
class GiftCardAcceptance(models.Model):
issuer = models.ForeignKey(
'Organizer',
related_name='gift_card_collector_acceptance',
related_name='gift_card_acceptor_acceptance',
on_delete=models.CASCADE
)
collector = models.ForeignKey(
acceptor = models.ForeignKey(
'Organizer',
related_name='gift_card_issuer_acceptance',
on_delete=models.CASCADE
)
active = models.BooleanField(default=True)
reusable_media = models.BooleanField(default=False)
class Meta:
unique_together = (('issuer', 'acceptor'),)
class GiftCard(LoggedModel):
@@ -114,7 +119,7 @@ class GiftCard(LoggedModel):
return self.transactions.aggregate(s=Sum('value'))['s'] or Decimal('0.00')
def accepted_by(self, organizer):
return self.issuer == organizer or GiftCardAcceptance.objects.filter(issuer=self.issuer, collector=organizer).exists()
return self.issuer == organizer or GiftCardAcceptance.objects.filter(issuer=self.issuer, acceptor=organizer, active=True).exists()
def save(self, *args, **kwargs):
if not self.secret:

View File

@@ -251,14 +251,20 @@ class Invoice(models.Model):
raise ValueError('Every invoice needs to be connected to an order')
if not self.event:
self.event = self.order.event
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'event'}.union(kwargs['update_fields'])
if not self.organizer:
self.organizer = self.order.event.organizer
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'organizer'}.union(kwargs['update_fields'])
if not self.prefix:
self.prefix = self.event.settings.invoice_numbers_prefix or (self.event.slug.upper() + '-')
if self.is_cancellation:
self.prefix = self.event.settings.invoice_numbers_prefix_cancellations or self.prefix
if '%' in self.prefix:
self.prefix = self.date.strftime(self.prefix)
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'prefix'}.union(kwargs['update_fields'])
if not self.invoice_no:
if self.order.testmode:
@@ -276,8 +282,13 @@ class Invoice(models.Model):
# Suppress duplicate key errors and try again
if i == 9:
raise
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'invoice_no'}.union(kwargs['update_fields'])
self.full_invoice_no = self.prefix + self.invoice_no
if self.full_invoice_no != self.prefix + self.invoice_no:
self.full_invoice_no = self.prefix + self.invoice_no
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'full_invoice_no'}.union(kwargs['update_fields'])
return super().save(*args, **kwargs)
def delete(self, *args, **kwargs):

View File

@@ -40,9 +40,10 @@ from collections import Counter, OrderedDict
from datetime import date, datetime, time, timedelta
from decimal import Decimal, DecimalException
from typing import Optional, Tuple
from zoneinfo import ZoneInfo
import dateutil.parser
import pytz
from dateutil.tz import datetime_exists
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import (
@@ -927,22 +928,22 @@ class Item(LoggedModel):
)
if self.validity_dynamic_duration_days:
replace_date += timedelta(days=self.validity_dynamic_duration_days)
valid_until = tz.localize(valid_until.replace(
valid_until = valid_until.replace(
year=replace_date.year,
month=replace_date.month,
day=replace_date.day,
hour=23, minute=59, second=59, microsecond=0,
tzinfo=None,
))
tzinfo=tz,
)
elif self.validity_dynamic_duration_days:
replace_date = valid_until.date() + timedelta(days=self.validity_dynamic_duration_days - 1)
valid_until = tz.localize(valid_until.replace(
valid_until = valid_until.replace(
year=replace_date.year,
month=replace_date.month,
day=replace_date.day,
hour=23, minute=59, second=59, microsecond=0,
tzinfo=None
))
tzinfo=tz
)
if self.validity_dynamic_duration_hours:
valid_until += timedelta(hours=self.validity_dynamic_duration_hours)
@@ -950,6 +951,9 @@ class Item(LoggedModel):
if self.validity_dynamic_duration_minutes:
valid_until += timedelta(minutes=self.validity_dynamic_duration_minutes)
if not datetime_exists(valid_until):
valid_until += timedelta(hours=1)
return requested_start, valid_until
else:
@@ -1589,6 +1593,8 @@ class Question(LoggedModel):
if not Question.objects.filter(event=self.event, identifier=code).exists():
self.identifier = code
break
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'identifier'}.union(kwargs['update_fields'])
super().save(*args, **kwargs)
if self.event:
self.event.cache.clear()
@@ -1678,7 +1684,7 @@ class Question(LoggedModel):
try:
dt = dateutil.parser.parse(answer)
if is_naive(dt):
dt = make_aware(dt, pytz.timezone(self.event.settings.timezone))
dt = make_aware(dt, ZoneInfo(self.event.settings.timezone))
except:
raise ValidationError(_('Invalid datetime input.'))
else:
@@ -1736,6 +1742,8 @@ class QuestionOption(models.Model):
if not QuestionOption.objects.filter(question__event=self.question.event, identifier=code).exists():
self.identifier = code
break
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'identifier'}.union(kwargs['update_fields'])
super().save(*args, **kwargs)
@staticmethod

View File

@@ -123,3 +123,25 @@ class ReusableMedium(LoggedModel):
unique_together = (("identifier", "type", "organizer"),)
index_together = (("identifier", "type", "organizer"), ("updated", "id"))
ordering = "identifier", "type", "organizer"
class MediumKeySet(models.Model):
organizer = models.ForeignKey('Organizer', on_delete=models.CASCADE, related_name='medium_key_sets')
public_id = models.BigIntegerField(
unique=True,
)
media_type = models.CharField(max_length=100)
active = models.BooleanField(default=True)
uid_key = models.BinaryField()
diversification_key = models.BinaryField()
objects = ScopedManager(organizer='organizer')
class Meta:
constraints = [
models.UniqueConstraint(
fields=["organizer", "media_type"],
condition=Q(active=True),
name="keyset_unique_active",
),
]

View File

@@ -42,10 +42,10 @@ from collections import Counter
from datetime import datetime, time, timedelta
from decimal import Decimal
from typing import Any, Dict, List, Union
from zoneinfo import ZoneInfo
import dateutil
import pycountry
import pytz
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models, transaction
@@ -461,14 +461,20 @@ class Order(LockModel, LoggedModel):
return '{event}-{code}'.format(event=self.event.slug.upper(), code=self.code)
def save(self, **kwargs):
if 'update_fields' in kwargs and 'last_modified' not in kwargs['update_fields']:
kwargs['update_fields'] = list(kwargs['update_fields']) + ['last_modified']
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'last_modified'}.union(kwargs['update_fields'])
if not self.code:
self.assign_code()
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'code'}.union(kwargs['update_fields'])
if not self.datetime:
self.datetime = now()
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'datetime'}.union(kwargs['update_fields'])
if not self.expires:
self.set_expires()
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'expires'}.union(kwargs['update_fields'])
is_new = not self.pk
update_fields = kwargs.get('update_fields', [])
@@ -496,7 +502,7 @@ class Order(LockModel, LoggedModel):
def set_expires(self, now_dt=None, subevents=None):
now_dt = now_dt or now()
tz = pytz.timezone(self.event.settings.timezone)
tz = ZoneInfo(self.event.settings.timezone)
mode = self.event.settings.get('payment_term_mode')
if mode == 'days':
exp_by_date = now_dt.astimezone(tz) + timedelta(days=self.event.settings.get('payment_term_days', as_type=int))
@@ -870,7 +876,7 @@ class Order(LockModel, LoggedModel):
@property
def payment_term_last(self):
tz = pytz.timezone(self.event.settings.timezone)
tz = ZoneInfo(self.event.settings.timezone)
term_last = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if term_last:
if self.event.has_subevents:
@@ -890,6 +896,33 @@ class Order(LockModel, LoggedModel):
), tz)
return term_last
@property
def payment_term_expire_date(self):
delay = self.event.settings.get('payment_term_expire_delay_days', as_type=int)
if not delay: # performance saver + backwards compatibility
return self.expires
term_last = self.payment_term_last
if term_last and self.expires > term_last: # backwards compatibility
return self.expires
expires = self.expires.date() + timedelta(days=delay)
if self.event.settings.get('payment_term_weekdays'):
if expires.weekday() == 5:
expires += timedelta(days=2)
elif expires.weekday() == 6:
expires += timedelta(days=1)
tz = ZoneInfo(self.event.settings.timezone)
expires = make_aware(datetime.combine(
expires,
time(hour=23, minute=59, second=59)
), tz)
if term_last:
return min(expires, term_last)
else:
return expires
def _can_be_paid(self, count_waitinglist=True, ignore_date=False, force=False) -> Union[bool, str]:
error_messages = {
'late_lastdate': _("The payment can not be accepted as the last date of payments configured in the "
@@ -1213,7 +1246,7 @@ class QuestionAnswer(models.Model):
@property
def is_image(self):
return any(self.file.name.lower().endswith(e) for e in ('.jpg', '.png', '.gif', '.tiff', '.bmp', '.jpeg'))
return any(self.file.name.lower().endswith(e) for e in settings.FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE)
@property
def file_name(self):
@@ -1230,7 +1263,7 @@ class QuestionAnswer(models.Model):
try:
d = dateutil.parser.parse(self.answer)
if self.orderposition:
tz = pytz.timezone(self.orderposition.order.event.settings.timezone)
tz = ZoneInfo(self.orderposition.order.event.settings.timezone)
d = d.astimezone(tz)
return date_format(d, "SHORT_DATETIME_FORMAT")
except ValueError:
@@ -1442,12 +1475,20 @@ class AbstractPosition(models.Model):
else self.variation.quotas.filter(subevent=self.subevent))
def save(self, *args, **kwargs):
update_fields = kwargs.get('update_fields', [])
update_fields = kwargs.get('update_fields', set())
if 'attendee_name_parts' in update_fields:
update_fields.append('attendee_name_cached')
self.attendee_name_cached = self.attendee_name
kwargs['update_fields'] = {'attendee_name_cached'}.union(kwargs['update_fields'])
name = self.attendee_name
if name != self.attendee_name_cached:
self.attendee_name_cached = name
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'attendee_name_cached'}.union(kwargs['update_fields'])
if self.attendee_name_parts is None:
self.attendee_name_parts = {}
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'attendee_name_parts'}.union(kwargs['update_fields'])
super().save(*args, **kwargs)
@property
@@ -1631,12 +1672,13 @@ class OrderPayment(models.Model):
if status_change:
self.order.create_transactions()
def fail(self, info=None, user=None, auth=None, log_data=None):
def fail(self, info=None, user=None, auth=None, log_data=None, send_mail=True):
"""
Marks the order as failed and sets info to ``info``, but only if the order is in ``created`` or ``pending``
state. This is equivalent to setting ``state`` to ``OrderPayment.PAYMENT_STATE_FAILED`` and logging a failure,
but it adds strong database logging since we do not want to report a failure for an order that has just
been marked as paid.
:param send_mail: Whether an email should be sent to the user about this event (default: ``True``).
"""
with transaction.atomic():
locked_instance = OrderPayment.objects.select_for_update(of=OF_SELF).get(pk=self.pk)
@@ -1661,6 +1703,17 @@ class OrderPayment(models.Model):
'info': info,
'data': log_data,
}, user=user, auth=auth)
if send_mail:
with language(self.order.locale, self.order.event.settings.region):
email_subject = self.order.event.settings.mail_subject_order_payment_failed
email_template = self.order.event.settings.mail_text_order_payment_failed
email_context = get_email_context(event=self.order.event, order=self.order)
self.order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.payment_failed', user=user, auth=auth,
)
return True
def confirm(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text='',
@@ -1827,6 +1880,8 @@ class OrderPayment(models.Model):
def save(self, *args, **kwargs):
if not self.local_id:
self.local_id = (self.order.payments.aggregate(m=Max('local_id'))['m'] or 0) + 1
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'local_id'}.union(kwargs['update_fields'])
super().save(*args, **kwargs)
def create_external_refund(self, amount=None, execution_date=None, info='{}'):
@@ -2025,6 +2080,8 @@ class OrderRefund(models.Model):
def save(self, *args, **kwargs):
if not self.local_id:
self.local_id = (self.order.refunds.aggregate(m=Max('local_id'))['m'] or 0) + 1
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'local_id'}.union(kwargs['update_fields'])
super().save(*args, **kwargs)
@@ -2443,14 +2500,20 @@ class OrderPosition(AbstractPosition):
assign_ticket_secret(
event=self.order.event, position=self, force_invalidate=True, save=False
)
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'secret'}.union(kwargs['update_fields'])
if not self.blocked:
if not self.blocked and self.blocked is not None:
self.blocked = None
elif not isinstance(self.blocked, list) or any(not isinstance(b, str) for b in self.blocked):
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'blocked'}.union(kwargs['update_fields'])
elif self.blocked and (not isinstance(self.blocked, list) or any(not isinstance(b, str) for b in self.blocked)):
raise TypeError("blocked needs to be a list of strings")
if not self.pseudonymization_id:
self.assign_pseudonymization_id()
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'pseudonymization_id'}.union(kwargs['update_fields'])
if not self.get_deferred_fields():
if Transaction.key(self) != self.__initial_transaction_key or self.canceled != self.__initial_canceled or not self.pk:
@@ -2936,10 +2999,17 @@ class InvoiceAddress(models.Model):
self.order.touch()
if self.name_parts:
self.name_cached = self.name
name = self.name
if self.name_cached != name:
self.name_cached = self.name
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'name_cached'}.union(kwargs['update_fields'])
else:
self.name_cached = ""
self.name_parts = {}
if self.name_cached != "" or self.name_parts != {}:
self.name_cached = ""
self.name_parts = {}
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'name_cached', 'name_parts'}.union(kwargs['update_fields'])
super().save(**kwargs)
def describe(self):
@@ -3085,11 +3155,7 @@ class BlockedTicketSecret(models.Model):
updated = models.DateTimeField(auto_now=True)
class Meta:
if 'mysql' not in settings.DATABASES['default']['ENGINE']:
# MySQL does not support indexes on TextField(). Django knows this and just ignores db_index, but it will
# not silently ignore the UNIQUE index, causing this table to fail. I'm so glad we're deprecating MySQL
# in a few months, so we'll just live without an unique index until then.
unique_together = (('event', 'secret'),)
unique_together = (('event', 'secret'),)
@receiver(post_delete, sender=CachedTicket)

View File

@@ -35,12 +35,12 @@
import string
from datetime import date, datetime, time
import pytz
import pytz_deprecation_shim
from django.conf import settings
from django.core.mail import get_connection
from django.core.validators import MinLengthValidator, RegexValidator
from django.db import models
from django.db.models import Exists, OuterRef, Q
from django.db.models import Q
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
@@ -102,6 +102,7 @@ class Organizer(LoggedModel):
is_new = not self.pk
obj = super().save(*args, **kwargs)
if is_new:
kwargs.pop('update_fields', None) # does not make sense here
self.set_defaults()
else:
self.get_cache().clear()
@@ -140,7 +141,7 @@ class Organizer(LoggedModel):
@property
def timezone(self):
return pytz.timezone(self.settings.timezone)
return pytz_deprecation_shim.timezone(self.settings.timezone)
@cached_property
def all_logentries_link(self):
@@ -156,17 +157,19 @@ class Organizer(LoggedModel):
return self.cache.get_or_set(
key='has_gift_cards',
timeout=15,
default=lambda: self.issued_gift_cards.exists() or self.gift_card_issuer_acceptance.exists()
default=lambda: self.issued_gift_cards.exists() or self.gift_card_issuer_acceptance.filter(active=True).exists()
)
@property
def accepted_gift_cards(self):
from .giftcards import GiftCard, GiftCardAcceptance
return GiftCard.objects.annotate(
accepted=Exists(GiftCardAcceptance.objects.filter(issuer=OuterRef('issuer'), collector=self))
).filter(
Q(issuer=self) | Q(accepted=True)
return GiftCard.objects.filter(
Q(issuer=self) |
Q(issuer__in=GiftCardAcceptance.objects.filter(
acceptor=self,
active=True,
).values_list('issuer', flat=True))
)
@property

View File

@@ -22,9 +22,12 @@
import json
from decimal import Decimal
import jsonschema
from django.contrib.staticfiles import finders
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.utils.deconstruct import deconstructible
from django.utils.formats import localize
from django.utils.translation import gettext_lazy as _, pgettext
from i18nfield.fields import I18nCharField
@@ -135,6 +138,25 @@ def cc_to_vat_prefix(country_code):
return country_code
@deconstructible
class CustomRulesValidator:
def __call__(self, value):
if not isinstance(value, dict):
try:
val = json.loads(value)
except ValueError:
raise ValidationError(_('Your layout file is not a valid JSON file.'))
else:
val = value
with open(finders.find('schema/tax-rules-custom.schema.json'), 'r') as f:
schema = json.loads(f.read())
try:
jsonschema.validate(val, schema)
except jsonschema.ValidationError as e:
e = str(e).replace('%', '%%')
raise ValidationError(_('Your set of rules is not valid. Error message: {}').format(e))
class TaxRule(LoggedModel):
event = models.ForeignKey('Event', related_name='tax_rules', on_delete=models.CASCADE)
internal_name = models.CharField(

View File

@@ -502,7 +502,10 @@ class Voucher(LoggedModel):
return seat
def save(self, *args, **kwargs):
self.code = self.code.upper()
if self.code != self.code.upper():
self.code = self.code.upper()
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'code'}.union(kwargs['update_fields'])
super().save(*args, **kwargs)
self.event.cache.set('vouchers_exist', True)

View File

@@ -126,12 +126,19 @@ class WaitingListEntry(LoggedModel):
raise ValidationError('Invalid input')
def save(self, *args, **kwargs):
update_fields = kwargs.get('update_fields', [])
update_fields = kwargs.get('update_fields', set())
if 'name_parts' in update_fields:
update_fields.append('name_cached')
self.name_cached = self.name
kwargs['update_fields'] = {'name_cached'}.union(kwargs['update_fields'])
name = self.name
if name != self.name_cached:
self.name_cached = name
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'name_cached'}.union(kwargs['update_fields'])
if self.name_parts is None:
self.name_parts = {}
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'name_parts'}.union(kwargs['update_fields'])
super().save(*args, **kwargs)
@property
@@ -211,7 +218,7 @@ class WaitingListEntry(LoggedModel):
'waitinglistentry': self.pk,
'subevent': self.subevent.pk if self.subevent else None,
}, user=user, auth=auth)
self.log_action('pretix.waitinglist.voucher', user=user, auth=auth)
self.log_action('pretix.event.orders.waitinglist.voucher_assigned', user=user, auth=auth)
self.voucher = v
self.save()

View File

@@ -28,6 +28,7 @@ import pycountry
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import EmailValidator
from django.db.models import Q
from django.utils import formats
from django.utils.functional import cached_property
from django.utils.translation import (
@@ -42,8 +43,8 @@ from phonenumbers import SUPPORTED_REGIONS
from pretix.base.channels import get_all_sales_channels
from pretix.base.forms.questions import guess_country
from pretix.base.models import (
ItemVariation, OrderPosition, Question, QuestionAnswer, QuestionOption,
Seat, SubEvent,
Customer, ItemVariation, OrderPosition, Question, QuestionAnswer,
QuestionOption, Seat, SubEvent,
)
from pretix.base.services.pricing import get_price
from pretix.base.settings import (
@@ -210,7 +211,7 @@ class SubeventColumn(ImportColumn):
for format in input_formats:
try:
d = datetime.datetime.strptime(value, format)
d = self.event.timezone.localize(d)
d = d.replace(tzinfo=self.event.timezone)
try:
se = self.event.subevents.get(
active=True,
@@ -660,7 +661,7 @@ class ValidFrom(ImportColumn):
for format in input_formats:
try:
d = datetime.datetime.strptime(value, format)
d = self.event.timezone.localize(d)
d = d.replace(tzinfo=self.event.timezone)
return d
except (ValueError, TypeError):
pass
@@ -683,7 +684,7 @@ class ValidUntil(ImportColumn):
for format in input_formats:
try:
d = datetime.datetime.strptime(value, format)
d = self.event.timezone.localize(d)
d = d.replace(tzinfo=self.event.timezone)
return d
except (ValueError, TypeError):
pass
@@ -826,6 +827,28 @@ class QuestionColumn(ImportColumn):
a.options.add(*a._options)
class CustomerColumn(ImportColumn):
identifier = 'customer'
verbose_name = gettext_lazy('Customer')
def clean(self, value, previous_values):
if value:
try:
value = self.event.organizer.customers.get(
Q(identifier=value) | Q(email__iexact=value) | Q(external_identifier=value)
)
except Customer.MultipleObjectsReturned:
value = self.event.organizer.customers.get(
Q(identifier=value)
)
except Customer.DoesNotExist:
raise ValidationError(_('No matching customer was found.'))
return value
def assign(self, value, order, position, invoice_address, **kwargs):
order.customer = value
def get_all_columns(event):
default = []
if event.has_subevents:
@@ -837,6 +860,10 @@ def get_all_columns(event):
Variation(event),
InvoiceAddressCompany(event),
]
if event.settings.customer_accounts:
default += [
CustomerColumn(event),
]
scheme = PERSON_NAME_SCHEMES.get(event.settings.name_scheme)
for n, l, w in scheme['fields']:
default.append(InvoiceAddressNamePart(event, n, l))

View File

@@ -39,8 +39,8 @@ import logging
from collections import OrderedDict
from decimal import ROUND_HALF_UP, Decimal
from typing import Any, Dict, Union
from zoneinfo import ZoneInfo
import pytz
from django import forms
from django.conf import settings
from django.contrib import messages
@@ -60,7 +60,7 @@ from pretix.base.channels import get_all_sales_channels
from pretix.base.forms import PlaceholderValidator
from pretix.base.models import (
CartPosition, Event, GiftCard, InvoiceAddress, Order, OrderPayment,
OrderRefund, Quota,
OrderRefund, Quota, TaxRule,
)
from pretix.base.reldate import RelativeDateField, RelativeDateWrapper
from pretix.base.settings import SettingsSandbox
@@ -78,6 +78,16 @@ from pretix.presale.views.cart import cart_session, get_or_create_cart_id
logger = logging.getLogger(__name__)
class WalletQueries:
APPLEPAY = 'applepay'
GOOGLEPAY = 'googlepay'
WALLETS = (
(APPLEPAY, pgettext_lazy('payment', 'Apple Pay')),
(GOOGLEPAY, pgettext_lazy('payment', 'Google Pay')),
)
class PaymentProviderForm(Form):
def clean(self):
cleaned_data = super().clean()
@@ -436,6 +446,19 @@ class BasePaymentProvider:
d['_restrict_to_sales_channels']._as_type = list
return d
@property
def walletqueries(self):
"""
.. warning:: This property is considered **experimental**. It might change or get removed at any time without
prior notice.
A list of wallet payment methods that should be dynamically joined to the public name of the payment method,
if they are available to the user.
The detection is made on a best effort basis with no guarantees of correctness and actual availability.
Wallets that pretix can check for are exposed through ``pretix.base.payment.WalletQueries``.
"""
return []
def settings_form_clean(self, cleaned_data):
"""
Overriding this method allows you to inject custom validation into the settings form.
@@ -518,7 +541,7 @@ class BasePaymentProvider:
def _is_still_available(self, now_dt=None, cart_id=None, order=None):
now_dt = now_dt or now()
tz = pytz.timezone(self.event.settings.timezone)
tz = ZoneInfo(self.event.settings.timezone)
availability_date = self.settings.get('_availability_date', as_type=RelativeDateWrapper)
if availability_date:
@@ -1015,7 +1038,11 @@ class FreeOrderProvider(BasePaymentProvider):
cart = get_cart(request)
total = get_cart_total(request)
total += sum([f.value for f in get_fees(self.event, request, total, None, None, cart)])
try:
total += sum([f.value for f in get_fees(self.event, request, total, None, None, cart)])
except TaxRule.SaleNotAllowed:
# ignore for now, will fail on order creation
pass
return total == 0
def order_change_allowed(self, order: Order) -> bool:

View File

@@ -43,16 +43,18 @@ import subprocess
import tempfile
import unicodedata
import uuid
from collections import OrderedDict
from collections import OrderedDict, defaultdict
from functools import partial
from io import BytesIO
import jsonschema
import reportlab.rl_config
from bidi.algorithm import get_display
from django.conf import settings
from django.contrib.staticfiles import finders
from django.core.exceptions import ValidationError
from django.db.models import Max, Min
from django.db.models.fields.files import FieldFile
from django.dispatch import receiver
from django.utils.deconstruct import deconstructible
from django.utils.formats import date_format
@@ -60,8 +62,8 @@ from django.utils.html import conditional_escape
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext
from i18nfield.strings import LazyI18nString
from pypdf import PdfReader
from pytz import timezone
from pypdf import PdfReader, PdfWriter, Transformation
from pypdf.generic import RectangleObject
from reportlab.graphics import renderPDF
from reportlab.graphics.barcode.qr import QrCodeWidget
from reportlab.graphics.shapes import Drawing
@@ -86,6 +88,9 @@ from pretix.presale.style import get_fonts
logger = logging.getLogger(__name__)
if not settings.DEBUG:
reportlab.rl_config.shapeChecking = 0
DEFAULT_VARIABLES = OrderedDict((
("secret", {
@@ -237,7 +242,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Event begin date and time"),
"editor_sample": _("2017-05-31 20:00"),
"evaluate": lambda op, order, ev: date_format(
ev.date_from.astimezone(timezone(ev.settings.timezone)),
ev.date_from.astimezone(ev.timezone),
"SHORT_DATETIME_FORMAT"
) if ev.date_from else ""
}),
@@ -245,7 +250,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Event begin date"),
"editor_sample": _("2017-05-31"),
"evaluate": lambda op, order, ev: date_format(
ev.date_from.astimezone(timezone(ev.settings.timezone)),
ev.date_from.astimezone(ev.timezone),
"SHORT_DATE_FORMAT"
) if ev.date_from else ""
}),
@@ -263,7 +268,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Event end date and time"),
"editor_sample": _("2017-05-31 22:00"),
"evaluate": lambda op, order, ev: date_format(
ev.date_to.astimezone(timezone(ev.settings.timezone)),
ev.date_to.astimezone(ev.timezone),
"SHORT_DATETIME_FORMAT"
) if ev.date_to else ""
}),
@@ -271,7 +276,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Event end date"),
"editor_sample": _("2017-05-31"),
"evaluate": lambda op, order, ev: date_format(
ev.date_to.astimezone(timezone(ev.settings.timezone)),
ev.date_to.astimezone(ev.timezone),
"SHORT_DATE_FORMAT"
) if ev.date_to else ""
}),
@@ -279,7 +284,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Event end time"),
"editor_sample": _("22:00"),
"evaluate": lambda op, order, ev: date_format(
ev.date_to.astimezone(timezone(ev.settings.timezone)),
ev.date_to.astimezone(ev.timezone),
"TIME_FORMAT"
) if ev.date_to else ""
}),
@@ -292,7 +297,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Event admission date and time"),
"editor_sample": _("2017-05-31 19:00"),
"evaluate": lambda op, order, ev: date_format(
ev.date_admission.astimezone(timezone(ev.settings.timezone)),
ev.date_admission.astimezone(ev.timezone),
"SHORT_DATETIME_FORMAT"
) if ev.date_admission else ""
}),
@@ -300,7 +305,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Event admission time"),
"editor_sample": _("19:00"),
"evaluate": lambda op, order, ev: date_format(
ev.date_admission.astimezone(timezone(ev.settings.timezone)),
ev.date_admission.astimezone(ev.timezone),
"TIME_FORMAT"
) if ev.date_admission else ""
}),
@@ -356,14 +361,9 @@ DEFAULT_VARIABLES = OrderedDict((
}),
("addons", {
"label": _("List of Add-Ons"),
"editor_sample": _("Add-on 1\nAdd-on 2"),
"editor_sample": _("Add-on 1\n2x Add-on 2"),
"evaluate": lambda op, order, ev: "\n".join([
'{} - {}'.format(p.item.name, p.variation.value) if p.variation else str(p.item.name)
for p in (
op.addons.all() if 'addons' in getattr(op, '_prefetched_objects_cache', {})
else op.addons.select_related('item', 'variation')
)
if not p.canceled
str(p) for p in generate_compressed_addon_list(op, order, ev)
])
}),
("organizer", {
@@ -385,7 +385,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Printing date"),
"editor_sample": _("2017-05-31"),
"evaluate": lambda op, order, ev: date_format(
now().astimezone(timezone(ev.settings.timezone)),
now().astimezone(ev.timezone),
"SHORT_DATE_FORMAT"
)
}),
@@ -393,7 +393,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Printing date and time"),
"editor_sample": _("2017-05-31 19:00"),
"evaluate": lambda op, order, ev: date_format(
now().astimezone(timezone(ev.settings.timezone)),
now().astimezone(ev.timezone),
"SHORT_DATETIME_FORMAT"
)
}),
@@ -401,7 +401,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Printing time"),
"editor_sample": _("19:00"),
"evaluate": lambda op, order, ev: date_format(
now().astimezone(timezone(ev.settings.timezone)),
now().astimezone(ev.timezone),
"TIME_FORMAT"
)
}),
@@ -409,7 +409,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Validity start date"),
"editor_sample": _("2017-05-31"),
"evaluate": lambda op, order, ev: date_format(
op.valid_from.astimezone(timezone(ev.settings.timezone)),
op.valid_from.astimezone(ev.timezone),
"SHORT_DATE_FORMAT"
) if op.valid_from else ""
}),
@@ -417,7 +417,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Validity start date and time"),
"editor_sample": _("2017-05-31 19:00"),
"evaluate": lambda op, order, ev: date_format(
op.valid_from.astimezone(timezone(ev.settings.timezone)),
op.valid_from.astimezone(ev.timezone),
"SHORT_DATETIME_FORMAT"
) if op.valid_from else ""
}),
@@ -425,7 +425,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Validity start time"),
"editor_sample": _("19:00"),
"evaluate": lambda op, order, ev: date_format(
op.valid_from.astimezone(timezone(ev.settings.timezone)),
op.valid_from.astimezone(ev.timezone),
"TIME_FORMAT"
) if op.valid_from else ""
}),
@@ -433,7 +433,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Validity end date"),
"editor_sample": _("2017-05-31"),
"evaluate": lambda op, order, ev: date_format(
op.valid_until.astimezone(timezone(ev.settings.timezone)),
op.valid_until.astimezone(ev.timezone),
"SHORT_DATE_FORMAT"
) if op.valid_until else ""
}),
@@ -441,7 +441,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Validity end date and time"),
"editor_sample": _("2017-05-31 19:00"),
"evaluate": lambda op, order, ev: date_format(
op.valid_until.astimezone(timezone(ev.settings.timezone)),
op.valid_until.astimezone(ev.timezone),
"SHORT_DATETIME_FORMAT"
) if op.valid_until else ""
}),
@@ -449,7 +449,7 @@ DEFAULT_VARIABLES = OrderedDict((
"label": _("Validity end time"),
"editor_sample": _("19:00"),
"evaluate": lambda op, order, ev: date_format(
op.valid_until.astimezone(timezone(ev.settings.timezone)),
op.valid_until.astimezone(ev.timezone),
"TIME_FORMAT"
) if op.valid_until else ""
}),
@@ -521,7 +521,7 @@ def images_from_questions(sender, *args, **kwargs):
else:
a = op.answers.filter(question_id=question_id).first() or a
if not a or not a.file or not any(a.file.name.lower().endswith(e) for e in (".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tif", ".tiff")):
if not a or not a.file or not any(a.file.name.lower().endswith(e) for e in settings.FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE):
return None
else:
if etag:
@@ -697,6 +697,30 @@ def get_seat(op: OrderPosition):
return None
def generate_compressed_addon_list(op, order, event):
itemcount = defaultdict(int)
addons = (
op.addons.all() if 'addons' in getattr(op, '_prefetched_objects_cache', {})
else op.addons.select_related('item', 'variation')
)
for pos in addons:
itemcount[pos.item, pos.variation] += 1
addonlist = []
for (item, variation), count in itemcount.items():
if variation:
if count > 1:
addonlist.append('{}x {} - {}'.format(count, item.name, variation.value))
else:
addonlist.append('{} - {}'.format(item.name, variation.value))
else:
if count > 1:
addonlist.append('{}x {}'.format(count, item.name))
else:
addonlist.append(item.name)
return addonlist
class Renderer:
def __init__(self, event, layout, background_file):
@@ -861,22 +885,37 @@ class Renderer:
image_file = None
if image_file:
ir = ThumbnailingImageReader(image_file)
try:
ir = ThumbnailingImageReader(image_file)
ir.resize(float(o['width']) * mm, float(o['height']) * mm, 300)
canvas.drawImage(
image=ir,
x=float(o['left']) * mm,
y=float(o['bottom']) * mm,
width=float(o['width']) * mm,
height=float(o['height']) * mm,
preserveAspectRatio=True,
anchor='c', # centered in frame
mask='auto'
)
if isinstance(image_file, FieldFile):
# ThumbnailingImageReader "closes" the file, so it's no use to use the same file pointer
# in case we need it again. For FieldFile, fortunately, there is an easy way to make the file
# refresh itself when it is used next.
del image_file.file
except:
logger.exception("Can not resize image")
pass
canvas.drawImage(
image=ir,
x=float(o['left']) * mm,
y=float(o['bottom']) * mm,
width=float(o['width']) * mm,
height=float(o['height']) * mm,
preserveAspectRatio=True,
anchor='c', # centered in frame
mask='auto'
)
logger.exception("Can not load or resize image")
canvas.saveState()
canvas.setFillColorRGB(.8, .8, .8, alpha=1)
canvas.rect(
x=float(o['left']) * mm,
y=float(o['bottom']) * mm,
width=float(o['width']) * mm,
height=float(o['height']) * mm,
stroke=0,
fill=1,
)
canvas.restoreState()
else:
canvas.saveState()
canvas.setFillColorRGB(.8, .8, .8, alpha=1)
@@ -930,7 +969,7 @@ class Renderer:
# reportlab does not support unicode combination characters
# It's important we do this before we use ArabicReshaper
text = unicodedata.normalize("NFKC", text)
text = unicodedata.normalize("NFC", text)
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
# to resolve all ligatures and python-bidi to switch RTL texts.
@@ -983,7 +1022,10 @@ class Renderer:
elif o['type'] == "poweredby":
self._draw_poweredby(canvas, op, o)
if self.bg_pdf:
page_size = (self.bg_pdf.pages[0].mediabox[2], self.bg_pdf.pages[0].mediabox[3])
page_size = (
self.bg_pdf.pages[0].mediabox[2] - self.bg_pdf.pages[0].mediabox[0],
self.bg_pdf.pages[0].mediabox[3] - self.bg_pdf.pages[0].mediabox[1]
)
if self.bg_pdf.pages[0].get('/Rotate') in (90, 270):
# swap dimensions due to pdf being rotated
page_size = page_size[::-1]
@@ -1011,14 +1053,12 @@ class Renderer:
with open(os.path.join(d, 'out.pdf'), 'rb') as f:
return BytesIO(f.read())
else:
from pypdf import PdfReader, PdfWriter, Transformation
from pypdf.generic import RectangleObject
buffer.seek(0)
new_pdf = PdfReader(buffer)
output = PdfWriter()
for i, page in enumerate(new_pdf.pages):
bg_page = copy.copy(self.bg_pdf.pages[i])
bg_page = copy.deepcopy(self.bg_pdf.pages[i])
bg_rotation = bg_page.get('/Rotate')
if bg_rotation:
# /Rotate is clockwise, transformation.rotate is counter-clockwise
@@ -1055,6 +1095,56 @@ class Renderer:
return outbuffer
def merge_background(fg_pdf, bg_pdf, out_file, compress):
if settings.PDFTK:
with tempfile.TemporaryDirectory() as d:
fg_filename = os.path.join(d, 'fg.pdf')
bg_filename = os.path.join(d, 'bg.pdf')
fg_pdf.write(fg_filename)
bg_pdf.write(bg_filename)
pdftk_cmd = [
settings.PDFTK,
fg_filename,
'multibackground',
bg_filename,
'output',
'-',
]
if compress:
pdftk_cmd.append('compress')
subprocess.run(pdftk_cmd, check=True, stdout=out_file)
else:
output = PdfWriter()
for i, page in enumerate(fg_pdf.pages):
bg_page = copy.deepcopy(bg_pdf.pages[i])
bg_rotation = bg_page.get('/Rotate')
if bg_rotation:
# /Rotate is clockwise, transformation.rotate is counter-clockwise
t = Transformation().rotate(bg_rotation)
w = float(page.mediabox.getWidth())
h = float(page.mediabox.getHeight())
if bg_rotation in (90, 270):
# offset due to rotation base
if bg_rotation == 90:
t = t.translate(h, 0)
else:
t = t.translate(0, w)
# rotate mediabox as well
page.mediabox = RectangleObject((
page.mediabox.left.as_numeric(),
page.mediabox.bottom.as_numeric(),
page.mediabox.top.as_numeric(),
page.mediabox.right.as_numeric(),
))
page.trimbox = page.mediabox
elif bg_rotation == 180:
t = t.translate(w, h)
page.add_transformation(t)
bg_page.merge_page(page)
output.add_page(bg_page)
output.write(out_file)
@deconstructible
class PdfLayoutValidator:
def __call__(self, value):

View File

@@ -22,8 +22,8 @@
import datetime
from collections import namedtuple
from typing import Union
from zoneinfo import ZoneInfo
import pytz
from dateutil import parser
from django import forms
from django.core.exceptions import ValidationError
@@ -67,7 +67,7 @@ class RelativeDateWrapper:
if self.data.minutes_before is not None:
raise ValueError('A minute-based relative datetime can not be used as a date')
tz = pytz.timezone(event.settings.timezone)
tz = ZoneInfo(event.settings.timezone)
if isinstance(event, SubEvent):
base_date = (
getattr(event, self.data.base_date_name)
@@ -86,7 +86,7 @@ class RelativeDateWrapper:
if isinstance(self.data, (datetime.datetime, datetime.date)):
return self.data
else:
tz = pytz.timezone(event.settings.timezone)
tz = ZoneInfo(event.settings.timezone)
if isinstance(event, SubEvent):
base_date = (
getattr(event, self.data.base_date_name)
@@ -99,8 +99,7 @@ class RelativeDateWrapper:
if self.data.minutes_before is not None:
return base_date.astimezone(tz) - datetime.timedelta(minutes=self.data.minutes_before)
else:
oldoffset = base_date.astimezone(tz).utcoffset()
new_date = base_date.astimezone(tz) - datetime.timedelta(days=self.data.days_before)
new_date = (base_date.astimezone(tz) - datetime.timedelta(days=self.data.days_before)).astimezone(tz)
if self.data.time:
new_date = new_date.replace(
hour=self.data.time.hour,
@@ -108,8 +107,6 @@ class RelativeDateWrapper:
second=self.data.time.second
)
new_date = new_date.astimezone(tz)
new_offset = new_date.utcoffset()
new_date += oldoffset - new_offset
return new_date
def to_string(self) -> str:

View File

@@ -141,9 +141,10 @@ error_messages = {
'price_not_a_number': gettext_lazy('The entered price is not a number.'),
'price_too_high': gettext_lazy('The entered price is to high.'),
'voucher_invalid': gettext_lazy('This voucher code is not known in our database.'),
'voucher_min_usages': gettext_lazy(
'The voucher code "%(voucher)s" can only be used if you select at least %(number)s '
'matching products.'
'voucher_min_usages': ngettext_lazy(
'The voucher code "%(voucher)s" can only be used if you select at least %(number)s matching products.',
'The voucher code "%(voucher)s" can only be used if you select at least %(number)s matching products.',
'number'
),
'voucher_min_usages_removed': ngettext_lazy(
'The voucher code "%(voucher)s" can only be used if you select at least %(number)s matching products. '
@@ -317,6 +318,9 @@ class CartManager:
def _delete_out_of_timeframe(self):
err = None
for cp in self.positions:
if not cp.pk:
continue
if cp.subevent and cp.subevent.presale_start and self.now_dt < cp.subevent.presale_start:
err = error_messages['some_subevent_not_started']
cp.addons.all().delete()

View File

@@ -32,12 +32,12 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import os
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from functools import partial, reduce
import dateutil
import dateutil.parser
import pytz
from dateutil.tz import datetime_exists
from django.core.files import File
from django.db import IntegrityError, transaction
from django.db.models import (
@@ -53,7 +53,8 @@ from django.utils.translation import gettext as _
from django_scopes import scope, scopes_disabled
from pretix.base.models import (
Checkin, CheckinList, Device, Order, OrderPosition, QuestionOption,
Checkin, CheckinList, Device, Event, ItemVariation, Order, OrderPosition,
QuestionOption,
)
from pretix.base.signals import checkin_created, order_placed, periodic_task
from pretix.helpers import OF_SELF
@@ -65,12 +66,13 @@ from pretix.helpers.jsonlogic_query import (
)
def _build_time(t=None, value=None, ev=None):
def _build_time(t=None, value=None, ev=None, now_dt=None):
now_dt = now_dt or now()
if t == "custom":
return dateutil.parser.parse(value)
elif t == "customtime":
parsed = dateutil.parser.parse(value)
return now().astimezone(ev.timezone).replace(
return now_dt.astimezone(ev.timezone).replace(
hour=parsed.hour,
minute=parsed.minute,
second=parsed.second,
@@ -84,7 +86,42 @@ def _build_time(t=None, value=None, ev=None):
return ev.date_admission or ev.date_from
def _logic_explain(rules, ev, rule_data):
def _logic_annotate_for_graphic_explain(rules, ev, rule_data, now_dt):
logic_environment = _get_logic_environment(ev, now_dt)
event = ev if isinstance(ev, Event) else ev.event
def _evaluate_inners(r):
if not isinstance(r, dict):
return r
operator = list(r.keys())[0]
values = r[operator]
if operator in ("and", "or"):
return {operator: [_evaluate_inners(v) for v in values]}
result = logic_environment.apply(r, rule_data)
return {**r, '__result': result}
def _add_var_values(r):
if not isinstance(r, dict):
return r
operator = [k for k in r.keys() if not k.startswith("__")][0]
values = r[operator]
if operator == "var":
var = values[0] if isinstance(values, list) else values
val = rule_data[var]
if var == "product":
val = str(event.items.get(pk=val))
elif var == "variation":
val = str(ItemVariation.objects.get(item__event=event, pk=val))
elif isinstance(val, datetime):
val = date_format(val.astimezone(ev.timezone), "SHORT_DATETIME_FORMAT")
return {"var": var, "__result": val}
else:
return {**r, operator: [_add_var_values(v) for v in values]}
return _add_var_values(_evaluate_inners(rules))
def _logic_explain(rules, ev, rule_data, now_dt=None):
"""
Explains when the logic denied the check-in. Only works for a denied check-in.
@@ -114,7 +151,8 @@ def _logic_explain(rules, ev, rule_data):
Additionally, we favor a "close failure". Therefore, in the above example, we'd show "You can only
get in before 17:00". In the middle of the night it would switch to "You can only get in after 09:00".
"""
logic_environment = _get_logic_environment(ev)
now_dt = now_dt or now()
logic_environment = _get_logic_environment(ev, now_dt)
_var_values = {'False': False, 'True': True}
_var_explanations = {}
@@ -191,16 +229,16 @@ def _logic_explain(rules, ev, rule_data):
for vname, data in _var_explanations.items():
var, operator, rhs = data['var'], data['operator'], data['rhs']
if var == 'now':
compare_to = _build_time(*rhs[0]['buildTime'], ev=ev).astimezone(ev.timezone)
compare_to = _build_time(*rhs[0]['buildTime'], ev=ev, now_dt=now_dt).astimezone(ev.timezone)
tolerance = timedelta(minutes=float(rhs[1])) if len(rhs) > 1 and rhs[1] else timedelta(seconds=0)
if operator == 'isBefore':
compare_to += tolerance
else:
compare_to -= tolerance
var_weights[vname] = (200, abs(now() - compare_to).total_seconds())
var_weights[vname] = (200, abs(now_dt - compare_to).total_seconds())
if abs(now() - compare_to) < timedelta(hours=12):
if abs(now_dt - compare_to) < timedelta(hours=12):
compare_to_text = date_format(compare_to, 'TIME_FORMAT')
else:
compare_to_text = date_format(compare_to, 'SHORT_DATETIME_FORMAT')
@@ -299,7 +337,7 @@ def _logic_explain(rules, ev, rule_data):
return ', '.join(var_texts[v] for v in paths_with_min_weight[0] if not _var_values[v])
def _get_logic_environment(ev):
def _get_logic_environment(ev, now_dt):
# Every change to our supported JSON logic must be done
# * in pretix.base.services.checkin
# * in pretix.base.models.checkin
@@ -316,7 +354,7 @@ def _get_logic_environment(ev):
logic.add_operation('objectList', lambda *objs: list(objs))
logic.add_operation('lookup', lambda model, pk, str: int(pk))
logic.add_operation('inList', lambda a, b: a in b)
logic.add_operation('buildTime', partial(_build_time, ev=ev))
logic.add_operation('buildTime', partial(_build_time, ev=ev, now_dt=now_dt))
logic.add_operation('isBefore', is_before)
logic.add_operation('isAfter', lambda t1, t2, tol=None: is_before(t2, t1, tol))
return logic
@@ -357,7 +395,7 @@ class LazyRuleVars:
@cached_property
def entries_today(self):
tz = self._clist.event.timezone
midnight = now().astimezone(tz).replace(hour=0, minute=0, second=0, microsecond=0)
midnight = self._dt.astimezone(tz).replace(hour=0, minute=0, second=0, microsecond=0)
return self._position.checkins.filter(type=Checkin.TYPE_ENTRY, list=self._clist, datetime__gte=midnight).count()
@cached_property
@@ -378,7 +416,7 @@ class LazyRuleVars:
# between platforms (None<1 is true on some, but not all), we rather choose something that is at least
# consistent.
return -1
return (now() - last_entry.datetime).total_seconds() // 60
return (self._dt - last_entry.datetime).total_seconds() // 60
@cached_property
def minutes_since_first_entry(self):
@@ -390,7 +428,7 @@ class LazyRuleVars:
# between platforms (None<1 is true on some, but not all), we rather choose something that is at least
# consistent.
return -1
return (now() - last_entry.datetime).total_seconds() // 60
return (self._dt - last_entry.datetime).total_seconds() // 60
class SQLLogic:
@@ -439,7 +477,7 @@ class SQLLogic:
if operator == 'buildTime':
if values[0] == "custom":
return Value(dateutil.parser.parse(values[1]).astimezone(pytz.UTC))
return Value(dateutil.parser.parse(values[1]).astimezone(timezone.utc))
elif values[0] == "customtime":
parsed = dateutil.parser.parse(values[1])
return Value(now().astimezone(self.list.event.timezone).replace(
@@ -447,7 +485,7 @@ class SQLLogic:
minute=parsed.minute,
second=parsed.second,
microsecond=parsed.microsecond,
).astimezone(pytz.UTC))
).astimezone(timezone.utc))
elif values[0] == 'date_from':
return Coalesce(
F('subevent__date_from'),
@@ -475,7 +513,7 @@ class SQLLogic:
return int(values[1])
elif operator == 'var':
if values[0] == 'now':
return Value(now().astimezone(pytz.UTC))
return Value(now().astimezone(timezone.utc))
elif values[0] == 'now_isoweekday':
return Value(now().astimezone(self.list.event.timezone).isoweekday())
elif values[0] == 'product':
@@ -693,7 +731,7 @@ def _save_answers(op, answers, given_answers):
def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict, force=False,
ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True,
user=None, auth=None, canceled_supported=False, type=Checkin.TYPE_ENTRY,
raw_barcode=None, raw_source_type=None, from_revoked_secret=False):
raw_barcode=None, raw_source_type=None, from_revoked_secret=False, simulate=False):
"""
Create a checkin for this particular order position and check-in list. Fails with CheckInError if the check in is
not valid at this time.
@@ -707,6 +745,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
:param questions_supported: When set to False, questions are ignored
:param nonce: A random nonce to prevent race conditions.
:param datetime: The datetime of the checkin, defaults to now.
:param simulate: If true, the check-in is not saved.
"""
# !!!!!!!!!
@@ -734,7 +773,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
'blocked'
)
if type != Checkin.TYPE_EXIT and op.valid_from and op.valid_from > now():
if type != Checkin.TYPE_EXIT and op.valid_from and op.valid_from > dt:
if force:
force_used = True
else:
@@ -748,7 +787,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
),
)
if type != Checkin.TYPE_EXIT and op.valid_until and op.valid_until < now():
if type != Checkin.TYPE_EXIT and op.valid_until and op.valid_until < dt:
if force:
force_used = True
else:
@@ -773,7 +812,8 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
if q not in given_answers and q not in answers:
require_answers.append(q)
_save_answers(op, answers, given_answers)
if not simulate:
_save_answers(op, answers, given_answers)
with transaction.atomic():
# Lock order positions, if it is an entry. We don't need it for exits, as a race condition wouldn't be problematic
@@ -821,7 +861,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
if type == Checkin.TYPE_ENTRY and clist.rules:
rule_data = LazyRuleVars(op, clist, dt)
logic = _get_logic_environment(op.subevent or clist.event)
logic = _get_logic_environment(op.subevent or clist.event, now_dt=dt)
if not logic.apply(clist.rules, rule_data):
if force:
force_used = True
@@ -859,30 +899,33 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
return
if entry_allowed or force:
ci = Checkin.objects.create(
position=op,
type=type,
list=clist,
datetime=dt,
device=device,
gate=device.gate if device else None,
nonce=nonce,
forced=force and (not entry_allowed or from_revoked_secret or force_used),
force_sent=force,
raw_barcode=raw_barcode,
raw_source_type=raw_source_type,
)
op.order.log_action('pretix.event.checkin', data={
'position': op.id,
'positionid': op.positionid,
'first': True,
'forced': force or op.order.status != Order.STATUS_PAID,
'datetime': dt,
'type': type,
'answers': {k.pk: str(v) for k, v in given_answers.items()},
'list': clist.pk
}, user=user, auth=auth)
checkin_created.send(op.order.event, checkin=ci)
if simulate:
return True
else:
ci = Checkin.objects.create(
position=op,
type=type,
list=clist,
datetime=dt,
device=device,
gate=device.gate if device else None,
nonce=nonce,
forced=force and (not entry_allowed or from_revoked_secret or force_used),
force_sent=force,
raw_barcode=raw_barcode,
raw_source_type=raw_source_type,
)
op.order.log_action('pretix.event.checkin', data={
'position': op.id,
'positionid': op.positionid,
'first': True,
'forced': force or op.order.status != Order.STATUS_PAID,
'datetime': dt,
'type': type,
'answers': {k.pk: str(v) for k, v in given_answers.items()},
'list': clist.pk
}, user=user, auth=auth)
checkin_created.send(op.order.event, checkin=ci)
else:
raise CheckInError(
_('This ticket has already been redeemed.'),
@@ -926,14 +969,11 @@ def process_exit_all(sender, **kwargs):
if cl.event.settings.get(f'autocheckin_dst_hack_{cl.pk}'): # move time back if yesterday was DST switch
d -= timedelta(hours=1)
cl.event.settings.delete(f'autocheckin_dst_hack_{cl.pk}')
try:
cl.exit_all_at = make_aware(datetime.combine(d.date() + timedelta(days=1), d.time()), cl.event.timezone)
except pytz.exceptions.AmbiguousTimeError:
cl.exit_all_at = make_aware(datetime.combine(d.date() + timedelta(days=1), d.time()), cl.event.timezone,
is_dst=False)
except pytz.exceptions.NonExistentTimeError:
cl.exit_all_at = make_aware(datetime.combine(d.date() + timedelta(days=1), d.time().replace(fold=1)), cl.event.timezone)
if not datetime_exists(cl.exit_all_at):
cl.event.settings.set(f'autocheckin_dst_hack_{cl.pk}', True)
d += timedelta(hours=1)
cl.exit_all_at = make_aware(datetime.combine(d.date() + timedelta(days=1), d.time()), cl.event.timezone)
cl.exit_all_at = make_aware(datetime.combine(d.date() + timedelta(days=1), d.time().replace(fold=1)), cl.event.timezone)
# AmbiguousTimeError shouldn't be possible since d.time() includes fold=0
cl.save(update_fields=['exit_all_at'])

View File

@@ -26,7 +26,7 @@ from typing import Any, Dict, Union
from celery.exceptions import MaxRetriesExceededError
from django.conf import settings
from django.core.files.base import ContentFile
from django.db import connection, transaction
from django.db import close_old_connections, connection, transaction
from django.dispatch import receiver
from django.utils.timezone import now, override
from django.utils.translation import gettext
@@ -86,9 +86,12 @@ def export(self, event: Event, fileid: str, provider: str, form_data: Dict[str,
gettext('Your export did not contain any data.')
)
file.filename, file.type, data = d
close_old_connections() # This task can run very long, we might need a new DB connection
f = ContentFile(data)
file.file.save(cachedfile_name(file, file.filename), f)
return file.pk
return str(file.pk)
@app.task(base=ProfiledOrganizerUserTask, throws=(ExportError,), bind=True)
@@ -154,9 +157,12 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
gettext('Your export did not contain any data.')
)
file.filename, file.type, data = d
close_old_connections() # This task can run very long, we might need a new DB connection
f = ContentFile(data)
file.file.save(cachedfile_name(file, file.filename), f)
return file.pk
return str(file.pk)
def _run_scheduled_export(schedule, context: Union[Event, Organizer], exporter, config_url, retry_func, has_permission):
@@ -214,6 +220,11 @@ def _run_scheduled_export(schedule, context: Union[Event, Organizer], exporter,
raise ExportError(
gettext('Your exported data exceeded the size limit for scheduled exports.')
)
conn = transaction.get_connection()
if not conn.in_atomic_block: # atomic execution only happens during tests or with celery always_eager on
close_old_connections() # This task can run very long, we might need a new DB connection
f = ContentFile(data)
file.file.save(cachedfile_name(file, file.filename), f)
except ExportEmptyError as e:
@@ -290,6 +301,8 @@ def scheduled_organizer_export(self, organizer: Organizer, schedule: int) -> Non
if isinstance(exporter, OrganizerLevelExportMixin):
if not schedule.owner.has_organizer_permission(organizer, exporter.organizer_required_permission):
has_permission = False
if exporter and not exporter.available_for_user(schedule.owner):
has_permission = False
_run_scheduled_export(
schedule,

View File

@@ -348,7 +348,7 @@ def generate_cancellation(invoice: Invoice, trigger_pdf=True):
cancellation.prefix = None
cancellation.refers = invoice
cancellation.is_cancellation = True
cancellation.date = timezone.now().date()
cancellation.date = timezone.now().astimezone(invoice.event.timezone).date()
cancellation.payment_provider_text = ''
cancellation.payment_provider_stamp = ''
cancellation.file = None
@@ -510,7 +510,7 @@ def send_invoices_to_organizer(sender, **kwargs):
with transaction.atomic():
qs = Invoice.objects.filter(
sent_to_organizer__isnull=True
).prefetch_related('event').select_for_update(of=OF_SELF, skip_locked=connection.features.has_select_for_update_skip_locked)
).prefetch_related('event', 'order').select_for_update(of=OF_SELF, skip_locked=connection.features.has_select_for_update_skip_locked)
for i in qs[:batch_size]:
if i.event.settings.invoice_email_organizer:
with language(i.event.settings.locale):
@@ -519,11 +519,12 @@ def send_invoices_to_organizer(sender, **kwargs):
subject=_('New invoice: {number}').format(number=i.number),
template=LazyI18nString.from_gettext(_(
'Hello,\n\n'
'a new invoice for {event} has been created, see attached.\n\n'
'a new invoice for order {order} at {event} has been created, see attached.\n\n'
'We are sending this email because you configured us to do so in your event settings.'
)),
context={
'event': str(i.event),
'order': str(i.order),
},
locale=i.event.settings.locale,
event=i.event,

View File

@@ -45,8 +45,8 @@ from email.mime.image import MIMEImage
from email.utils import formataddr
from typing import Any, Dict, List, Sequence, Union
from urllib.parse import urljoin, urlparse
from zoneinfo import ZoneInfo
import pytz
import requests
from bs4 import BeautifulSoup
from celery import chain
@@ -226,11 +226,11 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
if event:
timezone = event.timezone
elif user:
timezone = pytz.timezone(user.timezone)
timezone = ZoneInfo(user.timezone)
elif organizer:
timezone = organizer.timezone
else:
timezone = pytz.timezone(settings.TIME_ZONE)
timezone = ZoneInfo(settings.TIME_ZONE)
if settings_holder:
if settings_holder.settings.mail_bcc:

View File

@@ -0,0 +1,72 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import secrets
from django.db import IntegrityError
from django.db.models import Q
from django_scopes import scopes_disabled
from pretix.base.models import GiftCardAcceptance
from pretix.base.models.media import MediumKeySet
def create_nfc_mf0aes_keyset(organizer):
for i in range(20):
public_id = secrets.randbelow(2 ** 32)
uid_key = secrets.token_bytes(16)
diversification_key = secrets.token_bytes(16)
try:
return MediumKeySet.objects.create(
organizer=organizer,
media_type="nfc_mf0aes",
public_id=public_id,
diversification_key=diversification_key,
uid_key=uid_key,
active=True,
)
except IntegrityError: # either race condition with another thread or duplicate public ID
try:
return MediumKeySet.objects.get(
organizer=organizer,
media_type="nfc_mf0aes",
active=True,
)
except MediumKeySet.DoesNotExist:
continue # duplicate public ID, let's try again
@scopes_disabled()
def get_keysets_for_organizer(organizer):
sets = list(MediumKeySet.objects.filter(
Q(organizer=organizer) | Q(organizer__in=GiftCardAcceptance.objects.filter(
acceptor=organizer,
active=True,
reusable_media=True,
).values_list("issuer_id", flat=True))
))
if organizer.settings.reusable_media_type_nfc_mf0aes and not any(
ks.organizer == organizer and ks.media_type == "nfc_mf0aes" for ks in sets
):
new_set = create_nfc_mf0aes_keyset(organizer)
if new_set:
sets.append(new_set)
return sets

View File

@@ -935,7 +935,10 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
raise OrderError(e.message)
require_approval = any(p.requires_approval(invoice_address=address) for p in positions)
fees = _get_fees(positions, payment_requests, address, meta_info, event, require_approval=require_approval)
try:
fees = _get_fees(positions, payment_requests, address, meta_info, event, require_approval=require_approval)
except TaxRule.SaleNotAllowed:
raise OrderError(error_messages['country_blocked'])
total = pending_sum = sum([c.price for c in positions]) + sum([c.value for c in fees])
order = Order(
@@ -968,7 +971,10 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
for fee in fees:
fee.order = order
fee._calculate_tax()
try:
fee._calculate_tax()
except TaxRule.SaleNotAllowed:
raise OrderError(error_messages['country_blocked'])
if fee.tax_rule and not fee.tax_rule.pk:
fee.tax_rule = None # TODO: deprecate
fee.save()
@@ -1264,12 +1270,12 @@ def expire_orders(sender, **kwargs):
Exists(
OrderFee.objects.filter(order_id=OuterRef('pk'), fee_type=OrderFee.FEE_TYPE_CANCELLATION)
)
).select_related('event').order_by('event_id')
).prefetch_related('event').order_by('event_id')
for o in qs:
if o.event_id != event_id:
expire = o.event.settings.get('payment_term_expire_automatically', as_type=bool)
event_id = o.event_id
if expire:
if expire and now() >= o.payment_term_expire_date:
mark_order_expired(o)

View File

@@ -20,6 +20,7 @@
# <https://www.gnu.org/licenses/>.
#
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
#
@@ -31,7 +32,7 @@
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import inspect
import json
from datetime import timedelta
from tempfile import NamedTemporaryFile
@@ -41,10 +42,13 @@ from zipfile import ZipFile
from dateutil.parser import parse
from django.conf import settings
from django.utils.crypto import get_random_string
from django.utils.formats import date_format
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from pretix.base.models import CachedFile, Event, cachedfile_name
from pretix.base.i18n import language
from pretix.base.models import CachedFile, Event, User, cachedfile_name
from pretix.base.services.mail import SendMailException, mail
from pretix.base.services.tasks import ProfiledEventTask
from pretix.base.shredder import ShredError
from pretix.celery_app import app
@@ -101,8 +105,20 @@ def export(event: Event, shredders: List[str], session_key=None, cfid=None) -> N
return cf.pk
@app.task(base=ProfiledEventTask, throws=(ShredError,))
def shred(event: Event, fileid: str, confirm_code: str) -> None:
@app.task(base=ProfiledEventTask, throws=(ShredError,), bind=True)
def shred(self, event: Event, fileid: str, confirm_code: str, user: int=None, locale: str='en') -> None:
steps = []
if user:
user = User.objects.get(pk=user)
def set_progress(val):
if not self.request.called_directly:
self.update_state(
state='PROGRESS',
meta={'value': val, 'steps': steps}
)
known_shredders = event.get_data_shredders()
try:
cf = CachedFile.objects.get(pk=fileid)
@@ -124,8 +140,52 @@ def shred(event: Event, fileid: str, confirm_code: str) -> None:
if event.logentry_set.filter(datetime__gte=parse(indexdata['time'])):
raise ShredError(_("Something happened in your event after the export, please try again."))
for shredder in shredders:
shredder.shred_data()
event.log_action(
'pretix.event.shredder.started', user=user, data={
'indexdata': indexdata
}
)
for i, shredder in enumerate(shredders):
with language(locale):
steps.append({'label': str(shredder.verbose_name), 'done': False})
set_progress(i * 100 / len(shredders))
if 'progress_callback' in inspect.signature(shredder.shred_data).parameters:
shredder.shred_data(
progress_callback=lambda y: set_progress(
i * 100 / len(shredders) + min(max(y, 0), 100) / 100 * 100 / len(shredders)
)
)
else:
shredder.shred_data()
steps[-1]['done'] = True
cf.file.delete(save=False)
cf.delete()
event.log_action(
'pretix.event.shredder.completed', user=user, data={
'indexdata': indexdata
}
)
if user:
with language(user.locale):
try:
mail(
user.email,
_('Data shredding completed'),
'pretixbase/email/shred_completed.txt',
{
'user': user,
'organizer': event.organizer.name,
'event': str(event.name),
'start_time': date_format(parse(indexdata['time']).astimezone(event.timezone), 'SHORT_DATETIME_FORMAT'),
'shredders': ', '.join([str(s.verbose_name) for s in shredders])
},
event=None,
user=user,
locale=user.locale,
)
except SendMailException:
pass # Already logged

View File

@@ -259,6 +259,46 @@ DEFAULTS = {
label=_("Gift card currency"),
)
},
'reusable_media_type_nfc_mf0aes': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Active"),
)
},
'reusable_media_type_nfc_mf0aes_autocreate_giftcard': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Automatically create a new gift card if a new chip is encoded"),
)
},
'reusable_media_type_nfc_mf0aes_autocreate_giftcard_currency': {
'default': 'EUR',
'type': str,
'form_class': forms.ChoiceField,
'serializer_class': serializers.ChoiceField,
'serializer_kwargs': dict(
choices=[(c.alpha_3, c.alpha_3 + " - " + c.name) for c in settings.CURRENCIES],
),
'form_kwargs': dict(
choices=[(c.alpha_3, c.alpha_3 + " - " + c.name) for c in settings.CURRENCIES],
label=_("Gift card currency"),
)
},
'reusable_media_type_nfc_mf0aes_random_uid': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Use UID protection feature of NFC chip"),
)
},
'max_items_per_order': {
'default': '10',
'type': int,
@@ -893,6 +933,28 @@ DEFAULTS = {
"the pool and can be ordered by other people."),
)
},
'payment_term_expire_delay_days': {
'default': '0',
'type': int,
'form_class': forms.IntegerField,
'serializer_class': serializers.IntegerField,
'form_kwargs': dict(
label=_('Expiration delay'),
help_text=_("The order will only actually expire this many days after the expiration date communicated "
"to the customer. If you select \"Only end payment terms on weekdays\" above, this will also "
"be respected. However, this will not delay beyond the \"last date of payments\" "
"configured above, which is always enforced."),
# Every order in between the official expiry date and the delayed expiry date has a performance penalty
# for the cron job, so we limit this feature to 30 days to prevent arbitrary numbers of orders needing
# to be checked.
min_value=0,
max_value=30,
),
'serializer_kwargs': dict(
min_value=0,
max_value=30,
),
},
'payment_pending_hidden': {
'default': 'False',
'type': bool,
@@ -1409,7 +1471,7 @@ DEFAULTS = {
'form_class': forms.BooleanField,
'form_kwargs': dict(
label=_("Show number of check-ins to customer"),
help_text=_('With this option enabled, your customers will be able how many times they entered '
help_text=_('With this option enabled, your customers will be able to see how many times they entered '
'the event. This is usually not necessary, but might be useful in combination with tickets '
'that are usable a specific number of times, so customers can see how many times they have '
'already been used. Exits or failed scans will not be counted, and the user will not see '
@@ -2018,8 +2080,8 @@ to your order for {event}.
You can change your order details and view the status of your order at
{url}
Best regards,
Your {event} team"""))
Best regards,
Your {event} team""")) # noqa: W291
},
'mail_subject_resend_all_links': {
'type': LazyI18nString,
@@ -2034,8 +2096,8 @@ The list is as follows:
{orders}
Best regards,
Your {event} team"""))
Best regards,
Your {event} team""")) # noqa: W291
},
'mail_subject_order_free_attendee': {
'type': LazyI18nString,
@@ -2050,8 +2112,8 @@ you have been registered for {event} successfully.
You can view the details and status of your ticket here:
{url}
Best regards,
Your {event} team"""))
Best regards,
Your {event} team""")) # noqa: W291
},
'mail_send_order_free_attendee': {
'type': bool,
@@ -2071,8 +2133,8 @@ no payment is required.
You can change your order details and view the status of your order at
{url}
Best regards,
Your {event} team"""))
Best regards,
Your {event} team""")) # noqa: W291
},
'mail_subject_order_placed_require_approval': {
'type': LazyI18nString,
@@ -2089,8 +2151,8 @@ be patient and wait for our next email.
You can change your order details and view the status of your order at
{url}
Best regards,
Your {event} team"""))
Best regards,
Your {event} team""")) # noqa: W291
},
'mail_subject_order_placed': {
'type': LazyI18nString,
@@ -2108,8 +2170,8 @@ of {total_with_currency}. Please complete your payment before {expire_date}.
You can change your order details and view the status of your order at
{url}
Best regards,
Your {event} team"""))
Best regards,
Your {event} team""")) # noqa: W291
},
'mail_attachment_new_order': {
'default': None,
@@ -2119,11 +2181,14 @@ Your {event} team"""))
label=_('Attachment for new orders'),
ext_whitelist=(".pdf",),
max_size=settings.FILE_UPLOAD_MAX_SIZE_EMAIL_AUTO_ATTACHMENT,
help_text=_('This file will be attached to the first email that we send for every new order. Therefore it will be '
'combined with the "Placed order", "Free order", or "Received order" texts from above. It will be sent '
'to both order contacts and attendees. You can use this e.g. to send your terms of service. Do not use '
'it to send non-public information as this file might be sent before payment is confirmed or the order '
'is approved. To avoid this vital email going to spam, you can only upload PDF files of up to {size} MB.').format(
help_text=format_lazy(
_(
'This file will be attached to the first email that we send for every new order. Therefore it will be '
'combined with the "Placed order", "Free order", or "Received order" texts from above. It will be sent '
'to both order contacts and attendees. You can use this e.g. to send your terms of service. Do not use '
'it to send non-public information as this file might be sent before payment is confirmed or the order '
'is approved. To avoid this vital email going to spam, you can only upload PDF files of up to {size} MB.'
),
size=settings.FILE_UPLOAD_MAX_SIZE_EMAIL_AUTO_ATTACHMENT // (1024 * 1024),
)
),
@@ -2152,8 +2217,8 @@ a ticket for {event} has been ordered for you.
You can view the details and status of your ticket here:
{url}
Best regards,
Your {event} team"""))
Best regards,
Your {event} team""")) # noqa: W291
},
'mail_subject_order_changed': {
'type': LazyI18nString,
@@ -2168,8 +2233,8 @@ your order for {event} has been changed.
You can view the status of your order at
{url}
Best regards,
Your {event} team"""))
Best regards,
Your {event} team""")) # noqa: W291
},
'mail_subject_order_paid': {
'type': LazyI18nString,
@@ -2186,8 +2251,8 @@ we successfully received your payment for {event}. Thank you!
You can change your order details and view the status of your order at
{url}
Best regards,
Your {event} team"""))
Best regards,
Your {event} team""")) # noqa: W291
},
'mail_send_order_paid_attendee': {
'type': bool,
@@ -2206,8 +2271,8 @@ a ticket for {event} that has been ordered for you is now paid.
You can view the details and status of your ticket here:
{url}
Best regards,
Your {event} team"""))
Best regards,
Your {event} team""")) # noqa: W291
},
'mail_days_order_expire_warning': {
'form_class': forms.IntegerField,
@@ -2239,8 +2304,8 @@ your payment before {expire_date}.
You can view the payment information and the status of your order at
{url}
Best regards,
Your {event} team"""))
Best regards,
Your {event} team""")) # noqa: W291
},
'mail_subject_order_pending_warning': {
'type': LazyI18nString,
@@ -2256,8 +2321,8 @@ Please keep in mind that you are required to pay before {expire_date}.
You can view the payment information and the status of your order at
{url}
Best regards,
Your {event} team"""))
Best regards,
Your {event} team""")) # noqa: W291
},
'mail_subject_order_incomplete_payment': {
'type': LazyI18nString,
@@ -2276,8 +2341,26 @@ missing additional payment of **{pending_sum}**.
You can view the payment information and the status of your order at
{url}
Best regards,
Your {event} team"""))
Best regards,
Your {event} team""")) # noqa: W291
},
'mail_subject_order_payment_failed': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("Payment failed for your order: {code}")),
},
'mail_text_order_payment_failed': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
your payment attempt for your order for {event} has failed.
Your order is still valid and you can try to pay again using the same or a different payment method. Please complete your payment before {expire_date}.
You can retry the payment and view the status of your order at
{url}
Best regards,
Your {event} team""")) # noqa: W291
},
'mail_subject_waiting_list': {
'type': LazyI18nString,
@@ -2309,8 +2392,8 @@ as possible to the next person on the waiting list:
{url_remove}
Best regards,
Your {event} team"""))
Best regards,
Your {event} team""")) # noqa: W291
},
'mail_subject_order_canceled': {
'type': LazyI18nString,
@@ -2327,8 +2410,8 @@ your order {code} for {event} has been canceled.
You can view the details of your order at
{url}
Best regards,
Your {event} team"""))
Best regards,
Your {event} team""")) # noqa: W291
},
'mail_subject_order_approved': {
'type': LazyI18nString,
@@ -2347,8 +2430,8 @@ You can select a payment method and perform the payment here:
{url}
Best regards,
Your {event} team"""))
Best regards,
Your {event} team""")) # noqa: W291
},
'mail_send_order_approved_attendee': {
'type': bool,
@@ -2367,8 +2450,8 @@ we approved a ticket ordered for you for {event}.
You can view the details and status of your ticket here:
{url}
Best regards,
Your {event} team"""))
Best regards,
Your {event} team""")) # noqa: W291
},
'mail_subject_order_approved_free': {
'type': LazyI18nString,
@@ -2384,8 +2467,8 @@ at our event. As you only ordered free products, no payment is required.
You can change your order details and view the status of your order at
{url}
Best regards,
Your {event} team"""))
Best regards,
Your {event} team""")) # noqa: W291
},
'mail_send_order_approved_free_attendee': {
'type': bool,
@@ -2404,8 +2487,8 @@ we approved a ticket ordered for you for {event}.
You can view the details and status of your ticket here:
{url}
Best regards,
Your {event} team"""))
Best regards,
Your {event} team""")) # noqa: W291
},
'mail_subject_order_denied': {
'type': LazyI18nString,
@@ -2423,8 +2506,8 @@ You can view the details of your order here:
{url}
Best regards,
Your {event} team"""))
Best regards,
Your {event} team""")) # noqa: W291
},
'mail_text_order_custom_mail': {
'type': LazyI18nString,
@@ -2433,8 +2516,8 @@ Your {event} team"""))
You can change your order details and view the status of your order at
{url}
Best regards,
Your {event} team"""))
Best regards,
Your {event} team""")) # noqa: W291
},
'mail_days_download_reminder': {
'type': int,
@@ -2452,13 +2535,13 @@ Your {event} team"""))
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello {attendee_name},
you are registered for {event}.
you are registered for {event}.
If you did not do so already, you can download your ticket here:
{url}
If you did not do so already, you can download your ticket here:
{url}
Best regards,
Your {event} team"""))
Best regards,
Your {event} team""")) # noqa: W291
},
'mail_subject_download_reminder': {
'type': LazyI18nString,
@@ -2473,8 +2556,8 @@ you bought a ticket for {event}.
If you did not do so already, you can download your ticket here:
{url}
Best regards,
Your {event} team"""))
Best regards,
Your {event} team""")) # noqa: W291
},
'mail_subject_customer_registration': {
'type': LazyI18nString,
@@ -2494,9 +2577,9 @@ This link is valid for one day.
If you did not sign up yourself, please ignore this email.
Best regards,
Best regards,
Your {organizer} team"""))
Your {organizer} team""")) # noqa: W291
},
'mail_subject_customer_email_change': {
'type': LazyI18nString,
@@ -2516,9 +2599,9 @@ This link is valid for one day.
If you did not request this, please ignore this email.
Best regards,
Best regards,
Your {organizer} team"""))
Your {organizer} team""")) # noqa: W291
},
'mail_subject_customer_reset': {
'type': LazyI18nString,
@@ -2538,9 +2621,9 @@ This link is valid for one day.
If you did not request a new password, please ignore this email.
Best regards,
Best regards,
Your {organizer} team"""))
Your {organizer} team""")) # noqa: W291
},
'smtp_use_custom': {
'default': 'False',
@@ -2710,7 +2793,7 @@ Your {organizer} team"""))
'form_class': ExtFileField,
'form_kwargs': dict(
label=_('Header image'),
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
help_text=_('If you provide a logo image, we will by default not show your event name and date '
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
@@ -2753,7 +2836,7 @@ Your {organizer} team"""))
'form_class': ExtFileField,
'form_kwargs': dict(
label=_('Header image'),
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
help_text=_('If you provide a logo image, we will by default not show your organization name '
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
@@ -2793,7 +2876,7 @@ Your {organizer} team"""))
'form_class': ExtFileField,
'form_kwargs': dict(
label=_('Social media image'),
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
help_text=_('This picture will be used as a preview if you post links to your ticket shop on social media. '
'Facebook advises to use a picture size of 1200 x 630 pixels, however some platforms like '
@@ -2814,7 +2897,7 @@ Your {organizer} team"""))
'form_class': ExtFileField,
'form_kwargs': dict(
label=_('Logo image'),
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
required=False,
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
help_text=_('We will show your logo with a maximal height and width of 2.5 cm.')
@@ -3256,7 +3339,7 @@ def concatenation_for_salutation(d):
def get_name_parts_localized(name_parts, key):
value = name_parts.get(key, "")
if key == "salutation":
if key == "salutation" and value:
return pgettext_lazy("person_name_salutation", value)
return value
@@ -3583,8 +3666,10 @@ class SettingsSandbox:
def __delattr__(self, key: str) -> None:
del self._event.settings[self._convert_key(key)]
def get(self, key: str, default: Any = None, as_type: type = str):
return self._event.settings.get(self._convert_key(key), default=default, as_type=as_type)
def get(self, key: str, default: Any = None, as_type: type = str, binary_file: bool = False):
return self._event.settings.get(
self._convert_key(key), default=default, as_type=as_type, binary_file=binary_file
)
def set(self, key: str, value: Any):
self._event.settings.set(self._convert_key(key), value)
@@ -3630,14 +3715,10 @@ def validate_organizer_settings(organizer, settings_dict):
# This is not doing anything for the time being.
# But earlier we called validate_event_settings for the organizer, too - and that didn't do anything for
# organizer-settings either.
#
# N.B.: When actually fleshing out this stub, adding it to the OrganizerUpdateForm should be considered.
"""
if settings_dict.get('reusable_media_type_ntag_pretix1') and settings_dict.get('reusable_media_type_nfc_uid'):
if settings_dict.get('reusable_media_type_nfc_mf0aes') and settings_dict.get('reusable_media_type_nfc_uid'):
raise ValidationError({
'reusable_media_type_nfc_uid': _('This needs to be disabled if other NFC-based types are active.')
})
"""
def global_settings_object(holder):

View File

@@ -32,11 +32,12 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import copy
import json
import os
import time
from typing import List, Tuple
from django.db import transaction
from django.db.models import Max, Q
from django.db.models.functions import Greatest
from django.dispatch import receiver
@@ -99,11 +100,13 @@ class BaseDataShredder:
"""
raise NotImplementedError() # NOQA
def shred_data(self):
def shred_data(self, progress_callback=None):
"""
This method is called to actually remove the data from the system. You should remove any database objects
here.
You can call ``progress_callback`` with an integer value between 0 and 100 to communicate back your progress.
You should never delete ``LogEntry`` objects, but you might modify them to remove personal data. In this
case, set the ``LogEntry.shredded`` attribute to ``True`` to show that this is no longer original log data.
"""
@@ -151,6 +154,7 @@ class BaseDataShredder:
def shred_log_fields(logentry, banlist=None, whitelist=None):
d = logentry.parsed_data
initial_data = copy.copy(d)
shredded = False
if whitelist:
for k, v in d.items():
@@ -162,9 +166,61 @@ def shred_log_fields(logentry, banlist=None, whitelist=None):
if f in d:
d[f] = ''
shredded = True
logentry.data = json.dumps(d)
logentry.shredded = logentry.shredded or shredded
logentry.save(update_fields=['data', 'shredded'])
if d != initial_data:
logentry.data = json.dumps(d)
logentry.shredded = logentry.shredded or shredded
logentry.save(update_fields=['data', 'shredded'])
def slow_update(qs, batch_size=1000, sleep_time=.5, progress_callback=None, progress_offset=0, progress_total=None, **update):
"""
Doing UPDATE queries on hundreds of thousands of rows can cause outages due to high write load on the database.
This provides a throttled way to update rows. The condition for this to work properly is that the queryset has a
filter condition that no longer applies after the update!
Otherwise, this will be an endless loop!
"""
total_updated = 0
while True:
updated = qs.order_by().filter(
pk__in=qs.order_by().values_list('pk', flat=True)[:batch_size]
).update(**update)
total_updated += updated
if not updated:
break
if total_updated >= 0.8 * batch_size:
time.sleep(sleep_time)
if progress_callback and progress_total:
progress_callback((progress_offset + total_updated) / progress_total)
return total_updated
def slow_delete(qs, batch_size=1000, sleep_time=.5, progress_callback=None, progress_offset=0, progress_total=None):
"""
Doing DELETE queries on hundreds of thousands of rows can cause outages due to high write load on the database.
This provides a throttled way to update rows.
"""
total_deleted = 0
while True:
deleted = qs.order_by().filter(
pk__in=qs.order_by().values_list('pk', flat=True)[:batch_size]
).delete()[0]
total_deleted += deleted
if not deleted:
break
if total_deleted >= 0.8 * batch_size:
time.sleep(sleep_time)
return total_deleted
def _progress_helper(queryset, progress_callback, offset, total):
if not progress_callback:
yield from queryset
else:
for i, o in enumerate(queryset):
yield o
if i % 10 == 0:
progress_callback((i + offset) / total * 100)
class PhoneNumberShredder(BaseDataShredder):
@@ -177,18 +233,26 @@ class PhoneNumberShredder(BaseDataShredder):
o.code: o.phone for o in self.event.orders.filter(phone__isnull=False)
}, cls=CustomJSONEncoder, indent=4)
@transaction.atomic
def shred_data(self):
for o in self.event.orders.all():
def shred_data(self, progress_callback=None):
qs_orders = self.event.orders.all()
qs_orders_cnt = qs_orders.count()
qs_le = self.event.logentry_set.filter(action_type="pretix.event.order.phone.changed")
qs_le_cnt = qs_le.count()
total = qs_le_cnt + qs_orders_cnt
for o in _progress_helper(qs_orders, progress_callback, 0, total):
changed = bool(o.phone)
o.phone = None
d = o.meta_info_data
if d:
if 'contact_form_data' in d and 'phone' in d['contact_form_data']:
changed = True
del d['contact_form_data']['phone']
o.meta_info = json.dumps(d)
o.save(update_fields=['meta_info', 'phone'])
o.meta_info = json.dumps(d)
if changed:
o.save(update_fields=['meta_info', 'phone'])
for le in self.event.logentry_set.filter(action_type="pretix.event.order.phone.changed"):
for le in _progress_helper(qs_le, progress_callback, qs_orders_cnt, total):
shred_log_fields(le, banlist=['old_phone', 'new_phone'])
@@ -207,37 +271,66 @@ class EmailAddressShredder(BaseDataShredder):
for op in OrderPosition.all.filter(order__event=self.event, attendee_email__isnull=False)
}, indent=4)
@transaction.atomic
def shred_data(self):
OrderPosition.all.filter(order__event=self.event, attendee_email__isnull=False).update(attendee_email=None)
def shred_data(self, progress_callback=None):
qs_op = OrderPosition.all.filter(order__event=self.event, attendee_email__isnull=False)
qs_op_cnt = qs_op.count()
for o in self.event.orders.all():
qs_orders = self.event.orders.all()
qs_orders_cnt = qs_orders.count()
qs_le = self.event.logentry_set.filter(
Q(action_type__contains="order.email") | Q(action_type__contains="position.email") |
Q(action_type="pretix.event.order.contact.changed") |
Q(action_type="pretix.event.order.modified")
).exclude(data="")
qs_le_cnt = qs_le.count()
total = qs_op_cnt + qs_orders_cnt + qs_le_cnt
slow_update(
qs_op,
attendee_email=None,
progress_callback=progress_callback,
progress_offset=0,
progress_total=total,
# Updates to order position table are slow, since PostgreSQL needs to update many indexes, so let's
# take them really slowly to not overwhelm the database.
batch_size=100,
sleep_time=2,
)
for o in _progress_helper(qs_orders, progress_callback, qs_op_cnt, total):
changed = bool(o.email) or bool(o.customer)
o.email = None
o.customer = None
d = o.meta_info_data
if d:
if 'contact_form_data' in d and 'email' in d['contact_form_data']:
del d['contact_form_data']['email']
o.meta_info = json.dumps(d)
o.save(update_fields=['meta_info', 'email', 'customer'])
changed = True
o.meta_info = json.dumps(d)
if 'contact_form_data' in d and 'email_repeat' in d['contact_form_data']:
del d['contact_form_data']['email_repeat']
changed = True
if changed:
if d:
o.meta_info = json.dumps(d)
o.save(update_fields=['meta_info', 'email', 'customer'])
for le in self.event.logentry_set.filter(
Q(action_type__contains="order.email") | Q(action_type__contains="position.email"),
):
shred_log_fields(le, banlist=['recipient', 'message', 'subject', 'full_mail'])
for le in self.event.logentry_set.filter(action_type="pretix.event.order.contact.changed"):
shred_log_fields(le, banlist=['old_email', 'new_email'])
for le in self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data=""):
d = le.parsed_data
if 'data' in d:
for row in d['data']:
if 'attendee_email' in row:
row['attendee_email'] = ''
le.data = json.dumps(d)
le.shredded = True
le.save(update_fields=['data', 'shredded'])
for le in _progress_helper(qs_le, progress_callback, qs_op_cnt + qs_orders_cnt, total):
if le.action_type == "pretix.event.order.modified":
d = le.parsed_data
if 'data' in d:
for row in d['data']:
if 'attendee_email' in row:
row['attendee_email'] = ''
le.data = json.dumps(d)
le.shredded = True
le.save(update_fields=['data', 'shredded'])
else:
shred_log_fields(le, banlist=[
'recipient', 'message', 'subject', 'full_mail', 'old_email', 'new_email'
])
class WaitingListShredder(BaseDataShredder):
@@ -251,16 +344,35 @@ class WaitingListShredder(BaseDataShredder):
for wle in self.event.waitinglistentries.all()
], indent=4)
@transaction.atomic
def shred_data(self):
self.event.waitinglistentries.update(name_cached=None, name_parts={'_shredded': True}, email='', phone='')
def shred_data(self, progress_callback=None):
qs_wle = self.event.waitinglistentries.exclude(email='')
qs_wle_cnt = qs_wle.count()
for wle in self.event.waitinglistentries.select_related('voucher').filter(voucher__isnull=False):
qs_voucher = self.event.waitinglistentries.select_related('voucher').filter(voucher__isnull=False)
qs_voucher_cnt = qs_voucher.count()
qs_le = self.event.logentry_set.filter(action_type="pretix.voucher.added.waitinglist").exclude(data="")
qs_le_cnt = qs_le.count()
total = qs_voucher_cnt + qs_wle_cnt + qs_le_cnt
slow_update(
qs_wle,
name_cached=None,
name_parts={'_shredded': True},
email='',
phone='',
progress_callback=progress_callback,
progress_offset=0,
progress_total=total,
)
for wle in _progress_helper(qs_voucher, progress_callback, qs_wle_cnt, total):
if '@' in wle.voucher.comment:
wle.voucher.comment = ''
wle.voucher.save(update_fields=['comment'])
for le in self.event.logentry_set.filter(action_type="pretix.voucher.added.waitinglist").exclude(data=""):
for le in _progress_helper(qs_le, progress_callback, qs_wle_cnt + qs_voucher_cnt, total):
d = le.parsed_data
if 'name' in d:
d['name'] = ''
@@ -298,17 +410,41 @@ class AttendeeInfoShredder(BaseDataShredder):
)
}, indent=4)
@transaction.atomic
def shred_data(self):
OrderPosition.all.filter(
def shred_data(self, progress_callback=None):
qs_op = OrderPosition.all.filter(
order__event=self.event
).filter(
Q(attendee_name_cached__isnull=False) | Q(attendee_name_parts__isnull=False) |
Q(company__isnull=False) | Q(street__isnull=False) | Q(zipcode__isnull=False) | Q(city__isnull=False)
).update(attendee_name_cached=None, attendee_name_parts={'_shredded': True}, company=None, street=None,
zipcode=None, city=None)
Q(attendee_name_cached__isnull=False) |
Q(company__isnull=False) |
Q(street__isnull=False) |
Q(zipcode__isnull=False) |
Q(city__isnull=False)
)
qs_op_cnt = qs_op.count()
for le in self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data=""):
qs_le = self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data="")
qs_le_cnt = qs_le.count()
total = qs_op_cnt + qs_le_cnt
slow_update(
qs_op,
attendee_name_cached=None,
attendee_name_parts={'_shredded': True},
company=None,
street=None,
zipcode=None,
city=None,
progress_callback=progress_callback,
progress_total=total,
progress_offset=0,
# Updates to order position table are slow, since PostgreSQL needs to update many indexes, so let's
# take them really slowly to not overwhelm the database.
batch_size=100,
sleep_time=2,
)
for le in _progress_helper(qs_le, progress_callback, qs_op_cnt, total):
d = le.parsed_data
if 'data' in d:
for i, row in enumerate(d['data']):
@@ -343,11 +479,18 @@ class InvoiceAddressShredder(BaseDataShredder):
for ia in InvoiceAddress.objects.filter(order__event=self.event)
}, indent=4)
@transaction.atomic
def shred_data(self):
InvoiceAddress.objects.filter(order__event=self.event).delete()
def shred_data(self, progress_callback=None):
qs_ia = InvoiceAddress.objects.filter(order__event=self.event)
qs_ia_cnt = qs_ia.count()
for le in self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data=""):
qs_le = self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data="")
qs_le_cnt = qs_le.count()
total = qs_ia_cnt + qs_le_cnt
slow_delete(qs_ia, progress_callback=progress_callback, progress_total=total, progress_offset=0)
for le in _progress_helper(qs_le, progress_callback, qs_ia_cnt, total):
d = le.parsed_data
if 'invoice_data' in d and not isinstance(d['invoice_data'], bool):
for field in d['invoice_data']:
@@ -375,11 +518,18 @@ class QuestionAnswerShredder(BaseDataShredder):
).data
yield 'question-answers.json', 'application/json', json.dumps(d, indent=4)
@transaction.atomic
def shred_data(self):
QuestionAnswer.objects.filter(orderposition__order__event=self.event).delete()
def shred_data(self, progress_callback=None):
qs_qa = QuestionAnswer.objects.filter(orderposition__order__event=self.event)
qs_qa_cnt = qs_qa.count()
for le in self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data=""):
qs_le = self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data="")
qs_le_cnt = qs_le.count()
total = qs_qa_cnt + qs_le_cnt
slow_delete(qs_qa, progress_callback=progress_callback, progress_total=total, progress_offset=0)
for le in _progress_helper(qs_le, progress_callback, qs_qa_cnt, total):
d = le.parsed_data
if 'data' in d:
for i, row in enumerate(d['data']):
@@ -408,9 +558,11 @@ class InvoiceShredder(BaseDataShredder):
yield 'invoices/{}.pdf'.format(i.number), 'application/pdf', i.file.read()
i.file.close()
@transaction.atomic
def shred_data(self):
for i in self.event.invoices.filter(shredded=False):
def shred_data(self, progress_callback=None):
qs_i = self.event.invoices.filter(shredded=False)
total = qs_i.count()
for i in _progress_helper(qs_i, progress_callback, 0, total):
if i.file:
i.file.delete()
i.shredded = True
@@ -430,10 +582,17 @@ class CachedTicketShredder(BaseDataShredder):
def generate_files(self) -> List[Tuple[str, str, str]]:
pass
@transaction.atomic
def shred_data(self):
CachedTicket.objects.filter(order_position__order__event=self.event).delete()
CachedCombinedTicket.objects.filter(order__event=self.event).delete()
def shred_data(self, progress_callback=None):
qs_1 = CachedTicket.objects.filter(order_position__order__event=self.event)
qs_1_cnt = qs_1.count()
qs_2 = CachedCombinedTicket.objects.filter(order__event=self.event)
qs_2_cnt = qs_2.count()
total = qs_1_cnt + qs_2_cnt
slow_delete(qs_1, progress_callback=progress_callback, progress_total=total, progress_offset=0)
slow_delete(qs_2, progress_callback=progress_callback, progress_total=total, progress_offset=qs_1_cnt)
class PaymentInfoShredder(BaseDataShredder):
@@ -446,14 +605,21 @@ class PaymentInfoShredder(BaseDataShredder):
def generate_files(self) -> List[Tuple[str, str, str]]:
pass
@transaction.atomic
def shred_data(self):
def shred_data(self, progress_callback=None):
qs_p = OrderPayment.objects.filter(order__event=self.event)
qs_p_count = qs_p.count()
qs_r = OrderRefund.objects.filter(order__event=self.event)
qs_r_count = qs_r.count()
total = qs_p_count + qs_r_count
provs = self.event.get_payment_providers()
for obj in OrderPayment.objects.filter(order__event=self.event):
for obj in _progress_helper(qs_p, progress_callback, 0, total):
pprov = provs.get(obj.provider)
if pprov:
pprov.shred_payment_info(obj)
for obj in OrderRefund.objects.filter(order__event=self.event):
for obj in _progress_helper(qs_r, progress_callback, qs_p_count, total):
pprov = provs.get(obj.provider)
if pprov:
pprov.shred_payment_info(obj)

View File

@@ -787,3 +787,23 @@ return a dictionary mapping names of attributes in the settings store to DRF ser
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
customer_created = GlobalSignal()
"""
Arguments: ``customer``
This signal is sent out every time a customer account is created. The ``customer``
object is given as the first argument.
The ``sender`` keyword argument will contain the organizer.
"""
customer_signed_in = GlobalSignal()
"""
Arguments: ``customer``
This signal is sent out every time a customer signs in. The ``customer`` object
is given as the first argument.
The ``sender`` keyword argument will contain the organizer.
"""

View File

@@ -0,0 +1,6 @@
{# this is the version from django 3.x, prior to https://github.com/django/django/commit/5942ab5eb165ee2e759174e297148a40dd855920 so that django-bootstrap3 can keep doing its magic #}
{% with id=widget.attrs.id %}<ul{% if id %} id="{{ id }}"{% endif %}{% if widget.attrs.class %} class="{{ widget.attrs.class }}"{% endif %}>{% for group, options, index in widget.optgroups %}{% if group %}
<li>{{ group }}<ul{% if id %} id="{{ id }}_{{ index }}"{% endif %}>{% endif %}{% for option in options %}
<li>{% include option.template_name with widget=option %}</li>{% endfor %}{% if group %}
</ul></li>{% endif %}{% endfor %}
</ul>{% endwith %}

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