Compare commits

...

472 Commits

Author SHA1 Message Date
Raphael Michel
8fa715ac4b API: Allow to add debug_data to failed check-ins 2023-12-01 11:09:18 +01:00
Raphael Michel
6c479808d0 Fix crash PRETIXEU-9FC 2023-11-30 13:49:27 +01:00
Raphael Michel
bd14be485a Order change: Do not set invoice_dirty if invoicing is disabled 2023-11-30 11:51:41 +01:00
Raphael Michel
fbf362a91f Export management command: Fix bug in exporter detection 2023-11-30 11:49:16 +01:00
Raphael Michel
82704b60c7 Voucher form: Fix quota check for partially redeemed vouchers 2023-11-29 16:09:04 +01:00
Raphael Michel
b92feb382b Discounts: Fix scoping error with distinct subevents 2023-11-29 16:02:27 +01:00
Raphael Michel
66f934bba7 Bump version to 2023.11.0.dev0 2023-11-29 13:47:55 +01:00
Raphael Michel
13366b9985 Bump version to 2023.10.0 2023-11-29 13:47:08 +01:00
Raphael Michel
e971733d51 Add missing signal to documentation 2023-11-28 17:17:57 +01:00
Raphael Michel
3bd491151b Add upgrade notes to docs 2023-11-28 17:17:20 +01:00
Raphael Michel
580bc65c3e Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (219 of 219 strings)

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

powered by weblate
2023-11-28 16:53:09 +01:00
Raphael Michel
d2984548a7 Translations: Update German
Currently translated at 100.0% (219 of 219 strings)

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

powered by weblate
2023-11-28 16:53:09 +01:00
Raphael Michel
4d3090a590 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5520 of 5520 strings)

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

powered by weblate
2023-11-28 16:53:09 +01:00
Raphael Michel
53fa79b96c Translations: Update German
Currently translated at 100.0% (5520 of 5520 strings)

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

powered by weblate
2023-11-28 16:53:09 +01:00
Raphael Michel
293bdaedfe Translations: Extend wordlist 2023-11-28 16:48:50 +01:00
Raphael Michel
7498e5d6f7 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2023-11-28 16:26:27 +01:00
Raphael Michel
213049b52e Fix typo 2023-11-28 16:25:55 +01:00
Raphael Michel
6456aad16d Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2023-11-28 15:51:48 +01:00
Raphael Michel
8a3b313cb6 Check-in: Show more information (#3576)
* Check-in: Show more information

* Add change notes

* Rebase migration

* Add "expand" option to checkinrpc

* REmove accidental file

* Docs fixes

* REbase migration

* Rebase migration

* Fix typo

* REbase migration

* Make web-checkin look more like new android checkin
2023-11-28 14:52:12 +01:00
Raphael Michel
ab28086779 Copy event meta data when cloning events 2023-11-28 14:50:46 +01:00
Raphael Michel
965fcec9df Check-in: New error reason for unapproved orders (#3741)
* Check-in: New error reason for unapproved orders

* Fix documentation verbiage
2023-11-28 12:50:29 +01:00
Raphael Michel
1593eacb6b Widget: Fix tests 2023-11-28 12:49:07 +01:00
c0de-bender
abb5ae653c Translations: Update Polish
Currently translated at 38.4% (2114 of 5500 strings)

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

powered by weblate
2023-11-28 10:01:25 +01:00
Raphael Michel
bbf360b569 Widget: Allow to call subevents by URL isntead of attribute 2023-11-28 09:37:45 +01:00
Raphael Michel
1066a09612 Fix possible locking issue 2023-11-27 18:27:47 +01:00
Fast128
5e1d33f7f4 Translations: Update Polish
Currently translated at 38.4% (2114 of 5500 strings)

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

powered by weblate
2023-11-27 09:32:31 +01:00
c0de-bender
2813ea056b Translations: Update Polish
Currently translated at 99.5% (217 of 218 strings)

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

powered by weblate
2023-11-27 09:32:31 +01:00
c0de-bender
a8638e9bc8 Translations: Update Polish
Currently translated at 38.0% (2092 of 5500 strings)

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

powered by weblate
2023-11-27 09:32:31 +01:00
c0de-bender
74dc44546d Translations: Update Polish
Currently translated at 38.0% (2091 of 5500 strings)

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

powered by weblate
2023-11-27 09:32:31 +01:00
c0de-bender
f645b86963 Translations: Update Polish
Currently translated at 99.5% (217 of 218 strings)

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

powered by weblate
2023-11-27 09:32:31 +01:00
Joanna Kochel
7c30e29adf Translations: Update Polish
Currently translated at 99.5% (217 of 218 strings)

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

powered by weblate
2023-11-27 09:32:31 +01:00
c0de-bender
19c97b570e Translations: Update Polish
Currently translated at 38.0% (2091 of 5500 strings)

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

powered by weblate
2023-11-27 09:32:31 +01:00
Joanna Kochel
7a210b4ee0 Translations: Update Polish
Currently translated at 38.0% (2091 of 5500 strings)

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

powered by weblate
2023-11-27 09:32:31 +01:00
Maciej Sowilski
fdf3aa471c Translations: Update Polish
Currently translated at 93.5% (204 of 218 strings)

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

powered by weblate
2023-11-27 09:32:31 +01:00
Maciej Sowilski
2851c8a9cf Translations: Update Polish
Currently translated at 36.7% (2021 of 5500 strings)

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

powered by weblate
2023-11-27 09:32:31 +01:00
Raphael Michel
49d4324992 Event list filter: Save scroll position 2023-11-24 15:05:17 +01:00
Martin Gross
7648be7937 Stripe: Add Support for Affirm Pay Later (#3737)
Co-authored-by: Raphael Michel <michel@rami.io>
2023-11-23 13:02:29 +01:00
Raphael Michel
1dea908152 Translations: Update Polish
Currently translated at 36.4% (2003 of 5500 strings)

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

powered by weblate
2023-11-23 09:52:45 +01:00
c0de-bender
896f15222c Translations: Update Polish
Currently translated at 93.1% (203 of 218 strings)

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

powered by weblate
2023-11-23 09:52:45 +01:00
c0de-bender
540ced5bfc Translations: Update Polish
Currently translated at 36.4% (2002 of 5500 strings)

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

powered by weblate
2023-11-23 09:52:45 +01:00
c0de-bender
85ec235911 Translations: Update Polish
Currently translated at 92.2% (201 of 218 strings)

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

powered by weblate
2023-11-23 09:52:45 +01:00
c0de-bender
a810467200 Translations: Update Polish
Currently translated at 36.3% (2000 of 5500 strings)

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

powered by weblate
2023-11-23 09:52:45 +01:00
c0de-bender
354d03c38d Translations: Update Polish
Currently translated at 99.5% (217 of 218 strings)

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

powered by weblate
2023-11-23 09:52:45 +01:00
c0de-bender
83b17ee05b Translations: Update Polish
Currently translated at 36.4% (2002 of 5500 strings)

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

powered by weblate
2023-11-23 09:52:45 +01:00
Joanna Kochel
cb2da78cbf Translations: Update Polish
Currently translated at 100.0% (218 of 218 strings)

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

powered by weblate
2023-11-23 09:52:45 +01:00
Joanna Kochel
2fd8d1991b Translations: Update Polish
Currently translated at 51.3% (2825 of 5500 strings)

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

powered by weblate
2023-11-23 09:52:45 +01:00
Julian Baumann
8c80200fc0 Orders: Add bulk action to refund overpaid amount (#3721)
* add bulk action to refund overpaid amount

* display number of successful actions, use existing annotate method

* add tests, address review comments

* lint
2023-11-23 09:48:28 +01:00
Raphael Michel
b639ac850f LogEntry: Add a direct relationship to organizer (#3732)
* LogEntry: Add a direct relationship to organizer

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

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

* Fix condition

* Fix query count

* REbase migration

* Fix tests

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-11-22 16:22:33 +01:00
Raphael Michel
2ef015015a Allow to postpone invoice creation on order changes (#3716)
* Allow to postpone invoice creation on order changes

* Add tests

* isort fix

* Fix failures

* More tests

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-11-22 15:45:27 +01:00
c0de-bender
7921b67624 Translations: Update Polish
Currently translated at 91.2% (199 of 218 strings)

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

powered by weblate
2023-11-22 15:22:48 +01:00
c0de-bender
b8f735970e Translations: Update Polish
Currently translated at 31.9% (1756 of 5500 strings)

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

powered by weblate
2023-11-22 15:22:48 +01:00
Raphael Michel
5181fb38c4 Add Polish to community languages 2023-11-21 11:56:30 +01:00
Joanna Kochel
7439bc56b9 Translations: Update Polish
Currently translated at 91.2% (199 of 218 strings)

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

powered by weblate
2023-11-21 11:54:32 +01:00
c0de-bender
b31cd307e3 Translations: Update Polish
Currently translated at 31.9% (1756 of 5500 strings)

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

powered by weblate
2023-11-21 11:54:32 +01:00
Joanna Kochel
e2e226471a Translations: Update Polish
Currently translated at 31.9% (1756 of 5500 strings)

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

powered by weblate
2023-11-21 11:54:32 +01:00
c0de-bender
f2efe234ea Translations: Update Polish
Currently translated at 28.2% (1554 of 5500 strings)

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

powered by weblate
2023-11-21 11:54:32 +01:00
c0de-bender
3d172f2726 Translations: Update Polish
Currently translated at 28.2% (1553 of 5500 strings)

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

powered by weblate
2023-11-20 11:56:18 +01:00
c0de-bender
8b9dba2f97 Translations: Update Polish
Currently translated at 28.2% (1552 of 5500 strings)

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

powered by weblate
2023-11-20 11:56:18 +01:00
c0de-bender
e0bdd38e6f Translations: Update Polish
Currently translated at 44.4% (97 of 218 strings)

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

powered by weblate
2023-11-20 11:56:18 +01:00
c0de-bender
579fa88070 Translations: Update Polish
Currently translated at 28.2% (1552 of 5500 strings)

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

powered by weblate
2023-11-20 11:56:18 +01:00
Adrià Vilanova Martínez
a541db8487 Translations: Update Catalan
Currently translated at 35.0% (1929 of 5500 strings)

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

powered by weblate
2023-11-20 11:56:18 +01:00
liimee
83bdbb4c94 Translations: Update Indonesian
Currently translated at 98.6% (5426 of 5500 strings)

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

powered by weblate
2023-11-20 11:56:18 +01:00
Mira
9098450a7a Translations: Update German
Currently translated at 100.0% (5500 of 5500 strings)

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

powered by weblate
2023-11-20 11:56:18 +01:00
Ramazan Sancar
e0596595ed Translations: Update Turkish
Currently translated at 27.9% (61 of 218 strings)

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

powered by weblate
2023-11-20 11:56:18 +01:00
Ramazan Sancar
0da51cda89 Translations: Update Turkish
Currently translated at 44.8% (2468 of 5500 strings)

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

powered by weblate
2023-11-20 11:56:18 +01:00
Raphael Michel
7896368b36 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5500 of 5500 strings)

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

powered by weblate
2023-11-20 11:56:18 +01:00
Raphael Michel
915709519c Translations: Update German
Currently translated at 100.0% (5500 of 5500 strings)

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

powered by weblate
2023-11-20 11:56:18 +01:00
Raphael Michel
6f6def88a3 Fix password recovery even when reset is disabled 2023-11-20 11:36:54 +01:00
Mira
65dbf03a12 Fix #3701 -- Don't use static cookie_domain on custom domains (#3728) 2023-11-20 11:33:45 +01:00
Phin Wolkwitz
78609613bc Add notification signal (Z:#23127501) (#3725)
* Add and send signal for refund requests

* Add and send signal for notifications

* Revert changes

* Fix typo

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

* Document parameters

---------

Co-authored-by: Mira <weller@rami.io>
2023-11-20 11:33:12 +01:00
Raphael Michel
78acd8d118 Subevent editor: Fix enctype of form 2023-11-20 11:28:43 +01:00
Raphael Michel
f1969e783f Export form: Add note on multisheet exporters 2023-11-20 10:18:19 +01:00
Raphael Michel
3ad2429293 Event meta properties: Reorder edit form 2023-11-20 09:29:17 +01:00
Raphael Michel
ae72a6f574 Stripe: Improve help texts 2023-11-20 09:28:51 +01:00
Richard Schreiber
baf6144ee7 Add customizable terms of cancellation (Z#23135646) (#3704)
* Order: show user_cancel_deadline

* move to own parapgraph

* Remove date, add free text input for terms
2023-11-16 12:29:57 +01:00
Raphael Michel
71e515f9c2 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2023-11-15 15:27:34 +01:00
c0de-bender
ab6bda36c3 Translations: Update Dutch (informal) (nl_Informal)
Currently translated at 72.3% (3976 of 5498 strings)

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

powered by weblate
2023-11-15 15:27:06 +01:00
Thomas Vranken
d14790271d Translations: Update Dutch
Currently translated at 78.4% (171 of 218 strings)

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

powered by weblate
2023-11-15 15:27:06 +01:00
Thomas Vranken
1cb55186d2 Translations: Update Dutch
Currently translated at 82.8% (4554 of 5498 strings)

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

powered by weblate
2023-11-15 15:27:06 +01:00
Raphael Michel
77da4052b9 Order list export: Expose same "extended status" as in backend (#3674)
* Order list export: Expose same "extended status" as in backend

* Review notes
2023-11-15 15:20:30 +01:00
Raphael Michel
a631890db1 Improve styling for <p> and <ul> combinations in alerts 2023-11-14 17:48:58 +01:00
Charliecoleg
b233313a47 Translations: Update Welsh
Currently translated at 4.6% (258 of 5498 strings)

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

powered by weblate
2023-11-14 17:08:41 +01:00
Zona Vip
6431f1948d Translations: Update Spanish
Currently translated at 61.0% (3355 of 5498 strings)

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

powered by weblate
2023-11-14 17:08:41 +01:00
Mira
a65df3ed9a Fix the 'data-checkbox-dependency-visual' attribute (#3720) 2023-11-14 14:52:10 +01:00
Raphael Michel
fb28d6b927 Fix payment provider priority not respected 2023-11-14 13:05:11 +01:00
Richard Schreiber
0d82c3703d Widget: label button in event-list with "More info“ when availability is unknown (Z#23135197) (#3715)
* Widget: show "more info“ for unknown availability

* fix localization

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

* fix tests

---------

Co-authored-by: Raphael Michel <michel@rami.io>
2023-11-13 18:15:18 +01:00
Mira
1d5a8a5948 Add test for money filter + streamline rounding error protection logic (#3714)
* add test cases

* use rounding protection only for currencies with <2 decimal places

* add more test cases

* use parameterized tests
2023-11-13 17:37:18 +01:00
Christiaan de Die le Clercq
aa1c8ae054 Translations: Update Dutch
Currently translated at 82.8% (4553 of 5498 strings)

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

powered by weblate
2023-11-13 17:10:55 +01:00
Raphael Michel
b976615489 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5498 of 5498 strings)

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

powered by weblate
2023-11-13 17:10:55 +01:00
Raphael Michel
c06480634a Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (218 of 218 strings)

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

powered by weblate
2023-11-13 17:10:55 +01:00
Raphael Michel
33d79c5d28 Translations: Update German
Currently translated at 100.0% (218 of 218 strings)

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

powered by weblate
2023-11-13 17:10:55 +01:00
Raphael Michel
dbf5fc7e10 Translations: Update German
Currently translated at 100.0% (5498 of 5498 strings)

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

powered by weblate
2023-11-13 17:10:55 +01:00
Raphael Michel
17d5068ec7 Translations:Add words to word list 2023-11-13 17:07:02 +01:00
Raphael Michel
f4cc9ecc0d Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2023-11-13 16:14:52 +01:00
Raphael Michel
3c46c461c0 Translate question options in backend and PDFs (Z#23134850) (#3693)
* Translate question options in backend and PDFs

* Extend to invoices
2023-11-13 15:48:45 +01:00
Raphael Michel
ee70fec7ad Respect region of locale for money formatting (Z#23135361) (#3694) 2023-11-13 15:48:32 +01:00
Richard Schreiber
69c2a1b3c2 UI: Improve panel collapse visibility (Z#23132549) (#3709)
* Move collapse indicator in panels before label

* only hide collapse indicator in panel titles

* remove unneeded collapse-indicators in pretix-control

* remove unneeded collapse-indicators in presale

* move collapse-indicator to left in variants-toggle-button

* remove chevron and use default collapse-indicator in control-variants
2023-11-13 14:23:28 +01:00
Raphael Michel
c2ababb9d6 Do not allow offset refund to different currency 2023-11-13 13:09:34 +01:00
Raphael Michel
db9049130c Do not send password-reset for non-native users 2023-11-13 12:43:13 +01:00
Raphael Michel
65b74d0483 Do not allow password reset for disabled users 2023-11-13 12:43:13 +01:00
Raphael Michel
c21083bf80 Fix incomplete only() call 2023-11-13 12:43:13 +01:00
Fast128
47c281aaa6 Translations: Update Polish
Currently translated at 16.5% (904 of 5461 strings)

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

powered by weblate
2023-11-13 10:58:12 +01:00
Raphael Michel
e73e5e0340 Bank transfer: Discourage payments before an order code exists (Z#23135042) (#3692)
* Bank transfer: Discourage payments befor an order code exists

* Update src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html

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

---------

Co-authored-by: Mira <weller@rami.io>
2023-11-10 17:20:00 +01:00
Raphael Michel
1cb38e279c Fix test input for subevent creation 2023-11-10 14:22:37 +01:00
Raphael Michel
d8500128ea Refs #3691 -- Add note to documentation on device auth for settings 2023-11-10 12:16:30 +01:00
Raphael Michel
27e042baf7 Relative dates: Add UI to specify dates after reference date (#3707)
* Relative dates: Add UI to specify dates after reference date

* Do not use form fields twice

* Update src/pretix/base/reldate.py

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

* Update src/pretix/base/reldate.py

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

* Update src/pretix/base/reldate.py

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

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-11-10 12:13:33 +01:00
Raphael Michel
d7aa94d6ae Add public filters based on meta data (#3673)
* Add public filters based on meta data

* Fix licenseheaders

* ignore empty values

* Fix tests

* Full non-widget implementation

* Widget support

* Add a few tests

* Allow to reorder properties

* Fix isort

* Allow to opt-out for specific events

* Fix name clash between new and old field to make migration feasible
2023-11-10 12:10:01 +01:00
Raphael Michel
c0007a9566 Fix licenseheader 2023-11-10 11:54:54 +01:00
Raphael Michel
c45da95237 Fix isort 2023-11-10 11:53:16 +01:00
Thomas Vranken
45d17728d4 Translations: Update Dutch
Currently translated at 83.3% (4552 of 5461 strings)

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

powered by weblate
2023-11-10 10:28:57 +01:00
Raphael Michel
c4c81742d5 Order import: Catch CSV parsing errors during iteration (PRETIXEU-9AW, PRETIXEU-9B5) 2023-11-10 10:26:04 +01:00
Raphael Michel
4708509585 Revert "Order import: Catch CSV parsing errors during iteration (PRETIXEU-9AW)"
This reverts commit 77ff0298f1.
2023-11-10 10:19:05 +01:00
Raphael Michel
99b5f3cc3b Payment export: Fix mixup of form fields 2023-11-10 09:27:48 +01:00
Raphael Michel
dad3de9cd3 API: Fix crash in check-in RPC 2023-11-09 14:40:03 +01:00
Raphael Michel
3566fc877a Optimize N+1 query found by Sentry in event creation (PRETIXEU-9AV) 2023-11-09 14:31:05 +01:00
Raphael Michel
77ff0298f1 Order import: Catch CSV parsing errors during iteration (PRETIXEU-9AW) 2023-11-09 14:25:50 +01:00
Raphael Michel
22301f5429 Order change: Do not crash on empty fee input (PRETIXEU-9B2) 2023-11-09 14:15:39 +01:00
Raphael Michel
25a83adc78 Paginator: Add options for advanced users in backend (#3697)
* Paginator: Add options for advanced users in backend

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

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

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

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

* Add clickability

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-11-09 10:09:31 +01:00
Richard Schreiber
9dc1328b47 Keep sales-channels in same order (Z#23135800) (#3705)
* Control: sort sales-channels desc

* sort asc, force web first

* Update src/pretix/base/channels.py

---------

Co-authored-by: Raphael Michel <michel@rami.io>
2023-11-09 10:09:20 +01:00
Raphael Michel
5d28e5a959 Add autoscroll to widget after changing pages (Z#23135651) (#3698)
* Add autoscroll to widget after changing pages

* revert add autoscroll to widget

* add watcher on root.view

* check if scrollIntoView is necessary

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-11-09 10:05:02 +01:00
Maciej Sowilski
4ff07d12a6 Translations: Update Polish
Currently translated at 16.5% (903 of 5461 strings)

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

powered by weblate
2023-11-09 10:02:48 +01:00
c0de-bender
5688293288 Translations: Update Polish
Currently translated at 16.5% (903 of 5461 strings)

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

powered by weblate
2023-11-09 10:02:48 +01:00
Zona Vip
9b77403796 Translations: Update Spanish
Currently translated at 81.0% (175 of 216 strings)

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

powered by weblate
2023-11-07 15:39:11 +01:00
Zona Vip
c93c2fb6e8 Translations: Update Spanish
Currently translated at 60.1% (3284 of 5461 strings)

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

powered by weblate
2023-11-07 15:39:11 +01:00
Raphael Michel
5800babdab Event list: Add date filter 2023-11-07 09:40:39 +01:00
Raphael Michel
5e3600ac44 Fix missing form option 2023-11-06 18:37:26 +01:00
Raphael Michel
3af2342d7b Replace Item.hidden_if_available with relationship to other Item (#3686)
* draft

* Implementation that is closer to old one

* Fix tests

* Add tests

* Update src/pretix/control/forms/item.py

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

* Review notes

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-11-06 13:26:32 +01:00
Raphael Michel
3d68c83907 Improve check-in deletion (#3688)
* Improve check-in deletion

* Add pluralization

* Disable buttons if nothing selected
2023-11-06 10:30:02 +01:00
dependabot[bot]
6366df2c24 Bump vue and vue-template-compiler in /src/pretix/static/npm_dir (#3679)
Bumps [vue](https://github.com/vuejs/core) and [vue-template-compiler](https://github.com/vuejs/vue). These dependencies needed to be updated together.

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

Updates `vue-template-compiler` from 2.7.14 to 2.7.15
- [Release notes](https://github.com/vuejs/vue/releases)
- [Changelog](https://github.com/vuejs/vue/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/vue/compare/v2.7.14...v2.7.15)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-06 10:25:13 +01:00
Fast128
05ca336c1b Translations: Update Polish
Currently translated at 15.9% (869 of 5461 strings)

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

powered by weblate
2023-11-06 10:07:40 +01:00
Zona Vip
f643102696 Translations: Update Spanish
Currently translated at 60.0% (3277 of 5461 strings)

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

powered by weblate
2023-11-06 10:07:40 +01:00
Jāzeps Benjamins Baško
33ef50daea Translations: Update Latvian
Currently translated at 39.4% (2153 of 5461 strings)

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

powered by weblate
2023-11-06 10:07:40 +01:00
Raphael Michel
8071207bf3 Order change: Allow price reduction as long as no refund is required (Z#23135268) (#3689)
* Order change: Allow price reduction as long as no refund is required

* Update src/pretix/base/settings.py

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

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-11-06 10:07:21 +01:00
Martin Gross
767bb27175 PayPal: Add visibility EventListener for onApprove (Z#23135203) (#3687) 2023-11-03 15:18:53 +01:00
Raphael Michel
b7f240faf0 Emphasize risk of deleting check-in lists 2023-11-03 13:46:35 +01:00
Raphael Michel
e0e2b2d7f7 Allow hidden payment methods on payment method change (#3682)
* Allow hidden payment methods on payment method change

* Save hashes to meta data
2023-11-03 13:42:34 +01:00
Raphael Michel
10b515f1d1 Fix incorrect import 2023-11-03 13:18:36 +01:00
dependabot[bot]
d81c05c444 Bump @rollup/plugin-node-resolve from 15.2.1 to 15.2.3 in /src/pretix/static/npm_dir (#3680)
Bumps [@rollup/plugin-node-resolve](https://github.com/rollup/plugins/tree/HEAD/packages/node-resolve) from 15.2.1 to 15.2.3.
- [Changelog](https://github.com/rollup/plugins/blob/master/packages/node-resolve/CHANGELOG.md)
- [Commits](https://github.com/rollup/plugins/commits/node-resolve-v15.2.3/packages/node-resolve)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-03 13:12:57 +01:00
Raphael Michel
bc0a205f03 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5461 of 5461 strings)

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

powered by weblate
2023-11-03 13:06:29 +01:00
Raphael Michel
0e53ddc83b Translations: Update German
Currently translated at 100.0% (5461 of 5461 strings)

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

powered by weblate
2023-11-03 13:06:29 +01:00
Raphael Michel
0400b577bb Badges: Create templates for common paper sizes (#3660)
* Badges: Create templates for common paper sizes

* Add more sizes

* format lazy
2023-11-03 12:37:20 +01:00
Raphael Michel
ec2085f125 Docs: Fix change version number 2023-11-02 21:32:28 +01:00
Raphael Michel
6430427e3a Docs: Fix change version number 2023-11-02 21:32:18 +01:00
Raphael Michel
38a1b6a417 Ignore deprecation warning in compressor 2023-11-02 19:57:23 +01:00
dependabot[bot]
764d7a2f1c Bump @rollup/plugin-babel from 6.0.3 to 6.0.4 in /src/pretix/static/npm_dir (#3677)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-02 19:40:43 +01:00
dependabot[bot]
e65564234f Bump @babel/core from 7.23.0 to 7.23.2 in /src/pretix/static/npm_dir (#3678)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-02 19:40:35 +01:00
Raphael Michel
1ec18eb44a Payment method change: Hide explanation if there are no fees 2023-11-02 14:33:26 +01:00
Raphael Michel
4c51c28d7a Order change: Emphasize warning on payment state (Z#23135268) 2023-11-02 14:23:42 +01:00
fyksen
eaa134089e Translations: Update Norwegian Bokmål
Currently translated at 99.5% (215 of 216 strings)

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

powered by weblate
2023-11-02 14:07:01 +01:00
fyksen
a015c9ca2a Translations: Update Norwegian Bokmål
Currently translated at 94.7% (5173 of 5461 strings)

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

powered by weblate
2023-11-02 14:07:01 +01:00
Raphael Michel
d8ca865fc6 Email: Fix color for organizer-level emails 2023-11-02 11:29:27 +01:00
Raphael Michel
987b02d733 Add Norwegian Bokmål language 2023-11-02 10:14:58 +01:00
fyksen
3dd8d4349d Translations: Update Norwegian Bokmål
Currently translated at 93.9% (203 of 216 strings)

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

powered by weblate
2023-11-02 10:13:55 +01:00
fyksen
fd43908e4c Translations: Update Norwegian Bokmål
Currently translated at 6.3% (346 of 5461 strings)

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

powered by weblate
2023-11-02 10:13:55 +01:00
fyksen
257ed8ebc3 Translations: Update Norwegian Bokmål
Currently translated at 6.2% (339 of 5461 strings)

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

powered by weblate
2023-11-02 10:13:55 +01:00
Jāzeps Benjamins Baško
dfd8bf1c0f Translations: Update Latvian
Currently translated at 38.8% (2124 of 5461 strings)

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

powered by weblate
2023-11-02 10:13:55 +01:00
Raphael Michel
0f709c2275 Device API: Fix RSA encryption crash (PRETIXEU-8Y2) 2023-10-31 10:23:14 +01:00
Raphael Michel
3b64e6046c API: Add endpoints for scheduled exports (#3659)
* API: Add endpoints for scheduled exports

* ADd note to docs
2023-10-27 17:15:53 +02:00
Raphael Michel
26cbc24a10 Waiting list: Use a deterministic order 2023-10-27 17:01:31 +02:00
Martin Gross
33a5479809 Add KulturPass documentation (#3671)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
Co-authored-by: Raphael Michel <michel@rami.io>
2023-10-27 15:32:07 +02:00
Raphael Michel
000c64755d Free price: Allow to suggest a different price than the minimum (#3666)
* Free price: Allow to suggest a different price than the minimum

* Full implementation

* Widget tests

* Add min values to titles
2023-10-27 13:36:01 +02:00
Raphael Michel
b32249d48b Make redirections after voucher redemption more consistent 2023-10-27 11:47:22 +02:00
Raphael Michel
c325cc1120 Fix crash in gift card detail view (PRETIXEU-97N) 2023-10-27 10:46:05 +02:00
Raphael Michel
8d2791b32e Fix crash in event creation form (PRETIXEU-97T) 2023-10-27 10:43:46 +02:00
Raphael Michel
466fc15382 Scheduled exports: Fix handling of datetime fields 2023-10-27 10:39:46 +02:00
Raphael Michel
86cf3be225 Bump version to 2023.10.0.dev0 2023-10-27 10:05:08 +02:00
Raphael Michel
dcf23aa0a0 Bump version to 2023.9.0 2023-10-27 10:04:27 +02:00
Raphael Michel
36c823b14e Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5461 of 5461 strings)

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

powered by weblate
2023-10-27 10:03:02 +02:00
Raphael Michel
8c905d22ea Translations: Update German
Currently translated at 100.0% (5461 of 5461 strings)

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

powered by weblate
2023-10-27 10:03:02 +02:00
Raphael Michel
83692898ae Translations: Update wordlist for German 2023-10-27 09:40:41 +02:00
Raphael Michel
5c7858c181 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2023-10-26 14:47:21 +02:00
Raphael Michel
3aec460614 Add docs for 50f048fa8 2023-10-26 13:49:26 +02:00
Raphael Michel
18159a1b77 Webhooks: Use better event selection widget 2023-10-26 10:41:29 +02:00
Raphael Michel
216f931993 Stripe: Guard against invalid JSON in API 2023-10-25 13:24:34 +02:00
Raphael Michel
4587fed81b API: Allow to filter orders by customer 2023-10-25 13:24:26 +02:00
Raphael Michel
0642dcb7ba Docs: Fix default of registration flag 2023-10-25 09:35:20 +02:00
Raphael Michel
6267767ce7 Password reset: Set needs_password_change to false 2023-10-25 09:35:04 +02:00
Raphael Michel
3aa751aa10 Accounting report: Split gift card transactions in credit and debit 2023-10-24 11:18:55 +02:00
Raphael Michel
053cfdf3a9 Gift cards: Allow to sort by most recent transaction 2023-10-24 11:18:55 +02:00
Richard Schreiber
563583cd36 Improve help texts when deleting questions (Z#23134288) 2023-10-24 10:10:46 +02:00
Richard Schreiber
df1433786a Make select2-placeholder italic (#3664) 2023-10-23 18:55:56 +02:00
Raphael Michel
7f6365cc81 Add new check-in list flags to backend 2023-10-23 17:26:42 +02:00
Raphael Michel
56acb0929a Export: Gracefully handle ExportEmptyError 2023-10-23 16:57:08 +02:00
Raphael Michel
a0831890ad Check-in: New flags for check-in lists (#3577) 2023-10-23 15:52:06 +02:00
Richard Schreiber
da9aa3e133 Fix registration tests (#3663) 2023-10-23 14:40:43 +02:00
Richard Schreiber
91d99d5f14 Waitinglist: add email-address to confirmation text (#3662) 2023-10-23 13:42:57 +02:00
Raphael Michel
83c6dd4d6b Change default for self-service backend registration 2023-10-23 10:16:09 +02:00
Richard Schreiber
0ad13a4dbe Product settings: improve fallback labels for PDF-layouts (#3656) 2023-10-23 10:07:28 +02:00
Raphael Michel
3a2655e57d Require password change on initial user after installation 2023-10-23 09:42:04 +02:00
Raphael Michel
126fe34005 Mail: Do not use colons in sender names 2023-10-20 17:01:55 +02:00
dependabot[bot]
bd41c0e78e Bump @babel/traverse from 7.23.0 to 7.23.2 in /src/pretix/static/npm_dir (#3657)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-20 16:26:30 +02:00
Raphael Michel
c49dec3835 Mail: Log error message after retry 2023-10-19 14:03:26 +02:00
Raphael Michel
66a7c7e6b8 Mail: Do not retry on not-supported features
Drop other constraints since SSLError and SMTPError are subclasses of
OSError anyways
2023-10-19 14:03:02 +02:00
Martin Gross
50f048fa8b Subevent editor: Allow plugins to declare form titles (#3636)
Co-authored-by: Raphael Michel <michel@rami.io>
2023-10-19 10:48:54 +02:00
Raphael Michel
1035fa20e9 Check-in: Add export with all valid codes (Z#23133941) (#3652) 2023-10-19 10:48:44 +02:00
Raphael Michel
f4aaf2ad39 Mail settings: Add support for SPF redirect mechanism 2023-10-18 19:41:59 +02:00
Raphael Michel
1dec993673 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5451 of 5451 strings)

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

powered by weblate
2023-10-18 11:42:06 +02:00
Raphael Michel
3c557e1ad6 Translations: Update German
Currently translated at 100.0% (5451 of 5451 strings)

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

powered by weblate
2023-10-18 11:42:06 +02:00
Raphael Michel
3bb18f0440 Fix typo in translation files 2023-10-18 11:20:18 +02:00
Raphael Michel
c661d67f77 Add words to typocheck list 2023-10-18 10:57:55 +02:00
Raphael Michel
158d480bb3 Fix typo in error message 2023-10-18 10:57:31 +02:00
Raphael Michel
759aa5a8ea Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2023-10-18 09:26:04 +02:00
Felix Hartnagel
5ea482456e Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5443 of 5443 strings)

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

powered by weblate
2023-10-18 09:25:35 +02:00
Raphael Michel
e1a212156d Organizer index: Make event series more obvious (#3651) 2023-10-18 09:25:07 +02:00
Raphael Michel
9299ac5813 API: Fix N+1 query in gift card list 2023-10-17 14:55:42 +02:00
Richard Schreiber
abfd8a7d86 Check-in list export: Fall back to invoice address (Z#23133171) (#3650) 2023-10-17 13:40:50 +02:00
Raphael Michel
4d75c568b5 Support file upload in AsyncFormView and subevent editor 2023-10-16 17:59:32 +02:00
Raphael Michel
abda800953 Do not treat event as free if there are mandatory non-free addons (#3646) 2023-10-16 12:35:20 +02:00
Raphael Michel
43d26451d1 Add tests for locking in API (#3617) 2023-10-16 12:34:21 +02:00
Raphael Michel
2dd51b6f62 Order import: Allow to manually specify character set 2023-10-16 12:22:05 +02:00
dependabot[bot]
9d7d5389dc Bump @babel/preset-env from 7.22.15 to 7.23.2 in /src/pretix/static/npm_dir (#3647)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-16 11:16:33 +02:00
dependabot[bot]
fefa15fedd Bump @babel/core from 7.22.11 to 7.23.0 in /src/pretix/static/npm_dir (#3627)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-16 10:58:32 +02:00
Sanny
2c905b0b39 Translations: Update Italian
Currently translated at 21.4% (1167 of 5443 strings)

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

powered by weblate
2023-10-16 10:58:21 +02:00
Sanny
1c4d64a3b5 Translations: Update Italian
Currently translated at 21.3% (1161 of 5443 strings)

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

powered by weblate
2023-10-16 10:58:21 +02:00
Raphael Michel
b920795edf Translations: Update German
Currently translated at 100.0% (5443 of 5443 strings)

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

powered by weblate
2023-10-16 10:58:21 +02:00
Dennis Lichtenthäler
270d91080f Translations: Update German
Currently translated at 100.0% (5443 of 5443 strings)

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

powered by weblate
2023-10-16 10:58:21 +02:00
Richard Schreiber
3a26d6173e Widget: add single-item-select shopping cart (Z#23131246) (#3633) 2023-10-16 10:56:00 +02:00
Richard Schreiber
7ee630928b UI: improve distinction between placeholder and input (#3640) 2023-10-16 10:41:37 +02:00
Raphael Michel
215a2f2dbb API: Fix bulk voucher creation with server-generated codes 2023-10-15 16:16:32 +02:00
Raphael Michel
f1a29a7c5b Customer account: Catch IntegrityError during email change 2023-10-11 11:39:45 +02:00
Raphael Michel
e72f6746f2 Badges: Add Avery 63.5x96.6 template 2023-10-11 11:28:50 +02:00
lujoga
8343b1256b Docker: Change default number of workers to 2 * $(nproc) (#3634) 2023-10-09 18:05:21 +02:00
Sanny
d4c541b2bc Translations: Update Italian
Currently translated at 19.5% (1066 of 5443 strings)

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

powered by weblate
2023-10-09 18:04:03 +02:00
Ronan LE MEILLAT
43eff3b65f Translations: Update French
Currently translated at 100.0% (5443 of 5443 strings)

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

powered by weblate
2023-10-09 18:04:03 +02:00
Ronan LE MEILLAT
1c1beee5f6 Translations: Update French
Currently translated at 100.0% (5443 of 5443 strings)

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

powered by weblate
2023-10-09 18:04:03 +02:00
Takanori Suzuki
dc3fc3cb45 Translations: Update Japanese
Currently translated at 3.1% (169 of 5443 strings)

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

powered by weblate
2023-10-09 18:04:03 +02:00
Raphael Michel
f62a200f52 Translations: Update French
Currently translated at 99.9% (5439 of 5443 strings)

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

powered by weblate
2023-10-09 18:04:03 +02:00
Mossroy
49d048fc49 Translations: Update French
Currently translated at 99.9% (5439 of 5443 strings)

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

powered by weblate
2023-10-09 18:04:03 +02:00
Mossroy
fde1c67388 Translations: Update French
Currently translated at 99.8% (5437 of 5443 strings)

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

powered by weblate
2023-10-09 18:04:03 +02:00
Raphael Michel
c8e818f786 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5443 of 5443 strings)

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

powered by weblate
2023-10-09 18:04:03 +02:00
Raphael Michel
5221ce965b Translations: Update German
Currently translated at 100.0% (5443 of 5443 strings)

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

powered by weblate
2023-10-09 18:04:03 +02:00
Igor Támara
196eb3482c Translations: Update Spanish
Currently translated at 60.2% (3282 of 5448 strings)

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

powered by weblate
2023-10-09 18:04:03 +02:00
Igor Támara
1fe7fa91d5 Translations: Update Spanish
Currently translated at 59.4% (3238 of 5448 strings)

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

powered by weblate
2023-10-09 18:04:03 +02:00
Raphael Michel
d8ecb43e5d OrderChangeManager: Prevent race conditions (Z#23131769) (#3623) 2023-10-09 16:00:58 +02:00
Richard Schreiber
0cbb38f3d4 Widget: Fix waitinglist-URL for subevents (Z#23132845) (#3635) 2023-10-09 12:17:28 +02:00
Raphael Michel
ab0e26047e Docs: Add send_access_code to exhibitors API 2023-10-09 11:58:16 +02:00
Martin Gross
079324863b NoQA extra long default ApplePay Token line 2023-10-05 16:14:40 +02:00
Martin Gross
ae445c1460 Move apple-developer-merchantid-domain-association into setting (#3611) 2023-10-05 16:07:11 +02:00
Raphael Michel
6593a64b18 Badges: Add Avery 80x50 template 2023-10-05 15:44:47 +02:00
Raphael Michel
b8c448fbd7 Docs: Add new field to exhibitors API (#3624)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-10-04 16:29:38 +02:00
Raphael Michel
c1c828d8a2 Order detail page: Don't offer invoice generation for expired orders 2023-10-04 12:42:48 +02:00
Raphael Michel
1d0dbae77a Question detail view: Show total number of answers 2023-10-04 11:44:53 +02:00
Richard Schreiber
c6cf8b6f24 Calender: Always add end date (Z#23131739) (#3622) 2023-09-28 16:05:03 +02:00
Raphael Michel
78fbfc9c80 Migrate from AutoField to BigAutoField (#3493) 2023-09-27 08:59:10 +02:00
Raphael Michel
7ead117f2e Docs: Fix errors in exhibitor API documentation 2023-09-26 14:27:21 +02:00
Raphael Michel
a4417e97fd Check-in: Handle products without variation in simulator 2023-09-26 13:54:29 +02:00
Raphael Michel
10e9b9e12d Check-in: Handle non-existant IDs 2023-09-26 13:52:10 +02:00
Raphael Michel
755c4efba1 Bump to 2023.9.0.dev0 2023-09-26 11:54:04 +02:00
Raphael Michel
a5910debf2 Bump version to 2023.8.0 2023-09-26 11:52:53 +02:00
Raphael Michel
27e8be7e92 Docs: Update minimum version of PostgreSQL 2023-09-26 11:34:41 +02:00
Raphael Michel
9f2627004b Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (215 of 215 strings)

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

powered by weblate
2023-09-26 11:34:24 +02:00
Raphael Michel
bba8fc051a Translations: Update German
Currently translated at 100.0% (215 of 215 strings)

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

powered by weblate
2023-09-26 11:34:24 +02:00
Raphael Michel
12bef4ccdc Translations: Update German
Currently translated at 100.0% (5443 of 5443 strings)

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

powered by weblate
2023-09-26 11:34:24 +02:00
Raphael Michel
e556b51651 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5443 of 5443 strings)

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

powered by weblate
2023-09-26 11:34:24 +02:00
Raphael Michel
2b966b28f2 Translations: Update German
Currently translated at 100.0% (5443 of 5443 strings)

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

powered by weblate
2023-09-26 11:34:24 +02:00
Igor Támara
fd7d37cfa3 Translations: Update Spanish
Currently translated at 59.3% (3225 of 5436 strings)

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

powered by weblate
2023-09-26 11:34:24 +02:00
Richard Schreiber
ca79436e36 Vouchers: fix code min-length check on bulk creation 2023-09-26 09:15:23 +02:00
Raphael Michel
ca23073760 Translations: Add "Community" to word list 2023-09-26 01:50:41 +02:00
Raphael Michel
9c0acdf97f Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2023-09-26 01:12:08 +02:00
Felix Hartnagel
20edbe47d6 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5436 of 5436 strings)

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

powered by weblate
2023-09-26 01:09:26 +02:00
Mahdia Aliyy
9b6e7f369c Translations: Update Indonesian
Currently translated at 100.0% (5436 of 5436 strings)

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

powered by weblate
2023-09-22 13:26:42 +02:00
Raphael Michel
3ba36512d2 Add event_pattern like event_url 2023-09-22 12:24:01 +02:00
Richard Schreiber
d7d419c826 Remove empty optgroups from language selector widget 2023-09-21 15:41:31 +02:00
Mahdia Aliyy
acb35866ba Translations: Update Indonesian
Currently translated at 94.8% (201 of 212 strings)

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

powered by weblate
2023-09-20 16:08:12 +02:00
Mahdia Aliyy
11a04acae8 Translations: Update Indonesian
Currently translated at 100.0% (5436 of 5436 strings)

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

powered by weblate
2023-09-20 16:08:12 +02:00
Mahdia Aliyy
b882d84678 Translations: Update Indonesian
Currently translated at 94.8% (201 of 212 strings)

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

powered by weblate
2023-09-20 16:08:12 +02:00
Mahdia Aliyy
8e0ad694b7 Translations: Update Indonesian
Currently translated at 100.0% (5436 of 5436 strings)

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

powered by weblate
2023-09-20 16:08:12 +02:00
Raphael Michel
39a0eaff3e Add language translation score to selector widget (#3603)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-09-20 16:02:18 +02:00
Mahdia Aliyy
7b3eeaf411 Translations: Update Indonesian
Currently translated at 94.8% (201 of 212 strings)

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

powered by weblate
2023-09-19 09:21:13 +02:00
Mahdia Aliyy
3f536c2c7a Translations: Update Indonesian
Currently translated at 100.0% (5436 of 5436 strings)

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

powered by weblate
2023-09-19 09:21:13 +02:00
Martin Gross
36203d653b Display amounts with currency-derived decimals (#3604) 2023-09-18 17:48:34 +02:00
Mahdia Aliyy
fae45d066a Translations: Update Indonesian
Currently translated at 93.0% (5059 of 5436 strings)

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

powered by weblate
2023-09-18 17:47:44 +02:00
Raphael Michel
5de5642b59 Translations: Update Indonesian
Currently translated at 92.9% (5054 of 5436 strings)

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

powered by weblate
2023-09-18 17:47:44 +02:00
Mahdia Aliyy
5991a71316 Translations: Update Indonesian
Currently translated at 92.9% (5054 of 5436 strings)

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

powered by weblate
2023-09-18 17:47:44 +02:00
Mahdia Aliyy
da09baa712 Translations: Update Indonesian
Currently translated at 25.9% (55 of 212 strings)

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

powered by weblate
2023-09-18 17:47:44 +02:00
Mahdia Aliyy
271d58d2d9 Translations: Update Indonesian
Currently translated at 92.9% (5052 of 5436 strings)

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

powered by weblate
2023-09-18 17:47:44 +02:00
Raphael Michel
400967c9e2 Add Indonesian language 2023-09-18 17:43:00 +02:00
Raphael Michel
b37947a94a Translations: Update Indonesian
Currently translated at 9.0% (490 of 5436 strings)

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

powered by weblate
2023-09-18 13:20:04 +02:00
Mahdia Aliyy
f86e869702 Translations: Update Indonesian
Currently translated at 9.0% (490 of 5436 strings)

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

powered by weblate
2023-09-18 13:20:04 +02:00
Raphael Michel
43a27dc0b0 Translations: Update Indonesian
Currently translated at 8.9% (488 of 5436 strings)

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

powered by weblate
2023-09-18 13:20:04 +02:00
Mahdia Aliyy
2e475432f6 Translations: Update Indonesian
Currently translated at 8.9% (488 of 5436 strings)

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

powered by weblate
2023-09-18 13:20:04 +02:00
Mahdia Aliyy
03dfeab03f Translations: Update Indonesian
Currently translated at 7.9% (433 of 5436 strings)

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

powered by weblate
2023-09-18 13:20:04 +02:00
Aufa Fadhlurohman
e86a53a763 Translations: Update Indonesian
Currently translated at 10.3% (22 of 212 strings)

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

powered by weblate
2023-09-18 13:20:04 +02:00
Mahdia Aliyy
99deb7a78c Translations: Update Indonesian
Currently translated at 2.7% (152 of 5436 strings)

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

powered by weblate
2023-09-18 13:20:04 +02:00
Aufa Fadhlurohman
6c69bc6a78 Translations: Update Indonesian
Currently translated at 2.7% (152 of 5436 strings)

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

powered by weblate
2023-09-18 13:20:04 +02:00
Mahdia Aliyy
9f6b9f03e6 Translations: Update Indonesian
Currently translated at 1.2% (70 of 5436 strings)

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

powered by weblate
2023-09-18 13:20:04 +02:00
Aufa Fadhlurohman
d9b8d61ac6 Translations: Update Indonesian
Currently translated at 1.2% (70 of 5436 strings)

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

powered by weblate
2023-09-18 13:20:04 +02:00
Rubén Vargas
6eff5d0cf8 Translations: Update Spanish
Currently translated at 58.4% (3179 of 5436 strings)

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

powered by weblate
2023-09-18 13:20:04 +02:00
Aufa Fadhlurohman
d2a6c6600d Translations: Update Indonesian
Currently translated at 1.1% (61 of 5436 strings)

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

powered by weblate
2023-09-18 13:20:04 +02:00
Aufa Fadhlurohman
1f01ab232e Translations: Update Indonesian
Currently translated at 9.9% (21 of 212 strings)

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

powered by weblate
2023-09-18 13:20:04 +02:00
Mahdia Aliyy
c74bd17f59 Translations: Update Indonesian
Currently translated at 1.1% (61 of 5436 strings)

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

powered by weblate
2023-09-18 13:20:04 +02:00
Aufa Fadhlurohman
980d9a1671 Translations: Update Indonesian
Currently translated at 1.1% (61 of 5436 strings)

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

powered by weblate
2023-09-18 13:20:04 +02:00
Ed Bennett
0ac5290e90 Translations: Update Welsh
Currently translated at 4.3% (234 of 5436 strings)

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

powered by weblate
2023-09-18 13:20:04 +02:00
Ed Bennett
c7a9ff52e3 Translations: Update Welsh
Currently translated at 4.2% (230 of 5436 strings)

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

powered by weblate
2023-09-18 13:20:04 +02:00
Michael
111598db7a Translations: Update Czech
Currently translated at 78.8% (4287 of 5436 strings)

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

powered by weblate
2023-09-18 13:20:04 +02:00
Ed Bennett
be53751fc7 Translations: Update Welsh
Currently translated at 3.8% (207 of 5436 strings)

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

powered by weblate
2023-09-18 13:20:04 +02:00
Michael
8d07e2dc92 Translations: Update Czech
Currently translated at 96.6% (205 of 212 strings)

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

powered by weblate
2023-09-18 13:20:04 +02:00
Michael
c2a42db9cd Translations: Update Czech
Currently translated at 77.3% (4205 of 5436 strings)

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

powered by weblate
2023-09-18 13:20:04 +02:00
Raphael Michel
bedd841063 Metrics: Set batch size for redis hscan 2023-09-18 12:00:37 +02:00
Raphael Michel
543b089acf Fix #3599 -- Add instructions for PostgreSQL migration with docker 2023-09-18 11:03:52 +02:00
Raphael Michel
575858aec6 Check-in: Change explanation weight of gate (Z#23130674) 2023-09-15 17:20:59 +02:00
Ed Bennett
6ceb5bd7ce Translations: Update Welsh
Currently translated at 3.5% (191 of 5436 strings)

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

powered by weblate
2023-09-14 17:38:09 +02:00
Ed Bennett
162fd9e656 Translations: Update Welsh
Currently translated at 3.4% (188 of 5436 strings)

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

powered by weblate
2023-09-14 17:38:09 +02:00
Michael
ae73370a3e Translations: Update Czech
Currently translated at 76.3% (4151 of 5436 strings)

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

powered by weblate
2023-09-14 17:38:09 +02:00
Michael
5f6ec5e771 Translations: Update Czech
Currently translated at 75.9% (4129 of 5436 strings)

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

powered by weblate
2023-09-14 17:38:09 +02:00
Ed Bennett
bf548904f3 Translations: Update Welsh
Currently translated at 3.0% (166 of 5436 strings)

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

powered by weblate
2023-09-14 17:38:09 +02:00
Michael
f280eedc21 Translations: Update Czech
Currently translated at 75.7% (4119 of 5436 strings)

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

powered by weblate
2023-09-14 17:38:09 +02:00
Ed Bennett
57b5036f36 Translations: Update Welsh
Currently translated at 2.9% (162 of 5436 strings)

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

powered by weblate
2023-09-14 17:38:09 +02:00
Ed Bennett
545d911219 Translations: Update Welsh
Currently translated at 2.9% (158 of 5436 strings)

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

powered by weblate
2023-09-14 17:38:09 +02:00
Ed Bennett
c58cfb59ad Translations: Update Welsh
Currently translated at 2.7% (152 of 5436 strings)

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

powered by weblate
2023-09-14 17:38:09 +02:00
Raphael Michel
56803e3d65 Fix voucher and order views for users with read-only permissions (#3594) 2023-09-14 17:37:48 +02:00
Richard Schreiber
7d8a788361 Docs: fix code block in widget data-attributes docs 2023-09-14 12:02:06 +02:00
Raphael Michel
ecb4c34e08 Payment providers: Allow to set an availability start date per method (Z#23126769) (#3569)
Co-authored-by: Mira <weller@rami.io>
2023-09-14 11:08:37 +02:00
Richard Schreiber
75abab403a Widget: make data-attributes reactive (#3586) 2023-09-14 10:51:35 +02:00
Raphael Michel
dea7de4e6c Fix Order.can_modify_answers if only invoice name is required 2023-09-14 10:35:04 +02:00
Richard Schreiber
af3100a354 E-mail content: insert placeholder into text on click 2023-09-14 10:32:22 +02:00
Richard Schreiber
66ae5d1af2 Widget: fix running different versions of Vue on the same page 2023-09-14 10:31:18 +02:00
Ronan LE MEILLAT
9efb1653ee Translations: Update French
Currently translated at 100.0% (212 of 212 strings)

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

powered by weblate
2023-09-12 17:18:55 +02:00
Ronan LE MEILLAT
c6f1441976 Translations: Update French
Currently translated at 100.0% (5436 of 5436 strings)

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

powered by weblate
2023-09-12 17:18:55 +02:00
Ronan LE MEILLAT
7d08337c49 Translations: Update French
Currently translated at 100.0% (5436 of 5436 strings)

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

powered by weblate
2023-09-12 17:18:55 +02:00
Ronan LE MEILLAT
c92a1eb641 Translations: Update French
Currently translated at 100.0% (5436 of 5436 strings)

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

powered by weblate
2023-09-12 17:18:55 +02:00
Raphael Michel
babe0934a8 Fix incorrect usage of email subject setting 2023-09-12 16:58:51 +02:00
Raphael Michel
160e0e0531 Email settings: Fix wrong placeholder configuration 2023-09-12 16:58:41 +02:00
Raphael Michel
183387a683 Waiting list: Add email placeholder {name} 2023-09-12 16:53:12 +02:00
Raphael Michel
cea0ac59bf API: Fix parsing of time frames for exporters 2023-09-12 16:45:01 +02:00
Felix Freiberger
3b6f116f5d Fix typo in gift card acceptance page (#3592) 2023-09-12 12:37:45 +02:00
Raphael Michel
e884c9820f Move new settings to _base_settings 2023-09-12 11:56:49 +02:00
Raphael Michel
7545e92373 [SECURITY] Do not allow Pillow to parse EPS files 2023-09-12 11:50:01 +02:00
Raphael Michel
b16680e0e5 Docker: Upgrade to Debian bookworm (#3591) 2023-09-12 09:45:15 +02:00
Raphael Michel
eb04fdf4d2 Check-in rules: New variables (#3521) 2023-09-12 09:43:57 +02:00
Raphael Michel
c842ea597c New locking mechanism (#2408)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-09-11 11:44:50 +02:00
Raphael Michel
b2b3fa36be Fix incorrect handling of boolean configuration flags 2023-09-11 10:00:59 +02:00
Raphael Michel
af50bc2fa4 QR code generator: Lift permission requirement 2023-09-08 17:35:29 +02:00
Raphael Michel
734a116b6f Pass subevent to item_descriptions signal 2023-09-08 17:35:29 +02:00
Raphael Michel
8f36e36619 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5436 of 5436 strings)

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

powered by weblate
2023-09-08 15:58:29 +02:00
Raphael Michel
2f35e1fcc6 Translations: Update German
Currently translated at 100.0% (5436 of 5436 strings)

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

powered by weblate
2023-09-08 15:58:29 +02:00
Raphael Michel
cdfc2190d5 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2023-09-08 15:36:58 +02:00
Raphael Michel
8dbc7ac5d7 Bank transfer: Hide email form if email was already sent (Z#23128941) (#3570)
Co-authored-by: Mira <weller@rami.io>
2023-09-08 14:11:45 +02:00
Richard Schreiber
b04ec720dc Fix details sneak-peek keyup 2023-09-08 13:15:19 +02:00
Raphael Michel
1478f191a8 Fix crash in send_expiry_warnings 2023-09-08 12:35:02 +02:00
Raphael Michel
171b1de0c4 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5435 of 5435 strings)

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

powered by weblate
2023-09-08 10:24:29 +02:00
Raphael Michel
799f1a5388 Translations: Update German
Currently translated at 100.0% (5435 of 5435 strings)

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

powered by weblate
2023-09-08 10:24:29 +02:00
Raphael Michel
3cdb61fcf9 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5435 of 5435 strings)

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

powered by weblate
2023-09-08 10:24:29 +02:00
Raphael Michel
e5ce2deaeb Translations: Update German
Currently translated at 100.0% (5435 of 5435 strings)

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

powered by weblate
2023-09-08 10:24:29 +02:00
Raphael Michel
f986f20f6a Translations: Update wordlist 2023-09-08 10:10:36 +02:00
Raphael Michel
8f22d52e24 Fix misformatted translation strings 2023-09-08 10:05:08 +02:00
Raphael Michel
1c944baafa Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2023-09-08 09:50:53 +02:00
Raphael Michel
e55e3eeb0c Docs: Adjust install guide for Debian 12 2023-09-08 09:50:00 +02:00
Aufa Fadhlurohman
063967528f Translations: Update Indonesian
Currently translated at 0.1% (4 of 5426 strings)

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

powered by weblate
2023-09-08 09:49:07 +02:00
Aufa Fadhlurohman
edbe42c524 Translations: Update Indonesian
Currently translated at 5.6% (12 of 212 strings)

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

powered by weblate
2023-09-08 09:49:07 +02:00
Aufa Fadhlurohman
50a855a964 Translations: Update Indonesian
Currently translated at 0.1% (2 of 5426 strings)

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

powered by weblate
2023-09-08 09:49:07 +02:00
Tuomas Savela
1ad81d21a4 Translations: Update Finnish
Currently translated at 16.4% (893 of 5426 strings)

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

powered by weblate
2023-09-08 09:49:07 +02:00
Mira
0f8fa3fe47 Fix: keep is_business heuristic result for a longer time (Z#23126061) (#3582) 2023-09-08 09:44:19 +02:00
Martin Gross
45b9a705e2 Allow to refund currencies with zero decimals (#3583) 2023-09-07 17:55:43 +02:00
Phin Wolkwitz
3f07050d42 Payment: Add setting to prevent reminder mails (Z#23123914) (#3573)
Adds a checkbox in each payment provider's settings controlling whether sending out expiry reminders should be prevented
2023-09-07 14:27:09 +02:00
Raphael Michel
f97e6d0a71 Fix missing white space 2023-09-07 11:16:05 +02:00
Raphael Michel
c0031e4579 Add bulk operations for orders (#3548)
* Add bulk operations for orders

* UI tweaks

* Fix test failures

* Fix filter form

* Add tests

* Run isort
2023-09-06 17:02:21 +02:00
Raphael Michel
ce73d4831e Allow to sort the list of check-in lists (Z#23128978) (#3568) 2023-09-06 13:23:54 +02:00
Raphael Michel
fbfae0838a Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5426 of 5426 strings)

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

powered by weblate
2023-09-06 12:33:27 +02:00
Raphael Michel
5445a79572 Translations: Update German
Currently translated at 100.0% (5426 of 5426 strings)

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

powered by weblate
2023-09-06 12:33:27 +02:00
Raphael Michel
97a03f0aae Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5426 of 5426 strings)

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

powered by weblate
2023-09-06 12:33:27 +02:00
Raphael Michel
780a5cdeee Translations: Update German
Currently translated at 100.0% (5426 of 5426 strings)

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

powered by weblate
2023-09-06 12:33:27 +02:00
Phin Wolkwitz
991f245dce Scheduled emails: Extend filter by subevents (Z#23122902) (#3551)
To create automated mail rules for specific subevents only, this adds a selection widget to choose which, only appearing if there are any subevents to select.
2023-09-06 12:05:37 +02:00
Raphael Michel
8913d35838 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2023-09-06 10:12:50 +02:00
Ash So
7f2616cf80 Translations: Update Chinese (Traditional)
Currently translated at 99.7% (5385 of 5400 strings)

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

powered by weblate
2023-09-06 10:12:16 +02:00
pretix translation bot
7d9b54709a Update translations (#3561)
Co-authored-by: Ash So <ashs@vankaifong.com>
Co-authored-by: subpublic <anders@subpublic.com>
2023-09-06 10:11:31 +02:00
robbi5
231e05f967 API: Add parent event slug to subevent list search (#3567)
Co-authored-by: Raphael Michel <michel@rami.io>
2023-09-06 10:10:04 +02:00
Raphael Michel
e50db7844b Log pretix.customer.created when creating account through OIDC 2023-09-06 09:55:56 +02:00
Raphael Michel
b43523ea65 API: Fix order and invoice viewset with staff permissions 2023-09-06 09:38:42 +02:00
Raphael Michel
447370d7b3 API: Fix subevent_before/after on organizer-level order view 2023-09-06 09:38:42 +02:00
Raphael Michel
6a36efd18c Invoice settings: Reorder and explain recommended options (#3572) 2023-09-05 16:42:16 +02:00
dependabot[bot]
c1ebbfe82a Bump @rollup/plugin-node-resolve from 15.1.0 to 15.2.1 in /src/pretix/static/npm_dir (#3564)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-04 21:35:34 +02:00
dependabot[bot]
7f36a80dd8 Bump @babel/preset-env in /src/pretix/static/npm_dir (#3571)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.22.9 to 7.22.15.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.22.15/packages/babel-preset-env)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-04 18:42:24 +02:00
dependabot[bot]
c52b998ea8 Bump @babel/core from 7.22.9 to 7.22.11 in /src/pretix/static/npm_dir (#3563)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-04 17:29:32 +02:00
Raphael Michel
eecd002ffd API: Add webhooks for customer events (#3558) 2023-09-04 08:47:30 +02:00
Mira
53027b621f Update payment.py (#3559) 2023-08-31 15:18:23 +02:00
robbi5
826c54bce7 Questions: Enable phonenumber type during checkin (#3557) 2023-08-31 13:46:19 +02:00
Ash So
0cf2f06fcf Translations: Update Chinese (Traditional)
Currently translated at 99.7% (5385 of 5400 strings)

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

powered by weblate
2023-08-31 11:02:05 +02:00
Raphael Michel
12095c0c9c Sendmail: Rename "automated emails" to "scheduled emails" (#3556) 2023-08-31 11:01:50 +02:00
pretix translation bot
bca4cd1e23 Update translations (#3553)
Co-authored-by: Ash So <ashs@vankaifong.com>
Co-authored-by: Martin Gross <gross@rami.io>
2023-08-31 09:38:28 +02:00
Raphael Michel
53e84dfb08 API: Fix validation of duplicate customer email addresses 2023-08-30 16:57:15 +02:00
Raphael Michel
2e8447486c Improve edge cases in handling of check-in nonces (#3516) 2023-08-30 10:43:24 +02:00
robbi5
5b184bb1a0 Fix leaflet osm tile suggestion (#3549)
Co-authored-by: Raphael Michel <michel@rami.io>
2023-08-30 09:53:41 +02:00
Ash So
8c6f0a5dc1 Translations: Update Chinese (Traditional)
Currently translated at 99.7% (5384 of 5400 strings)

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

powered by weblate
2023-08-30 09:46:11 +02:00
Alain
6a53091b91 Translations: Update Dutch
Currently translated at 84.4% (4559 of 5400 strings)

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

powered by weblate
2023-08-30 09:46:11 +02:00
Mira
be4bc9a6f3 TemplateBasedMailRenderer: make markdown compiler call overridable (#3550) 2023-08-30 09:41:34 +02:00
Raphael Michel
efb1141d59 PayPal: Add missing payment.fail() statements 2023-08-29 15:10:05 +02:00
Raphael Michel
322a730eb2 PayPal: Fix incorrect Decimal comparison 2023-08-29 15:06:03 +02:00
Raphael Michel
8d2224e725 API: Allow organizer-level access of orders and invoices (#3547) 2023-08-28 16:54:42 +02:00
Raphael Michel
5b819b76f0 Check-in: Fix N+1 query issue identified by sentry 2023-08-28 16:54:09 +02:00
Raphael Michel
5d90a42acf Discounts: Allow "buy X to get Y" with different product sets for X and Y (#3543) 2023-08-28 16:21:52 +02:00
Raphael Michel
5398671fde Fix crash in invoice address detection (PRETIXEU-8XE) 2023-08-28 11:45:30 +02:00
Raphael Michel
f7d4460deb Fix N+1 query issues detected by Sentry 2023-08-26 16:24:03 +02:00
Alain
f76576a587 Translations: Update Dutch (informal) (nl_Informal)
Currently translated at 73.7% (3982 of 5400 strings)

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

powered by weblate
2023-08-24 18:07:17 +02:00
Alain
cf5f0dc7f9 Translations: Update Dutch
Currently translated at 80.6% (171 of 212 strings)

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

powered by weblate
2023-08-24 18:07:17 +02:00
Alain
567984bd5e Translations: Update Dutch
Currently translated at 84.2% (4549 of 5400 strings)

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

powered by weblate
2023-08-24 18:07:17 +02:00
Raphael Michel
1c6bd46d21 Order data export: Add product/variation ID (#3542) 2023-08-24 18:02:24 +02:00
Mira
9ba3227837 Checkout: Prefill is_business heuristically (Z#23126061) (#3533) 2023-08-24 17:06:47 +02:00
Richard Schreiber
21864885cb Checkout: improve heuristic to open invoice-panel (#3545) 2023-08-24 16:15:47 +02:00
Mira
38173e3a54 Tax rules: add custom rules for country subdivision (e.g. state) (Z#23111850) (#3520) 2023-08-24 14:11:10 +02:00
Phin Wolkwitz
4baf317934 Automated emails: Extend filter by check-in state (#3489)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
Co-authored-by: Raphael Michel <michel@rami.io>
2023-08-23 16:19:27 +02:00
Raphael Michel
c2b25bad06 Event dashboard: Fix incorrect optimization introduced in 8e9f0f07a (#3540) 2023-08-23 14:54:38 +02:00
Raphael Michel
9e3ad6c05c Order payment step: Pass info_data to checkout_confirm_render 2023-08-23 12:18:49 +02:00
Raphael Michel
f017de1a21 Voucher bulk creation: Fix validation issue 2023-08-23 12:18:30 +02:00
Raphael Michel
b56bd8541e Devices: Fix bulk edit query (#3541) 2023-08-23 11:20:26 +02:00
Raphael Michel
1c9219609a Fix progress callback for slow_delete helper 2023-08-23 10:50:39 +02:00
Raphael Michel
0c96f758a8 Fix quota cache mixup (#3539)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-08-23 10:09:50 +02:00
Richard Schreiber
9bd3444aad PDF: fix deduplicated list of addons (exclude canceled) (#3538) 2023-08-22 14:05:30 +02:00
Raphael Michel
10a83935d9 CartManager: Fix TransactionManagementError
Bug occured when extending a product and deleting it at the same time
2023-08-22 13:42:56 +02:00
Raphael Michel
e8ea6e0f5c Item creation: Fix failing test 2023-08-22 12:59:57 +02:00
Raphael Michel
e94e5be878 Item creation: Fix bug in copying meta data 2023-08-22 11:32:43 +02:00
Richard Schreiber
1073ea626e Banktransfer: make row-headers sticky (Z#23127000) (#3537) 2023-08-22 10:53:26 +02:00
Raphael Michel
23ab8df443 Translations: Add Welsh 2023-08-22 10:53:15 +02:00
Kian Cross
d6caf01a38 Add warning about configuration of Celery in development mode to docs (#3525) 2023-08-22 10:44:11 +02:00
Raphael Michel
1424ae78e9 Revert accidental change 2023-08-22 10:20:19 +02:00
Raphael Michel
827382edc3 Bump redis to 4.6.* 2023-08-22 09:43:21 +02:00
Maurice Kaag
85482bc939 Translations: Update French
Currently translated at 100.0% (5400 of 5400 strings)

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

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

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

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

* Add QR code to voucher URL view

* Fix allowed_hosts

---------

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

* Remove unnecessary "with" statement

* fix indentation

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Add a .

* Update doc/development/nfc/uid.rst

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

---------

Co-authored-by: robbi5 <richt@rami.io>
2023-07-26 13:26:00 +02:00
479 changed files with 317914 additions and 177468 deletions

View File

@@ -35,7 +35,7 @@ jobs:
- uses: actions/checkout@v2
- uses: harmon758/postgresql-action@v1
with:
postgresql version: '11'
postgresql version: '15'
postgresql db: 'pretix'
postgresql user: 'postgres'
postgresql password: 'postgres'
@@ -66,6 +66,10 @@ jobs:
- name: Run tests
working-directory: ./src
run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test -n 3 -p no:sugar --cov=./ --cov-report=xml --reruns 3 tests --maxfail=100
- name: Run concurrency tests
working-directory: ./src
run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test tests/concurrency_tests/ --reruns 0 --reuse-db
if: matrix.database == 'postgres'
- name: Upload coverage
uses: codecov/codecov-action@v1
with:

View File

@@ -1,4 +1,4 @@
FROM python:3.11-bullseye
FROM python:3.11-bookworm
RUN apt-get update && \
apt-get install -y --no-install-recommends \
@@ -20,20 +20,20 @@ RUN apt-get update && \
supervisor \
libmaxminddb0 \
libmaxminddb-dev \
zlib1g-dev && \
zlib1g-dev \
nodejs \
npm && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* && \
dpkg-reconfigure locales && \
locale-gen C.UTF-8 && \
/usr/sbin/update-locale LANG=C.UTF-8 && \
dpkg-reconfigure locales && \
locale-gen C.UTF-8 && \
/usr/sbin/update-locale LANG=C.UTF-8 && \
mkdir /etc/pretix && \
mkdir /data && \
useradd -ms /bin/bash -d /pretix -u 15371 pretixuser && \
echo 'pretixuser ALL=(ALL) NOPASSWD:SETENV: /usr/bin/supervisord' >> /etc/sudoers && \
mkdir /static && \
mkdir /etc/supervisord && \
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash - && \
apt-get install -y nodejs
mkdir /etc/supervisord
ENV LC_ALL=C.UTF-8 \
@@ -63,10 +63,10 @@ RUN pip3 install -U \
RUN chmod +x /usr/local/bin/pretix && \
rm /etc/nginx/sites-enabled/default && \
cd /pretix/src && \
rm -f pretix.cfg && \
mkdir -p data && \
chown -R pretixuser:pretixuser /pretix /data data && \
sudo -u pretixuser make production
rm -f pretix.cfg && \
mkdir -p data && \
chown -R pretixuser:pretixuser /pretix /data data && \
sudo -u pretixuser make production
USER pretixuser
VOLUME ["/etc/pretix", "/data"]

View File

@@ -5,7 +5,7 @@ export DATA_DIR=/data/
export HOME=/pretix
AUTOMIGRATE=${AUTOMIGRATE:-yes}
NUM_WORKERS_DEFAULT=$((2 * $(nproc --all)))
NUM_WORKERS_DEFAULT=$((2 * $(nproc)))
export NUM_WORKERS=${NUM_WORKERS:-$NUM_WORKERS_DEFAULT}
if [ ! -d /data/logs ]; then

View File

@@ -1,4 +1,4 @@
from pretix.settings import *
LOGGING['handlers']['mail_admins']['include_html'] = True
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
STORAGES["staticfiles"]["BACKEND"] = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'

View File

@@ -75,7 +75,7 @@ Example::
The cookie domain to be set. Defaults to ``None``.
``registration``
Enables or disables the registration of new admin users. Defaults to ``on``.
Enables or disables the registration of new admin users. Defaults to ``off``.
``password_reset``
Enables or disables password reset. Defaults to ``on``.
@@ -152,6 +152,7 @@ Example::
password=abcd
host=localhost
port=3306
advisory_lock_index=1
sslmode=require
sslrootcert=/etc/pretix/postgresql-ca.crt
sslcert=/etc/pretix/postgresql-client-crt.crt
@@ -167,11 +168,17 @@ Example::
``user``, ``password``, ``host``, ``port``
Connection details for the database connection. Empty by default.
``advisory_lock_index``
On PostgreSQL, pretix uses the "advisory lock" feature. However, advisory locks use a server-wide name space and
and are not scoped to a specific database. If you run multiple pretix applications with the same PostgreSQL server,
you should set separate values for this setting (integers up to 256).
``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

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`_ 11+ database server
* A `PostgreSQL`_ 12+ 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
@@ -276,7 +276,8 @@ Restarting the service can take a few seconds, especially if the update requires
Replace ``stable`` above with a specific version number like ``1.0`` or with ``latest`` for the development
version, if you want to.
Make sure to also read :ref:`update_notes` and the release notes of the version you are updating to.
Make sure to also read :ref:`update_notes` and the release notes of the version you are updating to. Pay special
attention to the "Runtime and server environment" section of all release notes between your current and new version.
.. _`docker_plugininstall`:

View File

@@ -12,7 +12,7 @@ solution with many things readily set-up, look at :ref:`dockersmallscale`.
get it right. If you're not feeling comfortable managing a Linux server, check out our hosting and service
offers at `pretix.eu`_.
We tested this guide on the Linux distribution **Debian 11.6** but it should work very similar on other
We tested this guide on the Linux distribution **Debian 12** but it should work very similar on other
modern distributions, especially on all systemd-based ones.
Requirements
@@ -24,7 +24,7 @@ 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
* A `PostgreSQL`_ 12+ database server
* A `redis`_ server
* A `nodejs`_ installation
@@ -64,7 +64,7 @@ Package dependencies
To build and run pretix, you will need the following debian packages::
# apt-get install git build-essential python-dev python3-venv python3 python3-pip \
# apt-get install git build-essential python3-dev python3-venv python3 python3-pip \
python3-dev libxml2-dev libxslt1-dev libffi-dev zlib1g-dev libssl-dev \
gettext libpq-dev libjpeg-dev libopenjp2-7-dev
@@ -130,9 +130,10 @@ We now install pretix, its direct dependencies and gunicorn::
Note that you need Python 3.9 or newer. You can find out your Python version using ``python -V``.
We also need to create a data directory::
We also need to create a data directory and allow your webserver to traverse to the root directory::
(venv)$ mkdir -p /var/pretix/data/media
(venv)$ chmod +x /var/pretix
Finally, we compile static files and translation data and create the database structure::
@@ -248,14 +249,14 @@ The following snippet is an example on how to configure a nginx proxy for pretix
}
location /static/ {
alias /var/pretix/venv/lib/python3.10/site-packages/pretix/static.dist/;
alias /var/pretix/venv/lib/python3.11/site-packages/pretix/static.dist/;
access_log off;
expires 365d;
add_header Cache-Control "public";
}
}
.. note:: Remember to replace the ``python3.10`` in the ``/static/`` path in the config
.. note:: Remember to replace the ``python3.11`` in the ``/static/`` path in the config
above with your python version.
We recommend reading about setting `strong encryption settings`_ for your web server.
@@ -285,7 +286,8 @@ To upgrade to a new pretix release, pull the latest code changes and run the fol
(venv)$ python -m pretix updatestyles
# systemctl restart pretix-web pretix-worker
Make sure to also read :ref:`update_notes` and the release notes of the version you are updating to.
Make sure to also read :ref:`update_notes` and the release notes of the version you are updating to. Pay special
attention to the "Runtime and server environment" section of all release notes between your current and new version.
.. _`manual_plugininstall`:

View File

@@ -16,12 +16,17 @@ already upgraded to pretix 5.0 or later, downgrade back to the last 4.x release
Update database schema
----------------------
Before you start, make sure your database schema is up to date::
Before you start, make sure your database schema is up to date. With a local installation::
# sudo -u pretix -s
$ source /var/pretix/venv/bin/activate
(venv)$ python -m pretix migrate
With a docker installation::
docker exec -it pretix.service pretix migrate
Install PostgreSQL
------------------
@@ -70,10 +75,14 @@ Of course, instead of all this you can also run a PostgreSQL docker container an
Stop pretix
-----------
To prevent any more changes to your data, stop pretix from running::
To prevent any more changes to your data, stop pretix from running. With a local installation::
# systemctl stop pretix-web pretix-worker
With docker::
# systemctl stop pretix
Change configuration
--------------------
@@ -90,12 +99,16 @@ Change the database configuration in your ``/etc/pretix/pretix.cfg`` file::
Create database schema
-----------------------
To create the schema in your new PostgreSQL database, use the following commands::
To create the schema in your new PostgreSQL database, use the following commands. With a local installation::
# sudo -u pretix -s
$ source /var/pretix/venv/bin/activate
(venv)$ python -m pretix migrate
With docker::
# docker run --rm -v /var/pretix-data:/data -v /etc/pretix:/etc/pretix -v /var/run/redis:/var/run/redis pretix/standalone:stable migrate
Migrate your data
-----------------
@@ -144,11 +157,18 @@ Afterwards, delete the file again::
Start pretix
------------
Now, restart pretix. Maybe stop your MySQL server as a verification step that you are no longer using it::
Stop your MySQL server as a verification step that you are no longer using it::
# systemctl stop mariadb
Then, restart pretix. With a local installation::
# systemctl start pretix-web pretix-worker
With a docker installation::
# systemctl start pretix
And you're done! After you've verified everything has been copied correctly, you can delete the old MySQL database.
.. note:: Don't forget to update your backup process to back up your PostgreSQL database instead of your MySQL database now.

View File

@@ -47,5 +47,30 @@ Or, with a docker installation::
$ docker exec -it pretix.service pretix create_order_transactions
Upgrade to 2023.6.0 or newer
""""""""""""""""""""""""""""
MariaDB and MySQL are no longer supported.
Upgrade to 2023.8.0 or newer
""""""""""""""""""""""""""""
PostgreSQL 11 is now required.
Upgrade to 2023.9.0 or newer
""""""""""""""""""""""""""""
This release includes a migration that changes the `id` column of all core database tables from `integer`
to `bigint`. If you have a large database, the migration step of the upgrade might take significantly longer than
usual, so plan the update accordingly.
The default value for the `registration` setting in `pretix.cfg` has changed to `false`.
Upgrade to 2023.10.0 or newer
"""""""""""""""""""""""""""""
This release includes a migration that changes retroactively fills an `organizer` column in the table
`pretixbase_logentry`. If you have a large database, the migration step of the upgrade might take significantly
longer than usual, so plan the update accordingly.
.. _blog: https://pretix.eu/about/en/blog/

View File

@@ -31,6 +31,7 @@ Checking a ticket in
This endpoint supports passing multiple check-in lists to perform a multi-event scan. However, each check-in list
passed needs to be from a distinct event.
:query string expand: Expand a field inside the ``position`` object into a full object. Currently ``subevent``, ``item``, ``variation``, and ``answers.question`` are supported. Can be passed multiple times.
:<json string secret: Scanned QR code corresponding to the ``secret`` attribute of a ticket.
:<json string source_type: Type of source the ``secret`` was obtained form. Defaults to ``"barcode"``.
:<json array lists: List of check-in list IDs to search on. No two check-in lists may be from the same event.
@@ -63,6 +64,7 @@ Checking a ticket in
``checkin_attention`` flag set. (3) If ``attendee_name`` is empty, it may automatically fall
back to values from a parent product or from invoice addresses.
:>json boolean require_attention: Whether or not the ``require_attention`` flag is set on the item or order.
:>json list checkin_texts: List of additional texts to show to the user.
:>json object list: Excerpt of information about the matching :ref:`check-in list <rest-checkinlists>` (if any was found),
including the attributes ``id``, ``name``, ``event``, ``subevent``, and ``include_pending``.
:>json object questions: List of questions to be answered for check-in, only set on status ``"incomplete"``.
@@ -103,6 +105,7 @@ Checking a ticket in
},
"require_attention": false,
"checkin_texts": [],
"list": {
"id": 1,
"name": "Default check-in list",
@@ -125,6 +128,7 @@ Checking a ticket in
},
"require_attention": false,
"checkin_texts": [],
"list": {
"id": 1,
"name": "Default check-in list",
@@ -142,6 +146,7 @@ Checking a ticket in
"position": 1,
"identifier": "WY3TP9SL",
"ask_during_checkin": true,
"show_during_checkin": true,
"options": [
{
"id": 1,
@@ -178,7 +183,8 @@ Checking a ticket in
"status": "error",
"reason": "invalid",
"reason_explanation": null,
"require_attention": false
"require_attention": false,
"checkin_texts": []
}
**Example error response (known, but invalid ticket)**:
@@ -193,6 +199,7 @@ Checking a ticket in
"reason": "unpaid",
"reason_explanation": null,
"require_attention": false,
"checkin_texts": [],
"list": {
"id": 1,
"name": "Default check-in list",
@@ -217,6 +224,7 @@ Checking a ticket in
* ``rules`` - Check-in prevented by a user-defined rule.
* ``ambiguous`` - Multiple tickets match scan, rejected.
* ``revoked`` - Ticket code has been revoked.
* ``unapproved`` - Order has not yet been approved.
* ``error`` - Internal error.
In case of reason ``rules`` and ``invalid_time``, there might be an additional response field ``reason_explanation``

View File

@@ -37,12 +37,18 @@ allow_entry_after_exit boolean If ``true``, su
rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged.
exit_all_at datetime Automatically check out (i.e. perform an exit scan) at this point in time. After this happened, this property will automatically be set exactly one day into the future. Note that this field is considered "internal configuration" and if you pull the list with ``If-Modified-Since``, the daily change in this field will not trigger a response.
addon_match boolean If ``true``, tickets on this list can be redeemed by scanning their parent ticket if this still leads to an unambiguous match.
ignore_in_statistics boolean If ``true``, check-ins on this list will be ignored in most reporting features.
consider_tickets_used boolean If ``true`` (default), tickets checked in on this list will be considered "used" by other functionality, i.e. when checking if they can still be canceled.
===================================== ========================== =======================================================
.. versionchanged:: 4.12
The ``addon_match`` attribute has been added.
.. versionchanged:: 2023.9
The ``ignore_in_statistics`` and ``consider_tickets_used`` attributes have been added.
Endpoints
---------
@@ -492,7 +498,7 @@ Order position endpoints
``attendee_name,positionid``
:query string order: Only return positions of the order with the given order code
:query string search: Fuzzy search matching the attendee name, order code, invoice address name as well as to the beginning of the secret.
:query string expand: Expand a field into a full object. Currently only ``subevent``, ``item``, and ``variation`` are supported. Can be passed multiple times.
:query string expand: Expand a field into a full object. Currently ``subevent``, ``item``, ``variation``, and ``answers.question`` are supported. Can be passed multiple times.
:query integer item: Only return positions with the purchased item matching the given ID.
:query integer item__in: Only return positions with the purchased item matching one of the given comma-separated IDs.
:query integer variation: Only return positions with the purchased item variation matching the given ID.
@@ -626,7 +632,8 @@ Order position endpoints
set this to ``false``. In that case, questions will just be ignored. Defaults
to ``true``.
:<json boolean canceled_supported: When this parameter is set to ``true``, the response code ``canceled`` may be
returned. Otherwise, canceled orders will return ``unpaid``.
returned. Otherwise, canceled orders will return ``unpaid``. (**Deprecated**, in
the future, this will be ignored and ``canceled`` may always be returned.)
:<json datetime datetime: Specifies the datetime of the check-in. If not supplied, the current time will be used.
:<json boolean force: Specifies that the check-in should succeed regardless of revoked barcode, previous check-ins or required
questions that have not been filled. This is usually used to upload offline scans that already happened,
@@ -700,6 +707,7 @@ Order position endpoints
"position": 1,
"identifier": "WY3TP9SL",
"ask_during_checkin": true,
"show_during_checkin": true,
"options": [
{
"id": 1,
@@ -752,6 +760,7 @@ Order position endpoints
* ``rules`` - Check-in prevented by a user-defined rule.
* ``ambiguous`` - Multiple tickets match scan, rejected.
* ``revoked`` - Ticket code has been revoked.
* ``unapproved`` - Order has not yet been approved.
In case of reason ``rules`` or ``invalid_time``, there might be an additional response field ``reason_explanation``
with a human-readable description of the violated rules. However, that field can also be missing or be ``null``.
@@ -767,4 +776,4 @@ Order position endpoints
:statuscode 404: The requested order position or check-in list does not exist.
.. _security issues: https://pretix.eu/about/de/blog/20220705-release-4111/
.. _security issues: https://pretix.eu/about/de/blog/20220705-release-4111/

View File

@@ -31,9 +31,9 @@ subevent_mode strings Determines h
``"same"`` (discount is only applied for groups within
the same date), or ``"distinct"`` (discount is only applied
for groups with no two same dates).
condition_all_products boolean If ``true``, the discount applies to all items.
condition_all_products boolean If ``true``, the discount condition applies to all items.
condition_limit_products list of integers If ``condition_all_products`` is not set, this is a list
of internal item IDs that the discount applies to.
of internal item IDs that the discount condition applies to.
condition_apply_to_addons boolean If ``true``, the discount applies to add-on products as well,
otherwise it only applies to top-level items. The discount never
applies to bundled products.
@@ -48,6 +48,17 @@ benefit_discount_matching_percent decimal (string) The percenta
benefit_only_apply_to_cheapest_n_matches integer If set higher than 0, the discount will only be applied to
the cheapest matches. Useful for a "3 for 2"-style discount.
Cannot be combined with ``condition_min_value``.
benefit_same_products boolean If ``true``, the discount benefit applies to the same set of items
as the condition (see above).
benefit_limit_products list of integers If ``benefit_same_products`` is not set, this is a list
of internal item IDs that the discount benefit applies to.
benefit_apply_to_addons boolean (Only used if ``benefit_same_products`` is ``false``.)
If ``true``, the discount applies to add-on products as well,
otherwise it only applies to top-level items. The discount never
applies to bundled products.
benefit_ignore_voucher_discounted boolean (Only used if ``benefit_same_products`` is ``false``.)
If ``true``, the discount does not apply to products which have
been discounted by a voucher.
======================================== ========================== =======================================================
@@ -94,6 +105,10 @@ Endpoints
"condition_ignore_voucher_discounted": false,
"condition_min_count": 3,
"condition_min_value": "0.00",
"benefit_same_products": true,
"benefit_limit_products": [],
"benefit_apply_to_addons": true,
"benefit_ignore_voucher_discounted": false,
"benefit_discount_matching_percent": "100.00",
"benefit_only_apply_to_cheapest_n_matches": 1
}
@@ -146,6 +161,10 @@ Endpoints
"condition_ignore_voucher_discounted": false,
"condition_min_count": 3,
"condition_min_value": "0.00",
"benefit_same_products": true,
"benefit_limit_products": [],
"benefit_apply_to_addons": true,
"benefit_ignore_voucher_discounted": false,
"benefit_discount_matching_percent": "100.00",
"benefit_only_apply_to_cheapest_n_matches": 1
}
@@ -184,6 +203,10 @@ Endpoints
"condition_ignore_voucher_discounted": false,
"condition_min_count": 3,
"condition_min_value": "0.00",
"benefit_same_products": true,
"benefit_limit_products": [],
"benefit_apply_to_addons": true,
"benefit_ignore_voucher_discounted": false,
"benefit_discount_matching_percent": "100.00",
"benefit_only_apply_to_cheapest_n_matches": 1
}
@@ -211,6 +234,10 @@ Endpoints
"condition_ignore_voucher_discounted": false,
"condition_min_count": 3,
"condition_min_value": "0.00",
"benefit_same_products": true,
"benefit_limit_products": [],
"benefit_apply_to_addons": true,
"benefit_ignore_voucher_discounted": false,
"benefit_discount_matching_percent": "100.00",
"benefit_only_apply_to_cheapest_n_matches": 1
}
@@ -267,6 +294,10 @@ Endpoints
"condition_ignore_voucher_discounted": false,
"condition_min_count": 3,
"condition_min_value": "0.00",
"benefit_same_products": true,
"benefit_limit_products": [],
"benefit_apply_to_addons": true,
"benefit_ignore_voucher_discounted": false,
"benefit_discount_matching_percent": "100.00",
"benefit_only_apply_to_cheapest_n_matches": 1
}

View File

@@ -565,6 +565,8 @@ organizer level.
.. warning:: This API is intended for advanced users. Even though we take care to validate your input, you will be
able to break your event using this API by creating situations of conflicting settings. Please take care.
.. note:: When authenticating with :ref:`rest-deviceauth`, only a limited subset of settings is available.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/settings/
Get current values of event settings.

View File

@@ -1,5 +1,7 @@
.. spelling:word-list:: checkin
.. _rest-exporters:
Data exporters
==============

View File

@@ -40,6 +40,7 @@ at :ref:`plugin-docs`.
webhooks
seatingplans
exporters
scheduled_exports
shredders
sendmail_rules
billing_invoices

View File

@@ -12,6 +12,7 @@ The invoice resource contains the following public fields:
Field Type Description
===================================== ========================== =======================================================
number string Invoice number (with prefix)
event string The slug of the parent event
order string Order code of the order this invoice belongs to
is_cancellation boolean ``true``, if this invoice is the cancellation of a
different invoice.
@@ -121,9 +122,13 @@ internal_reference string Customer's refe
The attribute ``lines.subevent`` has been added.
.. versionchanged:: 2023.8
Endpoints
---------
The ``event`` attribute has been added. The organizer-level endpoint has been added.
List of all invoices
--------------------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/invoices/
@@ -152,6 +157,7 @@ Endpoints
"results": [
{
"number": "SAMPLECONF-00001",
"event": "sampleconf",
"order": "ABC12",
"is_cancellation": false,
"invoice_from_name": "Big Events LLC",
@@ -221,6 +227,50 @@ Endpoints
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/invoices/
Returns a list of all invoices within all events of a given organizer (with sufficient access permissions).
Supported query parameters and output format of this endpoint are identical to the list endpoint within an event.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/invoices/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"number": "SAMPLECONF-00001",
"event": "sampleconf",
"order": "ABC12",
...
]
}
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
Fetching individual invoices
----------------------------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/invoices/(number)/
Returns information on one invoice, identified by its invoice number.
@@ -243,6 +293,7 @@ Endpoints
{
"number": "SAMPLECONF-00001",
"event": "sampleconf",
"order": "ABC12",
"is_cancellation": false,
"invoice_from_name": "Big Events LLC",
@@ -337,6 +388,12 @@ Endpoints
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
seconds.
Modifying invoices
------------------
Invoices cannot be edited directly, but the following actions can be triggered:
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(invoice_no)/reissue/
Cancels the invoice and creates a new one.

View File

@@ -18,6 +18,8 @@ default_price money (string) The price set d
price money (string) The price used for this variation. This is either the
same as ``default_price`` if that value is set or equal
to the item's ``default_price`` (read-only).
free_price_suggestion money (string) A suggested price, used as a default value if
``Item.free_price`` is set (or ``null``).
original_price money (string) An original price, shown for comparison, not used
for price calculations (or ``null``).
active boolean If ``false``, this variation will not be sold or shown.
@@ -27,6 +29,8 @@ position integer An integer, use
checkin_attention boolean If ``true``, the check-in app should show a warning
that this ticket requires special attention if such
a variation is being scanned.
checkin_text string Text that will be shown if a ticket of this type is
scanned (or ``null``).
require_approval boolean If ``true``, orders with this variation will need to be
approved by the event organizer before they can be
paid.
@@ -53,6 +57,12 @@ meta_data object Values set for
The ``meta_data`` and ``checkin_attention`` attributes have been added.
.. versionchanged:: 2023.10
The ``free_price_suggestion`` attribute has been added.
The ``checkin_text`` attribute has been added.
Endpoints
---------
@@ -88,6 +98,7 @@ Endpoints
},
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_hidden": false,
@@ -103,6 +114,7 @@ Endpoints
"default_price": "223.00",
"price": 223.0,
"original_price": null,
"free_price_suggestion": null,
"meta_data": {}
},
{
@@ -112,14 +124,21 @@ Endpoints
},
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
"hide_without_voucher": false,
"description": {},
"position": 1,
"default_price": null,
"price": 15.0,
"default_price": "223.00",
"price": 223.0,
"original_price": null,
"free_price_suggestion": null,
"meta_data": {}
}
]
@@ -163,8 +182,10 @@ Endpoints
"default_price": "10.00",
"price": "10.00",
"original_price": null,
"free_price_suggestion": null,
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_hidden": false,
@@ -204,6 +225,7 @@ Endpoints
"default_price": "10.00",
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_hidden": false,
@@ -231,8 +253,10 @@ Endpoints
"default_price": "10.00",
"price": "10.00",
"original_price": null,
"free_price_suggestion": null,
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_hidden": false,
@@ -291,8 +315,10 @@ Endpoints
"default_price": "10.00",
"price": "10.00",
"original_price": null,
"free_price_suggestion": null,
"active": false,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_hidden": false,

View File

@@ -29,6 +29,8 @@ free_price boolean If ``true``,
they buy the product (however, the price can't be set
lower than the price defined by ``default_price`` or
otherwise).
free_price_suggestion money (string) A suggested price, used as a default value if
``free_price`` is set (or ``null``).
tax_rate decimal (string) The VAT rate to be applied for this item (read-only,
set through ``tax_rule``).
tax_rule integer The internal ID of the applied tax rule (or ``null``).
@@ -50,9 +52,12 @@ available_from datetime The first dat
(or ``null``).
available_until datetime The last date time at which this item can be bought
(or ``null``).
hidden_if_available integer The internal ID of a quota object, or ``null``. If
hidden_if_available integer **DEPRECATED** The internal ID of a quota object, or ``null``. If
set, this item won't be shown publicly as long as this
quota is available.
hidden_if_item_available integer The internal ID of a different item, or ``null``. If
set, this item won't be shown publicly as long as this
other item is available.
require_voucher boolean If ``true``, this item can only be bought using a
voucher that is specifically assigned to this item.
hide_without_voucher boolean If ``true``, this item is only shown during the voucher
@@ -69,6 +74,8 @@ max_per_order integer This product
checkin_attention boolean If ``true``, the check-in app should show a warning
that this ticket requires special attention if such
a product is being scanned.
checkin_text string Text that will be shown if a ticket of this type is
scanned (or ``null``).
original_price money (string) An original price, shown for comparison, not used
for price calculations (or ``null``).
require_approval boolean If ``true``, orders with this product will need to be
@@ -123,6 +130,8 @@ variations list of objects A list with o
├ price money (string) The price used for this variation. This is either the
same as ``default_price`` if that value is set or equal
to the item's ``default_price``.
├ free_price_suggestion money (string) A suggested price, used as a default value if
``free_price`` is set (or ``null``).
├ original_price money (string) An original price, shown for comparison, not used
for price calculations (or ``null``).
├ active boolean If ``false``, this variation will not be sold or shown.
@@ -130,6 +139,8 @@ variations list of objects A list with o
├ checkin_attention boolean If ``true``, the check-in app should show a warning
that this ticket requires special attention if such
a variation is being scanned.
├ checkin_text string Text that will be shown if a ticket of this type is
scanned (or ``null``).
├ require_approval boolean If ``true``, orders with this variation will need to be
approved by the event organizer before they can be
paid.
@@ -196,6 +207,16 @@ meta_data object Values set fo
The ``media_policy`` and ``media_type`` attributes have been added.
.. versionchanged:: 2023.10
The ``checkin_text`` and ``variations[x].checkin_text`` attributes have been added.
The ``free_price_suggestion`` and ``variations[x].free_price_suggestion`` attributes have been added.
.. versionchanged:: 2023.10
The ``hidden_if_item_available`` attributes has been added, the ``hidden_if_available`` attribute has been
deprecated.
Notes
-----
@@ -246,6 +267,7 @@ Endpoints
"active": true,
"description": null,
"free_price": false,
"free_price_suggestion": null,
"tax_rate": "0.00",
"tax_rule": 1,
"admission": false,
@@ -259,12 +281,14 @@ Endpoints
"available_from": null,
"available_until": null,
"hidden_if_available": null,
"hidden_if_item_available": null,
"require_voucher": false,
"hide_without_voucher": false,
"allow_cancel": true,
"min_per_order": null,
"max_per_order": null,
"checkin_attention": false,
"checkin_text": null,
"has_variations": false,
"generate_tickets": null,
"allow_waitinglist": true,
@@ -291,8 +315,10 @@ Endpoints
"default_price": "10.00",
"price": "10.00",
"original_price": null,
"free_price_suggestion": null,
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_types": [],
@@ -309,8 +335,10 @@ Endpoints
"default_price": null,
"price": "23.00",
"original_price": null,
"free_price_suggestion": null,
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_types": [],
@@ -377,6 +405,7 @@ Endpoints
"active": true,
"description": null,
"free_price": false,
"free_price_suggestion": null,
"tax_rate": "0.00",
"tax_rule": 1,
"admission": false,
@@ -390,6 +419,7 @@ Endpoints
"available_from": null,
"available_until": null,
"hidden_if_available": null,
"hidden_if_item_available": null,
"require_voucher": false,
"hide_without_voucher": false,
"allow_cancel": true,
@@ -399,6 +429,7 @@ Endpoints
"min_per_order": null,
"max_per_order": null,
"checkin_attention": false,
"checkin_text": null,
"has_variations": false,
"require_approval": false,
"require_bundling": false,
@@ -422,8 +453,10 @@ Endpoints
"default_price": "10.00",
"price": "10.00",
"original_price": null,
"free_price_suggestion": null,
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_types": [],
@@ -440,8 +473,10 @@ Endpoints
"default_price": null,
"price": "23.00",
"original_price": null,
"free_price_suggestion": null,
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_types": [],
@@ -489,6 +524,7 @@ Endpoints
"active": true,
"description": null,
"free_price": false,
"free_price_suggestion": null,
"tax_rate": "0.00",
"tax_rule": 1,
"admission": false,
@@ -502,6 +538,7 @@ Endpoints
"available_from": null,
"available_until": null,
"hidden_if_available": null,
"hidden_if_item_available": null,
"require_voucher": false,
"hide_without_voucher": false,
"allow_cancel": true,
@@ -511,6 +548,7 @@ Endpoints
"min_per_order": null,
"max_per_order": null,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_bundling": false,
"require_membership": false,
@@ -533,8 +571,10 @@ Endpoints
"default_price": "10.00",
"price": "10.00",
"original_price": null,
"free_price_suggestion": null,
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_types": [],
@@ -551,8 +591,10 @@ Endpoints
"default_price": null,
"price": "23.00",
"original_price": null,
"free_price_suggestion": null,
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_types": [],
@@ -588,6 +630,7 @@ Endpoints
"active": true,
"description": null,
"free_price": false,
"free_price_suggestion": null,
"tax_rate": "0.00",
"tax_rule": 1,
"admission": false,
@@ -601,6 +644,7 @@ Endpoints
"available_from": null,
"available_until": null,
"hidden_if_available": null,
"hidden_if_item_available": null,
"require_voucher": false,
"hide_without_voucher": false,
"allow_cancel": true,
@@ -610,6 +654,7 @@ Endpoints
"allow_waitinglist": true,
"show_quota_left": null,
"checkin_attention": false,
"checkin_text": null,
"has_variations": true,
"require_approval": false,
"require_bundling": false,
@@ -633,8 +678,10 @@ Endpoints
"default_price": "10.00",
"price": "10.00",
"original_price": null,
"free_price_suggestion": null,
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_types": [],
@@ -651,8 +698,10 @@ Endpoints
"default_price": null,
"price": "23.00",
"original_price": null,
"free_price_suggestion": null,
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_types": [],
@@ -719,6 +768,7 @@ Endpoints
"active": true,
"description": null,
"free_price": false,
"free_price_suggestion": null,
"tax_rate": "0.00",
"tax_rule": 1,
"admission": false,
@@ -732,6 +782,7 @@ Endpoints
"available_from": null,
"available_until": null,
"hidden_if_available": null,
"hidden_if_item_available": null,
"require_voucher": false,
"hide_without_voucher": false,
"generate_tickets": null,
@@ -741,6 +792,7 @@ Endpoints
"min_per_order": null,
"max_per_order": null,
"checkin_attention": false,
"checkin_text": null,
"has_variations": true,
"require_approval": false,
"require_bundling": false,
@@ -764,8 +816,10 @@ Endpoints
"default_price": "10.00",
"price": "10.00",
"original_price": null,
"free_price_suggestion": null,
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_types": [],
@@ -782,8 +836,10 @@ Endpoints
"default_price": null,
"price": "23.00",
"original_price": null,
"free_price_suggestion": null,
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_types": [],

View File

@@ -20,6 +20,7 @@ The order resource contains the following public fields:
Field Type Description
===================================== ========================== =======================================================
code string Order code
event string The slug of the parent event
status string Order status, one of:
* ``n`` pending
@@ -45,6 +46,8 @@ custom_followup_at date Internal date f
checkin_attention boolean If ``true``, the check-in app should show a warning
that this ticket requires special attention if a ticket
of this order is scanned.
checkin_text string Text that will be shown if a ticket of this order is
scanned (or ``null``).
invoice_address object Invoice address information (can be ``null``)
├ last_modified datetime Last modification date of the address
├ company string Customer company name
@@ -130,6 +133,18 @@ last_modified datetime Last modificati
The ``valid_if_pending`` attribute has been added.
.. versionchanged:: 2023.8
The ``event`` attribute has been added. The organizer-level endpoint has been added.
.. versionchanged:: 2023.10
The ``checkin_text`` attribute has been added.
.. versionchanged:: 2023.9
The ``customer`` query parameter has been added.
.. _order-position-resource:
@@ -289,6 +304,7 @@ List of all orders
"results": [
{
"code": "ABC12",
"event": "sampleconf",
"status": "p",
"testmode": false,
"secret": "k24fiuwvu8kxz3y1",
@@ -308,6 +324,7 @@ List of all orders
"comment": "",
"custom_followup_at": null,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"valid_if_pending": false,
"invoice_address": {
@@ -414,6 +431,7 @@ List of all orders
:query string code: Only return orders that match the given order code
:query string status: Only return orders in the given order status (see above)
:query string search: Only return orders matching a given search query (matching for names, email addresses, and company names)
:query string customer: Only show orders linked to the given customer.
:query integer item: Only return orders with a position that contains this item ID. *Warning:* Result will also include orders if they contain mixed items, and it will even return orders where the item is only contained in a canceled position.
:query integer variation: Only return orders with a position that contains this variation ID. *Warning:* Result will also include orders if they contain mixed items and variations, and it will even return orders where the variation is only contained in a canceled position.
:query boolean testmode: Only return orders with ``testmode`` set to ``true`` or ``false``
@@ -441,6 +459,48 @@ List of all orders
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/orders/
Returns a list of all orders within all events of a given organizer (with sufficient access permissions).
Supported query parameters and output format of this endpoint are identical to the list endpoint within an event,
with the exception that the ``pdf_data`` parameter is not supported here.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/orders/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
X-Page-Generated: 2017-12-01T10:00:00Z
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"code": "ABC12",
"event": "sampleconf",
...
}
]
}
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
Fetching individual orders
--------------------------
@@ -466,6 +526,7 @@ Fetching individual orders
{
"code": "ABC12",
"event": "sampleconf",
"status": "p",
"testmode": false,
"secret": "k24fiuwvu8kxz3y1",
@@ -485,6 +546,7 @@ Fetching individual orders
"comment": "",
"custom_followup_at": null,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"valid_if_pending": false,
"invoice_address": {
@@ -655,6 +717,8 @@ Updating order fields
* ``checkin_attention``
* ``checkin_text``
* ``locale``
* ``comment``
@@ -870,6 +934,7 @@ Creating orders
* ``comment`` (optional)
* ``custom_followup_at`` (optional)
* ``checkin_attention`` (optional)
* ``checkin_text`` (optional)
* ``require_approval`` (optional)
* ``valid_if_pending`` (optional)
* ``invoice_address`` (optional)
@@ -1517,6 +1582,7 @@ List of all order positions
``order__datetime,positionid``
:query string order: Only return positions of the order with the given order code
:query string search: Fuzzy search matching the attendee name, order code, invoice address name as well as to the beginning of the secret.
:query string customer: Only show orders linked to the given customer.
:query integer item: Only return positions with the purchased item matching the given ID.
:query integer item__in: Only return positions with the purchased item matching one of the given comma-separated IDs.
:query integer variation: Only return positions with the purchased item variation matching the given ID.

View File

@@ -44,6 +44,8 @@ identifier string An arbitrary st
ask_during_checkin boolean If ``true``, this question will not be asked while
buying the ticket, but will show up when redeeming
the ticket instead.
show_during_checkin boolean If ``true``, the answer to the question will be shown
during check-in (if the check-in client supports it).
hidden boolean If ``true``, the question will only be shown in the
backend.
print_on_invoice boolean If ``true``, the question will only be shown on
@@ -77,6 +79,10 @@ dependency_value string An old version
for one value. **Deprecated.**
===================================== ========================== =======================================================
.. versionchanged:: 2023.8
The ``show_during_checkin`` attribute has been added.
Endpoints
---------
@@ -115,6 +121,7 @@ Endpoints
"position": 1,
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"show_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"valid_number_min": null,
@@ -194,6 +201,7 @@ Endpoints
"position": 1,
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"show_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"valid_number_min": null,
@@ -257,6 +265,7 @@ Endpoints
"items": [1, 2],
"position": 1,
"ask_during_checkin": false,
"show_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"dependency_question": null,
@@ -293,6 +302,7 @@ Endpoints
"position": 1,
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"show_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"dependency_question": null,
@@ -348,7 +358,7 @@ Endpoints
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/items/1/ HTTP/1.1
PATCH /api/v1/organizers/bigevents/events/sampleconf/questions/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
@@ -376,6 +386,7 @@ Endpoints
"position": 2,
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"show_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"dependency_question": null,
@@ -415,7 +426,7 @@ Endpoints
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the question to modify
:statuscode 200: no error
:statuscode 400: The item could not be modified due to invalid submitted data
:statuscode 400: The question could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
@@ -427,7 +438,7 @@ Endpoints
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/items/1/ HTTP/1.1
DELETE /api/v1/organizers/bigevents/events/sampleconf/questions/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
@@ -440,7 +451,7 @@ Endpoints
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the item to delete
:param id: The ``id`` field of the question to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.

View File

@@ -0,0 +1,556 @@
.. spelling:word-list:: checkin
Scheduled data exports
======================
pretix and it's plugins include a number of data exporters that allow you to bulk download various data from pretix in
different formats. You should read :ref:`rest-exporters` first to get an understanding of the basic mechanism.
Exports can be scheduled to be sent at specific times automatically, both on organizer level and event level.
Scheduled export resource
-------------------------
The scheduled export contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the schedule
owner string Email address of the user who created this schedule (read-only).
This address will always receive the export and the export
will only contain data that this user has permission
to access at the time of the export. **We consider this
field experimental, it's behaviour might change in the future.
Note that the email address of a user can change at any time.**
export_identifier string Identifier of the export to run, see :ref:`rest-exporters`
export_form_data object Input data for the export, format depends on the export,
see :ref:`rest-exporters` for more details.
locale string Language to run the export in
mail_additional_recipients string Email addresses to receive the export, comma-separated (or empty string)
mail_additional_recipients_cc string Email addresses to receive the export in copy, comma-separated (or empty string)
mail_additional_recipients_bcc string Email addresses to receive the exportin blind copy, comma-separated (or empty string)
mail_subject string Subject to use for the email (currently no variables supported)
mail_template string Text to use for the email (currently no variables supported)
schedule_rrule string Recurrence specification to determine the **days** this
schedule runs on in ``RRULE`` syntax following `RFC 5545`_
with some restrictions. Only one rule is allowed, only
one occurrence per day is allowed, and some features
are not supported (``BYMONTHDAY``, ``BYYEARDAY``,
``BYEASTER``, ``BYWEEKNO``).
schedule_rrule_time time Time of day to run this on on the specified days.
Will be interpreted as local time of the event for event-level
exports. For organizer-level exports, the timezone is given
in the field ``timezone``. The export will never run **before**
this time but it **may** run **later**.
timezone string Time zone to interpret the schedule in (only for organizer-level exports)
schedule_next_run datetime Next planned execution (read-only, computed by server)
error_counter integer Number of consecutive times this export failed (read-only).
After a number of failures (currently 5), the schedule no
longer is executed. Changing parameters resets the value.
===================================== ========================== =======================================================
Special notes on permissions
----------------------------
Permission handling for scheduled exports is more complex than for most other objects. The reason for this is that
there are two levels of access control involved here: First, you need permission to access or change the configuration
of the scheduled exports in the moment you are doing it. Second, you **continuously** need permission to access the
**data** that is exported as part of the schedule. For this reason, scheduled exports always need one user account
to be their **owner**.
Therefore, scheduled exports **must** be created by an API client using :ref:`OAuth authentication <rest-oauth>`.
It is impossible to create a scheduled export using token authentication. After the export is created, it can also be
modified using token authentication.
A user or token with the "can change settings" permission for a given organizer or event can see and change
**all** scheduled exports created for the respective organizer or event, regardless of who created them.
A user without this permission can only see **their own** scheduled exports.
A token without this permission can not see scheduled exports as all.
Endpoints for event exports
---------------------------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/scheduled_exports/
Returns a list of all scheduled exports the client has access to.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/scheduled_exports/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"owner": "john@example.com",
"export_identifier": "orderlist",
"export_form_data": {"_format": "xlsx", "date_range": "week_previous"},
"locale": "en",
"mail_additional_recipients": "mary@example.org",
"mail_additional_recipients_cc": "",
"mail_additional_recipients_bcc": "",
"mail_subject": "Order list",
"mail_template": "Here is last week's order list\n\nCheers\nJohn",
"schedule_rrule": "DTSTART:20230118T000000\nRRULE:FREQ=WEEKLY;BYDAY=TU,WE,TH",
"schedule_rrule_time": "04:00:00",
"schedule_next_run": "2023-10-26T02:00:00Z",
"error_counter": 0
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id``, ``export_identifier``, and ``schedule_next_run``.
Default: ``id``
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/scheduled_exports/(id)/
Returns information on one scheduled export, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/scheduled_exports/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"owner": "john@example.com",
"export_identifier": "orderlist",
"export_form_data": {"_format": "xlsx", "date_range": "week_previous"},
"locale": "en",
"mail_additional_recipients": "mary@example.org",
"mail_additional_recipients_cc": "",
"mail_additional_recipients_bcc": "",
"mail_subject": "Order list",
"mail_template": "Here is last week's order list\n\nCheers\nJohn",
"schedule_rrule": "DTSTART:20230118T000000\nRRULE:FREQ=WEEKLY;BYDAY=TU,WE,TH",
"schedule_rrule_time": "04:00:00",
"schedule_next_run": "2023-10-26T02:00:00Z",
"error_counter": 0
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param id: The ``id`` field of the scheduled export to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/scheduled_exports/
Schedule a new export.
.. note:: See above for special notes on permissions.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/scheduled_exports/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"export_identifier": "orderlist",
"export_form_data": {"_format": "xlsx", "date_range": "week_previous"},
"locale": "en",
"mail_additional_recipients": "mary@example.org",
"mail_additional_recipients_cc": "",
"mail_additional_recipients_bcc": "",
"mail_subject": "Order list",
"mail_template": "Here is last week's order list\n\nCheers\nJohn",
"schedule_rrule": "DTSTART:20230118T000000\nRRULE:FREQ=WEEKLY;BYDAY=TU,WE,TH",
"schedule_rrule_time": "04:00:00"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 1,
"owner": "john@example.com",
"export_identifier": "orderlist",
"export_form_data": {"_format": "xlsx", "date_range": "week_previous"},
"locale": "en",
"mail_additional_recipients": "mary@example.org",
"mail_additional_recipients_cc": "",
"mail_additional_recipients_bcc": "",
"mail_subject": "Order list",
"mail_template": "Here is last week's order list\n\nCheers\nJohn",
"schedule_rrule": "DTSTART:20230118T000000\nRRULE:FREQ=WEEKLY;BYDAY=TU,WE,TH",
"schedule_rrule_time": "04:00:00",
"schedule_next_run": "2023-10-26T02:00:00Z",
"error_counter": 0
}
:param organizer: The ``slug`` field of the organizer of the event to create an item for
:param event: The ``slug`` field of the event to create an item for
:statuscode 201: no error
:statuscode 400: The item could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/scheduled_exports/(id)/
Update a scheduled export. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
want to change.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/scheduled_exports/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"export_form_data": {"_format": "xlsx", "date_range": "week_this"},
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"owner": "john@example.com",
"export_identifier": "orderlist",
"export_form_data": {"_format": "xlsx", "date_range": "week_this"},
"locale": "en",
"mail_additional_recipients": "mary@example.org",
"mail_additional_recipients_cc": "",
"mail_additional_recipients_bcc": "",
"mail_subject": "Order list",
"mail_template": "Here is last week's order list\n\nCheers\nJohn",
"schedule_rrule": "DTSTART:20230118T000000\nRRULE:FREQ=WEEKLY;BYDAY=TU,WE,TH",
"schedule_rrule_time": "04:00:00",
"schedule_next_run": "2023-10-26T02:00:00Z",
"error_counter": 0
}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the export to modify
:statuscode 200: no error
:statuscode 400: The export could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/scheduled_exports/(id)/
Delete a scheduled export.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/scheduled_exports/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the export to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
Endpoints for organizer exports
---------------------------
.. http:get:: /api/v1/organizers/(organizer)/scheduled_exports/
Returns a list of all scheduled exports the client has access to.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/scheduled_exports/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"owner": "john@example.com",
"export_identifier": "orderlist",
"export_form_data": {"_format": "xlsx", "date_range": "week_previous"},
"locale": "en",
"mail_additional_recipients": "mary@example.org",
"mail_additional_recipients_cc": "",
"mail_additional_recipients_bcc": "",
"mail_subject": "Order list",
"mail_template": "Here is last week's order list\n\nCheers\nJohn",
"schedule_rrule": "DTSTART:20230118T000000\nRRULE:FREQ=WEEKLY;BYDAY=TU,WE,TH",
"schedule_rrule_time": "04:00:00",
"schedule_next_run": "2023-10-26T02:00:00Z",
"timezone": "Europe/Berlin",
"error_counter": 0
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id``, ``export_identifier``, and ``schedule_next_run``.
Default: ``id``
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/scheduled_exports/(id)/
Returns information on one scheduled export, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/scheduled_exports/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"owner": "john@example.com",
"export_identifier": "orderlist",
"export_form_data": {"_format": "xlsx", "date_range": "week_previous"},
"locale": "en",
"mail_additional_recipients": "mary@example.org",
"mail_additional_recipients_cc": "",
"mail_additional_recipients_bcc": "",
"mail_subject": "Order list",
"mail_template": "Here is last week's order list\n\nCheers\nJohn",
"schedule_rrule": "DTSTART:20230118T000000\nRRULE:FREQ=WEEKLY;BYDAY=TU,WE,TH",
"schedule_rrule_time": "04:00:00",
"schedule_next_run": "2023-10-26T02:00:00Z",
"timezone": "Europe/Berlin",
"error_counter": 0
}
:param organizer: The ``slug`` field of the organizer to fetch
:param id: The ``id`` field of the scheduled export to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/scheduled_exports/
Schedule a new export.
.. note:: See above for special notes on permissions.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/scheduled_exports/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"export_identifier": "orderlist",
"export_form_data": {"_format": "xlsx", "date_range": "week_previous"},
"locale": "en",
"mail_additional_recipients": "mary@example.org",
"mail_additional_recipients_cc": "",
"mail_additional_recipients_bcc": "",
"mail_subject": "Order list",
"mail_template": "Here is last week's order list\n\nCheers\nJohn",
"schedule_rrule": "DTSTART:20230118T000000\nRRULE:FREQ=WEEKLY;BYDAY=TU,WE,TH",
"schedule_rrule_time": "04:00:00",
"timezone": "Europe/Berlin"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 1,
"owner": "john@example.com",
"export_identifier": "orderlist",
"export_form_data": {"_format": "xlsx", "date_range": "week_previous"},
"locale": "en",
"mail_additional_recipients": "mary@example.org",
"mail_additional_recipients_cc": "",
"mail_additional_recipients_bcc": "",
"mail_subject": "Order list",
"mail_template": "Here is last week's order list\n\nCheers\nJohn",
"schedule_rrule": "DTSTART:20230118T000000\nRRULE:FREQ=WEEKLY;BYDAY=TU,WE,TH",
"schedule_rrule_time": "04:00:00",
"schedule_next_run": "2023-10-26T02:00:00Z",
"timezone": "Europe/Berlin",
"error_counter": 0
}
:param organizer: The ``slug`` field of the organizer of the event to create an item for
:statuscode 201: no error
:statuscode 400: The item could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/scheduled_exports/(id)/
Update a scheduled export. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
want to change.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/scheduled_exports/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"export_form_data": {"_format": "xlsx", "date_range": "week_this"},
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"owner": "john@example.com",
"export_identifier": "orderlist",
"export_form_data": {"_format": "xlsx", "date_range": "week_this"},
"locale": "en",
"mail_additional_recipients": "mary@example.org",
"mail_additional_recipients_cc": "",
"mail_additional_recipients_bcc": "",
"mail_subject": "Order list",
"mail_template": "Here is last week's order list\n\nCheers\nJohn",
"schedule_rrule": "DTSTART:20230118T000000\nRRULE:FREQ=WEEKLY;BYDAY=TU,WE,TH",
"schedule_rrule_time": "04:00:00",
"schedule_next_run": "2023-10-26T02:00:00Z",
"timezone": "Europe/Berlin",
"error_counter": 0
}
:param organizer: The ``slug`` field of the organizer to modify
:param id: The ``id`` field of the export to modify
:statuscode 200: no error
:statuscode 400: The export could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
.. http:delete:: /api/v1/organizers/(organizer)/scheduled_exports/(id)/
Delete a scheduled export.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/scheduled_exports/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer to modify
:param id: The ``id`` field of the export to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource.
.. _RFC 5545: https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.5.3

View File

@@ -1,10 +1,10 @@
Automated email rules
Scheduled email rules
=====================
Resource description
--------------------
Automated email rules that specify emails that the system will send automatically at a specific point in time, e.g.
Scheduled email rules that specify emails that the system will send automatically at a specific point in time, e.g.
the day of the event.
.. rst-class:: rest-resource-table
@@ -23,10 +23,14 @@ limit_products list of integers List of product
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,
``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"]``.
checked_in_status string Check-in status to restrict recipients to. Valid strings are:
``null`` for no filtering (default), ``checked_in`` for
limiting to attendees that are or have been checked in, and
``no_checkin`` for limiting to attendees who have not checked in.
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
@@ -89,6 +93,7 @@ Endpoints
"n__not_pending_approval_and_not_valid_if_pending",
"n__valid_if_pending"
],
"checked_in_status": null,
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
@@ -139,6 +144,7 @@ Endpoints
"n__not_pending_approval_and_not_valid_if_pending",
"n__valid_if_pending"
],
"checked_in_status": null,
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
@@ -180,6 +186,7 @@ Endpoints
"n__not_pending_approval_and_not_valid_if_pending",
"n__valid_if_pending"
],
"checked_in_status": "checked_in",
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
@@ -209,6 +216,7 @@ Endpoints
"n__not_pending_approval_and_not_valid_if_pending",
"n__valid_if_pending"
],
"checked_in_status": "checked_in",
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
@@ -266,6 +274,7 @@ Endpoints
"n__not_pending_approval_and_not_valid_if_pending",
"n__valid_if_pending"
],
"checked_in_status": "checked_in",
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",

View File

@@ -68,6 +68,10 @@ last_modified datetime Last modificati
The ``date_from_before``, ``date_from_after``, ``date_to_before``, and ``date_to_after`` query parameters have been
added.
.. versionchanged:: 2023.8.0
For the organizer-wide endpoint, the ``search`` query parameter has been modified to filter sub-events by their parent events slug too.
Endpoints
---------
@@ -472,6 +476,7 @@ Endpoints
: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.
: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
:param event: The ``slug`` field of the event to fetch

View File

@@ -67,6 +67,9 @@ The following values for ``action_types`` are valid with pretix core:
* ``pretix.event.live.deactivated``
* ``pretix.event.testmode.activated``
* ``pretix.event.testmode.deactivated``
* ``pretix.customer.created``
* ``pretix.customer.changed``
* ``pretix.customer.anonymized``
Installed plugins might register more valid values.

View File

@@ -11,7 +11,7 @@ Core
----
.. automodule:: pretix.base.signals
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types,
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification,
item_copy_data, register_sales_channels, register_global_settings, quota_availability, global_email_filter,
register_ticket_secret_generators, gift_card_transaction_display

View File

@@ -37,7 +37,7 @@ you to execute a piece of code with a different locale:
This is very useful e.g. when sending an email to a user that has a different language than the user performing the
action that causes the mail to be sent.
.. _translation features: https://docs.djangoproject.com/en/1.9/topics/i18n/translation/
.. _translation features: https://docs.djangoproject.com/en/4.2/topics/i18n/translation/
.. _GNU gettext: https://www.gnu.org/software/gettext/
.. _strings: https://django-i18nfield.readthedocs.io/en/latest/strings.html
.. _database fields: https://django-i18nfield.readthedocs.io/en/latest/quickstart.html

View File

@@ -18,3 +18,4 @@ Contents:
email
permissions
logging
locking

View File

@@ -0,0 +1,69 @@
.. highlight:: python
Resource locking
================
.. versionchanged:: 2023.8
Our locking mechanism changed heavily in version 2023.8. Read `this PR`_ for background information.
One of pretix's core objectives as a ticketing system could be described as the management of scarce resources.
Specifically, the following types of scarce-ness exist in pretix:
- Quotas can limit the number of tickets available
- Seats can only be booked once
- Vouchers can only be used a limited number of times
- Some memberships can only be used a limited number of times
For all of these, it is critical that we prevent race conditions.
While for some events it wouldn't be a big deal to sell a ticket more or less, for some it would be problematic and selling the same seat twice would always be catastrophic.
We therefore implement a standardized locking approach across the system to limit concurrency in cases where it could
be problematic.
To acquire a lock on a set of quotas to create a new order that uses that quota, you should follow the following pattern::
with transaction.atomic(durable=True):
quotas = Quota.objects.filter(...)
lock_objects(quotas, shared_lock_objects=[event])
check_quota(quotas)
create_ticket()
The lock will automatically be released at the end of your database transaction.
Generally, follow the following guidelines during your development:
- **Always** acquire a lock on every **quota**, **voucher** or **seat** that you "use" during your transaction. "Use"
here means any action after which the quota, voucher or seat will be **less available**, such as creating a cart
position, creating an order, creating a blocking voucher, etc.
- There is **no need** to acquire a lock if you **free up** capacity, e.g. by canceling an order, deleting a voucher, etc.
- **Always** acquire a shared lock on the ``event`` you are working in whenever you acquire a lock on a quota, voucher,
or seat.
- Only call ``lock_objects`` **once** per transaction. If you violate this rule, `deadlocks`_ become possible.
- For best performance, call ``lock_objects`` as **late** in your transaction as possible, but always before you check
if the desired resource is still available in sufficient quantity.
Behind the scenes, the locking is implemented through `PostgreSQL advisory locks`_. You should also be aware of the following
properties of our system:
- In some situations, an exclusive lock on the ``event`` is used, such as when the system can't determine for sure which
seats will become unavailable after the transaction.
- An exclusive lock on the event is also used if you pass more than 20 objects to ``lock_objects``. This is a performance
trade-off because it would take long to acquire all of the individual locks.
- If ``lock_objects`` is unable to acquire a lock within 3 seconds, a ``LockTimeoutException`` will be thrown.
.. note::
We currently do not use ``lock_objects`` for memberships. Instead, we use ``select_for_update()`` on the membership
model. This might change in the future, but you should usually not be concerned about it since
``validate_memberships_in_order(lock=True)`` will handle it for you.
.. _this PR: https://github.com/pretix/pretix/pull/2408
.. _deadlocks: https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-DEADLOCKS
.. _PostgreSQL advisory locks: https://www.postgresql.org/docs/11/explicit-locking.html#ADVISORY-LOCKS

View File

@@ -15,25 +15,33 @@ and the admin panel is available at ``https://pretix.eu/control/event/bigorg/awe
If the organizer now configures a custom domain like ``tickets.bigorg.com``, his event will
from now on be available on ``https://tickets.bigorg.com/awesomecon/``. The former URL at
``pretix.eu`` will redirect there. However, the admin panel will still only be available
on ``pretix.eu`` for convenience and security reasons.
``pretix.eu`` will redirect there. It's also possible to do this for just an event, in which
case the event will be available on ``https://tickets.awesomecon.org/``.
However, the admin panel will still only be available on ``pretix.eu`` for convenience and security reasons.
URL routing
-----------
The hard part about implementing this URL routing in Django is that
``https://pretix.eu/bigorg/awesomecon/`` contains two parameters of nearly arbitrary content
and ``https://tickets.bigorg.com/awesomecon/`` contains only one. The only robust way to do
this is by having *separate* URL configuration for those two cases. In pretix, we call the
former our ``maindomain`` config and the latter our ``subdomain`` config. For pretix's core
modules we do some magic to avoid duplicate configuration, but for a fairly simple plugin with
only a handful of routes, we recommend just configuring the two URL sets separately.
and ``https://tickets.bigorg.com/awesomecon/`` contains only one and ``https://tickets.awesomecon.org/`` does not contain any.
The only robust way to do this is by having *separate* URL configuration for those three cases.
In pretix, we therefore do not have a global URL configuration, but three, living in the following modules:
- ``pretix.multidomain.maindomain_urlconf``
- ``pretix.multidomain.organizer_domain_urlconf``
- ``pretix.multidomain.event_domain_urlconf``
We provide some helper utilities to work with these to avoid duplicate configuration of the individual URLs.
The file ``urls.py`` inside your plugin package will be loaded and scanned for URL configuration
automatically and should be provided by any plugin that provides any view.
However, unlike plain Django, we look not only for a ``urlpatterns`` attribute on the module but support other
attributes like ``event_patterns`` and ``organizer_patterns`` as well.
A very basic example that provides one view in the admin panel and one view in the frontend
could look like this::
For example, for a simple plugin that adds one URL to the backend and one event-level URL to the frontend, you can
create the following configuration in your ``urls.py``::
from django.urls import re_path
@@ -52,7 +60,7 @@ could look like this::
As you can see, the view in the frontend is not included in the standard Django ``urlpatterns``
setting but in a separate list with the name ``event_patterns``. This will automatically prepend
the appropriate parameters to the regex (e.g. the event or the event and the organizer, depending
on the called domain).
on the called domain). For organizer-level views, ``organizer_patterns`` works the same way.
If you only provide URLs in the admin area, you do not need to provide a ``event_patterns`` attribute.
@@ -71,11 +79,16 @@ is a python method that emulates a behavior similar to ``reverse``:
.. autofunction:: pretix.multidomain.urlreverse.eventreverse
If you need to communicate the URL externally, you can use a different method to ensure that it is always an absolute URL:
.. autofunction:: pretix.multidomain.urlreverse.build_absolute_uri
In addition, there is a template tag that works similar to ``url`` but takes an event or organizer object
as its first argument and can be used like this::
{% load eventurl %}
<a href="{% eventurl request.event "presale:event.checkout" step="payment" %}">Pay</a>
<a href="{% abseventurl request.event "presale:event.checkout" step="payment" %}">Pay</a>
Implementation details

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.

View File

@@ -96,6 +96,20 @@ http://localhost:8000/control/ for the admin view.
port (for example because you develop on `pretixdroid`_), you can check
`Django's documentation`_ for more options.
When running the local development webserver, ensure Celery is not configured
in ``pretix.cfg``. i.e., you should remove anything such as::
[celery]
backend=redis://redis:6379/2
broker=redis://redis:6379/2
If you choose to use Celery for development, you must also start a Celery worker
process::
celery -A pretix.celery_app worker -l info
However, beware that code changes will not auto-reload within Celery.
.. _`checksandtests`:
Code checks and unit tests

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

After

Width:  |  Height:  |  Size: 287 KiB

View File

@@ -25,27 +25,27 @@ partition "data-based check" {
else
-down->[yes] "Is one or more block set on the ticket?"
--> if "" then
-right->[no] "Return error BLOCKED"
-right->[yes] "Return error BLOCKED"
else
-down->[yes] "If this is not an exit, is the valid_from/valid_until\nconstraint on the ticket fulfilled?"
-down->[no] "Is the order in status PENDING and not yet approved?"
--> if "" then
-right->[no] "Return error INVALID_TIME"
-right->[yes] "Return error UNAPPROVED"
else
-down->[yes] "Is the product part of the check-in list?"
-down->[no] "If this is not an exit, is the valid_from/valid_until\nconstraint on the ticket fulfilled?"
--> if "" then
-right->[no] "Return error PRODUCT"
-right->[no] "Return error INVALID_TIME"
else
-down->[yes] "Is the subevent part of the check-in list?"
-down->[yes] "Is the product part of the check-in list?"
--> if "" then
-right->[no] "Return error INVALID"
note bottom: TODO\ninconsistent\nwith online\ncheck
-right->[no] "Return error PRODUCT"
else
-down->[yes] "Is the order in status PAID?"
-down->[yes] "Is the subevent part of the check-in list?"
--> if "" then
-right->[no] "Is Order.require_approval set?"
-right->[no] "Return error INVALID"
note bottom: TODO\ninconsistent\nwith online\ncheck
else
-down->[yes] "Is the order in status PAID?"
--> if "" then
-->[yes] "Return error UNPAID "
else
-right->[no] "Is Order.valid_if_pending set?"
--> if "" then
-->[yes] "Is this an entry or exit?"
@@ -62,9 +62,9 @@ partition "data-based check" {
endif
endif
endif
else
-down->[yes] "Is this an entry or exit?"
endif
else
-down->[yes] "Is this an entry or exit?"
endif
endif
endif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

After

Width:  |  Height:  |  Size: 183 KiB

View File

@@ -42,23 +42,25 @@ endif
else
-down->[yes || force] "Is one or more block set on the ticket?"
--> if "" then
-right->[no && !force] "Return error BLOCKED"
-right->[yes && !force] "Return error BLOCKED"
else
-down->[yes || force] "If this is not an exit, is the valid_from/valid_until\nconstraint on the ticket fulfilled?"
-down->[no || force] "Is the order in status PENDING and not yet approved?"
--> if "" then
-right->[no && !force] "Return error INVALID_TIME"
-right->[yes && !force] "Return error UNAPPROVED"
else
-down->[yes || force] "Is the product part of the check-in list?"
-down->[no || force] "If this is not an exit, is the valid_from/valid_until\nconstraint on the ticket fulfilled?"
--> if "" then
-right->[no && !force] "Return error PRODUCT"
-right->[no && !force] "Return error INVALID_TIME"
else
-down->[yes || force] "Is the subevent part of the check-in list?"
-down->[yes || force] "Is the product part of the check-in list?"
--> if "" then
-right->[no && !force] "Return error PRODUCT "
-right->[no && !force] "Return error PRODUCT"
else
-down->[yes] "Is the order in status PAID?"
-down->[yes || force] "Is the subevent part of the check-in list?"
--> if "" then
-right->[no && !force] "Is Order.require_approval set?"
-right->[no && !force] "Return error PRODUCT "
else
-down->[yes] "Is the order in status PAID?"
--> if "" then
-->[no] "Is Order.valid_if_pending set?"
--> if "" then
@@ -77,10 +79,8 @@ else
endif
endif
else
-->[yes] "Return error UNPAID "
-down->[yes || force] "Is this an entry or exit?\nIs the upload forced?"
endif
else
-down->[yes || force] "Is this an entry or exit?\nIs the upload forced?"
endif
endif
endif

View File

@@ -35,7 +35,7 @@ contact_name string Contact person
contact_name_parts object of strings Decomposition of contact name (i.e. given name, family name)
contact_email string Contact person email address (or ``null``)
booth string Booth number (or ``null``). Maximum 100 characters.
locale string Locale for communication with the exhibitor (or ``null``).
locale string Locale for communication with the exhibitor.
access_code string Access code for the exhibitor to access their data or use the lead scanning app (read-only).
allow_lead_scanning boolean Enables lead scanning app
allow_lead_access boolean Enables access to data gathered by the lead scanning app
@@ -230,7 +230,8 @@ Endpoints
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/(id)/vouchers/
Returns a list of all vouchers connected to an exhibitor. The response contains the same data as described in
:ref:`rest-vouchers`.
:ref:`rest-vouchers` as well as for each voucher an additional field ``exhibitor_comment`` that is shown to the exhibitor. It can only
be modified using the ``attach`` API call below.
**Example request**:
@@ -285,7 +286,7 @@ Endpoints
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/(id)/vouchers/attach/
Attaches an **existing** voucher to an exhibitor. You need to send either the ``id`` **or** the ``code`` field of
the voucher.
the voucher. You can call this method multiple times to update the optional ``exhibitor_comment`` field.
**Example request**:
@@ -296,7 +297,8 @@ Endpoints
Accept: application/json, text/javascript
{
"id": 15
"id": 15,
"exhibitor_comment": "Free ticket"
}
**Example request**:
@@ -308,7 +310,8 @@ Endpoints
Accept: application/json, text/javascript
{
"code": "43K6LKM37FBVR2YG"
"code": "43K6LKM37FBVR2YG",
"exhibitor_comment": "Free ticket"
}
**Example response**:
@@ -356,7 +359,6 @@ Endpoints
"contact_email": "johnson@as.example.org",
"booth": "A2",
"locale": "de",
"access_code": "VKHZ2FU8",
"allow_lead_scanning": true,
"allow_lead_access": true,
"allow_voucher_access": true,
@@ -411,7 +413,7 @@ Endpoints
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/1/ HTTP/1.1
PATCH /api/v1/organizers/bigevents/events/sampleconf/exhibitors/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
@@ -459,6 +461,36 @@ Endpoints
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/exhibitor does not exist **or** you have no permission to change it.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/(id)/send_access_code/
Sends an email to the exhibitor with their access code.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/exhibitors/1/send_access_code/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param code: The ``id`` field of the exhibitor to send an email for
:statuscode 200: no error
:statuscode 400: The exhibitor does not have an email address associated
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested exhibitor does not exist.
:statuscode 503: The email could not be sent.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/(id)/

View File

@@ -24,3 +24,4 @@ If you want to **create** a plugin, please go to the
imported_secrets
webinar
presale-saml
kulturpass

193
doc/plugins/kulturpass.rst Normal file
View File

@@ -0,0 +1,193 @@
KulturPass
=========
.. note::
Since the KulturPass is specific to event organizers within Germany, the following page is also only provided in
German. Should you require assistance with the KulturPass and do not speak this language, please feel free reach
out to support@pretix.eu.
Einführung
----------
Der `KulturPass`_ ist ein Angebot der Bundesregierung für alle, die im laufenden Jahr ihren 18. Geburtstag feiern.
Sie erhalten ab ihrem 18. Geburtstag ein Budget von 200 Euro, das sie für Eintrittskarten, Bücher, CDs, Platten und
vieles andere einsetzen können. So wird Kultur vor Ort noch einfacher erlebbar. Gleichzeitig stärkt das die Nachfrage
bei den Anbietenden.
Da pretix ein Ticketing-System ist, stellen wir ausschließlich einen automatisierten Prozess für den Verkauf von
Eintrittskarten über den KulturPass-Marktplatz bereit.
Registrierung und Einrichtung
-----------------------------
Um als Unternehmen oder Kultureinrichtung Angebote auf dem KulturPass-Marktplatz anbieten zu können, ist zunächst eine
Registerung und die Einrichtung eines "Shops" sowie der dazugehörigen Angebote notwendig.
1. Registrierung
Registrieren Sie sich zunächst unter https://www.kulturpass.de/anbietende/layer als Anbieter. Im Zuge der
Registrierung beantworten Sie einige Fragen zu Ihrem Unternehmen/Ihrer Kultureinrichtung, hinterlegen Ihre
E-Mail-Adresse und beantworten Fragen zu Ihren Angebotsformen sowie Finanzierung Ihrer Einrichtung.
2. Anlegen eines KulturPass Shops
Nach Ihrer Registrierung müssen Sie der Weitergabe Ihrer Daten an die technische Platform hinter dem KulturPass,
Mirakl, zustimmen. Hier benennen Sie auch Ihren Shop.
3. Identifizierung mit ELSTER-Zertifikat
Als nächsten Schritt müssen Sie Ihr Unternehmen oder Ihre Einrichtung mit Hilfe eines sog. ELSTER-Zertifikates
identifizieren. Dieses Zertifikat nutzen Sie auch bereits jetzt schon, wenn Sie auf elektronischem Wege mit der
Finanzverwaltung kommunizieren.
4. Ersteinrichtung in pretix
Hinterlegen Sie nun die ID-Nummer Ihres KulturPass Marktplatz-Shops sowie einen API-Key in den
`Einstellungen Ihres Veranstalterkontos`_ (Veranstalter-Konto -> Einstellungen -> KulturPass). Diese Daten müssen
Sie nur einmalig für alle Ihre Veranstaltungen angeben.
Im `KulturPass-Backend`_ finden Sie die benötigten Informationen indem Sie auf das Benutzer-Symbol in der oberen,
rechten Ecke klicken, "Profil" und dann "API Schlüssel" auswählen bzw. indem Sie auf "Einstellungen" in der
Navigation links und dann "Shop" auswählen.
.. note::
Zu jedem Zeitpunkt kann nur ein Hintergrundsystem mit dem KulturPass-System verbunden sein. Werden
unterschiedliche Systeme oder gar mehrere pretix-Veranstalterkonten mit dem gleichen KulturPass-System verbunden,
können keine Bestellungen mehr verarbeitet werden und Angebote nicht automatisiert an den KulturPass-Marktplatz
übermittelt werden. Eingehende Bestellungen von Jugendlichen werden in diesem Fall automatisch abgelehnt, da diese
nicht eindeutig zugeordnet werden können. Ebenso überschreibt die Bereitstellung der Angebote eines Systems die
Angebote eines anderen Systems.
Wenn Sie mehrere Systeme haben, die den KulturPass-Marktplatz bedienen sollen, wenden Sie sich bitte an den
KulturPass-Support, um sich einen weiteren Shop einrichten zu lassen.
5. Aktivierung der KulturPass-Erweiterungen
Alle Veranstaltungen, die Sie über den KulturPass anbieten möchten, benötigen die `KulturPass-Erweiterung`_.
Aktivieren Sie diese bitte in jeder relevanten Veranstaltung über Einstellungen -> Erweiterungen -> Tab
"Integrationen" -> KulturPass.
6. Konfiguration der Artikel
Nachdem die KulturPass-Erweiterung aktiviert wurde, müssen Sie sich entscheiden, welche Produkte Sie über den
KulturPass-Marktplatz anbieten möchten. In der Bearbeitungs-Ansicht des jeweiligen Produktes finden Sie hierzu im
Tab "Zusätzliche Einstellungen" eine Checkbox "Das Produkt kann mit dem KulturPass erworben werden".
.. note::
Die Eigenschaft, dass ein Produkt durch den KulturPass-Marktplatz erworben werden kann, kann für beliebig viele
Produkte aktiviert werden. Auf Grund der Funktionsweise des KulturPasses sollten Sie jedoch gerade bei vielen
Artikeln mit unterschiedlich hohen Preisen darauf achten, dass die Preisspanne nicht zu hoch ausfällt.
Aktivieren Sie die Option für drei Produkte für 1, 10 und 100 Euro, so wird Ihr Angebot im KulturPass-Marktplatz
für 100 Euro gelistet werden. Dies bedeutet im Umkehrschluss auch, dass das KulturPass-Guthaben eines Jugendlichen
auch mindestens 100 Euro betragen muss, damit er Ihr Angebot in Anspruch nehmen kann - auch wenn die betroffene
Person lediglich das 1 Euro-Angebot wahrnehmen möchte. Erst mit dem 100 Euro KulturPass-Einlösecode wählt die
kaufende Person in Ihrem pretix-Shop aus, welches Produkt erworben werden soll. Ein Restguthaben wird nach dem Kauf
automatisch zurückerstattet und dem KulturPass-Konto wieder gutgeschrieben.
7. Konfiguration des Marktplatz-Eintrages
Je nach dem, ob es sich bei Ihrer Veranstaltung um eine Einzelveranstaltung oder eine Veranstaltungsreihe handelt,
müssen Sie die folgende Einstellung einmalig oder pro Veranstaltungstermin vornehmen.
Einzelveranstaltungen konfigurieren Sie über den Menüpunkt "KulturPass" in den Einstellungen Ihrer Veranstaltung;
Veranstaltungsreihen beim Anlegen oder Editieren eines jeden einzelnen Termins am Ende der Seite.
Um eine Veranstaltung oder einen Veranstaltungstermin im KulturPass-Marktplatz anzubieten, aktivieren Sie zunächst
die Option "Diese Veranstaltung via KulturPass anbieten". Geben Sie im folgenden die benötigten Informationen an.
Bitte beachten Sie, dass Sie bei den Angaben präzise Titel und Beschreibungen verwenden, da der KulturPass-
Marktplatz ausschließlich die Informationen aus diesem Bereich verwendet. Etwaige andere Informationen die Sie
bspw. in den "Text auf Startseite"-Felder eingeben haben, erreichen das KulturPass-System nicht.
.. note::
Gerade bei Veranstaltungsreihen nutzen viele pretix-Veranstalter gerne verkürzte Termin-Namen. Ein Schwimmbad würde
beispielsweise Ihre Veranstaltungsreihe "Freibad Musterstadt" und die einzelnen Termine nur "Schwimmen" nennen.
Während dies im pretix-Shop in einem gemeinsamen Kontext wunderbar funktioniert, würde eine Veranstaltung mit dem
Titel "Schwimmen" im KulturPass-Marktplatz Informationen vermissen lassen. Wählen Sie daher für das Eingabefeld
"Veranstaltungstitel" in der KulturPass-Konfiguration einen sprechenden Wert.
8. Übermittlung der Angebote
Sobald Sie Ihre ersten Veranstaltungen konfiguriert und live geschaltet haben, übermittelt pretix automatisch in
regelmäßigen Abständen alle von Ihnen angebotenen Veranstaltungen an das KulturPass System (Mirakl). Bitte beachten
Sie jedoch, dass der Import der Produkte und Angebote einige Zeit in Anspruch nehmen kann. Zum einen müssen
Angebote initial händisch von den Betreibern der KulturPass-Platform freigegeben werden, zum anderen muss auch eine
Synchronisation zwischen dem Hintergrundsystem und der KulturPass-App erfolgen. Auf die Dauer dieser Prozesse hat
pretix keinen Einfluss.
9. Freischalten des Marktplatz-Shops
Nachdem pretix erstmalig Angebote an das KulturPass-System übermittelt hat, müssen Sie Ihren Shop KulturPass-Shop
einmalig freischalten. Loggen Sie sich hierzu in das `KulturPass-Backend`_ ein.
Verwalten von KulturPass-Bestellungen
-------------------------------------
Durch die Nutzung der pretix-Integration mit dem KulturPass-System müssen Sie sich - bis auf die Kennzeichnung von
Produkten, die per KulturPass erworben werden dürfen, sowie die Bereitstellung von Veranstaltungs-Informationen für den
KulturPass-Marktplatz - um nichts kümmern: pretix übermittelt automatisch Ihre Veranstaltungen, wickelt die Einlösung
der Tickets ab und führt die Abrechnung mit dem Hintergrund-System durch.
Für Ihre Kunden verhält sich der KulturPass wie eine Zahlungsmethode im Bestellprozess und wird dort neben Ihren
anderen Zahlungsmethoden mit angeboten.
Die Gelder für mit dem KulturPass bezahlte Tickets erhalten Sie in Form einer Sammel-Überweisung von der Stiftung
Digitale Chancen auf das von Ihnen beim KulturPass Onboarding angegeben Bankkonto.
In Ihrem `KulturPass-Backend`_ können Sie über den Menüpunkt "Buchhaltung" Ihre bereits erfolgten und kommenden
Auszahlungen betrachten.
.. note::
Es ist von äußerster Wichtigkeit, dass Sie weder die eingehenden Bestellungen noch die Produkte und Angebote im
KulturPass-Backend händisch bearbeiten - auch wenn dies möglich wäre.
Bei händischen Änderungen riskieren Sie, dass die Datenbasis zwischen pretix und dem KulturPass-System divergiert
und es zu fehlerhaften Buchungen kommt. Wann immer möglich, sollten Sie Korrekturbuchungen und Änderungen
ausschließlich über pretix vornehmen.
Sollte eine händische Änderung/Korrektur notwendig werden, wenden Sie sich bitte an den pretix-Support, damit wir
die Auswirkungen evaluieren und vorab mit Ihnen besprechen können!
Erstattungen für Stornos und Absagen können Sie wie gehabt über das pretix-Backend vornehmen. Der jeweilige Betrag wird
dem KulturPass-Konto dann automatisch gutgeschrieben.
Da nach Ausgabe eines KulturPass Einlöse-Codes dieser vom Kunden jederzeit oder vom System bei
Nicht-(Komplett)Einlösung binnen 48 Stunden storniert werden kann, kann das im KulturPass-Backend angezeigte,
auszuzahlende Guthaben fluktuieren. Da in der Regel Auszahlungen frühestens 48 Stunden nach der Aufgabe einer
KulturPass-Bestellungen erfolgen, sollte Ihr Guthaben in der Regel nicht ins Negative gehen.
Ablauf für Kunden
-----------------
Ihre Kunden erhalten - nachdem sie sich ein eigenes Konto in der KulturPass-App angelegt und sich mit ihrem
elektronischen Personalausweis identifiziert haben - ein Guthaben von 200 Euro, welches für Leistungen aus dem
KulturPass-Marktplatz eingelöst werden kann.
Im Falle von Veranstaltungen, die per pretix verkauft werden, wählt der Kunde ein Angebot aus und erhält im folgenden
binnen kurzer Zeit (ca. 10-20 Minuten) einen Code und einen Link, um diesen einzulösen. Der Link bringt den Kunden direkt auf die Seite der
betreffenden pretix-Veranstaltung. Hier wird der Kunde darauf hingewiesen, für welche Produkte der Code genutzt werden
kann.
Im Bezahlschritt des Verkaufsprozesses wird dem Kunden vorgeschlagen, seinen KulturPass Einlösecode nun zu nutzen, um
die gewünschte Leistung zu erhalten.
Wurde ein Artikel gewählt, welcher günstiger als der Wert des Einlösecodes war, wird das Restguthaben automatisch auf
das KulturPass-Konto erstattet.
Wurden hingegen mehrere Artikel in den Warenkorb gelegt, so kann die Differenz mit einem anderen, regulären
Zahlungsmittel erfolgen.
Einlösecodes, die vom Kunden nicht binnen 48 Stunden eingelöst werden, werden automatisch storniert und dem
KulturPass-Konto wieder gutgeschrieben. Dieser Mechanismus greift auch, wenn eine Veranstaltung mittlerweile
ausverkauft ist und daher der Einlösecode nicht mehr Nutzbar ist.
Unterstützung
-------------
Weitergehende Informationen zum KulturPass finden Sie auch auf der `Webseite des KulturPasses`_, sowie im
`KulturPass Serviceportal`_.
.. _KulturPass: https://www.kulturpass.de/
.. _Einstellungen Ihres Veranstalterkontos: https://pretix.eu/control/organizer/-/settings/kulturpass
.. _KulturPass-Erweiterung: https://pretix.eu/control/event/-/-/settings/plugins#tab-0-2-open
.. _KulturPass-Backend: https://kulturpass-de.mirakl.net/
.. _Webseite des KulturPasses: https://www.kulturpass.de/
.. _KulturPass Serviceportal: https://service.kulturpass.de/help/

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -124,6 +124,24 @@ If you want to disable voucher input in the widget, you can pass the ``disable-v
<pretix-widget event="https://pretix.eu/demo/democon/" disable-vouchers></pretix-widget>
Enabling the button-style single item select
--------------------------------------------
By default, the widget uses a checkbox to select items, that can only be bought in quantities of one. If you want to match
the button-style of that checkbox with the one in the pretix shop, you can use the ``single-item-select`` attribute::
<pretix-widget event="https://pretix.eu/demo/democon/" single-item-select="button"></pretix-widget>
.. image:: img/widget_checkbox_button.png
:align: center
:class: screenshot
.. note::
Due to compatibilty with existing widget installations, the default value for ``single-item-select``
is ``checkbox``. This might change in the future, so make sure, to set the attribute to
``single-item-select="checkbox"`` if you need it.
Filtering products
------------------
@@ -178,6 +196,10 @@ settings. For example, if you set up a meta data property called "Promoted" that
<pretix-widget event="https://pretix.eu/demo/series/" list-type="list" filter="attr[Promoted]=Yes"></pretix-widget>
If you have enabled public filters in your meta data attribute configuration, a filter formshows up. To disable, use::
<pretix-widget event="https://pretix.eu/demo/democon/" disable-filters></pretix-widget>
pretix Button
-------------
@@ -293,6 +315,16 @@ with that information::
</pretix-widget>
This works for the pretix Button as well, if you also specify a product.
As data-attributes are reactive, you can change them with JavaScript as well. Please note, that once the user
started the checkout process, we do not update the data-attributes in the existing checkout process to not
interrupt the checkout UX.
When updating data-attributes through JavaScript, make sure you do not have a stale reference to the HTMLNode of the
widget. When the widget is created, the original HTMLNode can happen to be replaced. So make sure to always have a
fresh reference like so
``document.querySelectorAll("pretix-widget, pretix-button, .pretix-widget-wrapper")``
Currently, the following attributes are understood by pretix itself:
* ``data-email`` will pre-fill the order email field as well as the attendee email field (if enabled).
@@ -329,125 +361,72 @@ Hosted or pretix Enterprise are active, you can pass the following fields:
* If you use the campaigns plugin, you can pass a campaign ID as a value to ``data-campaign``. This way, all orders
made through this widget will be counted towards this campaign.
* If you use the tracking plugin, you can enable cross-domain tracking. To do so, you need to initialize the
pretix-widget manually. Use the html code to embed the widget and add one the following code snippets. Make sure to
replace all occurrences of <MEASUREMENT_ID> with your Google Analytics MEASUREMENT_ID (UA-XXXXXXX-X or G-XXXXXXXX)
* If you use the tracking plugin, you can enable cross-domain tracking. Please note: when you run your pretix-shop on a
subdomain of your main tracking domain, then you do not need cross-domain tracking as tracking automatically works
across subdomains. See :ref:`custom_domain` for how to set this up.
Please also make sure to add the embedding website to your `Referral exclusions
Please make sure to add the embedding website to your `Referral exclusions
<https://support.google.com/analytics/answer/2795830>`_ in your Google Analytics settings.
If you use Google Analytics 4 (GA4 G-XXXXXXXX)::
Add Google Analytics as you normally would with all your `window.dataLayer` and `gtag` configurations. Also add the
widget code normally. Then you have two options:
<script async src="https://www.googletagmanager.com/gtag/js?id=<MEASUREMENT_ID>"></script>
<script type="text/javascript">
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '<MEASUREMENT_ID>');
* Block loading of the widget at most 2 seconds or until Googles client- and session-ID are loaded. This method
uses `window.pretixWidgetCallback`. Note that if it takes longer than 2 seconds to load, client- and session-ID
are never passed to the widget. Make sure to replace all occurrences of <MEASUREMENT_ID> with your Google
Analytics MEASUREMENT_ID (G-XXXXXXXX)::
window.pretixWidgetCallback = function () {
window.PretixWidget.build_widgets = false;
window.addEventListener('load', function() { // Wait for GA to be loaded
if (!window['google_tag_manager']) {
window.PretixWidget.buildWidgets();
return;
}
<script type="text/javascript">
window.pretixWidgetCallback = function () {
window.PretixWidget.build_widgets = false;
window.addEventListener('load', function() { // Wait for GA to be loaded
if (!window['google_tag_manager']) {
window.PretixWidget.buildWidgets();
return;
}
var clientId;
var sessionId;
var loadingTimeout;
function build() {
// use loadingTimeout to make sure build() is only called once
if (!loadingTimeout) return;
window.clearTimeout(loadingTimeout);
loadingTimeout = null;
if (clientId) window.PretixWidget.widget_data["tracking-ga-id"] = clientId;
if (sessionId) window.PretixWidget.widget_data["tracking-ga-sessid"] = sessionId;
window.PretixWidget.buildWidgets();
};
// make sure to build pretix-widgets if gtag fails to load either client_id or session_id
loadingTimeout = window.setTimeout(build, 2000);
var clientId;
var sessionId;
var loadingTimeout;
function build() {
// use loadingTimeout to make sure build() is only called once
if (!loadingTimeout) return;
window.clearTimeout(loadingTimeout);
loadingTimeout = null;
if (clientId) window.PretixWidget.widget_data["tracking-ga-id"] = clientId;
if (sessionId) window.PretixWidget.widget_data["tracking-ga-sessid"] = sessionId;
window.PretixWidget.buildWidgets();
};
// make sure to build pretix-widgets if gtag fails to load either client_id or session_id
loadingTimeout = window.setTimeout(build, 2000);
gtag('get', '<MEASUREMENT_ID>', 'client_id', function(id) {
clientId = id;
if (sessionId !== undefined) build();
});
gtag('get', '<MEASUREMENT_ID>', 'session_id', function(id) {
sessionId = id;
if (clientId !== undefined) build();
});
});
};
</script>
* Or asynchronously set data-attributes the widgets are shown immediately, but once the user has started checkout,
data-attributes are not updated. Make sure to replace all occurrences of <MEASUREMENT_ID> with your Google
Analytics MEASUREMENT_ID (G-XXXXXXXX)::
<script type="text/javascript">
window.addEventListener('load', function() {
gtag('get', '<MEASUREMENT_ID>', 'client_id', function(id) {
clientId = id;
if (sessionId !== undefined) build();
const widgets = document.querySelectorAll("pretix-widget, pretix-button, .pretix-widget-wrapper");
widgets.forEach(widget => widget.setAttribute("data-tracking-ga-id", id))
});
gtag('get', '<MEASUREMENT_ID>', 'session_id', function(id) {
sessionId = id;
if (clientId !== undefined) build();
const widgets = document.querySelectorAll("pretix-widget, pretix-button, .pretix-widget-wrapper");
widgets.forEach(widget => widget.setAttribute("data-tracking-ga-sessid", id))
});
});
};
</script>
If you use Universal Analytics with ``gtag.js`` (UA-XXXXXXX-X)::
<script async src="https://www.googletagmanager.com/gtag/js?id=<MEASUREMENT_ID>"></script>
<script type="text/javascript">
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '<MEASUREMENT_ID>');
window.pretixWidgetCallback = function () {
window.PretixWidget.build_widgets = false;
window.addEventListener('load', function() { // Wait for GA to be loaded
if (!window['google_tag_manager']) {
window.PretixWidget.buildWidgets();
return;
}
// make sure to build pretix-widgets if gtag fails to load client_id
var loadingTimeout = window.setTimeout(function() {
loadingTimeout = null;
window.PretixWidget.buildWidgets();
}, 1000);
gtag('get', '<MEASUREMENT_ID>', 'client_id', function(id) {
if (loadingTimeout) {
window.clearTimeout(loadingTimeout);
window.PretixWidget.widget_data["tracking-ga-id"] = id;
window.PretixWidget.buildWidgets();
}
});
});
};
</script>
If you use ``analytics.js`` (Universal Analytics)::
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', '<MEASUREMENT_ID>', 'auto');
ga('send', 'pageview');
window.pretixWidgetCallback = function () {
window.PretixWidget.build_widgets = false;
window.addEventListener('load', function() { // Wait for GA to be loaded
if (!window['ga'] || !ga.create) {
// Tracking is probably blocked
window.PretixWidget.buildWidgets()
return;
}
var loadingTimeout = window.setTimeout(function() {
loadingTimeout = null;
window.PretixWidget.buildWidgets();
}, 1000);
ga(function(tracker) {
if (loadingTimeout) {
window.clearTimeout(loadingTimeout);
window.PretixWidget.widget_data["tracking-ga-id"] = tracker.get('clientId');
window.PretixWidget.buildWidgets();
}
});
});
};
</script>
</script>
.. _Let's Encrypt: https://letsencrypt.org/

View File

@@ -36,7 +36,7 @@ dependencies = [
"css-inline==0.8.*",
"defusedcsv>=1.1.0",
"dj-static",
"Django==4.1.*",
"Django==4.2.*",
"django-bootstrap3==23.1.*",
"django-compressor==4.3.*",
"django-countries==7.5.*",
@@ -90,7 +90,7 @@ dependencies = [
"pytz-deprecation-shim==0.1.*",
"pyuca",
"qrcode==7.4.*",
"redis==4.5.*,>=4.5.4",
"redis==4.6.*",
"reportlab==4.0.*",
"requests==2.31.*",
"sentry-sdk==1.15.*",
@@ -110,8 +110,10 @@ dependencies = [
[project.optional-dependencies]
memcached = ["pylibmc"]
dev = [
"aiohttp==3.8.*",
"coverage",
"coveralls",
"fakeredis==2.18.*",
"flake8==6.0.*",
"freezegun",
"isort==5.12.*",
@@ -119,6 +121,7 @@ dev = [
"potypo",
"pycodestyle==2.10.*",
"pyflakes==3.0.*",
"pytest-asyncio",
"pytest-cache",
"pytest-cov",
"pytest-django==4.*",

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__ = "2023.7.0.dev0"
__version__ = "2023.11.0.dev0"

View File

@@ -89,8 +89,10 @@ ALL_LANGUAGES = [
('fi', _('Finnish')),
('gl', _('Galician')),
('el', _('Greek')),
('id', _('Indonesian')),
('it', _('Italian')),
('lv', _('Latvian')),
('nb-no', _('Norwegian Bokmål')),
('pl', _('Polish')),
('pt-pt', _('Portuguese (Portugal)')),
('pt-br', _('Portuguese (Brazil)')),
@@ -107,7 +109,7 @@ LANGUAGES_RTL = {
'ar', 'hw'
}
LANGUAGES_INCUBATING = {
'pl', 'fi', 'pt-br', 'gl',
'fi', 'pt-br', 'gl',
}
LOCALE_PATHS = [
os.path.join(os.path.dirname(__file__), 'locale'),
@@ -196,7 +198,14 @@ STATICFILES_DIRS = [
STATICI18N_ROOT = os.path.join(BASE_DIR, "pretix/static")
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage",
},
}
# if os.path.exists(os.path.join(DATA_DIR, 'static')):
# STATICFILES_DIRS.insert(0, os.path.join(DATA_DIR, 'static'))
@@ -252,3 +261,20 @@ PRETIX_PRIMARY_COLOR = '#8E44B3'
# stressful for some cache setups so it is enabled by default and currently can't be enabled through pretix.cfg
CACHE_LARGE_VALUES_ALLOWED = False
CACHE_LARGE_VALUES_ALIAS = 'default'
# Allowed file extensions for various places plus matching Pillow formats.
# Never allow EPS, it is full of dangerous bugs.
FILE_UPLOAD_EXTENSIONS_IMAGE = (".png", ".jpg", ".gif", ".jpeg")
PILLOW_FORMATS_IMAGE = ('PNG', 'GIF', 'JPEG')
FILE_UPLOAD_EXTENSIONS_FAVICON = (".ico", ".png", "jpg", ".gif", ".jpeg")
FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE = (".png", "jpg", ".gif", ".jpeg", ".bmp", ".tif", ".tiff", ".jfif")
PILLOW_FORMATS_QUESTIONS_IMAGE = ('PNG', 'GIF', 'JPEG', 'BMP', 'TIFF')
FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT = (
".png", ".jpg", ".gif", ".jpeg", ".pdf", ".txt", ".docx", ".gif", ".svg",
".pptx", ".ppt", ".doc", ".xlsx", ".xls", ".jfif", ".heic", ".heif", ".pages",
".bmp", ".tif", ".tiff"
)
FILE_UPLOAD_EXTENSIONS_OTHER = FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT

View File

@@ -0,0 +1,91 @@
# Generated by Django 4.2.4 on 2023-09-26 12:01
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("pretixapi", "0010_webhook_comment"),
]
operations = [
migrations.AlterField(
model_name="apicall",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="oauthaccesstoken",
name="user",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(app_label)s_%(class)s",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name="oauthapplication",
name="user",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(app_label)s_%(class)s",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name="oauthgrant",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="%(app_label)s_%(class)s",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name="oauthidtoken",
name="user",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(app_label)s_%(class)s",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name="oauthrefreshtoken",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="%(app_label)s_%(class)s",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name="webhook",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="webhookcall",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="webhookeventlistener",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
]

View File

@@ -38,7 +38,7 @@ class CheckinListSerializer(I18nAwareModelSerializer):
model = CheckinList
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
'include_pending', 'auto_checkin_sales_channels', 'allow_multiple_entries', 'allow_entry_after_exit',
'rules', 'exit_all_at', 'addon_match')
'rules', 'exit_all_at', 'addon_match', 'ignore_in_statistics', 'consider_tickets_used')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

View File

@@ -32,11 +32,13 @@ class DiscountSerializer(I18nAwareModelSerializer):
'available_until', 'subevent_mode', 'condition_all_products', 'condition_limit_products',
'condition_apply_to_addons', 'condition_min_count', 'condition_min_value',
'benefit_discount_matching_percent', 'benefit_only_apply_to_cheapest_n_matches',
'condition_ignore_voucher_discounted')
'benefit_same_products', 'benefit_limit_products', 'benefit_apply_to_addons',
'benefit_ignore_voucher_discounted', 'condition_ignore_voucher_discounted')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['condition_limit_products'].queryset = self.context['event'].items.all()
self.fields['benefit_limit_products'].queryset = self.context['event'].items.all()
def validate(self, data):
data = super().validate(data)

View File

@@ -230,8 +230,8 @@ class EventSerializer(I18nAwareModelSerializer):
for key, v in value['meta_data'].items():
if key not in self.meta_properties:
raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key))
if self.meta_properties[key].allowed_values:
if v not in [_v.strip() for _v in self.meta_properties[key].allowed_values.splitlines()]:
if self.meta_properties[key].choices:
if v not in self.meta_properties[key].choice_keys:
raise ValidationError(_('Meta data property \'{name}\' does not allow value \'{value}\'.').format(name=key, value=v))
return value
@@ -528,8 +528,8 @@ class SubEventSerializer(I18nAwareModelSerializer):
for key, v in value['meta_data'].items():
if key not in self.meta_properties:
raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key))
if self.meta_properties[key].allowed_values:
if v not in [_v.strip() for _v in self.meta_properties[key].allowed_values.splitlines()]:
if self.meta_properties[key].choices:
if v not in self.meta_properties[key].choice_keys:
raise ValidationError(_('Meta data property \'{name}\' does not allow value \'{value}\'.').format(name=key, value=v))
return value
@@ -705,6 +705,7 @@ class EventSettingsSerializer(SettingsSerializer):
'frontpage_subevent_ordering',
'event_list_type',
'event_list_available_only',
'event_list_filters',
'event_calendar_future_only',
'frontpage_text',
'event_info_text',
@@ -795,6 +796,8 @@ class EventSettingsSerializer(SettingsSerializer):
'cancel_allow_user_paid_refund_as_giftcard',
'cancel_allow_user_paid_require_approval',
'cancel_allow_user_paid_require_approval_fee_unknown',
'cancel_terms_paid',
'cancel_terms_unpaid',
'change_allow_user_variation',
'change_allow_user_addons',
'change_allow_user_until',

View File

@@ -20,11 +20,14 @@
# <https://www.gnu.org/licenses/>.
#
from django import forms
from django.conf import settings
from django.http import QueryDict
from pytz import common_timezones
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from pretix.base.exporter import OrganizerLevelExportMixin
from pretix.base.models import ScheduledEventExport, ScheduledOrganizerExport
from pretix.base.timeframes import DateFrameField, SerializerDateFrameField
@@ -197,3 +200,92 @@ class JobRunSerializer(serializers.Serializer):
raise ValidationError(self.errors)
return not bool(self._errors)
class ScheduledExportSerializer(serializers.ModelSerializer):
schedule_next_run = serializers.DateTimeField(read_only=True)
export_identifier = serializers.ChoiceField(choices=[])
locale = serializers.ChoiceField(choices=settings.LANGUAGES, default='en')
owner = serializers.SlugRelatedField(slug_field='email', read_only=True)
error_counter = serializers.IntegerField(read_only=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['export_identifier'].choices = [(e, e) for e in self.context['exporters']]
def validate(self, attrs):
if attrs.get("export_form_data"):
identifier = attrs.get('export_identifier', self.instance.export_identifier if self.instance else None)
exporter = self.context['exporters'].get(identifier)
if exporter:
try:
JobRunSerializer(exporter=exporter).to_internal_value(attrs["export_form_data"])
except ValidationError as e:
raise ValidationError({"export_form_data": e.detail})
else:
raise ValidationError({"export_identifier": ["Unknown exporter."]})
return attrs
def validate_mail_additional_recipients(self, value):
d = value.replace(' ', '')
if len(d.split(',')) > 25:
raise ValidationError('Please enter less than 25 recipients.')
return d
def validate_mail_additional_recipients_cc(self, value):
d = value.replace(' ', '')
if len(d.split(',')) > 25:
raise ValidationError('Please enter less than 25 recipients.')
return d
def validate_mail_additional_recipients_bcc(self, value):
d = value.replace(' ', '')
if len(d.split(',')) > 25:
raise ValidationError('Please enter less than 25 recipients.')
return d
class ScheduledEventExportSerializer(ScheduledExportSerializer):
class Meta:
model = ScheduledEventExport
fields = [
'id',
'owner',
'export_identifier',
'export_form_data',
'locale',
'mail_additional_recipients',
'mail_additional_recipients_cc',
'mail_additional_recipients_bcc',
'mail_subject',
'mail_template',
'schedule_rrule',
'schedule_rrule_time',
'schedule_next_run',
'error_counter',
]
class ScheduledOrganizerExportSerializer(ScheduledExportSerializer):
timezone = serializers.ChoiceField(default=settings.TIME_ZONE, choices=[(a, a) for a in common_timezones])
class Meta:
model = ScheduledOrganizerExport
fields = [
'id',
'owner',
'export_identifier',
'export_form_data',
'locale',
'mail_additional_recipients',
'mail_additional_recipients_cc',
'mail_additional_recipients_bcc',
'mail_subject',
'mail_template',
'schedule_rrule',
'schedule_rrule_time',
'schedule_next_run',
'timezone',
'error_counter',
]

View File

@@ -59,9 +59,9 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
class Meta:
model = ItemVariation
fields = ('id', 'value', 'active', 'description',
'position', 'default_price', 'price', 'original_price', 'require_approval',
'position', 'default_price', 'price', 'original_price', 'free_price_suggestion', 'require_approval',
'require_membership', 'require_membership_types', 'require_membership_hidden',
'checkin_attention', 'available_from', 'available_until',
'checkin_attention', 'checkin_text', 'available_from', 'available_until',
'sales_channels', 'hide_without_voucher', 'meta_data')
def __init__(self, *args, **kwargs):
@@ -83,9 +83,9 @@ class ItemVariationSerializer(I18nAwareModelSerializer):
class Meta:
model = ItemVariation
fields = ('id', 'value', 'active', 'description',
'position', 'default_price', 'price', 'original_price', 'require_approval',
'position', 'default_price', 'price', 'original_price', 'free_price_suggestion', 'require_approval',
'require_membership', 'require_membership_types', 'require_membership_hidden',
'checkin_attention', 'available_from', 'available_until',
'checkin_attention', 'checkin_text', 'available_from', 'available_until',
'sales_channels', 'hide_without_voucher', 'meta_data')
def __init__(self, *args, **kwargs):
@@ -234,12 +234,13 @@ class ItemSerializer(I18nAwareModelSerializer):
class Meta:
model = Item
fields = ('id', 'category', 'name', 'internal_name', 'active', 'sales_channels', 'description',
'default_price', 'free_price', 'tax_rate', 'tax_rule', 'admission', 'personalized',
'position', 'picture', 'available_from', 'available_until',
'default_price', 'free_price', 'free_price_suggestion', 'tax_rate', 'tax_rule', 'admission',
'personalized', 'position', 'picture', 'available_from', 'available_until',
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
'min_per_order', 'max_per_order', 'checkin_attention', 'checkin_text', 'has_variations', 'variations',
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
'show_quota_left', 'hidden_if_available', 'allow_waitinglist', 'issue_giftcard', 'meta_data',
'show_quota_left', 'hidden_if_available', 'hidden_if_item_available', 'allow_waitinglist',
'issue_giftcard', 'meta_data',
'require_membership', 'require_membership_types', 'require_membership_hidden', 'grant_membership_type',
'grant_membership_duration_like_event', 'grant_membership_duration_days',
'grant_membership_duration_months', 'validity_mode', 'validity_fixed_from', 'validity_fixed_until',
@@ -439,7 +440,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
class Meta:
model = Question
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
'ask_during_checkin', 'show_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
'hidden', 'dependency_value', 'print_on_invoice', 'help_text', 'valid_number_min',
'valid_number_max', 'valid_date_min', 'valid_date_max', 'valid_datetime_min', 'valid_datetime_max',
'valid_string_length_max', 'valid_file_portrait')
@@ -485,6 +486,9 @@ class QuestionSerializer(I18nAwareModelSerializer):
if full_data.get('ask_during_checkin') and full_data.get('type') in Question.ASK_DURING_CHECKIN_UNSUPPORTED:
raise ValidationError(_('This type of question cannot be asked during check-in.'))
if full_data.get('show_during_checkin') and full_data.get('type') in Question.SHOW_DURING_CHECKIN_UNSUPPORTED:
raise ValidationError(_('This type of question cannot be shown during check-in.'))
Question.clean_items(event, full_data.get('items'))
return data

View File

@@ -22,11 +22,13 @@
import logging
import os
from collections import Counter, defaultdict
from datetime import timedelta
from decimal import Decimal
import pycountry
from django.conf import settings
from django.core.files import File
from django.db import models
from django.db.models import F, Q
from django.utils.encoding import force_str
from django.utils.timezone import now
@@ -42,7 +44,7 @@ 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 (
InlineItemVariationSerializer, ItemSerializer,
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
)
from pretix.base.channels import get_all_sales_channels
from pretix.base.decimal import round_decimal
@@ -58,10 +60,11 @@ from pretix.base.models.orders import (
)
from pretix.base.pdf import get_images, get_variables
from pretix.base.services.cart import error_messages
from pretix.base.services.locking import NoLockManager
from pretix.base.services.locking import LOCK_TRUST_WINDOW, lock_objects
from pretix.base.services.pricing import (
apply_discounts, get_line_price, get_listed_price, is_included_for_free,
)
from pretix.base.services.quotas import QuotaAvailability
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
from pretix.base.signals import register_ticket_outputs
from pretix.helpers.countries import CachedCountries
@@ -283,11 +286,12 @@ class FailedCheckinSerializer(I18nAwareModelSerializer):
raw_item = serializers.PrimaryKeyRelatedField(queryset=Item.objects.none(), required=False, allow_null=True)
raw_variation = serializers.PrimaryKeyRelatedField(queryset=ItemVariation.objects.none(), required=False, allow_null=True)
raw_subevent = serializers.PrimaryKeyRelatedField(queryset=SubEvent.objects.none(), required=False, allow_null=True)
nonce = serializers.CharField(required=False, allow_null=True)
class Meta:
model = Checkin
fields = ('error_reason', 'error_explanation', 'raw_barcode', 'raw_item', 'raw_variation',
'raw_subevent', 'datetime', 'type', 'position')
'raw_subevent', 'nonce', 'datetime', 'type', 'position')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -372,11 +376,15 @@ class PdfDataSerializer(serializers.Field):
self.context['vars_images'] = get_images(self.context['event'])
for k, f in self.context['vars'].items():
try:
res[k] = f['evaluate'](instance, instance.order, ev)
except:
logger.exception('Evaluating PDF variable failed')
res[k] = '(error)'
if 'evaluate_bulk' in f:
# Will be evaluated later by our list serializers
res[k] = (f['evaluate_bulk'], instance)
else:
try:
res[k] = f['evaluate'](instance, instance.order, ev)
except:
logger.exception('Evaluating PDF variable failed')
res[k] = '(error)'
if not hasattr(ev, '_cached_meta_data'):
ev._cached_meta_data = ev.meta_data
@@ -429,6 +437,38 @@ class PdfDataSerializer(serializers.Field):
return res
class OrderPositionListSerializer(serializers.ListSerializer):
def to_representation(self, data):
# We have a custom implementation of this method because PdfDataSerializer() might keep some elements unevaluated
# with a (callable, input) tuple. We'll loop over these entries and evaluate them bulk-wise to save on SQL queries.
if isinstance(self.parent, OrderSerializer) and isinstance(self.parent.parent, OrderListSerializer):
# Do not execute our custom code because it will be executed by OrderListSerializer later for the
# full result set.
return super().to_representation(data)
iterable = data.all() if isinstance(data, models.Manager) else data
data = []
evaluate_queue = defaultdict(list)
for item in iterable:
entry = self.child.to_representation(item)
if "pdf_data" in entry:
for k, v in entry["pdf_data"].items():
if isinstance(v, tuple) and callable(v[0]):
evaluate_queue[v[0]].append((v[1], entry, k))
data.append(entry)
for func, entries in evaluate_queue.items():
results = func([item for (item, entry, k) in entries])
for (item, entry, k), result in zip(entries, results):
entry["pdf_data"][k] = result
return data
class OrderPositionSerializer(I18nAwareModelSerializer):
checkins = CheckinSerializer(many=True, read_only=True)
answers = AnswerSerializer(many=True)
@@ -440,6 +480,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
attendee_name = serializers.CharField(required=False)
class Meta:
list_serializer_class = OrderPositionListSerializer
model = OrderPosition
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
'company', 'street', 'zipcode', 'city', 'country', 'state', 'discount',
@@ -460,7 +501,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
# /events/…/checkinlists/…/positions/
# We're unable to check this on this level if we're on /checkinrpc/, in which case we rely on the view
# layer to not set pdf_data=true in the first place.
request and hasattr(request, 'event') and 'can_view_orders' not in request.eventpermset
request and hasattr(request, 'eventpermset') and 'can_view_orders' not in request.eventpermset
)
if ('pdf_data' in self.context and not self.context['pdf_data']) or pdf_data_forbidden:
self.fields.pop('pdf_data', None)
@@ -468,6 +509,20 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
def validate(self, data):
raise TypeError("this serializer is readonly")
def to_representation(self, data):
if isinstance(self.parent, (OrderListSerializer, OrderPositionListSerializer)):
# Do not execute our custom code because it will be executed by OrderListSerializer later for the
# full result set.
return super().to_representation(data)
entry = super().to_representation(data)
if "pdf_data" in entry:
for k, v in entry["pdf_data"].items():
if isinstance(v, tuple) and callable(v[0]):
entry["pdf_data"][k] = v[0]([v[1]])[0]
return entry
class RequireAttentionField(serializers.Field):
def to_representation(self, instance: OrderPosition):
@@ -530,6 +585,9 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
if 'variation' in self.context['expand']:
self.fields['variation'] = InlineItemVariationSerializer(read_only=True)
if 'answers.question' in self.context['expand']:
self.fields['answers'].child.fields['question'] = QuestionSerializer(read_only=True)
class OrderPaymentTypeField(serializers.Field):
# TODO: Remove after pretix 2.2
@@ -562,7 +620,7 @@ class PaymentURLField(serializers.URLField):
def to_representation(self, instance: OrderPayment):
if instance.state != OrderPayment.PAYMENT_STATE_CREATED:
return None
return build_absolute_uri(self.context['event'], 'presale:event.order.pay', kwargs={
return build_absolute_uri(instance.order.event, 'presale:event.order.pay', kwargs={
'order': instance.order.code,
'secret': instance.order.secret,
'payment': instance.pk,
@@ -607,13 +665,42 @@ class OrderRefundSerializer(I18nAwareModelSerializer):
class OrderURLField(serializers.URLField):
def to_representation(self, instance: Order):
return build_absolute_uri(self.context['event'], 'presale:event.order', kwargs={
return build_absolute_uri(instance.event, 'presale:event.order', kwargs={
'order': instance.code,
'secret': instance.secret,
})
class OrderListSerializer(serializers.ListSerializer):
def to_representation(self, data):
# We have a custom implementation of this method because PdfDataSerializer() might keep some elements
# unevaluated with a (callable, input) tuple. We'll loop over these entries and evaluate them bulk-wise to
# save on SQL queries.
iterable = data.all() if isinstance(data, models.Manager) else data
data = []
evaluate_queue = defaultdict(list)
for item in iterable:
entry = self.child.to_representation(item)
for p in entry.get("positions", []):
if "pdf_data" in p:
for k, v in p["pdf_data"].items():
if isinstance(v, tuple) and callable(v[0]):
evaluate_queue[v[0]].append((v[1], p, k))
data.append(entry)
for func, entries in evaluate_queue.items():
results = func([item for (item, entry, k) in entries])
for (item, entry, k), result in zip(entries, results):
entry["pdf_data"][k] = result
return data
class OrderSerializer(I18nAwareModelSerializer):
event = SlugRelatedField(slug_field='slug', read_only=True)
invoice_address = InvoiceAddressSerializer(allow_null=True)
positions = OrderPositionSerializer(many=True, read_only=True)
fees = OrderFeeSerializer(many=True, read_only=True)
@@ -627,10 +714,11 @@ class OrderSerializer(I18nAwareModelSerializer):
class Meta:
model = Order
list_serializer_class = OrderListSerializer
fields = (
'code', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
'url', 'customer', 'valid_if_pending'
)
read_only_fields = (
@@ -686,8 +774,8 @@ class OrderSerializer(I18nAwareModelSerializer):
def update(self, instance, validated_data):
# Even though all fields that shouldn't be edited are marked as read_only in the serializer
# (hopefully), we'll be extra careful here and be explicit about the model fields we update.
update_fields = ['comment', 'custom_followup_at', 'checkin_attention', 'email', 'locale', 'phone',
'valid_if_pending']
update_fields = ['comment', 'custom_followup_at', 'checkin_attention', 'checkin_text', 'email', 'locale',
'phone', 'valid_if_pending']
if 'invoice_address' in validated_data:
iadata = validated_data.pop('invoice_address')
@@ -951,9 +1039,9 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
class Meta:
model = Order
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts',
'force', 'send_email', 'simulate', 'customer', 'custom_followup_at', 'require_approval',
'valid_if_pending')
'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date',
'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at',
'require_approval', 'valid_if_pending')
def validate_payment_provider(self, pp):
if pp is None:
@@ -1061,338 +1149,367 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
else:
ia = None
lock_required = False
quotas_by_item = {}
quota_diff_for_locking = Counter()
voucher_diff_for_locking = Counter()
seat_diff_for_locking = Counter()
quota_usage = Counter()
voucher_usage = Counter()
seat_usage = Counter()
v_budget = {}
now_dt = now()
delete_cps = []
consume_carts = validated_data.pop('consume_carts', [])
for pos_data in positions_data:
pos_data['_quotas'] = list(
pos_data.get('variation').quotas.filter(subevent=pos_data.get('subevent'))
if pos_data.get('variation')
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent'))
)
if pos_data.get('voucher') or pos_data.get('seat') or any(q.size is not None for q in pos_data['_quotas']):
lock_required = True
if (pos_data.get('item'), pos_data.get('variation'), pos_data.get('subevent')) not in quotas_by_item:
quotas_by_item[pos_data.get('item'), pos_data.get('variation'), pos_data.get('subevent')] = list(
pos_data.get('variation').quotas.filter(subevent=pos_data.get('subevent'))
if pos_data.get('variation')
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent'))
)
for q in quotas_by_item[pos_data.get('item'), pos_data.get('variation'), pos_data.get('subevent')]:
quota_diff_for_locking[q] += 1
if pos_data.get('voucher'):
voucher_diff_for_locking[pos_data['voucher']] += 1
if pos_data.get('seat'):
try:
seat = self.context['event'].seats.get(seat_guid=pos_data['seat'], subevent=pos_data.get('subevent'))
except Seat.DoesNotExist:
pos_data['seat'] = Seat.DoesNotExist
else:
pos_data['seat'] = seat
seat_diff_for_locking[pos_data['seat']] += 1
lockfn = self.context['event'].lock
if simulate or not lock_required:
lockfn = NoLockManager
with lockfn() as now_dt:
free_seats = set()
seats_seen = set()
consume_carts = validated_data.pop('consume_carts', [])
delete_cps = []
quota_avail_cache = {}
v_budget = {}
voucher_usage = Counter()
if consume_carts:
for cp in CartPosition.objects.filter(
event=self.context['event'], cart_id__in=consume_carts, expires__gt=now()
):
quotas = (cp.variation.quotas.filter(subevent=cp.subevent)
if cp.variation else cp.item.quotas.filter(subevent=cp.subevent))
for quota in quotas:
if quota not in quota_avail_cache:
quota_avail_cache[quota] = list(quota.availability())
if quota_avail_cache[quota][1] is not None:
quota_avail_cache[quota][1] += 1
if cp.voucher:
voucher_usage[cp.voucher] -= 1
if cp.expires > now_dt:
if cp.seat:
free_seats.add(cp.seat)
delete_cps.append(cp)
if consume_carts:
offset = now() + timedelta(seconds=LOCK_TRUST_WINDOW)
for cp in CartPosition.objects.filter(
event=self.context['event'], cart_id__in=consume_carts, expires__gt=now_dt
):
quotas = (cp.variation.quotas.filter(subevent=cp.subevent)
if cp.variation else cp.item.quotas.filter(subevent=cp.subevent))
for quota in quotas:
if cp.expires > offset:
quota_diff_for_locking[quota] -= 1
quota_usage[quota] -= 1
if cp.voucher:
if cp.expires > offset:
voucher_diff_for_locking[cp.voucher] -= 1
voucher_usage[cp.voucher] -= 1
if cp.seat:
if cp.expires > offset:
seat_diff_for_locking[cp.seat] -= 1
seat_usage[cp.seat] -= 1
delete_cps.append(cp)
errs = [{} for p in positions_data]
if not simulate:
full_lock_required = seat_diff_for_locking and self.context['event'].settings.seating_minimal_distance > 0
if full_lock_required:
# We lock the entire event in this case since we don't want to deal with fine-granular locking
# in the case of seating distance enforcement
lock_objects([self.context['event']])
else:
lock_objects(
[q for q, d in quota_diff_for_locking.items() if d > 0 and q.size is not None and not force] +
[v for v, d in voucher_diff_for_locking.items() if d > 0 and not force] +
[s for s, d in seat_diff_for_locking.items() if d > 0],
shared_lock_objects=[self.context['event']]
)
for i, pos_data in enumerate(positions_data):
qa = QuotaAvailability()
qa.queue(*[q for q, d in quota_diff_for_locking.items() if d > 0])
qa.compute()
if pos_data.get('voucher'):
v = pos_data['voucher']
# These are not technically correct as diff use due to the time offset applied above, so let's prevent accidental
# use further down
del quota_diff_for_locking, voucher_diff_for_locking, seat_diff_for_locking
if pos_data.get('addon_to'):
errs[i]['voucher'] = ['Vouchers are currently not supported for add-on products.']
continue
errs = [{} for p in positions_data]
if not v.applies_to(pos_data['item'], pos_data.get('variation')):
errs[i]['voucher'] = [error_messages['voucher_invalid_item']]
continue
for i, pos_data in enumerate(positions_data):
if pos_data.get('voucher'):
v = pos_data['voucher']
if v.subevent_id and pos_data.get('subevent').pk != v.subevent_id:
errs[i]['voucher'] = [error_messages['voucher_invalid_subevent']]
continue
if pos_data.get('addon_to'):
errs[i]['voucher'] = ['Vouchers are currently not supported for add-on products.']
continue
if v.valid_until is not None and v.valid_until < now_dt:
errs[i]['voucher'] = [error_messages['voucher_expired']]
continue
if not v.applies_to(pos_data['item'], pos_data.get('variation')):
errs[i]['voucher'] = [error_messages['voucher_invalid_item']]
continue
voucher_usage[v] += 1
if voucher_usage[v] > 0:
redeemed_in_carts = CartPosition.objects.filter(
Q(voucher=pos_data['voucher']) & Q(event=self.context['event']) & Q(expires__gte=now_dt)
).exclude(pk__in=[cp.pk for cp in delete_cps])
v_avail = v.max_usages - v.redeemed - redeemed_in_carts.count()
if v_avail < voucher_usage[v]:
errs[i]['voucher'] = [
'The voucher has already been used the maximum number of times.'
]
if v.subevent_id and pos_data.get('subevent').pk != v.subevent_id:
errs[i]['voucher'] = [error_messages['voucher_invalid_subevent']]
continue
if v.budget is not None:
price = pos_data.get('price')
listed_price = get_listed_price(pos_data.get('item'), pos_data.get('variation'), pos_data.get('subevent'))
if v.valid_until is not None and v.valid_until < now_dt:
errs[i]['voucher'] = [error_messages['voucher_expired']]
continue
if pos_data.get('voucher'):
price_after_voucher = pos_data.get('voucher').calculate_price(listed_price)
else:
price_after_voucher = listed_price
if price is None:
price = price_after_voucher
voucher_usage[v] += 1
if voucher_usage[v] > 0:
redeemed_in_carts = CartPosition.objects.filter(
Q(voucher=pos_data['voucher']) & Q(event=self.context['event']) & Q(expires__gte=now_dt)
).exclude(pk__in=[cp.pk for cp in delete_cps])
v_avail = v.max_usages - v.redeemed - redeemed_in_carts.count()
if v_avail < voucher_usage[v]:
errs[i]['voucher'] = [
'The voucher has already been used the maximum number of times.'
]
if v not in v_budget:
v_budget[v] = v.budget - v.budget_used()
disc = max(listed_price - price, 0)
if disc > v_budget[v]:
new_disc = v_budget[v]
v_budget[v] -= new_disc
if new_disc == Decimal('0.00') or pos_data.get('price') is not None:
errs[i]['voucher'] = [
'The voucher has a remaining budget of {}, therefore a discount of {} can not be '
'given.'.format(v_budget[v] + new_disc, disc)
]
continue
pos_data['price'] = price + (disc - new_disc)
else:
v_budget[v] -= disc
if v.budget is not None:
price = pos_data.get('price')
listed_price = get_listed_price(pos_data.get('item'), pos_data.get('variation'), pos_data.get('subevent'))
seated = pos_data.get('item').seat_category_mappings.filter(subevent=pos_data.get('subevent')).exists()
if pos_data.get('seat'):
if pos_data.get('addon_to'):
errs[i]['seat'] = ['Seats are currently not supported for add-on products.']
continue
if not seated:
errs[i]['seat'] = ['The specified product does not allow to choose a seat.']
try:
seat = self.context['event'].seats.get(seat_guid=pos_data['seat'], subevent=pos_data.get('subevent'))
except Seat.DoesNotExist:
errs[i]['seat'] = ['The specified seat does not exist.']
else:
pos_data['seat'] = seat
if (seat not in free_seats and not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web'))) or seat in seats_seen:
errs[i]['seat'] = [gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
seats_seen.add(seat)
elif seated:
errs[i]['seat'] = ['The specified product requires to choose a seat.']
requested_valid_from = pos_data.pop('requested_valid_from', None)
if 'valid_from' not in pos_data and 'valid_until' not in pos_data:
valid_from, valid_until = pos_data['item'].compute_validity(
requested_start=(
max(requested_valid_from, now())
if requested_valid_from and pos_data['item'].validity_dynamic_start_choice
else now()
),
enforce_start_limit=True,
override_tz=self.context['event'].timezone,
)
pos_data['valid_from'] = valid_from
pos_data['valid_until'] = valid_until
if not force:
for i, pos_data in enumerate(positions_data):
if pos_data.get('voucher'):
if pos_data['voucher'].allow_ignore_quota or pos_data['voucher'].block_quota:
price_after_voucher = pos_data.get('voucher').calculate_price(listed_price)
else:
price_after_voucher = listed_price
if price is None:
price = price_after_voucher
if v not in v_budget:
v_budget[v] = v.budget - v.budget_used()
disc = max(listed_price - price, 0)
if disc > v_budget[v]:
new_disc = v_budget[v]
v_budget[v] -= new_disc
if new_disc == Decimal('0.00') or pos_data.get('price') is not None:
errs[i]['voucher'] = [
'The voucher has a remaining budget of {}, therefore a discount of {} can not be '
'given.'.format(v_budget[v] + new_disc, disc)
]
continue
pos_data['price'] = price + (disc - new_disc)
else:
v_budget[v] -= disc
if pos_data.get('subevent'):
if pos_data.get('item').pk in pos_data['subevent'].item_overrides and pos_data['subevent'].item_overrides[pos_data['item'].pk].disabled:
errs[i]['item'] = [gettext_lazy('The product "{}" is not available on this date.').format(
str(pos_data.get('item'))
)]
if (
pos_data.get('variation') and pos_data['variation'].pk in pos_data['subevent'].var_overrides and
pos_data['subevent'].var_overrides[pos_data['variation'].pk].disabled
):
errs[i]['item'] = [gettext_lazy('The product "{}" is not available on this date.').format(
str(pos_data.get('item'))
)]
seated = pos_data.get('item').seat_category_mappings.filter(subevent=pos_data.get('subevent')).exists()
if pos_data.get('seat'):
if pos_data.get('addon_to'):
errs[i]['seat'] = ['Seats are currently not supported for add-on products.']
continue
if not seated:
errs[i]['seat'] = ['The specified product does not allow to choose a seat.']
seat = pos_data['seat']
if seat is Seat.DoesNotExist:
errs[i]['seat'] = ['The specified seat does not exist.']
else:
seat_usage[seat] += 1
if (seat_usage[seat] > 0 and not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web'))) or seat_usage[seat] > 1:
errs[i]['seat'] = [gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
elif seated:
errs[i]['seat'] = ['The specified product requires to choose a seat.']
new_quotas = pos_data['_quotas']
if len(new_quotas) == 0:
errs[i]['item'] = [gettext_lazy('The product "{}" is not assigned to a quota.').format(
requested_valid_from = pos_data.pop('requested_valid_from', None)
if 'valid_from' not in pos_data and 'valid_until' not in pos_data:
valid_from, valid_until = pos_data['item'].compute_validity(
requested_start=(
max(requested_valid_from, now())
if requested_valid_from and pos_data['item'].validity_dynamic_start_choice
else now()
),
enforce_start_limit=True,
override_tz=self.context['event'].timezone,
)
pos_data['valid_from'] = valid_from
pos_data['valid_until'] = valid_until
if not force:
for i, pos_data in enumerate(positions_data):
if pos_data.get('voucher'):
if pos_data['voucher'].allow_ignore_quota or pos_data['voucher'].block_quota:
continue
if pos_data.get('subevent'):
if pos_data.get('item').pk in pos_data['subevent'].item_overrides and pos_data['subevent'].item_overrides[pos_data['item'].pk].disabled:
errs[i]['item'] = [gettext_lazy('The product "{}" is not available on this date.').format(
str(pos_data.get('item'))
)]
if (
pos_data.get('variation') and pos_data['variation'].pk in pos_data['subevent'].var_overrides and
pos_data['subevent'].var_overrides[pos_data['variation'].pk].disabled
):
errs[i]['item'] = [gettext_lazy('The product "{}" is not available on this date.').format(
str(pos_data.get('item'))
)]
else:
for quota in new_quotas:
if quota not in quota_avail_cache:
quota_avail_cache[quota] = list(quota.availability())
if quota_avail_cache[quota][1] is not None:
quota_avail_cache[quota][1] -= 1
if quota_avail_cache[quota][1] < 0:
errs[i]['item'] = [
gettext_lazy('There is not enough quota available on quota "{}" to perform the operation.').format(
quota.name
)
]
if any(errs):
raise ValidationError({'positions': errs})
if validated_data.get('locale', None) is None:
validated_data['locale'] = self.context['event'].settings.locale
order = Order(event=self.context['event'], **validated_data)
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
order.meta_info = "{}"
order.total = Decimal('0.00')
if validated_data.get('require_approval') is not None:
order.require_approval = validated_data['require_approval']
if simulate:
order = WrappedModel(order)
order.last_modified = now()
order.code = 'PREVIEW'
else:
order.save()
if ia:
if not simulate:
ia.order = order
ia.save()
new_quotas = quotas_by_item[pos_data.get('item'), pos_data.get('variation'), pos_data.get('subevent')]
if len(new_quotas) == 0:
errs[i]['item'] = [gettext_lazy('The product "{}" is not assigned to a quota.').format(
str(pos_data.get('item'))
)]
else:
order.invoice_address = ia
ia.last_modified = now()
for quota in new_quotas:
quota_usage[quota] += 1
if quota_usage[quota] > 0 and qa.results[quota][1] is not None:
if qa.results[quota][1] < quota_usage[quota]:
errs[i]['item'] = [
gettext_lazy('There is not enough quota available on quota "{}" to perform the operation.').format(
quota.name
)
]
# Generate position objects
pos_map = {}
for pos_data in positions_data:
addon_to = pos_data.pop('addon_to', None)
attendee_name = pos_data.pop('attendee_name', '')
if attendee_name and not pos_data.get('attendee_name_parts'):
pos_data['attendee_name_parts'] = {
'_legacy': attendee_name
}
pos = OrderPosition(**{k: v for k, v in pos_data.items() if k != 'answers' and k != '_quotas' and k != 'use_reusable_medium'})
if simulate:
pos.order = order._wrapped
else:
pos.order = order
if addon_to:
if simulate:
pos.addon_to = pos_map[addon_to]
else:
pos.addon_to = pos_map[addon_to]
if any(errs):
raise ValidationError({'positions': errs})
pos_map[pos.positionid] = pos
pos_data['__instance'] = pos
# Calculate prices if not set
for pos_data in positions_data:
pos = pos_data['__instance']
if pos.addon_to_id and is_included_for_free(pos.item, pos.addon_to):
listed_price = Decimal('0.00')
else:
listed_price = get_listed_price(pos.item, pos.variation, pos.subevent)
if pos.price is None:
if pos.voucher:
price_after_voucher = pos.voucher.calculate_price(listed_price)
else:
price_after_voucher = listed_price
line_price = get_line_price(
price_after_voucher=price_after_voucher,
custom_price_input=None,
custom_price_input_is_net=False,
tax_rule=pos.item.tax_rule,
invoice_address=ia,
bundled_sum=Decimal('0.00'),
)
pos.price = line_price.gross
pos._auto_generated_price = True
else:
if pos.voucher:
if not pos.item.tax_rule or pos.item.tax_rule.price_includes_tax:
price_after_voucher = max(pos.price, pos.voucher.calculate_price(listed_price))
else:
price_after_voucher = max(pos.price - pos.tax_value, pos.voucher.calculate_price(listed_price))
else:
price_after_voucher = listed_price
pos._auto_generated_price = False
pos._voucher_discount = listed_price - price_after_voucher
if pos.voucher:
pos.voucher_budget_use = max(listed_price - price_after_voucher, Decimal('0.00'))
order_positions = [pos_data['__instance'] for pos_data in positions_data]
discount_results = apply_discounts(
self.context['event'],
order.sales_channel,
[
(cp.item_id, cp.subevent_id, cp.price, bool(cp.addon_to), cp.is_bundled, pos._voucher_discount)
for cp in order_positions
]
)
for cp, (new_price, discount) in zip(order_positions, discount_results):
if new_price != pos.price and pos._auto_generated_price:
pos.price = new_price
pos.discount = discount
# Save instances
for pos_data in positions_data:
answers_data = pos_data.pop('answers', [])
use_reusable_medium = pos_data.pop('use_reusable_medium', None)
pos = pos_data['__instance']
pos._calculate_tax()
if simulate:
pos = WrappedModel(pos)
pos.id = 0
answers = []
for answ_data in answers_data:
options = answ_data.pop('options', [])
answ = WrappedModel(QuestionAnswer(**answ_data))
answ.options = WrappedList(options)
answers.append(answ)
pos.answers = answers
pos.pseudonymization_id = "PREVIEW"
pos.checkins = []
pos_map[pos.positionid] = pos
else:
if pos.voucher:
Voucher.objects.filter(pk=pos.voucher.pk).update(redeemed=F('redeemed') + 1)
pos.save()
seen_answers = set()
for answ_data in answers_data:
# Workaround for a pretixPOS bug :-(
if answ_data.get('question') in seen_answers:
continue
seen_answers.add(answ_data.get('question'))
options = answ_data.pop('options', [])
if isinstance(answ_data['answer'], File):
an = answ_data.pop('answer')
answ = pos.answers.create(**answ_data, answer='')
answ.file.save(os.path.basename(an.name), an, save=False)
answ.answer = 'file://' + answ.file.name
answ.save()
else:
answ = pos.answers.create(**answ_data)
answ.options.add(*options)
if use_reusable_medium:
use_reusable_medium.linked_orderposition = pos
use_reusable_medium.save(update_fields=['linked_orderposition'])
use_reusable_medium.log_action(
'pretix.reusable_medium.linked_orderposition.changed',
data={
'by_order': order.code,
'linked_orderposition': pos.pk,
}
)
if validated_data.get('locale', None) is None:
validated_data['locale'] = self.context['event'].settings.locale
order = Order(event=self.context['event'], **validated_data)
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
order.meta_info = "{}"
order.total = Decimal('0.00')
if validated_data.get('require_approval') is not None:
order.require_approval = validated_data['require_approval']
if simulate:
order = WrappedModel(order)
order.last_modified = now()
order.code = 'PREVIEW'
else:
order.save()
if ia:
if not simulate:
for cp in delete_cps:
if cp.addon_to_id:
ia.order = order
ia.save()
else:
order.invoice_address = ia
ia.last_modified = now()
# Generate position objects
pos_map = {}
for pos_data in positions_data:
addon_to = pos_data.pop('addon_to', None)
attendee_name = pos_data.pop('attendee_name', '')
if attendee_name and not pos_data.get('attendee_name_parts'):
pos_data['attendee_name_parts'] = {
'_legacy': attendee_name
}
pos = OrderPosition(**{k: v for k, v in pos_data.items() if k != 'answers' and k != '_quotas' and k != 'use_reusable_medium'})
if simulate:
pos.order = order._wrapped
else:
pos.order = order
if addon_to:
if simulate:
pos.addon_to = pos_map[addon_to]
else:
pos.addon_to = pos_map[addon_to]
pos_map[pos.positionid] = pos
pos_data['__instance'] = pos
# Calculate prices if not set
for pos_data in positions_data:
pos = pos_data['__instance']
if pos.addon_to_id and is_included_for_free(pos.item, pos.addon_to):
listed_price = Decimal('0.00')
else:
listed_price = get_listed_price(pos.item, pos.variation, pos.subevent)
if pos.price is None:
if pos.voucher:
price_after_voucher = pos.voucher.calculate_price(listed_price)
else:
price_after_voucher = listed_price
line_price = get_line_price(
price_after_voucher=price_after_voucher,
custom_price_input=None,
custom_price_input_is_net=False,
tax_rule=pos.item.tax_rule,
invoice_address=ia,
bundled_sum=Decimal('0.00'),
)
pos.price = line_price.gross
pos._auto_generated_price = True
else:
if pos.voucher:
if not pos.item.tax_rule or pos.item.tax_rule.price_includes_tax:
price_after_voucher = max(pos.price, pos.voucher.calculate_price(listed_price))
else:
price_after_voucher = max(pos.price - pos.tax_value, pos.voucher.calculate_price(listed_price))
else:
price_after_voucher = listed_price
pos._auto_generated_price = False
pos._voucher_discount = listed_price - price_after_voucher
if pos.voucher:
pos.voucher_budget_use = max(listed_price - price_after_voucher, Decimal('0.00'))
order_positions = [pos_data['__instance'] for pos_data in positions_data]
discount_results = apply_discounts(
self.context['event'],
order.sales_channel,
[
(cp.item_id, cp.subevent_id, cp.price, bool(cp.addon_to), cp.is_bundled, pos._voucher_discount)
for cp in order_positions
]
)
for cp, (new_price, discount) in zip(order_positions, discount_results):
if new_price != pos.price and pos._auto_generated_price:
pos.price = new_price
pos.discount = discount
# Save instances
for pos_data in positions_data:
answers_data = pos_data.pop('answers', [])
use_reusable_medium = pos_data.pop('use_reusable_medium', None)
pos = pos_data['__instance']
pos._calculate_tax()
if simulate:
pos = WrappedModel(pos)
pos.id = 0
answers = []
for answ_data in answers_data:
options = answ_data.pop('options', [])
answ = WrappedModel(QuestionAnswer(**answ_data))
answ.options = WrappedList(options)
answers.append(answ)
pos.answers = answers
pos.pseudonymization_id = "PREVIEW"
pos.checkins = []
pos_map[pos.positionid] = pos
else:
if pos.voucher:
Voucher.objects.filter(pk=pos.voucher.pk).update(redeemed=F('redeemed') + 1)
pos.save()
seen_answers = set()
for answ_data in answers_data:
# Workaround for a pretixPOS bug :-(
if answ_data.get('question') in seen_answers:
continue
cp.addons.all().delete()
cp.delete()
seen_answers.add(answ_data.get('question'))
options = answ_data.pop('options', [])
if isinstance(answ_data['answer'], File):
an = answ_data.pop('answer')
answ = pos.answers.create(**answ_data, answer='')
answ.file.save(os.path.basename(an.name), an, save=False)
answ.answer = 'file://' + answ.file.name
answ.save()
else:
answ = pos.answers.create(**answ_data)
answ.options.add(*options)
if use_reusable_medium:
use_reusable_medium.linked_orderposition = pos
use_reusable_medium.save(update_fields=['linked_orderposition'])
use_reusable_medium.log_action(
'pretix.reusable_medium.linked_orderposition.changed',
data={
'by_order': order.code,
'linked_orderposition': pos.pk,
}
)
if not simulate:
for cp in delete_cps:
if cp.addon_to_id:
continue
cp.addons.all().delete()
cp.delete()
order.total = sum([p.price for p in pos_map.values()])
fees = []
@@ -1512,6 +1629,7 @@ class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
class InvoiceSerializer(I18nAwareModelSerializer):
event = SlugRelatedField(slug_field='slug', read_only=True)
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
refers = serializers.SlugRelatedField(slug_field='full_invoice_no', read_only=True)
lines = InlineInvoiceLineSerializer(many=True)
@@ -1520,7 +1638,7 @@ class InvoiceSerializer(I18nAwareModelSerializer):
class Meta:
model = Invoice
fields = ('order', 'number', 'is_cancellation', 'invoice_from', 'invoice_from_name', 'invoice_from_zipcode',
fields = ('event', 'order', 'number', 'is_cancellation', 'invoice_from', 'invoice_from_name', 'invoice_from_zipcode',
'invoice_from_city', 'invoice_from_country', 'invoice_from_tax_id', 'invoice_from_vat_id',
'invoice_to', 'invoice_to_company', 'invoice_to_name', 'invoice_to_street', 'invoice_to_zipcode',
'invoice_to_city', 'invoice_to_state', 'invoice_to_country', 'invoice_to_vat_id', 'invoice_to_beneficiary',

View File

@@ -94,6 +94,14 @@ class CustomerSerializer(I18nAwareModelSerializer):
data['name_parts']['_scheme'] = self.context['request'].organizer.settings.name_scheme
return data
def validate_email(self, value):
qs = Customer.objects.filter(organizer=self.context['organizer'], email__iexact=value)
if self.instance and self.instance.pk:
qs = qs.exclude(pk=self.instance.pk)
if qs.exists():
raise ValidationError(_("An account with this email address is already registered."))
return value
class CustomerCreateSerializer(CustomerSerializer):
send_email = serializers.BooleanField(default=False, required=False, allow_null=True)

View File

@@ -24,10 +24,16 @@ from rest_framework.exceptions import ValidationError
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import Seat, Voucher
from pretix.base.models.vouchers import generate_codes
class VoucherListSerializer(serializers.ListSerializer):
def create(self, validated_data):
vouchers_without_codes = [v for v in validated_data if not v.get('code')]
for voucher_data, code in zip(vouchers_without_codes, generate_codes(self.context['event'].organizer, num=len(vouchers_without_codes), prefix=None)):
voucher_data['code'] = code
codes = set()
seats = set()
errs = []
@@ -94,8 +100,13 @@ class VoucherSerializer(I18nAwareModelSerializer):
)
if check_quota:
Voucher.clean_quota_check(
full_data, 1, self.instance, self.context.get('event'),
full_data.get('quota'), full_data.get('item'), full_data.get('variation')
full_data,
full_data.get('max_usages', 1) - (self.instance.redeemed if self.instance else 0),
self.instance,
self.context.get('event'),
full_data.get('quota'),
full_data.get('item'),
full_data.get('variation')
)
Voucher.clean_voucher_code(full_data, self.context.get('event'), self.instance.pk if self.instance else None)

View File

@@ -61,6 +61,9 @@ orga_router.register(r'membershiptypes', organizer.MembershipTypeViewSet)
orga_router.register(r'reusablemedia', media.ReusableMediaViewSet)
orga_router.register(r'teams', organizer.TeamViewSet)
orga_router.register(r'devices', organizer.DeviceViewSet)
orga_router.register(r'orders', order.OrganizerOrderViewSet)
orga_router.register(r'invoices', order.InvoiceViewSet)
orga_router.register(r'scheduled_exports', exporters.ScheduledOrganizerExportViewSet)
orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters')
team_router = routers.DefaultRouter()
@@ -77,7 +80,7 @@ event_router.register(r'questions', item.QuestionViewSet)
event_router.register(r'discounts', discount.DiscountViewSet)
event_router.register(r'quotas', item.QuotaViewSet)
event_router.register(r'vouchers', voucher.VoucherViewSet)
event_router.register(r'orders', order.OrderViewSet)
event_router.register(r'orders', order.EventOrderViewSet)
event_router.register(r'orderpositions', order.OrderPositionViewSet)
event_router.register(r'invoices', order.InvoiceViewSet)
event_router.register(r'revokedsecrets', order.RevokedSecretViewSet, basename='revokedsecrets')
@@ -86,6 +89,7 @@ event_router.register(r'taxrules', event.TaxRuleViewSet)
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
event_router.register(r'checkinlists', checkin.CheckinListViewSet)
event_router.register(r'cartpositions', cart.CartPositionViewSet)
event_router.register(r'scheduled_exports', exporters.ScheduledEventExportViewSet)
event_router.register(r'exporters', exporters.EventExportersViewSet, basename='exporters')
event_router.register(r'shredders', shredders.EventShreddersViewSet, basename='shredders')
event_router.register(r'item_meta_properties', event.ItemMetaPropertiesViewSet)

View File

@@ -25,6 +25,7 @@ from typing import List
from django.db import transaction
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import gettext as _
from rest_framework import status, viewsets
from rest_framework.decorators import action
@@ -41,7 +42,7 @@ from pretix.base.models import CartPosition
from pretix.base.services.cart import (
_get_quota_availability, _get_voucher_availability, error_messages,
)
from pretix.base.services.locking import NoLockManager
from pretix.base.services.locking import lock_objects
class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
@@ -150,12 +151,21 @@ class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
quota_diff[q] += 1
seats_seen = set()
now_dt = now()
with transaction.atomic():
full_lock_required = seat_diff and self.request.event.settings.seating_minimal_distance > 0
if full_lock_required:
# We lock the entire event in this case since we don't want to deal with fine-granular locking
# in the case of seating distance enforcement
lock_objects([self.request.event])
else:
lock_objects(
[q for q, d in quota_diff.items() if q.size is not None and d > 0] +
[v for v, d in voucher_use_diff.items() if d > 0] +
[s for s, d in seat_diff.items() if d > 0],
shared_lock_objects=[self.request.event]
)
lockfn = NoLockManager
if self._require_locking(quota_diff, voucher_use_diff, seat_diff):
lockfn = self.request.event.lock
with lockfn() as now_dt, transaction.atomic():
vouchers_ok, vouchers_depend_on_cart = _get_voucher_availability(
self.request.event,
voucher_use_diff,

View File

@@ -152,6 +152,11 @@ class CheckinListViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['POST'], url_name='failed_checkins')
@transaction.atomic()
def failed_checkins(self, *args, **kwargs):
additional_log_data = {}
if 'debug_data' in self.request.data:
# Intentionally undocumented, might be removed again
additional_log_data['debug_data'] = self.request.data.pop('debug_data')
serializer = FailedCheckinSerializer(
data=self.request.data,
context={'event': self.request.event}
@@ -164,8 +169,21 @@ class CheckinListViewSet(viewsets.ModelViewSet):
secret=serializer.validated_data['raw_barcode']
).first()
clist = self.get_object()
if serializer.validated_data.get('nonce'):
if kwargs.get('position'):
prev = kwargs['position'].all_checkins.filter(nonce=serializer.validated_data['nonce']).first()
else:
prev = clist.checkins.filter(
nonce=serializer.validated_data['nonce'],
raw_barcode=serializer.validated_data['raw_barcode'],
).first()
if prev:
# Ignore because nonce is already handled
return Response(serializer.data, status=201)
c = serializer.save(
list=self.get_object(),
list=clist,
successful=False,
forced=True,
force_sent=True,
@@ -181,14 +199,16 @@ class CheckinListViewSet(viewsets.ModelViewSet):
'reason_explanation': c.error_explanation,
'datetime': c.datetime,
'type': c.type,
'list': c.list.pk
'list': c.list.pk,
**additional_log_data,
}, user=self.request.user, auth=self.request.auth)
else:
self.request.event.log_action('pretix.event.checkin.unknown', data={
'datetime': c.datetime,
'type': c.type,
'list': c.list.pk,
'barcode': c.raw_barcode
'barcode': c.raw_barcode,
**additional_log_data,
}, user=self.request.user, auth=self.request.auth)
return Response(serializer.data, status=201)
@@ -265,6 +285,7 @@ with scopes_disabled():
def __init__(self, *args, **kwargs):
self.checkinlist = kwargs.pop('checkinlist')
self.gate = kwargs.pop('gate')
super().__init__(*args, **kwargs)
def has_checkin_qs(self, queryset, name, value):
@@ -274,7 +295,7 @@ with scopes_disabled():
if not self.checkinlist.rules:
return queryset
return queryset.filter(
SQLLogic(self.checkinlist).apply(self.checkinlist.rules)
SQLLogic(self.checkinlist, self.gate).apply(self.checkinlist.rules)
).filter(
Q(valid_from__isnull=True) | Q(valid_from__lte=now()),
Q(valid_until__isnull=True) | Q(valid_until__gte=now()),
@@ -396,7 +417,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, simulate=False):
source_type='barcode', legacy_url_support=False, simulate=False, gate=None):
if not checkinlists:
raise ValidationError('No check-in list passed.')
@@ -404,7 +425,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
prefetch_related_objects([cl for cl in checkinlists if not cl.all_products], 'limit_products')
device = auth if isinstance(auth, Device) else None
gate = auth.gate if isinstance(auth, Device) else None
gate = gate or (auth.gate if isinstance(auth, Device) else None)
context = {
'request': request,
@@ -522,6 +543,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'reason': Checkin.REASON_ALREADY_REDEEMED,
'reason_explanation': None,
'require_attention': False,
'checkin_texts': [],
'__warning': 'Compatibility hack active due to detected old pretixSCAN version',
}, status=400)
except: # we don't care e.g. about invalid version numbers
@@ -533,6 +555,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'reason': Checkin.REASON_INVALID,
'reason_explanation': None,
'require_attention': False,
'checkin_texts': [],
'list': MiniCheckinListSerializer(checkinlists[0]).data,
}, status=404)
elif revoked_matches and force:
@@ -562,6 +585,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'reason': Checkin.REASON_REVOKED,
'reason_explanation': None,
'require_attention': False,
'checkin_texts': [],
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, revoked_matches[
0].event)).data,
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
@@ -617,6 +641,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'reason': Checkin.REASON_AMBIGUOUS,
'reason_explanation': None,
'require_attention': op.require_checkin_attention,
'checkin_texts': op.checkin_texts,
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
}, status=400)
@@ -659,11 +684,13 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
raw_source_type=source_type,
from_revoked_secret=from_revoked_secret,
simulate=simulate,
gate=gate,
)
except RequiredQuestionsError as e:
return Response({
'status': 'incomplete',
'require_attention': op.require_checkin_attention,
'checkin_texts': op.checkin_texts,
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
'questions': [
QuestionSerializer(q).data for q in e.questions
@@ -694,6 +721,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'reason': e.code,
'reason_explanation': e.reason,
'require_attention': op.require_checkin_attention,
'checkin_texts': op.checkin_texts,
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
}, status=400)
@@ -701,6 +729,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
return Response({
'status': 'ok',
'require_attention': op.require_checkin_attention,
'checkin_texts': op.checkin_texts,
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
}, status=201)
@@ -757,6 +786,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
def get_filterset_kwargs(self):
return {
'checkinlist': self.checkinlist,
'gate': self.request.auth.gate if isinstance(self.request.auth, Device) else None,
}
@cached_property

View File

@@ -89,6 +89,8 @@ class UpdateRequestSerializer(serializers.Serializer):
class RSAEncryptedField(serializers.Field):
def to_representation(self, value):
if isinstance(value, memoryview):
value = value.tobytes()
public_key = load_pem_public_key(
self.context['device'].rsa_pubkey.encode(), Backend()
)
@@ -166,7 +168,6 @@ class InitializeView(APIView):
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()

View File

@@ -381,16 +381,29 @@ with scopes_disabled():
| Q(location__icontains=i18ncomp(value))
)
class OrganizerSubEventFilter(SubEventFilter):
def search_qs(self, queryset, name, value):
return queryset.filter(
Q(name__icontains=i18ncomp(value))
| Q(event__slug__icontains=value)
| Q(location__icontains=i18ncomp(value))
)
class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer_class = SubEventSerializer
queryset = SubEvent.objects.none()
write_permission = 'can_change_event_settings'
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
filterset_class = SubEventFilter
ordering = ('date_from',)
ordering_fields = ('id', 'date_from', 'last_modified')
@property
def filterset_class(self):
if getattr(self.request, 'event', None):
return SubEventFilter
return OrganizerSubEventFilter
def get_queryset(self):
if getattr(self.request, 'event', None):
qs = self.request.event.subevents
@@ -415,6 +428,7 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
'subeventitem_set',
'subeventitemvariation_set',
'meta_values',
'meta_values__property',
Prefetch(
'seat_category_mappings',
to_attr='_seat_category_mappings',

View File

@@ -29,14 +29,20 @@ from django.utils.functional import cached_property
from django.utils.timezone import now
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from rest_framework.reverse import reverse
from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.exporters import (
ExporterSerializer, JobRunSerializer,
ExporterSerializer, JobRunSerializer, ScheduledEventExportSerializer,
ScheduledOrganizerExportSerializer,
)
from pretix.base.exporter import OrganizerLevelExportMixin
from pretix.base.models import CachedFile, Device, Event, TeamAPIToken
from pretix.base.models import (
CachedFile, Device, Event, ScheduledEventExport, ScheduledOrganizerExport,
TeamAPIToken,
)
from pretix.base.services.export import export, multiexport
from pretix.base.signals import (
register_data_exporters, register_multievent_data_exporters,
@@ -199,3 +205,152 @@ class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
'provider': instance.identifier,
'form_data': data
})
class ScheduledExportersViewSet(viewsets.ModelViewSet):
filter_backends = (TotalOrderingFilter,)
ordering = ('id',)
ordering_fields = ('id', 'export_identifier', 'schedule_next_run')
class ScheduledEventExportViewSet(ScheduledExportersViewSet):
serializer_class = ScheduledEventExportSerializer
queryset = ScheduledEventExport.objects.none()
permission = 'can_view_orders'
def get_queryset(self):
perm_holder = self.request.auth if isinstance(self.request.auth, (TeamAPIToken, Device)) else self.request.user
if not perm_holder.has_event_permission(self.request.organizer, self.request.event, 'can_change_event_settings',
request=self.request):
if self.request.user.is_authenticated:
qs = self.request.event.scheduled_exports.filter(owner=self.request.user)
else:
raise PermissionDenied('Scheduled exports require either permission to change event settings or '
'user-specific API access.')
else:
qs = self.request.event.scheduled_exports
return qs.select_related("owner")
def perform_create(self, serializer):
if not self.request.user.is_authenticated:
raise PermissionDenied('Creation of exports requires user-specific API access.')
serializer.save(event=self.request.event, owner=self.request.user)
serializer.instance.compute_next_run()
serializer.instance.save(update_fields=["schedule_next_run"])
self.request.event.log_action(
'pretix.event.export.schedule.added',
user=self.request.user,
auth=self.request.auth,
data=self.request.data
)
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['exporters'] = self.exporters
return ctx
@cached_property
def exporters(self):
responses = register_data_exporters.send(self.request.event)
exporters = [response(self.request.event, self.request.organizer) for r, response in responses if response]
return {e.identifier: e for e in exporters}
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.compute_next_run()
serializer.instance.error_counter = 0
serializer.instance.error_last_message = None
serializer.instance.save(update_fields=["schedule_next_run", "error_counter", "error_last_message"])
self.request.event.log_action(
'pretix.event.export.schedule.changed',
user=self.request.user,
auth=self.request.auth,
data=self.request.data
)
def perform_destroy(self, instance):
self.request.event.log_action(
'pretix.event.export.schedule.deleted',
user=self.request.user,
auth=self.request.auth,
)
super().perform_destroy(instance)
class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
serializer_class = ScheduledOrganizerExportSerializer
queryset = ScheduledOrganizerExport.objects.none()
permission = None
def get_queryset(self):
perm_holder = self.request.auth if isinstance(self.request.auth, (TeamAPIToken, Device)) else self.request.user
if not perm_holder.has_organizer_permission(self.request.organizer, 'can_change_organizer_settings',
request=self.request):
if self.request.user.is_authenticated:
qs = self.request.organizer.scheduled_exports.filter(owner=self.request.user)
else:
raise PermissionDenied('Scheduled exports require either permission to change organizer settings or '
'user-specific API access.')
else:
qs = self.request.organizer.scheduled_exports
return qs.select_related("owner")
def perform_create(self, serializer):
if not self.request.user.is_authenticated:
raise PermissionDenied('Creation of exports requires user-specific API access.')
serializer.save(organizer=self.request.organizer, owner=self.request.user)
serializer.instance.compute_next_run()
serializer.instance.save(update_fields=["schedule_next_run"])
self.request.organizer.log_action(
'pretix.organizer.export.schedule.added',
user=self.request.user,
auth=self.request.auth,
data=self.request.data
)
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['organizer'] = self.request.organizer
ctx['exporters'] = self.exporters
return ctx
@cached_property
def events(self):
if isinstance(self.request.auth, (TeamAPIToken, Device)):
return self.request.auth.get_events_with_permission('can_view_orders')
elif self.request.user.is_authenticated:
return self.request.user.get_events_with_permission('can_view_orders', self.request).filter(
organizer=self.request.organizer
)
@cached_property
def exporters(self):
responses = register_multievent_data_exporters.send(self.request.organizer)
exporters = [
response(Event.objects.none() if issubclass(response, OrganizerLevelExportMixin) else self.events,
self.request.organizer)
for r, response in responses if response
]
return {e.identifier: e for e in exporters}
def perform_update(self, serializer):
serializer.save(organizer=self.request.organizer)
serializer.instance.compute_next_run()
serializer.instance.error_counter = 0
serializer.instance.error_last_message = None
serializer.instance.save(update_fields=["schedule_next_run", "error_counter", "error_last_message"])
self.request.organizer.log_action(
'pretix.organizer.export.schedule.changed',
user=self.request.user,
auth=self.request.auth,
data=self.request.data
)
def perform_destroy(self, instance):
self.request.organizer.log_action(
'pretix.organizer.export.schedule.deleted',
user=self.request.user,
auth=self.request.auth,
)
super().perform_destroy(instance)

View File

@@ -26,6 +26,7 @@ from decimal import Decimal
from zoneinfo import ZoneInfo
import django_filters
from django.conf import settings
from django.db import transaction
from django.db.models import (
Exists, F, OuterRef, Prefetch, Q, Subquery, prefetch_related_objects,
@@ -44,6 +45,7 @@ from rest_framework.exceptions import (
APIException, NotFound, PermissionDenied, ValidationError,
)
from rest_framework.mixins import CreateModelMixin
from rest_framework.permissions import SAFE_METHODS
from rest_framework.response import Response
from pretix.api.models import OAuthAccessToken
@@ -108,19 +110,24 @@ with scopes_disabled():
item = django_filters.CharFilter(field_name='all_positions', lookup_expr='item_id', distinct=True)
variation = django_filters.CharFilter(field_name='all_positions', lookup_expr='variation_id', distinct=True)
subevent = django_filters.CharFilter(field_name='all_positions', lookup_expr='subevent_id', distinct=True)
customer = django_filters.CharFilter(field_name='customer__identifier')
class Meta:
model = Order
fields = ['code', 'status', 'email', 'locale', 'testmode', 'require_approval']
fields = ['code', 'status', 'email', 'locale', 'testmode', 'require_approval', 'customer']
@scopes_disabled()
def subevent_after_qs(self, qs, name, value):
if getattr(self.request, 'event', None):
subevents = self.request.event.subevents
else:
subevents = SubEvent.objects.filter(event__organizer=self.request.organizer)
qs = qs.filter(
pk__in=Subquery(
OrderPosition.all.filter(
subevent_id__in=SubEvent.objects.filter(
subevent_id__in=subevents.filter(
Q(date_to__gt=value) | Q(date_from__gt=value, date_to__isnull=True),
event=self.request.event
).values_list('id'),
).values_list('order_id')
)
@@ -128,12 +135,16 @@ with scopes_disabled():
return qs
def subevent_before_qs(self, qs, name, value):
if getattr(self.request, 'event', None):
subevents = self.request.event.subevents
else:
subevents = SubEvent.objects.filter(event__organizer=self.request.organizer)
qs = qs.filter(
pk__in=Subquery(
OrderPosition.all.filter(
subevent_id__in=SubEvent.objects.filter(
subevent_id__in=subevents.filter(
Q(date_from__lt=value),
event=self.request.event
).values_list('id'),
).values_list('order_id')
)
@@ -185,7 +196,7 @@ with scopes_disabled():
)
class OrderViewSet(viewsets.ModelViewSet):
class OrderViewSetMixin:
serializer_class = OrderSerializer
queryset = Order.objects.none()
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
@@ -193,19 +204,12 @@ class OrderViewSet(viewsets.ModelViewSet):
ordering_fields = ('datetime', 'code', 'status', 'last_modified')
filterset_class = OrderFilter
lookup_field = 'code'
permission = 'can_view_orders'
write_permission = 'can_change_orders'
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false') == 'true'
ctx['exclude'] = self.request.query_params.getlist('exclude')
ctx['include'] = self.request.query_params.getlist('include')
return ctx
def get_base_queryset(self):
raise NotImplementedError()
def get_queryset(self):
qs = self.request.event.orders
qs = self.get_base_queryset()
if 'fees' not in self.request.GET.getlist('exclude'):
if self.request.query_params.get('include_canceled_fees', 'false') == 'true':
fqs = OrderFee.all
@@ -227,11 +231,12 @@ class OrderViewSet(viewsets.ModelViewSet):
opq = OrderPosition.all
else:
opq = OrderPosition.objects
if request.query_params.get('pdf_data', 'false') == 'true':
if request.query_params.get('pdf_data', 'false') == 'true' and getattr(request, 'event', None):
prefetch_related_objects([request.organizer], 'meta_properties')
prefetch_related_objects(
[request.event],
Prefetch('meta_values', queryset=EventMetaValue.objects.select_related('property'), to_attr='meta_values_cached'),
Prefetch('meta_values', queryset=EventMetaValue.objects.select_related('property'),
to_attr='meta_values_cached'),
'questions',
'item_meta_properties',
)
@@ -266,13 +271,12 @@ class OrderViewSet(viewsets.ModelViewSet):
)
)
def _get_output_provider(self, identifier):
responses = register_ticket_outputs.send(self.request.event)
for receiver, response in responses:
prov = response(self.request.event)
if prov.identifier == identifier:
return prov
raise NotFound('Unknown output provider.')
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['exclude'] = self.request.query_params.getlist('exclude')
ctx['include'] = self.request.query_params.getlist('include')
ctx['pdf_data'] = False
return ctx
@scopes_disabled() # we are sure enough that get_queryset() is correct, so we save some perforamnce
def list(self, request, **kwargs):
@@ -289,6 +293,45 @@ class OrderViewSet(viewsets.ModelViewSet):
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data, headers={'X-Page-Generated': date})
class OrganizerOrderViewSet(OrderViewSetMixin, viewsets.ReadOnlyModelViewSet):
def get_base_queryset(self):
perm = "can_view_orders" if self.request.method in SAFE_METHODS else "can_change_orders"
if isinstance(self.request.auth, (TeamAPIToken, Device)):
return Order.objects.filter(
event__organizer=self.request.organizer,
event__in=self.request.auth.get_events_with_permission(perm, request=self.request)
)
elif self.request.user.is_authenticated:
return Order.objects.filter(
event__organizer=self.request.organizer,
event__in=self.request.user.get_events_with_permission(perm, request=self.request)
)
else:
raise PermissionDenied()
class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
permission = 'can_view_orders'
write_permission = 'can_change_orders'
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false') == 'true'
return ctx
def get_base_queryset(self):
return self.request.event.orders
def _get_output_provider(self, identifier):
responses = register_ticket_outputs.send(self.request.event)
for receiver, response in responses:
prov = response(self.request.event)
if prov.identifier == identifier:
return prov
raise NotFound('Unknown output provider.')
@action(detail=True, url_name='download', url_path='download/(?P<output>[^/]+)')
def download(self, request, output, **kwargs):
provider = self._get_output_provider(output)
@@ -784,6 +827,16 @@ class OrderViewSet(viewsets.ModelViewSet):
}
)
if 'checkin_text' in self.request.data and serializer.instance.checkin_text != self.request.data.get('checkin_text'):
serializer.instance.log_action(
'pretix.event.order.checkin_text',
user=self.request.user,
auth=self.request.auth,
data={
'new_value': self.request.data.get('checkin_text')
}
)
if 'valid_if_pending' in self.request.data and serializer.instance.valid_if_pending != self.request.data.get('valid_if_pending'):
serializer.instance.log_action(
'pretix.event.order.valid_if_pending',
@@ -1191,7 +1244,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'
@@ -1782,11 +1835,24 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
write_permission = 'can_change_orders'
def get_queryset(self):
return self.request.event.invoices.prefetch_related('lines').select_related('order', 'refers').annotate(
perm = "can_view_orders" if self.request.method in SAFE_METHODS else "can_change_orders"
if getattr(self.request, 'event', None):
qs = self.request.event.invoices
elif isinstance(self.request.auth, (TeamAPIToken, Device)):
qs = Invoice.objects.filter(
event__organizer=self.request.organizer,
event__in=self.request.auth.get_events_with_permission(perm, request=self.request)
)
elif self.request.user.is_authenticated:
qs = Invoice.objects.filter(
event__organizer=self.request.organizer,
event__in=self.request.user.get_events_with_permission(perm, request=self.request)
)
return qs.prefetch_related('lines').select_related('order', 'refers').annotate(
nr=Concat('prefix', 'invoice_no')
)
@action(detail=True, )
@action(detail=True)
def download(self, request, **kwargs):
invoice = self.get_object()
@@ -1805,7 +1871,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
return resp
@action(detail=True, methods=['POST'])
def regenerate(self, request, **kwarts):
def regenerate(self, request, **kwargs):
inv = self.get_object()
if inv.canceled:
raise ValidationError('The invoice has already been canceled.')
@@ -1815,7 +1881,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
raise PermissionDenied('The invoice file is no longer stored on the server.')
elif inv.sent_to_organizer:
raise PermissionDenied('The invoice file has already been exported.')
elif now().astimezone(self.request.event.timezone).date() - inv.date > datetime.timedelta(days=1):
elif now().astimezone(inv.event.timezone).date() - inv.date > datetime.timedelta(days=1):
raise PermissionDenied('The invoice file is too old to be regenerated.')
else:
inv = regenerate_invoice(inv)
@@ -1830,7 +1896,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
return Response(status=204)
@action(detail=True, methods=['POST'])
def reissue(self, request, **kwarts):
def reissue(self, request, **kwargs):
inv = self.get_object()
if inv.canceled:
raise ValidationError('The invoice has already been canceled.')

View File

@@ -24,6 +24,8 @@ from decimal import Decimal
import django_filters
from django.contrib.auth.hashers import make_password
from django.db import transaction
from django.db.models import OuterRef, Subquery, Sum
from django.db.models.functions import Coalesce
from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
@@ -155,8 +157,13 @@ class GiftCardViewSet(viewsets.ModelViewSet):
qs = self.request.organizer.accepted_gift_cards
else:
qs = self.request.organizer.issued_gift_cards.all()
s = GiftCardTransaction.objects.filter(
card=OuterRef('pk')
).order_by().values('card').annotate(s=Sum('value')).values('s')
return qs.prefetch_related(
'issuer'
).annotate(
cached_value=Coalesce(Subquery(s), Decimal('0.00'))
)
def get_serializer_context(self):

View File

@@ -19,8 +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 contextlib
from django.db import transaction
from django.db.models import F, Q
from django.utils.timezone import now
@@ -69,30 +67,9 @@ class VoucherViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return self.request.event.vouchers.select_related('seat').all()
def _predict_quota_check(self, data, instance):
# This method predicts if Voucher.clean_quota_needs_checking
# *migh* later require a quota check. It is only approximate
# and returns True a little too often. The point is to avoid
# locks when we know we won't need them.
if 'allow_ignore_quota' in data and data.get('allow_ignore_quota'):
return False
if instance and 'allow_ignore_quota' not in data and instance.allow_ignore_quota:
return False
if 'block_quota' in data and not data.get('block_quota'):
return False
if instance and 'block_quota' not in data and not instance.block_quota:
return False
return True
@transaction.atomic()
def create(self, request, *args, **kwargs):
if self._predict_quota_check(request.data, None):
lockfn = request.event.lock
else:
lockfn = contextlib.suppress # noop context manager
with lockfn():
return super().create(request, *args, **kwargs)
return super().create(request, *args, **kwargs)
def perform_create(self, serializer):
serializer.save(event=self.request.event)
@@ -108,13 +85,9 @@ class VoucherViewSet(viewsets.ModelViewSet):
ctx['event'] = self.request.event
return ctx
@transaction.atomic()
def update(self, request, *args, **kwargs):
if self._predict_quota_check(request.data, self.get_object()):
lockfn = request.event.lock
else:
lockfn = contextlib.suppress # noop context manager
with lockfn():
return super().update(request, *args, **kwargs)
return super().update(request, *args, **kwargs)
def perform_update(self, serializer):
serializer.save(event=self.request.event)
@@ -140,22 +113,18 @@ class VoucherViewSet(viewsets.ModelViewSet):
super().perform_destroy(instance)
@action(detail=False, methods=['POST'])
@transaction.atomic()
def batch_create(self, request, *args, **kwargs):
if any(self._predict_quota_check(d, None) for d in request.data):
lockfn = request.event.lock
else:
lockfn = contextlib.suppress # noop context manager
with lockfn():
serializer = self.get_serializer(data=request.data, many=True)
serializer.is_valid(raise_exception=True)
with transaction.atomic():
serializer.save(event=self.request.event)
for i, v in enumerate(serializer.instance):
v.log_action(
'pretix.voucher.added',
user=self.request.user,
auth=self.request.auth,
data=self.request.data[i]
)
serializer = self.get_serializer(data=request.data, many=True)
serializer.is_valid(raise_exception=True)
with transaction.atomic():
serializer.save(event=self.request.event)
for i, v in enumerate(serializer.instance):
v.log_action(
'pretix.voucher.added',
user=self.request.user,
auth=self.request.auth,
data=self.request.data[i]
)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

View File

@@ -202,6 +202,21 @@ class ParametrizedWaitingListEntryWebhookEvent(ParametrizedWebhookEvent):
}
class ParametrizedCustomerWebhookEvent(ParametrizedWebhookEvent):
def build_payload(self, logentry: LogEntry):
customer = logentry.content_object
if not customer:
return None
return {
'notification_id': logentry.pk,
'organizer': customer.organizer.slug,
'customer': customer.identifier,
'action': logentry.action_type,
}
@receiver(register_webhook_events, dispatch_uid="base_register_default_webhook_events")
def register_default_webhook_events(sender, **kwargs):
return (
@@ -350,6 +365,18 @@ def register_default_webhook_events(sender, **kwargs):
'pretix.event.orders.waitinglist.voucher_assigned',
_('Waiting list entry received voucher'),
),
ParametrizedCustomerWebhookEvent(
'pretix.customer.created',
_('Customer account created'),
),
ParametrizedCustomerWebhookEvent(
'pretix.customer.changed',
_('Customer account changed'),
),
ParametrizedCustomerWebhookEvent(
'pretix.customer.anonymized',
_('Customer account anonymized'),
),
)
@@ -357,7 +384,7 @@ def register_default_webhook_events(sender, **kwargs):
def notify_webhooks(logentry_ids: list):
if not isinstance(logentry_ids, list):
logentry_ids = [logentry_ids]
qs = LogEntry.all.select_related('event', 'event__organizer').filter(id__in=logentry_ids)
qs = LogEntry.all.select_related('event', 'event__organizer', 'organizer_link').filter(id__in=logentry_ids)
_org, _at, webhooks = None, None, None
for logentry in qs:
if not logentry.organizer:

View File

@@ -62,27 +62,27 @@ class NamespacedCache:
prefix = int(time.time())
self.cache.set(self.prefixkey, prefix)
def set(self, key: str, value: str, timeout: int=300):
def set(self, key: str, value: any, timeout: int=300):
return self.cache.set(self._prefix_key(key), value, timeout)
def get(self, key: str) -> str:
def get(self, key: str) -> any:
return self.cache.get(self._prefix_key(key, known_prefix=self._last_prefix))
def get_or_set(self, key: str, default: Callable, timeout=300) -> str:
def get_or_set(self, key: str, default: Callable, timeout=300) -> any:
return self.cache.get_or_set(
self._prefix_key(key, known_prefix=self._last_prefix),
default=default,
timeout=timeout
)
def get_many(self, keys: List[str]) -> Dict[str, str]:
def get_many(self, keys: List[str]) -> Dict[str, any]:
values = self.cache.get_many([self._prefix_key(key) for key in keys])
newvalues = {}
for k, v in values.items():
newvalues[self._strip_prefix(k)] = v
return newvalues
def set_many(self, values: Dict[str, str], timeout=300):
def set_many(self, values: Dict[str, any], timeout=300):
newvalues = {}
for k, v in values.items():
newvalues[self._prefix_key(k)] = v

View File

@@ -103,15 +103,17 @@ def get_all_sales_channels():
if _ALL_CHANNELS:
return _ALL_CHANNELS
types = OrderedDict()
channels = []
for recv, ret in register_sales_channels.send(None):
if isinstance(ret, (list, tuple)):
for r in ret:
types[r.identifier] = r
channels += ret
else:
types[ret.identifier] = ret
_ALL_CHANNELS = types
return types
channels.append(ret)
channels.sort(key=lambda c: c.identifier)
_ALL_CHANNELS = OrderedDict([(c.identifier, c) for c in channels])
if 'web' in _ALL_CHANNELS:
_ALL_CHANNELS.move_to_end('web', last=False)
return _ALL_CHANNELS
class WebshopSalesChannel(SalesChannel):

View File

@@ -134,8 +134,11 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
def template_name(self):
raise NotImplementedError()
def compile_markdown(self, plaintext):
return markdown_compile_email(plaintext)
def render(self, plain_body: str, plain_signature: str, subject: str, order, position) -> str:
body_md = markdown_compile_email(plain_body)
body_md = self.compile_markdown(plain_body)
htmlctx = {
'site': settings.PRETIX_INSTANCE_NAME,
'site_url': settings.SITE_URL,
@@ -146,6 +149,7 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
}
if self.organizer:
htmlctx['organizer'] = self.organizer
htmlctx['color'] = self.organizer.settings.primary_color
if self.event:
htmlctx['event'] = self.event
@@ -153,7 +157,7 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
if plain_signature:
signature_md = plain_signature.replace('\n', '<br>\n')
signature_md = markdown_compile_email(signature_md)
signature_md = self.compile_markdown(signature_md)
htmlctx['signature'] = signature_md
if order:
@@ -666,6 +670,11 @@ def base_placeholders(sender, **kwargs):
lambda waiting_list_entry: concatenation_for_salutation(waiting_list_entry.name_parts),
_("Mr Doe"),
))
ph.append(SimpleFunctionalMailTextPlaceholder(
"name", ["waiting_list_entry"],
lambda waiting_list_entry: waiting_list_entry.name or "",
_("Mr Doe"),
))
ph.append(SimpleFunctionalMailTextPlaceholder(
"name_for_salutation", ["position_or_address"],
lambda position_or_address: concatenation_for_salutation(get_best_name(position_or_address, parts=True)),

View File

@@ -88,6 +88,7 @@ class ItemDataExporter(ListExporter):
_("Minimum amount per order"),
_("Maximum amount per order"),
_("Requires special attention"),
_("Check-in text"),
_("Original price"),
_("This product is a gift card"),
_("Require a valid membership"),
@@ -162,6 +163,7 @@ class ItemDataExporter(ListExporter):
i.min_per_order if i.min_per_order is not None else "",
i.max_per_order if i.max_per_order is not None else "",
_("Yes") if i.checkin_attention else "",
i.checkin_text or "",
v.original_price or i.original_price or "",
_("Yes") if i.issue_giftcard else "",
_("Yes") if i.require_membership or v.require_membership else "",
@@ -206,6 +208,7 @@ class ItemDataExporter(ListExporter):
i.min_per_order if i.min_per_order is not None else "",
i.max_per_order if i.max_per_order is not None else "",
_("Yes") if i.checkin_attention else "",
i.checkin_text or "",
i.original_price or "",
_("Yes") if i.issue_giftcard else "",
_("Yes") if i.require_membership else "",

View File

@@ -96,6 +96,7 @@ class JSONExporter(BaseExporter):
'min_per_order': item.min_per_order,
'max_per_order': item.max_per_order,
'checkin_attention': item.checkin_attention,
'checkin_text': item.checkin_text,
'original_price': item.original_price,
'issue_giftcard': item.issue_giftcard,
'meta_data': item.meta_data,
@@ -110,6 +111,7 @@ class JSONExporter(BaseExporter):
'description': str(variation.description),
'position': variation.position,
'checkin_attention': variation.checkin_attention,
'checkin_text': variation.checkin_text,
'require_approval': variation.require_approval,
'require_membership': variation.require_membership,
'sales_channels': variation.sales_channels,
@@ -164,6 +166,7 @@ class JSONExporter(BaseExporter):
'custom_followup_at': order.custom_followup_at,
'require_approval': order.require_approval,
'checkin_attention': order.checkin_attention,
'checkin_text': order.checkin_text,
'sales_channel': order.sales_channel,
'expires': order.expires,
'datetime': order.datetime,

View File

@@ -275,6 +275,7 @@ class OrderListExporter(MultiSheetListExporter):
headers.append(_('Invoice numbers'))
headers.append(_('Sales channel'))
headers.append(_('Requires special attention'))
headers.append(_('Check-in text'))
headers.append(_('Comment'))
headers.append(_('Follow-up date'))
headers.append(_('Positions'))
@@ -332,7 +333,7 @@ class OrderListExporter(MultiSheetListExporter):
self.event_object_cache[order.event_id].slug,
order.code,
order.total,
order.get_status_display(),
order.get_extended_status_display(),
order.email,
str(order.phone) if order.phone else '',
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
@@ -384,6 +385,7 @@ class OrderListExporter(MultiSheetListExporter):
row.append(order.invoice_numbers)
row.append(order.sales_channel)
row.append(_('Yes') if order.checkin_attention else _('No'))
row.append(order.checkin_text or "")
row.append(order.comment or "")
row.append(order.custom_followup_at.strftime("%Y-%m-%d") if order.custom_followup_at else "")
row.append(order.pcnt)
@@ -463,7 +465,7 @@ class OrderListExporter(MultiSheetListExporter):
row = [
self.event_object_cache[order.event_id].slug,
order.code,
_("canceled") if op.canceled else order.get_status_display(),
_("canceled") if op.canceled else order.get_extended_status_display(),
order.email,
str(order.phone) if order.phone else '',
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
@@ -549,7 +551,9 @@ class OrderListExporter(MultiSheetListExporter):
headers.append(_('End date'))
headers += [
_('Product'),
_('Product ID'),
_('Variation'),
_('Variation ID'),
_('Price'),
_('Tax rate'),
_('Tax rule'),
@@ -636,7 +640,7 @@ class OrderListExporter(MultiSheetListExporter):
self.event_object_cache[order.event_id].slug,
order.code,
op.positionid,
_("canceled") if op.canceled else order.get_status_display(),
_("canceled") if op.canceled else order.get_extended_status_display(),
order.email,
str(order.phone) if order.phone else '',
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
@@ -656,7 +660,9 @@ class OrderListExporter(MultiSheetListExporter):
row.append('')
row += [
str(op.item),
str(op.item_id),
str(op.variation) if op.variation else '',
str(op.variation_id) if op.variation_id else '',
op.price,
op.tax_rate,
str(op.tax_rule) if op.tax_rule else '',
@@ -1003,20 +1009,20 @@ class PaymentListExporter(ListExporter):
if form_data.get('end_date_range'):
dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['end_date_range'], self.timezone)
if dt_start:
payments = payments.filter(created__gte=dt_start)
refunds = refunds .filter(created__gte=dt_start)
payments = payments.filter(payment_date__gte=dt_start)
refunds = refunds.filter(execution_date__gte=dt_start)
if dt_end:
payments = payments.filter(created__lt=dt_end)
refunds = refunds .filter(created__lt=dt_end)
payments = payments.filter(payment_date__lt=dt_end)
refunds = refunds.filter(execution_date__lt=dt_end)
if form_data.get('start_end_date_range'):
dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['start_date_range'], self.timezone)
if dt_start:
payments = payments.filter(payment_date__gte=dt_start)
refunds = refunds .filter(execution_date__gte=dt_start)
payments = payments.filter(created__gte=dt_start)
refunds = refunds.filter(created__gte=dt_start)
if dt_end:
payments = payments.filter(payment_date__lt=dt_end)
refunds = refunds.filter(execution_date__lt=dt_end)
payments = payments.filter(created__lt=dt_end)
refunds = refunds.filter(created__lt=dt_end)
objs = sorted(list(payments) + list(refunds), key=lambda o: o.created)

View File

@@ -500,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:
@@ -566,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)
@@ -826,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

@@ -60,6 +60,18 @@ def replace_arabic_numbers(inp):
return inp.translate(table)
def format_placeholders_help_text(placeholders, event=None):
placeholders = [(k, v.render_sample(event) if event else v) for k, v in placeholders.items()]
placeholders.sort(key=lambda x: x[0])
phs = [
'<button type="button" class="content-placeholder" title="%s">{%s}</button>' % (_("Sample: %s") % v if v else "", k)
for k, v in placeholders
]
return _('Available placeholders: {list}').format(
list=' '.join(phs)
)
class DatePickerWidget(forms.DateInput):
def __init__(self, attrs=None, date_format=None):
attrs = attrs or {}

View File

@@ -104,7 +104,7 @@ class Command(BaseCommand):
with language(locale), override(timezone):
for receiver, response in signal_result:
if not response:
return None
continue
ex = response(e, o, report_status)
if ex.identifier == options['export_provider']:
params = json.loads(options.get('parameters') or '{}')

View File

@@ -264,7 +264,7 @@ def metric_values():
# Metrics from redis
if settings.HAS_REDIS:
for key, value in redis.hscan_iter(REDIS_KEY):
for key, value in redis.hscan_iter(REDIS_KEY, count=1000):
dkey = key.decode("utf-8")
splitted = dkey.split("{", 2)
value = float(value.decode("utf-8"))

View File

@@ -271,6 +271,8 @@ class SecurityMiddleware(MiddlewareMixin):
(url.url_name == "event.checkout" and url.kwargs['step'] == "payment")
):
h['script-src'].append('https://pay.google.com')
h['frame-src'].append('https://pay.google.com')
h['connect-src'].append('https://google.com/pay')
if settings.LOG_CSP:
h['report-uri'] = ["/csp_report/"]
if 'Content-Security-Policy' in resp:

View File

@@ -10,6 +10,7 @@ def initial_user(apps, schema_editor):
user = User(email='admin@localhost')
user.is_staff = True
user.is_superuser = True
user.needs_password_change = True
user.password = make_password('admin')
user.save()

View File

@@ -0,0 +1,34 @@
# Generated by Django 4.2.4 on 2023-08-28 12:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0244_mediumkeyset"),
]
operations = [
migrations.AddField(
model_name="discount",
name="benefit_apply_to_addons",
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name="discount",
name="benefit_ignore_voucher_discounted",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="discount",
name="benefit_limit_products",
field=models.ManyToManyField(
related_name="benefit_discounts", to="pretixbase.item"
),
),
migrations.AddField(
model_name="discount",
name="benefit_same_products",
field=models.BooleanField(default=True),
),
]

View File

@@ -0,0 +1,509 @@
# Generated by Django 4.2.4 on 2023-09-26 12:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0245_discount_benefit_products"),
]
operations = [
migrations.RenameIndex(
model_name="logentry",
new_name="pretixbase__datetim_b1fe5a_idx",
old_fields=("datetime", "id"),
),
migrations.RenameIndex(
model_name="order",
new_name="pretixbase__datetim_66aff0_idx",
old_fields=("datetime", "id"),
),
migrations.RenameIndex(
model_name="order",
new_name="pretixbase__last_mo_4ebf8b_idx",
old_fields=("last_modified", "id"),
),
migrations.RenameIndex(
model_name="reusablemedium",
new_name="pretixbase__updated_093277_idx",
old_fields=("updated", "id"),
),
migrations.RenameIndex(
model_name="transaction",
new_name="pretixbase__datetim_b20405_idx",
old_fields=("datetime", "id"),
),
migrations.AlterField(
model_name="attendeeprofile",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="blockedticketsecret",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="cachedcombinedticket",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="cachedticket",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="cancellationrequest",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="cartposition",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="checkin",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="checkinlist",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="customer",
name="locale",
field=models.CharField(default="de", max_length=50),
),
migrations.AlterField(
model_name="device",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="discount",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="event",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="event_settingsstore",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="eventfooterlink",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="eventmetaproperty",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="eventmetavalue",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="exchangerate",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="gate",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="giftcard",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="giftcardacceptance",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="giftcardtransaction",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="globalsettingsobject_settingsstore",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="invoice",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="invoiceaddress",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="invoiceline",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="item",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="itemaddon",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="itembundle",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="itemcategory",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="itemmetaproperty",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="itemmetavalue",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="itemvariation",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="itemvariationmetavalue",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="logentry",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="mediumkeyset",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="notificationsetting",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="order",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="orderfee",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="orderpayment",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="orderposition",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="orderrefund",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="organizer",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="organizer_settingsstore",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="organizerfooterlink",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="question",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="questionanswer",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="questionoption",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="quota",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="revokedticketsecret",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="seat",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="seatcategorymapping",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="seatingplan",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="staffsession",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="staffsessionauditlog",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="subevent",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="subeventitem",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="subeventitemvariation",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="subeventmetavalue",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="taxrule",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="team",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="teamapitoken",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="teaminvite",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="u2fdevice",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="user",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="user",
name="locale",
field=models.CharField(default="de", max_length=50),
),
migrations.AlterField(
model_name="voucher",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="waitinglistentry",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="webauthndevice",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 4.2.4 on 2023-09-06 11:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0246_bigint"),
]
operations = [
migrations.AddField(
model_name="checkinlist",
name="consider_tickets_used",
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name="checkinlist",
name="ignore_in_statistics",
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 4.2.4 on 2023-10-25 12:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0247_checkinlist"),
]
operations = [
migrations.AddField(
model_name="item",
name="free_price_suggestion",
field=models.DecimalField(decimal_places=2, max_digits=13, null=True),
),
migrations.AddField(
model_name="itemvariation",
name="free_price_suggestion",
field=models.DecimalField(decimal_places=2, max_digits=13, null=True),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 4.2.4 on 2023-10-30 11:50
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0248_item_free_price_suggestion"),
]
operations = [
migrations.AddField(
model_name="item",
name="hidden_if_item_available",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="pretixbase.item",
),
),
]

View File

@@ -0,0 +1,52 @@
# Generated by Django 4.2.4 on 2023-10-31 10:08
import i18nfield.fields
from django.db import migrations, models
import pretix.helpers.json
def convert_allowed_values(apps, schema_editor):
EventMetaProperty = apps.get_model('pretixbase', 'EventMetaProperty')
for emp in EventMetaProperty.objects.filter(allowed_values__isnull=False):
emp.choices = [
{"key": _v.strip(), "label": {"en": _v.strip()}}
for _v in emp.allowed_values.splitlines()
]
emp.save(update_fields=["choices"])
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0249_hidden_if_item_available"),
]
operations = [
migrations.AddField(
model_name="eventmetaproperty",
name="filter_public",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="eventmetaproperty",
name="public_label",
field=i18nfield.fields.I18nCharField(null=True),
),
migrations.AddField(
model_name="eventmetaproperty",
name="position",
field=models.IntegerField(default=0),
),
migrations.AddField(
model_name="eventmetaproperty",
name="choices",
field=models.JSONField(null=True, encoder=pretix.helpers.json.CustomJSONEncoder),
),
migrations.RunPython(
convert_allowed_values,
migrations.RunPython.noop
),
migrations.RemoveField(
model_name="eventmetaproperty",
name="allowed_values",
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 4.2.4 on 2023-11-13 16:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0250_eventmetaproperty_filter_public"),
]
operations = [
migrations.AddField(
model_name="order",
name="invoice_dirty",
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,56 @@
# Generated by Django 4.2.4 on 2023-11-20 12:38
import django.db.models.deletion
from django.db import migrations, models
from django.db.models import F, OuterRef, Subquery
def backfill_organizer(apps, schema_editor):
LogEntry = apps.get_model("pretixbase", "LogEntry")
Event = apps.get_model("pretixbase", "Event")
ContentType = apps.get_model("contenttypes", "ContentType")
LogEntry.objects.filter(
organizer_link__isnull=True, event__isnull=False
).update(organizer_link_id=Subquery(
Event.objects.filter(pk=OuterRef('event_id')).values('organizer_id'),
)
)
for ct in ContentType.objects.all():
try:
model = apps.get_model(ct.app_label, ct.model)
except LookupError:
continue
if "organizer" in model._meta.fields:
LogEntry.objects.filter(
organizer_link__isnull=True, event__isnull=True, content_type=ct,
).update(
organizer_link_id=Subquery(model.objects.filter(pk=OuterRef('object_id')).values('organizer_id'))
)
elif "event" in model._meta.fields:
LogEntry.objects.filter(
organizer_link__isnull=True, event__isnull=True, content_type=ct,
).update(
organizer_link_id=Subquery(model.objects.filter(pk=OuterRef('object_id')).values('event__organizer_id'))
)
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0251_order_invoice_dirty"),
]
operations = [
migrations.AddField(
model_name="logentry",
name="organizer_link",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="pretixbase.organizer",
),
),
migrations.RunPython(
backfill_organizer,
)
]

View File

@@ -0,0 +1,32 @@
# Generated by Django 4.2.4 on 2023-09-06 09:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0252_logentry_organizer"),
]
operations = [
migrations.AddField(
model_name="item",
name="checkin_text",
field=models.TextField(null=True),
),
migrations.AddField(
model_name="itemvariation",
name="checkin_text",
field=models.TextField(null=True),
),
migrations.AddField(
model_name="order",
name="checkin_text",
field=models.TextField(null=True),
),
migrations.AddField(
model_name="question",
name="show_during_checkin",
field=models.BooleanField(default=False),
),
]

View File

@@ -97,7 +97,7 @@ def _transactions_mark_order_dirty(order_id, using=None):
if getattr(dirty_transactions, 'order_ids', None) is None:
dirty_transactions.order_ids = set()
if _check_for_dirty_orders not in [func for savepoint_id, func in conn.run_on_commit]:
if _check_for_dirty_orders not in [func for (savepoint_id, func, *__) in conn.run_on_commit]:
transaction.on_commit(_check_for_dirty_orders, using)
dirty_transactions.order_ids.clear() # This is necessary to clean up after old threads with rollbacked transactions

View File

@@ -84,13 +84,21 @@ class LoggingMixin:
from .devices import Device
from .event import Event
from .log import LogEntry
from .organizer import TeamAPIToken
from .organizer import Organizer, TeamAPIToken
event = None
if isinstance(self, Event):
organizer_id = None
if isinstance(self, Organizer):
organizer_id = self.pk
elif isinstance(self, Event):
event = self
organizer_id = self.organizer_id
elif hasattr(self, 'event'):
event = self.event
organizer_id = self.event.organizer_id
elif hasattr(self, 'organizer_id'):
organizer_id = self.organizer_id
if user and not user.is_authenticated:
user = None
@@ -106,7 +114,8 @@ class LoggingMixin:
elif isinstance(api_token, TeamAPIToken):
kwargs['api_token'] = api_token
logentry = LogEntry(content_object=self, user=user, action_type=action, event=event, **kwargs)
logentry = LogEntry(content_object=self, user=user, action_type=action, event=event,
organizer_link_id=organizer_id, **kwargs)
if isinstance(data, dict):
sensitivekeys = ['password', 'secret', 'api_key']

View File

@@ -62,6 +62,16 @@ class CheckinList(LoggedModel):
'and valid for check-in regardless of which date they are purchased for. '
'You can limit their validity through the advanced check-in rules, '
'though.'))
ignore_in_statistics = models.BooleanField(
verbose_name=pgettext_lazy('checkin', 'Ignore check-ins on this list in statistics'),
default=False
)
consider_tickets_used = models.BooleanField(
verbose_name=pgettext_lazy('checkin', 'Tickets with a check-in on this list should be considered "used"'),
help_text=_('This is relevant in various situations, e.g. for deciding if a ticket can still be canceled by '
'the customer.'),
default=True
)
include_pending = models.BooleanField(verbose_name=pgettext_lazy('checkin', 'Include pending orders'),
default=False,
help_text=_('With this option, people will be able to check in even if the '
@@ -265,16 +275,16 @@ class CheckinList(LoggedModel):
# * in pretix.helpers.jsonlogic_boolalg
# * in checkinrules.js
# * in libpretixsync
# * in pretixscan-ios (in the future)
# * in pretixscan-ios
top_level_operators = {
'<', '<=', '>', '>=', '==', '!=', 'inList', 'isBefore', 'isAfter', 'or', 'and'
}
allowed_operators = top_level_operators | {
'buildTime', 'objectList', 'lookup', 'var',
'buildTime', 'objectList', 'lookup', 'var', 'entries_since', 'entries_before'
}
allowed_vars = {
'product', 'variation', 'now', 'now_isoweekday', 'entries_number', 'entries_today', 'entries_days',
'minutes_since_last_entry', 'minutes_since_first_entry',
'minutes_since_last_entry', 'minutes_since_first_entry', 'gate',
}
if not rules or not isinstance(rules, dict):
return rules
@@ -299,6 +309,10 @@ class CheckinList(LoggedModel):
raise ValidationError(f'Logic variable "{values[0]}" is currently not allowed.')
return rules
if operator in ('entries_since', 'entries_before'):
if len(values) != 1 or "buildTime" not in values[0]:
raise ValidationError(f'Operator "{operator}" takes exactly one "buildTime" argument.')
if operator in ('or', 'and') and seen_nonbool:
raise ValidationError('You cannot use OR/AND logic on a level below a comparison operator.')
@@ -338,6 +352,7 @@ class Checkin(models.Model):
REASON_AMBIGUOUS = 'ambiguous'
REASON_ERROR = 'error'
REASON_BLOCKED = 'blocked'
REASON_UNAPPROVED = 'unapproved'
REASON_INVALID_TIME = 'invalid_time'
REASONS = (
(REASON_CANCELED, _('Order canceled')),
@@ -351,6 +366,7 @@ class Checkin(models.Model):
(REASON_AMBIGUOUS, _('Ticket code is ambiguous on list')),
(REASON_ERROR, _('Server error')),
(REASON_BLOCKED, _('Ticket blocked')),
(REASON_UNAPPROVED, _('Order not approved')),
(REASON_INVALID_TIME, _('Ticket not valid at this time')),
)

View File

@@ -99,7 +99,7 @@ class Discount(LoggedModel):
)
condition_apply_to_addons = models.BooleanField(
default=True,
verbose_name=_("Apply to add-on products"),
verbose_name=_("Count add-on products"),
help_text=_("Discounts never apply to bundled products"),
)
condition_ignore_voucher_discounted = models.BooleanField(
@@ -107,7 +107,7 @@ class Discount(LoggedModel):
verbose_name=_("Ignore products discounted by a voucher"),
help_text=_("If this option is checked, products that already received a discount through a voucher will not "
"be considered for this discount. However, products that use a voucher only to e.g. unlock a "
"hidden product or gain access to sold-out quota will still receive the discount."),
"hidden product or gain access to sold-out quota will still be considered."),
)
condition_min_count = models.PositiveIntegerField(
verbose_name=_('Minimum number of matching products'),
@@ -120,6 +120,19 @@ class Discount(LoggedModel):
default=Decimal('0.00'),
)
benefit_same_products = models.BooleanField(
default=True,
verbose_name=_("Apply discount to same set of products"),
help_text=_("By default, the discount is applied across the same selection of products than the condition for "
"the discount given above. If you want, you can however also select a different selection of "
"products.")
)
benefit_limit_products = models.ManyToManyField(
'Item',
verbose_name=_("Apply discount to specific products"),
related_name='benefit_discounts',
blank=True
)
benefit_discount_matching_percent = models.DecimalField(
verbose_name=_('Percentual discount on matching products'),
decimal_places=2,
@@ -139,6 +152,18 @@ class Discount(LoggedModel):
blank=True,
validators=[MinValueValidator(1)],
)
benefit_apply_to_addons = models.BooleanField(
default=True,
verbose_name=_("Apply to add-on products"),
help_text=_("Discounts never apply to bundled products"),
)
benefit_ignore_voucher_discounted = models.BooleanField(
default=False,
verbose_name=_("Ignore products discounted by a voucher"),
help_text=_("If this option is checked, products that already received a discount through a voucher will not "
"be discounted. However, products that use a voucher only to e.g. unlock a hidden product or gain "
"access to sold-out quota will still receive the discount."),
)
# more feature ideas:
# - max_usages_per_order
@@ -187,6 +212,14 @@ class Discount(LoggedModel):
'on a minimum value.')
)
if data.get('subevent_mode') == cls.SUBEVENT_MODE_DISTINCT and not data.get('benefit_same_products'):
raise ValidationError(
{'benefit_same_products': [
_('You cannot apply the discount to a different set of products if the discount is only valid '
'for bookings of different dates.')
]}
)
def allow_delete(self):
return not self.orderposition_set.exists()
@@ -197,6 +230,7 @@ class Discount(LoggedModel):
'condition_min_value': self.condition_min_value,
'benefit_only_apply_to_cheapest_n_matches': self.benefit_only_apply_to_cheapest_n_matches,
'subevent_mode': self.subevent_mode,
'benefit_same_products': self.benefit_same_products,
})
def is_available_by_time(self, now_dt=None) -> bool:
@@ -207,14 +241,14 @@ class Discount(LoggedModel):
return False
return True
def _apply_min_value(self, positions, idx_group, result):
if self.condition_min_value and sum(positions[idx][2] for idx in idx_group) < self.condition_min_value:
def _apply_min_value(self, positions, condition_idx_group, benefit_idx_group, result):
if self.condition_min_value and sum(positions[idx][2] for idx in condition_idx_group) < self.condition_min_value:
return
if self.condition_min_count or self.benefit_only_apply_to_cheapest_n_matches:
raise ValueError('Validation invariant violated.')
for idx in idx_group:
for idx in benefit_idx_group:
previous_price = positions[idx][2]
new_price = round_decimal(
previous_price * (Decimal('100.00') - self.benefit_discount_matching_percent) / Decimal('100.00'),
@@ -222,8 +256,8 @@ class Discount(LoggedModel):
)
result[idx] = new_price
def _apply_min_count(self, positions, idx_group, result):
if len(idx_group) < self.condition_min_count:
def _apply_min_count(self, positions, condition_idx_group, benefit_idx_group, result):
if len(condition_idx_group) < self.condition_min_count:
return
if not self.condition_min_count or self.condition_min_value:
@@ -233,15 +267,17 @@ class Discount(LoggedModel):
if not self.condition_min_count:
raise ValueError('Validation invariant violated.')
idx_group = sorted(idx_group, key=lambda idx: (positions[idx][2], -idx)) # sort by line_price
condition_idx_group = sorted(condition_idx_group, key=lambda idx: (positions[idx][2], -idx)) # sort by line_price
benefit_idx_group = sorted(benefit_idx_group, key=lambda idx: (positions[idx][2], -idx)) # sort by line_price
# Prevent over-consuming of items, i.e. if our discount is "buy 2, get 1 free", we only
# want to match multiples of 3
consume_idx = idx_group[:len(idx_group) // self.condition_min_count * self.condition_min_count]
benefit_idx = idx_group[:len(idx_group) // self.condition_min_count * self.benefit_only_apply_to_cheapest_n_matches]
n_groups = min(len(condition_idx_group) // self.condition_min_count, len(benefit_idx_group))
consume_idx = condition_idx_group[:n_groups * self.condition_min_count]
benefit_idx = benefit_idx_group[:n_groups * self.benefit_only_apply_to_cheapest_n_matches]
else:
consume_idx = idx_group
benefit_idx = idx_group
consume_idx = condition_idx_group
benefit_idx = benefit_idx_group
for idx in benefit_idx:
previous_price = positions[idx][2]
@@ -276,7 +312,7 @@ class Discount(LoggedModel):
limit_products = {p.pk for p in self.condition_limit_products.all()}
# First, filter out everything not even covered by our product scope
initial_candidates = [
condition_candidates = [
idx
for idx, (item_id, subevent_id, line_price_gross, is_addon_to, voucher_discount) in positions.items()
if (
@@ -286,11 +322,25 @@ class Discount(LoggedModel):
)
]
if self.benefit_same_products:
benefit_candidates = list(condition_candidates)
else:
benefit_products = {p.pk for p in self.benefit_limit_products.all()}
benefit_candidates = [
idx
for idx, (item_id, subevent_id, line_price_gross, is_addon_to, voucher_discount) in positions.items()
if (
item_id in benefit_products and
(self.benefit_apply_to_addons or not is_addon_to) and
(not self.benefit_ignore_voucher_discounted or voucher_discount is None or voucher_discount == Decimal('0.00'))
)
]
if self.subevent_mode == self.SUBEVENT_MODE_MIXED: # also applies to non-series events
if self.condition_min_count:
self._apply_min_count(positions, initial_candidates, result)
self._apply_min_count(positions, condition_candidates, benefit_candidates, result)
else:
self._apply_min_value(positions, initial_candidates, result)
self._apply_min_value(positions, condition_candidates, benefit_candidates, result)
elif self.subevent_mode == self.SUBEVENT_MODE_SAME:
def key(idx):
@@ -299,17 +349,18 @@ class Discount(LoggedModel):
# Build groups of candidates with the same subevent, then apply our regular algorithm
# to each group
_groups = groupby(sorted(initial_candidates, key=key), key=key)
candidate_groups = [list(g) for k, g in _groups]
_groups = groupby(sorted(condition_candidates, key=key), key=key)
candidate_groups = [(k, list(g)) for k, g in _groups]
for g in candidate_groups:
for subevent_id, g in candidate_groups:
benefit_g = [idx for idx in benefit_candidates if positions[idx][1] == subevent_id]
if self.condition_min_count:
self._apply_min_count(positions, g, result)
self._apply_min_count(positions, g, benefit_g, result)
else:
self._apply_min_value(positions, g, result)
self._apply_min_value(positions, g, benefit_g, result)
elif self.subevent_mode == self.SUBEVENT_MODE_DISTINCT:
if self.condition_min_value:
if self.condition_min_value or not self.benefit_same_products:
raise ValueError('Validation invariant violated.')
# Build optimal groups of candidates with distinct subevents, then apply our regular algorithm
@@ -336,7 +387,7 @@ class Discount(LoggedModel):
candidates = []
cardinality = None
for se, l in subevent_to_idx.items():
l = [ll for ll in l if ll in initial_candidates and ll not in current_group]
l = [ll for ll in l if ll in condition_candidates and ll not in current_group]
if cardinality and len(l) != cardinality:
continue
if se not in {positions[idx][1] for idx in current_group}:
@@ -373,5 +424,10 @@ class Discount(LoggedModel):
break
for g in candidate_groups:
self._apply_min_count(positions, g, result)
self._apply_min_count(
positions,
[idx for idx in g if idx in condition_candidates],
[idx for idx in g if idx in benefit_candidates],
result
)
return result

View File

@@ -20,7 +20,6 @@
# <https://www.gnu.org/licenses/>.
#
import logging
# 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>.
#
@@ -33,6 +32,7 @@ import logging
# 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 logging
import os
import string
import uuid
@@ -71,7 +71,7 @@ from pretix.base.validators import EventSlugBanlistValidator
from pretix.helpers.database import GroupConcat
from pretix.helpers.daterange import daterange
from pretix.helpers.hierarkey import clean_filename
from pretix.helpers.json import safe_string
from pretix.helpers.json import CustomJSONEncoder, safe_string
from pretix.helpers.thumb import get_thumbnail
from ..settings import settings_hierarkey
@@ -743,12 +743,7 @@ class Event(EventMixin, LoggedModel):
return ObjectRelatedCache(self)
def lock(self):
"""
Returns a contextmanager that can be used to lock an event for bookings.
"""
from pretix.base.services import locking
return locking.LockManager(self)
raise NotImplementedError("this method has been removed")
def get_mail_backend(self, timeout=None):
"""
@@ -803,6 +798,11 @@ class Event(EventMixin, LoggedModel):
self.save()
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
for emv in EventMetaValue.objects.filter(event=other):
emv.pk = None
emv.event = self
emv.save(force_insert=True)
for fl in EventFooterLink.objects.filter(event=other):
fl.pk = None
fl.event = self
@@ -862,6 +862,10 @@ class Event(EventMixin, LoggedModel):
v.item = i
v.save(force_insert=True)
for i in self.items.filter(hidden_if_item_available__isnull=False):
i.hidden_if_item_available = item_map[i.hidden_if_item_available_id]
i.save()
for imv in ItemMetaValue.objects.filter(item__event=other):
imv.pk = None
imv.property = item_meta_properties_map[imv.property_id]
@@ -907,14 +911,18 @@ class Event(EventMixin, LoggedModel):
self.items.filter(hidden_if_available_id=oldid).update(hidden_if_available=q)
for d in Discount.objects.filter(event=other).prefetch_related('condition_limit_products'):
items = list(d.condition_limit_products.all())
c_items = list(d.condition_limit_products.all())
b_items = list(d.benefit_limit_products.all())
d.pk = None
d.event = self
d.save(force_insert=True)
d.log_action('pretix.object.cloned')
for i in items:
for i in c_items:
if i.pk in item_map:
d.condition_limit_products.add(item_map[i.pk])
for i in b_items:
if i.pk in item_map:
d.benefit_limit_products.add(item_map[i.pk])
question_map = {}
for q in Question.objects.filter(event=other).prefetch_related('items', 'options'):
@@ -998,6 +1006,7 @@ class Event(EventMixin, LoggedModel):
'presale_widget_css_file',
'presale_widget_css_checksum',
)
settings_to_save = []
for s in other.settings._objects.all():
if s.key in skip_settings:
continue
@@ -1015,16 +1024,17 @@ class Event(EventMixin, LoggedModel):
)
newname = default_storage.save(fname, fi)
s.value = 'file://' + newname
s.save()
settings_to_save.append(s)
elif s.key == 'tax_rate_default':
try:
if int(s.value) in tax_map:
s.value = tax_map.get(int(s.value)).pk
s.save()
settings_to_save.append(s)
except ValueError:
pass
else:
s.save()
settings_to_save.append(s)
other.settings._objects.bulk_create(settings_to_save)
self.settings.flush()
event_copy_data.send(
@@ -1642,26 +1652,40 @@ class EventMetaProperty(LoggedModel):
help_text=_("If checked, an event can only be taken live if the property is set. In event series, its always "
"optional to set a value for individual dates")
)
allowed_values = models.TextField(
choices = models.JSONField(
null=True, blank=True,
encoder=CustomJSONEncoder,
verbose_name=_("Valid values"),
help_text=_("If you keep this empty, any value is allowed. Otherwise, enter one possible value per line.")
)
filter_public = models.BooleanField(
default=False, verbose_name=_("Show filter option to customers"),
help_text=_("This field will be shown to filter events in the public event list and calendar.")
)
public_label = I18nCharField(
verbose_name=_("Public name"),
null=True, blank=True,
)
filter_allowed = models.BooleanField(
default=True, verbose_name=_("Can be used for filtering"),
help_text=_("This field will be shown to filter events or reports in the backend, and it can also be used "
"for hidden filter parameters in the frontend (e.g. using the widget).")
)
position = models.IntegerField(
default=0
)
def full_clean(self, exclude=None, validate_unique=True):
super().full_clean(exclude, validate_unique)
if self.default and self.required:
raise ValidationError(_("A property can either be required or have a default value, not both."))
if self.default and self.allowed_values and self.default not in self.allowed_values.splitlines():
raise ValidationError(_("You cannot set a default value that is not a valid value."))
class Meta:
ordering = ("name",)
ordering = ("position", "name",)
@property
def choice_keys(self):
if self.choices:
return [v["key"] for v in self.choices]
class EventMetaValue(LoggedModel):

View File

@@ -79,7 +79,7 @@ class AbstractScheduledExport(LoggedModel):
)
schedule_rrule = models.TextField(
null=True, blank=True, validators=[RRuleValidator()]
null=True, blank=True, validators=[RRuleValidator(enforce_simple=True)]
)
schedule_rrule_time = models.TimeField(
verbose_name=_("Requested start time"),

View File

@@ -116,6 +116,8 @@ class GiftCard(LoggedModel):
@property
def value(self):
if hasattr(self, 'cached_value'):
return self.cached_value or Decimal('0.00')
return self.transactions.aggregate(s=Sum('value'))['s'] or Decimal('0.00')
def accepted_by(self, organizer):

View File

@@ -43,6 +43,7 @@ from typing import Optional, Tuple
from zoneinfo import ZoneInfo
import dateutil.parser
import django_redis
from dateutil.tz import datetime_exists
from django.conf import settings
from django.core.exceptions import ValidationError
@@ -57,7 +58,6 @@ from django.utils.functional import cached_property
from django.utils.timezone import is_naive, make_aware, now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_countries.fields import Country
from django_redis import get_redis_connection
from django_scopes import ScopedManager
from i18nfield.fields import I18nCharField, I18nTextField
@@ -336,6 +336,8 @@ class Item(LoggedModel):
:type min_per_order: int
:param checkin_attention: Requires special attention at check-in
:type checkin_attention: bool
:param checkin_text: Additional text to show at check-in
:type checkin_text: bool
:param original_price: The item's "original" price. Will not be used for any calculations, will just be shown.
:type original_price: decimal.Decimal
:param require_approval: If set to ``True``, orders containing this product can only be processed and paid after approved by an administrator
@@ -431,6 +433,12 @@ class Item(LoggedModel):
"additional donations for your event. This is currently not supported for products that are "
"bought as an add-on to other products.")
)
free_price_suggestion = models.DecimalField(
verbose_name=_("Suggested price"),
help_text=_("This price will be used as the default value of the input field. The user can choose a lower "
"value, but not lower than the price this product would have without the free price option."),
max_digits=13, decimal_places=2, null=True, blank=True,
)
tax_rule = models.ForeignKey(
'TaxRule',
verbose_name=_('Sales tax'),
@@ -488,13 +496,24 @@ class Item(LoggedModel):
'Quota',
null=True, blank=True,
on_delete=models.SET_NULL,
verbose_name=_("Only show after sellout of"),
verbose_name=pgettext_lazy("hidden_if_available_legacy", "Only show after sellout of"),
help_text=_("If you select a quota here, this product will only be shown when that quota is "
"unavailable. If combined with the option to hide sold-out products, this allows you to "
"swap out products for more expensive ones once they are sold out. There might be a short period "
"in which both products are visible while all tickets in the referenced quota are reserved, "
"but not yet sold.")
)
hidden_if_item_available = models.ForeignKey(
'Item',
null=True, blank=True,
on_delete=models.SET_NULL,
verbose_name=_("Only show after sellout of"),
help_text=_("If you select a product here, this product will only be shown when that product is "
"sold out. If combined with the option to hide sold-out products, this allows you to "
"swap out products for more expensive ones once the cheaper option is sold out. There might "
"be a short period in which both products are visible while all tickets of the referenced "
"product are reserved, but not yet sold.")
)
require_voucher = models.BooleanField(
verbose_name=_('This product can only be bought using a voucher.'),
default=False,
@@ -549,6 +568,11 @@ class Item(LoggedModel):
'attention. You can use this for example for student tickets to indicate to the person at '
'check-in that the student ID card still needs to be checked.')
)
checkin_text = models.TextField(
verbose_name=_('Check-in text'),
null=True, blank=True,
help_text=_('This text will be shown by the check-in app if a ticket of this type is scanned.')
)
original_price = models.DecimalField(
verbose_name=_('Original price'),
blank=True, null=True,
@@ -1021,6 +1045,12 @@ class ItemVariation(models.Model):
help_text=_('If set, this will be displayed next to the current price to show that the current price is a '
'discounted one. This is just a cosmetic setting and will not actually impact pricing.')
)
free_price_suggestion = models.DecimalField(
verbose_name=_("Suggested price"),
help_text=_("This price will be used as the default value of the input field. The user can choose a lower "
"value, but not lower than the price this product would have without the free price option."),
max_digits=13, decimal_places=2, null=True, blank=True,
)
require_approval = models.BooleanField(
verbose_name=_('Require approval'),
default=False,
@@ -1073,6 +1103,11 @@ class ItemVariation(models.Model):
'attention. You can use this for example for student tickets to indicate to the person at '
'check-in that the student ID card still needs to be checked.')
)
checkin_text = models.TextField(
verbose_name=_('Check-in text'),
null=True, blank=True,
help_text=_('This text will be shown by the check-in app if a ticket of this type is scanned.')
)
objects = ScopedManager(organizer='item__event__organizer')
@@ -1427,6 +1462,8 @@ class Question(LoggedModel):
:param items: A set of ``Items`` objects that this question should be applied to
:param ask_during_checkin: Whether to ask this question during check-in instead of during check-out.
:type ask_during_checkin: bool
:param show_during_checkin: Whether to show the answer to this question during check-in.
:type show_during_checkin: bool
:param hidden: Whether to only show the question in the backend
:type hidden: bool
:param identifier: An arbitrary, internal identifier
@@ -1463,7 +1500,8 @@ class Question(LoggedModel):
(TYPE_PHONENUMBER, _("Phone number")),
)
UNLOCALIZED_TYPES = [TYPE_DATE, TYPE_TIME, TYPE_DATETIME]
ASK_DURING_CHECKIN_UNSUPPORTED = [TYPE_PHONENUMBER]
ASK_DURING_CHECKIN_UNSUPPORTED = []
SHOW_DURING_CHECKIN_UNSUPPORTED = [TYPE_FILE]
event = models.ForeignKey(
Event,
@@ -1515,6 +1553,11 @@ class Question(LoggedModel):
help_text=_('Not supported by all check-in apps for all question types.'),
default=False
)
show_during_checkin = models.BooleanField(
verbose_name=_('Show answer during check-in'),
help_text=_('Not supported by all check-in apps for all question types.'),
default=False
)
hidden = models.BooleanField(
verbose_name=_('Hidden question'),
help_text=_('This question will only show up in the backend.'),
@@ -1910,8 +1953,13 @@ class Quota(LoggedModel):
def rebuild_cache(self, now_dt=None):
if settings.HAS_REDIS:
rc = get_redis_connection("redis")
rc.hdel(f'quotas:{self.event_id}:availabilitycache', str(self.pk))
rc = django_redis.get_redis_connection("redis")
p = rc.pipeline()
p.hdel(f'quotas:{self.event_id}:availabilitycache', str(self.pk))
p.hdel(f'quotas:{self.event_id}:availabilitycache:nocw', str(self.pk))
p.hdel(f'quotas:{self.event_id}:availabilitycache:igcl', str(self.pk))
p.hdel(f'quotas:{self.event_id}:availabilitycache:nocw:igcl', str(self.pk))
p.execute()
self.availability(now_dt=now_dt)
def availability(

View File

@@ -78,6 +78,7 @@ class LogEntry(models.Model):
device = models.ForeignKey('Device', null=True, blank=True, on_delete=models.PROTECT)
oauth_application = models.ForeignKey('pretixapi.OAuthApplication', null=True, blank=True, on_delete=models.PROTECT)
event = models.ForeignKey('Event', null=True, blank=True, on_delete=models.SET_NULL)
organizer_link = models.ForeignKey('Organizer', null=True, blank=True, on_delete=models.PROTECT)
action_type = models.CharField(max_length=255)
data = models.TextField(default='{}')
visible = models.BooleanField(default=True)
@@ -88,9 +89,7 @@ class LogEntry(models.Model):
class Meta:
ordering = ('-datetime', '-id')
index_together = [
['datetime', 'id']
]
indexes = [models.Index(fields=["datetime", "id"])]
def display(self):
from ..signals import logentry_display
@@ -128,7 +127,9 @@ class LogEntry(models.Model):
def organizer(self):
from .organizer import Organizer
if self.event:
if self.organizer_link:
return self.organizer_link
elif self.event:
return self.event.organizer
elif hasattr(self.content_object, 'event'):
return self.content_object.event.organizer

View File

@@ -121,7 +121,10 @@ class ReusableMedium(LoggedModel):
class Meta:
unique_together = (("identifier", "type", "organizer"),)
index_together = (("identifier", "type", "organizer"), ("updated", "id"))
indexes = [
models.Index(fields=("identifier", "type", "organizer")),
models.Index(fields=("updated", "id")),
]
ordering = "identifier", "type", "organizer"

View File

@@ -37,10 +37,13 @@ import copy
import hashlib
import json
import logging
import operator
import string
from collections import Counter
from datetime import datetime, time, timedelta
from decimal import Decimal
from functools import reduce
from time import sleep
from typing import Any, Dict, List, Union
from zoneinfo import ZoneInfo
@@ -75,7 +78,6 @@ from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import Customer, User
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.services.locking import LOCK_TIMEOUT, NoLockManager
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import order_gracefully_delete
@@ -83,6 +85,7 @@ from ...helpers import OF_SELF
from ...helpers.countries import CachedCountries, FastCountryField
from ...helpers.format import format_map
from ...helpers.names import build_name
from ...testutils.middleware import debugflags_var
from ._transactions import (
_fail, _transactions_mark_order_clean, _transactions_mark_order_dirty,
)
@@ -241,6 +244,11 @@ class Order(LockModel, LoggedModel):
'special attention. This will not show any details or custom message, so you need to brief your '
'check-in staff how to handle these cases.')
)
checkin_text = models.TextField(
verbose_name=_('Check-in text'),
null=True, blank=True,
help_text=_('This text will be shown by the check-in app if a ticket of this order is scanned.')
)
expiry_reminder_sent = models.BooleanField(
default=False
)
@@ -263,6 +271,10 @@ class Order(LockModel, LoggedModel):
default=False,
verbose_name=_('E-mail address verified')
)
invoice_dirty = models.BooleanField(
# Invoice needs to be re-issued when the order is paid again
default=False,
)
objects = ScopedManager(organizer='event__organizer')
@@ -270,9 +282,9 @@ class Order(LockModel, LoggedModel):
verbose_name = _("Order")
verbose_name_plural = _("Orders")
ordering = ("-datetime", "-pk")
index_together = [
["datetime", "id"],
["last_modified", "id"],
indexes = [
models.Index(fields=["datetime", "id"]),
models.Index(fields=["last_modified", "id"]),
]
def __str__(self):
@@ -322,6 +334,18 @@ class Order(LockModel, LoggedModel):
def email_confirm_hash(self):
return hashlib.sha256(settings.SECRET_KEY.encode() + self.secret.encode()).hexdigest()[:9]
def get_extended_status_display(self):
# Changes in this method should to be replicated in pretixcontrol/orders/fragment_order_status.html
# and pretixpresale/event/fragment_order_status.html
if self.status == Order.STATUS_PENDING:
if self.require_approval:
return _("approval pending")
elif self.valid_if_pending:
return pgettext_lazy("order state", "pending (confirmed)")
elif self.status == Order.STATUS_PAID and self.count_positions == 0:
return _("canceled (paid fee)")
return self.get_status_display()
@property
def fees(self):
"""
@@ -630,7 +654,7 @@ class Order(LockModel, LoggedModel):
positions = list(
self.positions.all().annotate(
has_variations=Exists(ItemVariation.objects.filter(item_id=OuterRef('item_id'))),
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk')))
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk'), list__consider_tickets_used=True))
).select_related('item').prefetch_related('issued_gift_cards')
)
if self.event.settings.change_allow_user_if_checked_in:
@@ -662,7 +686,7 @@ class Order(LockModel, LoggedModel):
return False
positions = list(
self.positions.all().annotate(
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk')))
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk'), list__consider_tickets_used=True))
).select_related('item').prefetch_related('issued_gift_cards')
)
cancelable = all([op.item.allow_cancel and not op.has_checkin and not op.blocked for op in positions])
@@ -817,7 +841,7 @@ class Order(LockModel, LoggedModel):
positions = list(
self.positions.all().annotate(
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk')))
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk'), list__consider_tickets_used=True))
).select_related('item').prefetch_related('item__questions')
)
if not self.event.settings.allow_modifications_after_checkin:
@@ -825,7 +849,7 @@ class Order(LockModel, LoggedModel):
if cp.has_checkin:
return False
if self.event.settings.get('invoice_address_asked', as_type=bool):
if self.event.settings.get('invoice_address_asked', as_type=bool) or self.event.settings.get('invoice_name_required', as_type=bool):
return True
ask_names = self.event.settings.get('attendee_names_asked', as_type=bool)
for cp in positions:
@@ -907,6 +931,11 @@ class Order(LockModel, LoggedModel):
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(
@@ -918,7 +947,7 @@ class Order(LockModel, LoggedModel):
else:
return expires
def _can_be_paid(self, count_waitinglist=True, ignore_date=False, force=False) -> Union[bool, str]:
def _can_be_paid(self, count_waitinglist=True, ignore_date=False, force=False, lock=False) -> Union[bool, str]:
error_messages = {
'late_lastdate': _("The payment can not be accepted as the last date of payments configured in the "
"payment settings is over."),
@@ -939,10 +968,11 @@ class Order(LockModel, LoggedModel):
if not self.event.settings.get('payment_term_accept_late') and not ignore_date and not force:
return error_messages['late']
return self._is_still_available(count_waitinglist=count_waitinglist, force=force)
return self._is_still_available(count_waitinglist=count_waitinglist, force=force, lock=lock)
def _is_still_available(self, now_dt: datetime=None, count_waitinglist=True, force=False,
def _is_still_available(self, now_dt: datetime=None, count_waitinglist=True, lock=False, force=False,
check_voucher_usage=False, check_memberships=False) -> Union[bool, str]:
from pretix.base.services.locking import lock_objects
from pretix.base.services.memberships import (
validate_memberships_in_order,
)
@@ -961,10 +991,21 @@ class Order(LockModel, LoggedModel):
try:
if check_memberships:
try:
validate_memberships_in_order(self.customer, positions, self.event, lock=False, testmode=self.testmode)
validate_memberships_in_order(self.customer, positions, self.event, lock=lock, testmode=self.testmode)
except ValidationError as e:
raise Quota.QuotaExceededException(e.message)
for cp in positions:
cp._cached_quotas = list(cp.quotas) if not force else []
if lock:
lock_objects(
[q for q in reduce(operator.or_, (set(cp._cached_quotas) for cp in positions), set()) if q.size is not None] +
[op.voucher for op in positions if op.voucher and not force] +
[op.seat for op in positions if op.seat],
shared_lock_objects=[self.event]
)
for i, op in enumerate(positions):
if op.seat:
if not op.seat.is_available(ignore_orderpos=op):
@@ -989,7 +1030,7 @@ class Order(LockModel, LoggedModel):
voucher=op.voucher.code
))
quotas = list(op.quotas)
quotas = op._cached_quotas
if len(quotas) == 0:
raise Quota.QuotaExceededException(error_messages['unavailable'].format(
item=str(op.item) + (' - ' + str(op.variation) if op.variation else '')
@@ -1011,6 +1052,9 @@ class Order(LockModel, LoggedModel):
))
except Quota.QuotaExceededException as e:
return str(e)
if 'sleep-after-quota-check' in debugflags_var.get():
sleep(2)
return True
def send_mail(self, subject: Union[str, LazyI18nString], template: Union[str, LazyI18nString],
@@ -1241,13 +1285,28 @@ 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):
return self.file.name.split('.', 1)[-1]
def __str__(self):
return self.to_string(use_cached=True)
def to_string_i18n(self):
return self.to_string(use_cached=False)
def to_string(self, use_cached=True):
"""
Render this answer as a string.
:param use_cached: If ``True`` (default), choice and multiple choice questions will show their cached
value, i.e. the value of the selected options at the time of saving and in the language
the answer was saved in. If ``False``, the values will instead be loaded from the
database, yielding current and translated values of the options. However, additional database
queries might be required.
"""
if self.question.type == Question.TYPE_BOOLEAN and self.answer == "True":
return str(_("Yes"))
elif self.question.type == Question.TYPE_BOOLEAN and self.answer == "False":
@@ -1282,6 +1341,8 @@ class QuestionAnswer(models.Model):
return PhoneNumber.from_string(self.answer).as_international
except NumberParseException:
return self.answer
elif self.question.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE) and self.answer and not use_cached:
return ", ".join(str(o.answer) for o in self.options.all())
else:
return self.answer
@@ -1642,9 +1703,10 @@ class OrderPayment(models.Model):
return self.order.event.get_payment_providers(cached=True).get(self.provider)
@transaction.atomic()
def _mark_paid_inner(self, force, count_waitinglist, user, auth, ignore_date=False, overpaid=False):
def _mark_paid_inner(self, force, count_waitinglist, user, auth, ignore_date=False, overpaid=False, lock=False):
from pretix.base.signals import order_paid
can_be_paid = self.order._can_be_paid(count_waitinglist=count_waitinglist, ignore_date=ignore_date, force=force)
can_be_paid = self.order._can_be_paid(count_waitinglist=count_waitinglist, ignore_date=ignore_date, force=force,
lock=lock)
if can_be_paid is not True:
self.order.log_action('pretix.event.order.quotaexceeded', {
'message': can_be_paid
@@ -1671,7 +1733,7 @@ class OrderPayment(models.Model):
"""
Marks the order as failed and sets info to ``info``, but only if the order is in ``created`` or ``pending``
state. This is equivalent to setting ``state`` to ``OrderPayment.PAYMENT_STATE_FAILED`` and logging a failure,
but it adds strong database logging since we do not want to report a failure for an order that has just
but it adds strong database locking since we do not want to report a failure for an order that has just
been marked as paid.
:param send_mail: Whether an email should be sent to the user about this event (default: ``True``).
"""
@@ -1775,25 +1837,24 @@ class OrderPayment(models.Model):
))
return
self._mark_order_paid(count_waitinglist, send_mail, force, user, auth, mail_text, ignore_date, lock, payment_sum - refund_sum,
generate_invoice)
with transaction.atomic():
self._mark_order_paid(count_waitinglist, send_mail, force, user, auth, mail_text, ignore_date, lock, payment_sum - refund_sum,
generate_invoice)
def _mark_order_paid(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text='',
ignore_date=False, lock=True, payment_refund_sum=0, allow_generate_invoice=True):
from pretix.base.services.invoices import (
generate_invoice, invoice_qualified,
generate_cancellation, generate_invoice, invoice_qualified,
)
from pretix.base.services.locking import LOCK_TRUST_WINDOW
if (self.order.status == Order.STATUS_PENDING and self.order.expires > now() + timedelta(seconds=LOCK_TIMEOUT * 2)) or not lock:
if lock and self.order.status == Order.STATUS_PENDING and self.order.expires > now() + timedelta(seconds=LOCK_TRUST_WINDOW):
# Performance optimization. In this case, there's really no reason to lock everything and an atomic
# database transaction is more than enough.
lockfn = NoLockManager
else:
lockfn = self.order.event.lock
lock = False
with lockfn():
self._mark_paid_inner(force, count_waitinglist, user, auth, overpaid=payment_refund_sum > self.order.total,
ignore_date=ignore_date)
self._mark_paid_inner(force, count_waitinglist, user, auth, overpaid=payment_refund_sum > self.order.total,
ignore_date=ignore_date, lock=lock)
invoice = None
if invoice_qualified(self.order) and allow_generate_invoice:
@@ -1801,9 +1862,14 @@ class OrderPayment(models.Model):
cancellations = self.order.invoices.filter(is_cancellation=True).count()
gen_invoice = (
(invoices == 0 and self.order.event.settings.get('invoice_generate') in ('True', 'paid')) or
0 < invoices <= cancellations
0 < invoices <= cancellations or
self.order.invoice_dirty
)
if gen_invoice:
if invoices:
last_i = self.order.invoices.filter(is_cancellation=False).last()
if not last_i.canceled:
generate_cancellation(last_i)
invoice = generate_invoice(
self.order,
trigger_pdf=not send_mail or not self.order.event.settings.invoice_email_attachment
@@ -2364,6 +2430,17 @@ class OrderPosition(AbstractPosition):
return True
return False
@cached_property
def checkin_texts(self):
texts = []
if self.order.checkin_text:
texts.append(self.order.checkin_text)
if self.variation_id and self.variation.checkin_text:
texts.append(self.variation.checkin_text)
if self.item.checkin_text:
texts.append(self.item.checkin_text)
return texts
@property
def checkins(self):
"""
@@ -2606,7 +2683,7 @@ class OrderPosition(AbstractPosition):
with language(self.order.locale, self.order.event.settings.region):
email_template = self.event.settings.mail_text_resend_link
email_context = get_email_context(event=self.order.event, order=self.order, position=self)
email_subject = self.event.settings.mail_subject_resend_link
email_subject = self.event.settings.mail_subject_resend_link_attendee
self.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.resend', user=user, auth=auth,
@@ -2751,8 +2828,8 @@ class Transaction(models.Model):
class Meta:
ordering = 'datetime', 'pk'
index_together = [
['datetime', 'id']
indexes = [
models.Index(fields=['datetime', 'id'])
]
def save(self, *args, **kwargs):

View File

@@ -340,10 +340,17 @@ class TaxRule(LoggedModel):
rules = self._custom_rules
if invoice_address:
for r in rules:
if r['country'] == 'EU' and not is_eu_country(invoice_address.country):
continue
if r['country'] not in ('ZZ', 'EU') and r['country'] != str(invoice_address.country):
continue
if r['country'] == 'ZZ': # Rule: Any country
pass
elif r['country'] == 'EU': # Rule: Any EU country
if not is_eu_country(invoice_address.country):
continue
elif '-' in r['country']: # Rule: Specific country and state
if r['country'] != str(invoice_address.country) + '-' + str(invoice_address.state):
continue
else: # Rule: Specific country
if r['country'] != str(invoice_address.country):
continue
if r['address_type'] == 'individual' and invoice_address.is_business:
continue
if r['address_type'] in ('business', 'business_vat_id') and not invoice_address.is_business:

View File

@@ -435,28 +435,37 @@ class Voucher(LoggedModel):
@staticmethod
def clean_quota_check(data, cnt, old_instance, event, quota, item, variation):
from ..services.locking import lock_objects
from ..services.quotas import QuotaAvailability
old_quotas = Voucher.clean_quota_get_ignored(old_instance)
if event.has_subevents and data.get('block_quota') and not data.get('subevent'):
raise ValidationError(_('If you want this voucher to block quota, you need to select a specific date.'))
if quota:
if quota in old_quotas:
return
else:
avail = quota.availability(count_waitinglist=False)
new_quotas = {quota}
elif item and item.has_variations and not variation:
raise ValidationError(_('You can only block quota if you specify a specific product variation. '
'Otherwise it might be unclear which quotas to block.'))
elif item and variation:
avail = variation.check_quotas(ignored_quotas=old_quotas, subevent=data.get('subevent'))
new_quotas = set(variation.quotas.filter(subevent=data.get('subevent')))
elif item and not item.has_variations:
avail = item.check_quotas(ignored_quotas=old_quotas, subevent=data.get('subevent'))
new_quotas = set(item.quotas.filter(subevent=data.get('subevent')))
else:
raise ValidationError(_('You need to select a specific product or quota if this voucher should reserve '
'tickets.'))
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < cnt):
if not (new_quotas - old_quotas):
return
lock_objects([q for q in (new_quotas - old_quotas) if q.size is not None], shared_lock_objects=[event])
qa = QuotaAvailability(count_waitinglist=False)
qa.queue(*(new_quotas - old_quotas))
qa.compute()
if any(r[0] != Quota.AVAILABILITY_OK or (r[1] is not None and r[1] < cnt) for r in qa.results.values()):
raise ValidationError(_('You cannot create a voucher that blocks quota as the selected product or '
'quota is currently sold out or completely reserved.'))

View File

@@ -805,7 +805,7 @@ class QuestionColumn(ImportColumn):
return self.q.clean_answer(value)
def assign(self, value, order, position, invoice_address, **kwargs):
if value:
if value is not None:
if not hasattr(order, '_answers'):
order._answers = []
if isinstance(value, QuestionOption):

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