Compare commits

...

361 Commits

Author SHA1 Message Date
Raphael Michel
0ce2082f00 Event dashboard: Use intcomma in numbers (Z#23175343) 2024-12-05 10:23:16 +01:00
Raphael Michel
70f06a8f40 Fix an incorrect exception handling 2024-12-04 16:59:58 +01:00
Mira Weller
a747ab154a Fix cursor on elements in disabled fieldsets' legends 2024-12-04 14:54:49 +01:00
Mira
6317233150 New accordion panels using <fieldset> (#4681) 2024-12-04 14:34:42 +01:00
Richard Schreiber
4d94158ff0 Improve organizer/event-series calendar UI on mobile 2024-12-04 08:17:52 +01:00
Raphael Michel
8f92eb2d2d remove debug statement 2024-12-03 12:40:29 +01:00
Richard Schreiber
f29896b267 [A11y] Fix missing aria-hidden and translation 2024-12-03 12:10:16 +01:00
Raphael Michel
2dc625cf31 Add the option to introduce rich-text placeholders (#4657)
* Add the option to introduce rich-text placeholders

* Add tests in test_format

* Add some css

* Block vs inline

* Some fixed css

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

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

* Add missing docstring prat

---------

Co-authored-by: Mira <weller@rami.io>
2024-12-03 11:38:15 +01:00
dependabot[bot]
855226d37c Update ua-parser requirement from ==0.18.* to ==1.0.* (#4665)
Updates the requirements on [ua-parser](https://github.com/ua-parser/uap-python) to permit the latest version.
- [Release notes](https://github.com/ua-parser/uap-python/releases)
- [Commits](https://github.com/ua-parser/uap-python/compare/0.18.0...1.0.0)

---
updated-dependencies:
- dependency-name: ua-parser
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-03 11:18:37 +01:00
dependabot[bot]
648c0da9fe Update webauthn requirement from ==2.2.* to ==2.3.* (#4655)
Updates the requirements on [webauthn](https://github.com/duo-labs/py_webauthn) to permit the latest version.
- [Release notes](https://github.com/duo-labs/py_webauthn/releases)
- [Changelog](https://github.com/duo-labs/py_webauthn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/duo-labs/py_webauthn/compare/v2.2.0...v2.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-03 11:01:05 +01:00
Raphael Michel
59e3494fa2 Add fee type for late fees (#4656) 2024-12-03 11:00:11 +01:00
Mira
c4ff57c07a Change error message for unavailable addon products (#4673)
This can not only happen in case of sold-out addons, but also if they are e.g. not available via the current sales channel.
2024-12-03 10:59:15 +01:00
Raphael Michel
cc4fbfe4c7 API: Allow to block/unblock seats in bulk (#4668)
* API: Allow to block/unblock seats in bulk

* Update doc/api/resources/seats.rst

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

* Update doc/api/resources/seats.rst

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

* Update doc/api/resources/seats.rst

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

* Update src/pretix/api/views/event.py

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

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-12-02 16:03:11 +01:00
Raphael Michel
e99ee91573 Allow to use custom domains for some but not all events (Z#23153875) (#4627)
* Allow to use custom domains for some but not all events

* Update src/pretix/multidomain/urlreverse.py

* Apply suggestions from code review

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

* Logging for domain config changes

---------

Co-authored-by: Mira <weller@rami.io>
2024-12-02 15:58:50 +01:00
Patrick Chilton
e2753686ee Translations: Update Hungarian
Currently translated at 10.8% (629 of 5782 strings)

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

powered by weblate
2024-12-02 15:58:41 +01:00
CVZ-es
33f8b9851e Translations: Update Spanish
Currently translated at 100.0% (5782 of 5782 strings)

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

powered by weblate
2024-12-02 15:58:41 +01:00
CVZ-es
e3d8cf07af Translations: Update French
Currently translated at 100.0% (5782 of 5782 strings)

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

powered by weblate
2024-12-02 15:58:41 +01:00
Mira Weller
0279ca7d94 Add missing error handling to addressform.js 2024-12-02 10:15:16 +01:00
Richard Schreiber
d1989c3cd3 Fix all-optional in address-form for resellers (#4672) 2024-12-02 09:46:33 +01:00
Raphael Michel
61cb2e15cf Fix validation crash of InvoiceNameForm 2024-11-29 20:08:36 +01:00
Mira Weller
f2ee1d00b3 Don't use animation for address information load indicator 2024-11-29 17:09:14 +01:00
Mira
e8e9698a31 Update address field logic (Z#23163120) (#4659)
* Move country-dependent JS logic to separate file (avoids code duplication for presale and control)
* Correctly apply "required" attribute to address state field
* Load address format information when selecting country
* Fix some other bugs and inconsistencies
2024-11-29 14:56:56 +01:00
Richard Schreiber
a1bf7be244 [A11y] Improve customer account pages (#4654) 2024-11-29 14:16:40 +01:00
Patrick Chilton
f4ca9a5681 Translations: Update Hungarian
Currently translated at 45.6% (106 of 232 strings)

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

powered by weblate
2024-11-29 09:22:29 +01:00
dependabot[bot]
e6d984538f Update django-statici18n requirement from ==2.5.* to ==2.6.* (#4664)
Updates the requirements on [django-statici18n](https://github.com/zyegfryed/django-statici18n) to permit the latest version.
- [Changelog](https://github.com/zyegfryed/django-statici18n/blob/main/docs/changelog.rst)
- [Commits](https://github.com/zyegfryed/django-statici18n/compare/v2.5.0...v2.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-29 09:22:04 +01:00
dependabot[bot]
9f1ee9157f Update protobuf requirement from ==5.28.* to ==5.29.* (#4666)
Updates the requirements on [protobuf](https://github.com/protocolbuffers/protobuf) to permit the latest version.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Changelog](https://github.com/protocolbuffers/protobuf/blob/main/protobuf_release.bzl)
- [Commits](https://github.com/protocolbuffers/protobuf/compare/v5.28.0-rc1...v5.29.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-29 09:21:54 +01:00
Raphael Michel
242e5af4b5 Bump version to 2024.12.0.dev0 2024-11-27 13:57:03 +01:00
Raphael Michel
7d6e98e6da Bump version to 2024.11.0 2024-11-27 13:56:37 +01:00
Mira
27f964f3ae Checkout flow: Observe direction when skipping AddOnsStep (#4658) 2024-11-27 11:13:07 +01:00
Patrick Chilton
84b3060c0f Translations: Update Hungarian
Currently translated at 10.7% (623 of 5782 strings)

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

powered by weblate
2024-11-27 11:10:55 +01:00
CVZ-es
25dcb72f92 Translations: Update Spanish
Currently translated at 100.0% (5782 of 5782 strings)

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

powered by weblate
2024-11-27 11:10:55 +01:00
CVZ-es
4b078867c6 Translations: Update French
Currently translated at 100.0% (5782 of 5782 strings)

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

powered by weblate
2024-11-27 11:10:55 +01:00
Jakub Stribrny
c595a59d4a Translations: Update Czech
Currently translated at 73.5% (4250 of 5782 strings)

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

powered by weblate
2024-11-26 18:48:58 +01:00
Patrick Chilton
f164daeaee Translations: Update Hungarian
Currently translated at 10.7% (621 of 5782 strings)

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

powered by weblate
2024-11-26 18:48:58 +01:00
gabriblas
c6b6dd8d49 Translations: Update Italian
Currently translated at 24.3% (1409 of 5782 strings)

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

powered by weblate
2024-11-26 18:48:58 +01:00
CVZ-es
8038c87963 Translations: Update French
Currently translated at 100.0% (5782 of 5782 strings)

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

powered by weblate
2024-11-26 18:48:58 +01:00
Ryo
c45a970d32 Translations: Update Japanese
Currently translated at 3.8% (220 of 5782 strings)

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

powered by weblate
2024-11-26 18:48:58 +01:00
kei ogane
a34517233d Translations: Update Japanese
Currently translated at 3.8% (220 of 5782 strings)

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

powered by weblate
2024-11-26 18:48:58 +01:00
Yasunobu YesNo Kawaguchi
8fb2e5383c Translations: Update Japanese
Currently translated at 3.4% (200 of 5782 strings)

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

powered by weblate
2024-11-26 18:48:58 +01:00
CVZ-es
86a00f3338 Translations: Update Spanish
Currently translated at 100.0% (5782 of 5782 strings)

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

powered by weblate
2024-11-26 18:48:58 +01:00
CVZ-es
c8c0d3e7f5 Translations: Update French
Currently translated at 100.0% (5782 of 5782 strings)

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

powered by weblate
2024-11-26 18:48:58 +01:00
Raphael Michel
7dd455ce15 Fix #4641 -- Make usage of argon2id optional (#4643) 2024-11-26 17:31:27 +01:00
Richard Schreiber
391eda25da [A11y] Improve color combinations for alerts 2024-11-21 13:58:19 +01:00
Raphael Michel
fcff5a522d Fix inconsistent labels 2024-11-19 16:36:09 +01:00
Raphael Michel
7e93d38a01 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5782 of 5782 strings)

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

powered by weblate
2024-11-19 16:33:52 +01:00
Raphael Michel
6469381899 Translations: Update German
Currently translated at 100.0% (5782 of 5782 strings)

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

powered by weblate
2024-11-19 16:33:52 +01:00
Raphael Michel
761706c60c Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2024-11-19 16:16:36 +01:00
CVZ-es
f91315c88e Translations: Update Spanish
Currently translated at 100.0% (5809 of 5809 strings)

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

powered by weblate
2024-11-19 16:16:13 +01:00
CVZ-es
bc05afeab9 Translations: Update French
Currently translated at 100.0% (5779 of 5779 strings)

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

powered by weblate
2024-11-19 16:16:13 +01:00
Raphael Michel
02d495d287 Revert "Update po files"
This reverts commit 894878d9da.
2024-11-19 16:15:55 +01:00
Raphael Michel
894878d9da Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2024-11-19 16:15:26 +01:00
Raphael Michel
5896ca0197 Event creation: Prevent accidentally creating events without tax rate (#4623)
* Event creation: Prevent accidentally creating events without tax rate

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

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

* Fix tests

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-11-19 16:14:56 +01:00
Raphael Michel
fe6fc8df32 Fix placehodler sample in empty series (PRETIXEU-ATN) 2024-11-19 16:14:13 +01:00
dependabot[bot]
9de8f3a775 Update aiohttp requirement from ==3.10.* to ==3.11.*
Updates the requirements on [aiohttp](https://github.com/aio-libs/aiohttp) to permit the latest version.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.10.0b0...v3.11.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-19 15:08:00 +01:00
Martin Gross
c92bb9cb8b Stripe: (FIX) Make MobilePay optional 2024-11-19 13:17:52 +01:00
Raphael Michel
76ecec8b98 Scheduled mails: Allow subevent-dependent placeholders (Z#23171818) (#4629) 2024-11-19 10:50:10 +01:00
Mira
4b8416df8f docs: update nginx config example (#4640) 2024-11-19 09:28:01 +01:00
Martin Gross
a601c75923 CheckIns: Display a source_type icon (barcode/nfc) where known (#4628)
Co-authored-by: Raphael Michel <michel@rami.io>
2024-11-18 17:50:43 +01:00
Raphael Michel
f94227f00f Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5779 of 5779 strings)

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

powered by weblate
2024-11-18 17:49:24 +01:00
Raphael Michel
a0c1e5369c Translations: Update German
Currently translated at 100.0% (5779 of 5779 strings)

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

powered by weblate
2024-11-18 17:49:24 +01:00
Jakub Stribrny
633bfcf73a Translations: Update Czech
Currently translated at 73.5% (4253 of 5781 strings)

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

powered by weblate
2024-11-18 17:49:24 +01:00
CVZ-es
0d3b5b82c1 Translations: Update Spanish
Currently translated at 100.0% (232 of 232 strings)

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

powered by weblate
2024-11-18 17:49:24 +01:00
CVZ-es
ab95f33546 Translations: Update Spanish
Currently translated at 100.0% (5784 of 5784 strings)

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

powered by weblate
2024-11-18 17:49:24 +01:00
Raphael Michel
5034b366c5 Stripe: Disable SOFORT (#4616)
* Stripe: Disable SOFORT

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

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

---------

Co-authored-by: Martin Gross <gross@rami.io>
2024-11-18 17:24:01 +01:00
Raphael Michel
03d3c389da Fix #1674 -- Change spelling of e-mail to email (#4636)
* Fix #1674 -- Change spelling of e-mail to email

* Conflicts and word list

* Add MobilePay to wordlist

* fix usage in tests
2024-11-18 17:21:29 +01:00
Raphael Michel
3e934acfa0 Allow "open in new tab" for event typeahead (#4631)
* Allow "open in new tab" for event typeahead

* use default link-behaviour for e.g. open in new ...

* navigate in typeahead with tab, add esc to close

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-11-18 17:16:02 +01:00
Raphael Michel
d2a364e848 Docs: Fix documentation of invoice API 2024-11-18 17:08:51 +01:00
Martin Gross
2824b40299 Stripe: Add MobilePay as a supported payment method (Z#23172523) (#4635)
Co-authored-by: robbi5 <richt@rami.io>
2024-11-18 15:03:32 +01:00
Damiano
c6c2c90908 Translations: Update Italian
Currently translated at 24.3% (1405 of 5781 strings)

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

powered by weblate
2024-11-18 10:48:18 +01:00
Pavle Ergović
d4ae7df2ec Translations: Update Croatian
Currently translated at 1.7% (4 of 232 strings)

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

powered by weblate
2024-11-18 10:48:18 +01:00
Pavle Ergović
79dd7fb596 Translations: Update Croatian
Currently translated at 0.3% (23 of 5781 strings)

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

powered by weblate
2024-11-18 10:48:18 +01:00
CVZ-es
5ed87cd019 Translations: Update Spanish
Currently translated at 100.0% (5784 of 5784 strings)

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

powered by weblate
2024-11-18 10:48:18 +01:00
CVZ-es
ccdcbe0cc5 Translations: Update French
Currently translated at 100.0% (232 of 232 strings)

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

powered by weblate
2024-11-18 10:48:18 +01:00
CVZ-es
4f8607a9db Translations: Update French
Currently translated at 100.0% (5781 of 5781 strings)

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

powered by weblate
2024-11-18 10:48:18 +01:00
Patrick Chilton
57ecaa2676 Translations: Update Hungarian
Currently translated at 10.7% (622 of 5781 strings)

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

powered by weblate
2024-11-18 10:48:18 +01:00
CVZ-es
96fd2b1a95 Translations: Update Spanish
Currently translated at 100.0% (232 of 232 strings)

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

powered by weblate
2024-11-18 10:48:18 +01:00
CVZ-es
5cf24fb6a6 Translations: Update Spanish
Currently translated at 100.0% (5784 of 5784 strings)

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

powered by weblate
2024-11-18 10:48:18 +01:00
Eva-Maria Obermann
1d2ea35a39 Translations: Update French
Currently translated at 100.0% (232 of 232 strings)

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

powered by weblate
2024-11-18 10:48:18 +01:00
CVZ-es
ac98ae7941 Translations: Update French
Currently translated at 100.0% (232 of 232 strings)

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

powered by weblate
2024-11-18 10:48:18 +01:00
CVZ-es
a0d055e202 Translations: Update French
Currently translated at 99.3% (5742 of 5781 strings)

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

powered by weblate
2024-11-18 10:48:18 +01:00
Raphael Michel
27ec5ca006 Change order of menu items in backend (#4633) 2024-11-18 10:45:27 +01:00
Eva-Maria Obermann
9d2edc405d Translations: Update French
Currently translated at 98.7% (229 of 232 strings)

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

powered by weblate
2024-11-15 17:21:50 +01:00
CVZ-es
fb95fe7cf6 Translations: Update French
Currently translated at 94.6% (5474 of 5781 strings)

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

powered by weblate
2024-11-15 17:21:50 +01:00
CVZ-es
5b5360ef8b Translations: Update French
Currently translated at 98.7% (229 of 232 strings)

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

powered by weblate
2024-11-15 17:21:50 +01:00
Gravity Fox
129d10ca35 Translations: Update Portuguese (Brazil)
Currently translated at 13.5% (784 of 5781 strings)

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

powered by weblate
2024-11-15 17:21:50 +01:00
CVZ-es
093a705ff9 Translations: Update French
Currently translated at 94.6% (5474 of 5781 strings)

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

powered by weblate
2024-11-15 17:21:50 +01:00
CVZ-es
6130ae4630 Translations: Update Spanish
Currently translated at 100.0% (232 of 232 strings)

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

powered by weblate
2024-11-15 17:21:50 +01:00
CVZ-es
11a8ed6c7a Translations: Update Spanish
Currently translated at 100.0% (5784 of 5784 strings)

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

powered by weblate
2024-11-15 17:21:50 +01:00
arjan-s
f6392592c5 Translations: Update Dutch
Currently translated at 100.0% (5784 of 5784 strings)

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

powered by weblate
2024-11-15 17:21:50 +01:00
CVZ-es
ecb9ad28ea Translations: Update Spanish
Currently translated at 100.0% (5784 of 5784 strings)

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

powered by weblate
2024-11-15 17:21:50 +01:00
Raphael Michel
45a506fd37 Translations: Update German
Currently translated at 100.0% (5781 of 5781 strings)

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

powered by weblate
2024-11-15 17:21:50 +01:00
Raphael Michel
3b16e6356b Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5781 of 5781 strings)

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

powered by weblate
2024-11-15 17:21:50 +01:00
Raphael Michel
9583a50c4e Voucher import: Allow empty subevent (Z#23171356) (#4622) 2024-11-13 12:00:48 +01:00
Martin Gross
6e6d6b2746 PayPal2: Skip webhook order capture if no OrderPayment exists yet 2024-11-13 10:13:50 +01:00
arjan-s
7266d90c6b Translations: Update Dutch
Currently translated at 99.0% (5731 of 5784 strings)

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

powered by weblate
2024-11-12 09:36:00 +01:00
Richard Schreiber
5e4e88c91d Fix creating to many ScheduledMails on subevent creation (#4620) 2024-11-12 09:22:25 +01:00
Raphael Michel
e74d12e8b8 API: Don't require sales channel in input (PRETIXEU-AST) 2024-11-11 17:22:06 +01:00
Raphael Michel
a5c39271dd Make API security profiles pluggable (#4597)
* Make API security profiles pluggable

* Update src/pretix/api/signals.py

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

* REmove dead class

---------

Co-authored-by: robbi5 <richt@rami.io>
2024-11-11 17:13:53 +01:00
Mira
3170744c56 Bleach 6 update (#4610)
* Update bleach requirement from ==5.0.* to ==6.2.*

Updates the requirements on [bleach](https://github.com/mozilla/bleach) to permit the latest version.
- [Changelog](https://github.com/mozilla/bleach/blob/main/CHANGES)
- [Commits](https://github.com/mozilla/bleach/compare/v5.0.0...v6.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

* Update bleach parameter types

* Fix tests

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-11 15:34:40 +01:00
Damiano
9ec161561b Translations: Update Italian
Currently translated at 24.2% (1402 of 5781 strings)

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

powered by weblate
2024-11-11 13:49:23 +01:00
CVZ-es
aff4f4b8f8 Translations: Update Spanish
Currently translated at 100.0% (5784 of 5784 strings)

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

powered by weblate
2024-11-11 13:49:23 +01:00
Damiano
75addfe9f4 Translations: Update Italian
Currently translated at 24.0% (1393 of 5781 strings)

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

powered by weblate
2024-11-11 13:49:23 +01:00
CVZ-es
4b05ce5835 Translations: Update Spanish
Currently translated at 100.0% (5784 of 5784 strings)

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

powered by weblate
2024-11-11 13:49:23 +01:00
CVZ-es
34c247f423 Translations: Update Spanish
Currently translated at 100.0% (5774 of 5774 strings)

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

powered by weblate
2024-11-11 13:49:23 +01:00
arjan-s
3aad6852cb Translations: Update Dutch
Currently translated at 96.8% (5592 of 5774 strings)

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

powered by weblate
2024-11-11 13:49:23 +01:00
CVZ-es
5cdb07bce6 Translations: Update Spanish
Currently translated at 100.0% (5774 of 5774 strings)

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

powered by weblate
2024-11-11 13:49:23 +01:00
arjan-s
6cb2d68948 Translations: Update Dutch
Currently translated at 76.7% (178 of 232 strings)

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

powered by weblate
2024-11-11 13:49:23 +01:00
arjan-s
4a7a6273c6 Translations: Update Dutch
Currently translated at 95.8% (5535 of 5774 strings)

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

powered by weblate
2024-11-11 13:49:23 +01:00
Raphael Michel
ebe343458a Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2024-11-08 14:45:14 +01:00
Mira
f9a93b765c Use correct LogEntry type for changes to product categories (#4617) 2024-11-08 10:45:46 +01:00
Raphael Michel
5aba1f9a23 Do not try to delete cart positions twice (PRETIXEU-ARN) 2024-11-07 14:28:35 +01:00
Damiano
a4eed87396 Translations: Update Italian
Currently translated at 23.7% (1370 of 5774 strings)

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

powered by weblate
2024-11-07 11:27:00 +01:00
Damiano
08879d0d55 Translations: Update Italian
Currently translated at 23.5% (1361 of 5774 strings)

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

powered by weblate
2024-11-07 11:27:00 +01:00
Christiaan de Die le Clercq
c276a19bcc Translations: Update Dutch
Currently translated at 87.7% (5067 of 5774 strings)

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

powered by weblate
2024-11-07 11:27:00 +01:00
Raphael Michel
1e3c6e0b68 Order detail: Do not show expire date for pending approval (Z#23171168) (#4613) 2024-11-07 09:26:10 +01:00
CVZ-es
4e283eb560 Translations: Update Spanish
Currently translated at 100.0% (5774 of 5774 strings)

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

powered by weblate
2024-11-06 12:32:10 +01:00
CVZ-es
52a1983630 Translations: Update French
Currently translated at 94.8% (5475 of 5774 strings)

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

powered by weblate
2024-11-06 12:32:10 +01:00
Raphael Michel
3d85d9d865 Remove deprecated auto_checkin_sales_channels (#4587)
* Remove deprecated auto_checkin_sales_channels

* Fix Query count
2024-11-06 12:30:41 +01:00
Raphael Michel
4ca9a43890 Fix performance issue in filtering checkin list (Z#23170917) (#4607)
* Fix performance issue in filtering checkin list

* remove test
2024-11-06 12:30:37 +01:00
mscherer
d8bac7db65 Remove slimit dependencies (#4609)
It doesn't work anymore on python 3.13, since
2to3 is deprecated and removed (PEP 594).

Trying to build on Fedora 41 result in:

	Downloading slimit-0.8.1.zip (88 kB)
	Installing build dependencies: started
	Installing build dependencies: finished with status 'done'
	Getting requirements to build wheel: started
	Getting requirements to build wheel: finished with status 'error'
	error: subprocess-exited-with-error
	× Getting requirements to build wheel did not run successfully.
	│ exit code: 1
	╰─> [1 lines of output]
	Python 3.X support requires the 2to3 tool.
	[end of output]

And slimit is not used in the code base nor anywhere in git (no single
match)
2024-11-06 12:28:43 +01:00
Mira
91de0f93e6 Allow organizers to manually add fees to an existing order (#4590) 2024-11-05 14:37:50 +01:00
Richard Schreiber
901565203b Hide input dependencies recursively if input will be disabled (#4599)
---------

Co-authored-by: Mira <weller@rami.io>
2024-11-05 14:34:42 +01:00
dependabot[bot]
14c6c9c0d7 Update sentry-sdk requirement from ==2.17.* to ==2.18.* (#4608)
Updates the requirements on [sentry-sdk](https://github.com/getsentry/sentry-python) to permit the latest version.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.17.0...2.18.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-05 09:55:13 +01:00
CVZ-es
6de6cf6c08 Translations: Update Spanish
Currently translated at 100.0% (5774 of 5774 strings)

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

powered by weblate
2024-11-05 09:54:50 +01:00
CVZ-es
29306b3a4d Translations: Update Spanish
Currently translated at 100.0% (5774 of 5774 strings)

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

powered by weblate
2024-11-05 09:54:50 +01:00
Katrine Tella
ca69996611 Translations: Update Danish
Currently translated at 48.6% (2809 of 5774 strings)

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

powered by weblate
2024-11-05 09:54:50 +01:00
Casper Schack
16419b6ae4 Translations: Update Danish
Currently translated at 48.6% (2809 of 5774 strings)

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

powered by weblate
2024-11-05 09:54:50 +01:00
Damiano
d6258b9b54 Translations: Update Italian
Currently translated at 23.3% (1351 of 5774 strings)

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

powered by weblate
2024-11-05 09:54:50 +01:00
Christiaan de Die le Clercq
6f75608196 Translations: Update Dutch
Currently translated at 87.7% (5064 of 5774 strings)

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

powered by weblate
2024-11-05 09:54:50 +01:00
Mika Andrianarijaona
6ef88e009b Translations: Update French
Currently translated at 94.8% (5475 of 5774 strings)

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

powered by weblate
2024-11-05 09:54:50 +01:00
CVZ-es
957100a195 Translations: Update Spanish
Currently translated at 100.0% (232 of 232 strings)

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

powered by weblate
2024-11-05 09:54:50 +01:00
CVZ-es
112ef0908f Translations: Update Spanish
Currently translated at 100.0% (5774 of 5774 strings)

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

powered by weblate
2024-11-05 09:54:50 +01:00
CVZ-es
91aaff7359 Translations: Update Spanish
Currently translated at 96.5% (5575 of 5774 strings)

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

powered by weblate
2024-11-05 09:54:50 +01:00
CVZ-es
8ab61e2c38 Translations: Update French
Currently translated at 91.8% (213 of 232 strings)

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

powered by weblate
2024-11-05 09:54:50 +01:00
CVZ-es
c8ba5cc427 Translations: Update French
Currently translated at 94.8% (5474 of 5774 strings)

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

powered by weblate
2024-11-05 09:54:50 +01:00
CVZ-es
5ebad31b7d Translations: Update Spanish
Currently translated at 96.4% (5567 of 5774 strings)

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

powered by weblate
2024-11-05 09:54:50 +01:00
CVZ-es
0429377f7d Translations: Update French
Currently translated at 91.3% (212 of 232 strings)

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

powered by weblate
2024-11-05 09:54:50 +01:00
Mossroy
76e4b797a1 Translations: Update French
Currently translated at 94.6% (5466 of 5774 strings)

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

powered by weblate
2024-11-05 09:54:50 +01:00
Jonathan Berger
5f0009c996 Translations: Update French
Currently translated at 94.6% (5466 of 5774 strings)

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

powered by weblate
2024-11-05 09:54:50 +01:00
cpoisnel
de63a4be01 Translations: Update French
Currently translated at 94.6% (5466 of 5774 strings)

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

powered by weblate
2024-11-05 09:54:50 +01:00
CVZ-es
f3432139cb Translations: Update French
Currently translated at 94.6% (5466 of 5774 strings)

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

powered by weblate
2024-11-05 09:54:50 +01:00
dependabot[bot]
0b82ac9115 Bump @babel/preset-env in /src/pretix/static/npm_dir (#4604)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.25.4 to 7.26.0.
- [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.26.0/packages/babel-preset-env)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-04 20:55:31 +01:00
Martin Gross
eb685b5141 API: Expose cancellation_date on order endpoint (Z#23170733) (#4606)
Co-authored-by: robbi5 <richt@rami.io>
2024-11-04 16:41:44 +01:00
dependabot[bot]
5f7f0bd8f1 Bump @babel/core from 7.25.2 to 7.26.0 in /src/pretix/static/npm_dir
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.25.2 to 7.26.0.
- [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.26.0/packages/babel-core)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-04 14:44:32 +01:00
Raphael Michel
9fcef2dcaa API: Fix deleting checkin-lists (Z#23170525) (PRETIXEU-AS5) (#4600) 2024-10-31 17:44:04 +01:00
Raphael Michel
fc3b186b93 Badge export: Allow to filter and sort by order date (Z#23168742) (#4588) 2024-10-31 14:42:44 +01:00
Tinna Sandström
a406884575 Translations: Update Swedish
Currently translated at 98.3% (5679 of 5774 strings)

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

powered by weblate
2024-10-31 13:02:34 +01:00
Anarion Dunedain
57ccd5f289 Translations: Update Polish
Currently translated at 100.0% (5774 of 5774 strings)

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

powered by weblate
2024-10-31 13:02:34 +01:00
Mira
f4ac7e7f65 Add warning that cross-selling can't prevent unwanted combinations (#4596)
* Add warning that cross-selling can't prevent unwanted combinations

* Update src/pretix/control/templates/pretixcontrol/items/category.html

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

---------

Co-authored-by: Raphael Michel <michel@rami.io>
2024-10-31 12:59:28 +01:00
Raphael Michel
81d7045b31 Public event filter: Make "all" option clearer (Z#23169843) (#4585)
* Public event filter: Make "all" option clearer

* Fix widget tests

* Update src/tests/presale/test_widget.py

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

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-10-29 17:31:51 +01:00
Richard Schreiber
f9502a3212 Fix testutils redis mock (#4586) 2024-10-29 16:37:06 +01:00
Richard Schreiber
a31f624417 Dev: add support for asyncio_default_fixture_loop_scope in pytest-asyncio (#4589) 2024-10-29 16:36:43 +01:00
Raphael Michel
3f99e0bece Bump version to 2024.11.0.dev0 2024-10-29 11:47:44 +01:00
Raphael Michel
7e64f2b38a Bump version to 2024.10.0 2024-10-29 11:47:10 +01:00
Raphael Michel
ee2bc93608 Fix handling of device_info_updated signal 2024-10-29 11:30:35 +01:00
Raphael Michel
fb4bed9d0d Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5774 of 5774 strings)

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

powered by weblate
2024-10-29 10:08:27 +01:00
Raphael Michel
aec75e4d0c Translations: Update German
Currently translated at 100.0% (5774 of 5774 strings)

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

powered by weblate
2024-10-29 10:08:27 +01:00
Raphael Michel
e7e41470fb Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2024-10-29 09:54:05 +01:00
Raphael Michel
0aa9dda90a Cross-selling: Use different label if there are no add-on products (#4577)
* Cross-selling: Use different label if there are no add-on products

* Update src/pretix/presale/templates/pretixpresale/event/checkout_addons.html

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

* Fix a11y problem

* Fix headline order

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-10-29 09:53:36 +01:00
Mira
d97c983b6f Fix API serializer for auto_checkin_rules to accept sales channel identifiers (Z#23170095) (#4579)
* Fix API serializer for auto_checkin_rules to accept sales channel identifiers instead of PKs

* False -> false in json
2024-10-29 09:14:22 +01:00
dependabot[bot]
6c957f31ca Update django-hijack requirement from ==3.6.* to ==3.7.* (#4581)
Updates the requirements on [django-hijack](https://github.com/django-hijack/django-hijack) to permit the latest version.
- [Release notes](https://github.com/django-hijack/django-hijack/releases)
- [Changelog](https://github.com/django-hijack/django-hijack/blob/master/docs/release-button.png)
- [Commits](https://github.com/django-hijack/django-hijack/compare/3.6.0...3.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-29 09:14:04 +01:00
dependabot[bot]
8e6b4b3ec7 Update pypdf requirement from ==5.0.* to ==5.1.* (#4582)
Updates the requirements on [pypdf](https://github.com/py-pdf/pypdf) to permit the latest version.
- [Release notes](https://github.com/py-pdf/pypdf/releases)
- [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/py-pdf/pypdf/compare/5.0.0...5.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-29 09:13:58 +01:00
arjan-s
b24de62f73 Translations: Update Dutch
Currently translated at 85.5% (4938 of 5773 strings)

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

powered by weblate
2024-10-29 09:13:45 +01:00
Raphael Michel
cdbd220a12 Fix time zone of parsed print log 2024-10-28 17:49:47 +01:00
Raphael Michel
2f11aee512 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5773 of 5773 strings)

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

powered by weblate
2024-10-28 13:04:02 +01:00
Raphael Michel
8ea475ce39 Translations: Update German
Currently translated at 100.0% (5773 of 5773 strings)

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

powered by weblate
2024-10-28 13:04:02 +01:00
Raphael Michel
b29bc9db96 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5773 of 5773 strings)

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

powered by weblate
2024-10-28 13:04:02 +01:00
Raphael Michel
6bd6694132 Translations: Update German
Currently translated at 100.0% (5773 of 5773 strings)

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

powered by weblate
2024-10-28 13:04:02 +01:00
Raphael Michel
110e6e248e Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2024-10-28 12:31:20 +01:00
Davide Manzella
985f4d969d Translations: Update Italian
Currently translated at 23.1% (1333 of 5762 strings)

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

powered by weblate
2024-10-28 12:30:56 +01:00
Mira
826bd07b01 Add action buttons to "missing quota" warnings (#4533)
* Add action buttons to "missing quota" warnings

* Update item.py
2024-10-28 12:25:03 +01:00
Raphael Michel
3e4e86742a Add print logs (#4475)
* Add print logs

* Add attribute successful

* Rebase migration

* Fix tests on postgres
2024-10-28 12:24:23 +01:00
Raphael Michel
ef5fcde5d9 Seating plans: Validate duplicate seat IDs (#4564) 2024-10-28 11:27:27 +01:00
Raphael Michel
8f1d53d016 Tests: Add separation of database lock indices by pytest worker (#4553)
* Add separation of database lock indices by pytest worker

* Fix ID derivation

* Update conftest.py
2024-10-25 22:05:02 +02:00
Raphael Michel
9ca1573fcf Tests: Remove pytest-rerunfailures (#4572) 2024-10-25 22:00:14 +02:00
Raphael Michel
5795aa6492 Docs: Fix various sphinx warnings 2024-10-25 21:58:14 +02:00
Raphael Michel
6e0613a2af CI: Remove warnings from PostgreSQL about health check (#4571)
* CI: Remove warnings from PostgreSQL about health check

* Update tests.yml

* Update tests.yml
2024-10-25 21:45:34 +02:00
Raphael Michel
b43ed38483 CI: Do not fail if codecov upload fails (#4570)
This happens all the time with external PRs until https://github.com/codecov/engineering-team/issues/1574 is solved and is doing more harm than good. We still see if there is no coverage attached.
2024-10-25 21:40:03 +02:00
Raphael Michel
f0fedf0001 Tests: Resolve deprecation warning for pytest-asyncio (#4569) 2024-10-25 21:27:38 +02:00
Raphael Michel
19373b8f91 CI: Update codecov-action (#4568) 2024-10-25 21:19:48 +02:00
dependabot[bot]
45fd13786a Update pillow requirement from ==10.4.* to ==11.0.* (#4566)
Updates the requirements on [pillow](https://github.com/python-pillow/Pillow) to permit the latest version.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/10.4.0...11.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-25 21:14:30 +02:00
dependabot[bot]
ae5111ee7e Update zeep requirement from ==4.2.* to ==4.3.* (#4567)
Updates the requirements on [zeep](https://github.com/mvantellingen/python-zeep) to permit the latest version.
- [Release notes](https://github.com/mvantellingen/python-zeep/releases)
- [Changelog](https://github.com/mvantellingen/python-zeep/blob/main/CHANGES)
- [Commits](https://github.com/mvantellingen/python-zeep/compare/4.2.0...4.3.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-25 20:53:11 +02:00
dependabot[bot]
d8bf3065d0 Update redis requirement from ==5.1.* to ==5.2.* (#4565)
Updates the requirements on [redis](https://github.com/redis/redis-py) to permit the latest version.
- [Release notes](https://github.com/redis/redis-py/releases)
- [Changelog](https://github.com/redis/redis-py/blob/master/CHANGES)
- [Commits](https://github.com/redis/redis-py/compare/v5.1.0a1...v5.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-25 20:51:30 +02:00
Raphael Michel
54f077665c CI: Remove mentions of travis CI, we have migrated away years ago (#4562) 2024-10-25 17:47:04 +02:00
Raphael Michel
482a66c546 CI: Run PostgreSQL as a native service (#4560)
* GH Actions: Fix PostgreSQL issues

* Map PostgreSQL port
2024-10-25 17:18:42 +02:00
Raphael Michel
e4cef6e46b CI: Make sure apt is non-interactive (to not break act) (#4559) 2024-10-25 17:06:11 +02:00
Raphael Michel
cbee1b71fe CI: Install dependencies with uv for speedup (#4558) 2024-10-25 17:05:58 +02:00
Raphael Michel
0cd1290624 Tests: Fix two flaky tests (#4557)
* Test: Fix test that relies on unstable database result order

* Tests: Fix test that fails between 0am and 1am
2024-10-25 17:04:16 +02:00
Raphael Michel
565f5e2ea7 Tests: Run fakeredis on different virtual ports per pytest worker (#4555) 2024-10-25 16:44:22 +02:00
Raphael Michel
b46c0eba0c pytest: Fix failure with fakeserver and xdist 2024-10-25 14:32:28 +02:00
Raphael Michel
39c3aef7bc Fix fakeredis usage 2024-10-25 12:54:25 +02:00
dependabot[bot]
cf3087453c Update fakeredis requirement from ==2.24.* to ==2.26.* (#4551)
Updates the requirements on [fakeredis](https://github.com/cunla/fakeredis-py) to permit the latest version.
- [Release notes](https://github.com/cunla/fakeredis-py/releases)
- [Commits](https://github.com/cunla/fakeredis-py/compare/v2.24.0...v2.26.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-25 12:28:22 +02:00
Raphael Michel
7a870ee521 Fix test that does not work on postgresql 2024-10-25 00:02:52 +02:00
dependabot[bot]
3922290633 Update dnspython requirement from ==2.6.* to ==2.7.* (#4511)
Updates the requirements on [dnspython](https://github.com/rthalley/dnspython) to permit the latest version.
- [Release notes](https://github.com/rthalley/dnspython/releases)
- [Changelog](https://github.com/rthalley/dnspython/blob/main/doc/whatsnew.rst)
- [Commits](https://github.com/rthalley/dnspython/compare/v2.6.0rc1...v2.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-24 22:54:28 +02:00
dependabot[bot]
8aa13d7e3e Update sentry-sdk requirement from ==2.15.* to ==2.17.* (#4536)
Updates the requirements on [sentry-sdk](https://github.com/getsentry/sentry-python) to permit the latest version.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.15.0...2.17.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-24 22:53:58 +02:00
Raphael Michel
22e9a6eb92 Add warning to backend if cronjob is not running (#4550) 2024-10-24 22:50:34 +02:00
arjan-s
2b6f82502e Translations: Update Dutch
Currently translated at 85.6% (4936 of 5762 strings)

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

powered by weblate
2024-10-24 13:06:26 +02:00
Mira
a10bf2a939 Include "Cross-selling condition products" option when cloning category (#4534)
* Include "Cross-selling condition products" option when cloning category

* Add test case

* Remove print
2024-10-23 17:12:03 +02:00
arjan-s
a80b7087d9 Translations: Update Dutch
Currently translated at 75.8% (176 of 232 strings)

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

powered by weblate
2024-10-23 17:10:32 +02:00
arjan-s
4b143e98eb Translations: Update Dutch
Currently translated at 85.6% (4935 of 5762 strings)

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

powered by weblate
2024-10-23 17:10:32 +02:00
Yasunobu YesNo Kawaguchi
bdb8b597d0 Translations: Update Japanese
Currently translated at 3.4% (198 of 5762 strings)

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

powered by weblate
2024-10-23 17:10:32 +02:00
Ryo Tagami
b1c9f40bc8 Translations: Update Japanese
Currently translated at 3.4% (198 of 5762 strings)

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

powered by weblate
2024-10-23 17:10:32 +02:00
CVZ-es
a3b6a008b5 Translations: Update Spanish
Currently translated at 99.5% (231 of 232 strings)

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

powered by weblate
2024-10-23 17:10:32 +02:00
CVZ-es
9ce05e5cb9 Translations: Update Spanish
Currently translated at 96.4% (5560 of 5762 strings)

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

powered by weblate
2024-10-23 17:10:32 +02:00
CVZ-es
f306527981 Translations: Update Spanish
Currently translated at 95.1% (5485 of 5762 strings)

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

powered by weblate
2024-10-23 17:10:32 +02:00
CVZ-es
3e17ff9faa Translations: Update Spanish
Currently translated at 91.7% (5288 of 5762 strings)

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

powered by weblate
2024-10-23 17:10:32 +02:00
CVZ-es
2a16cd4655 Translations: Update Spanish
Currently translated at 90.7% (5228 of 5762 strings)

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

powered by weblate
2024-10-23 17:10:32 +02:00
Ryo Tagami
d1078da5bf Translations: Update Japanese
Currently translated at 3.3% (195 of 5762 strings)

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

powered by weblate
2024-10-23 17:10:32 +02:00
CVZ-es
483e7bc4ad Translations: Update Spanish
Currently translated at 89.8% (5175 of 5762 strings)

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

powered by weblate
2024-10-23 17:10:32 +02:00
Mira
401218b0a3 API: Allow filtering subevents by is_public (#4544) 2024-10-23 16:42:22 +02:00
Raphael Michel
19175258fd API: Fix event cloning, limit_sales_channels should never be set when all_sales_channels is set (Z#23169537) 2024-10-23 16:30:00 +02:00
Mira
22c36b89da Show proper error message in case of add-ons that are required, sold-out and hidden (#4545) 2024-10-21 16:46:32 +02:00
Mira
2697ed0c5d QuestionView: Carry over subevent filter to order list (#4543) 2024-10-21 14:45:04 +02:00
pretix translation bot
f81d820a02 Update translations (#4537)
Co-authored-by: Patrick Chilton <chpatrick@gmail.com>
Co-authored-by: Lovro <lovrogrilc@gmail.com>
Co-authored-by: CVZ-es <damien.bremont@casadevelazquez.org>
Co-authored-by: Ryo Tagami <rtagami@airstrip.jp>
Co-authored-by: Ryo <saremba@rami.io>
2024-10-21 13:28:34 +02:00
Mira
f8df66e621 Redirect to next step if AddOnsStep would show up empty (Z#23167007) (#4530) 2024-10-21 13:27:43 +02:00
Mira
2d9bfc80dc Fix "Your order qualifies for a discount" notice for discounted variation products (#4541) 2024-10-21 13:26:20 +02:00
Martin Gross
17b2e95569 Questions: Add SubEvent-Filter; Renew Filter-Form Layout (Z#23168996) (#4538) 2024-10-21 13:25:18 +02:00
Martin Gross
e49f938eb3 BulkVouchers: Append mail-recipient to comment instead of overwriting it (Z#23168852) 2024-10-16 10:37:06 +02:00
Martin Gross
8d63906341 Remove nl_BE from setup.cfg 2024-10-15 16:22:50 +02:00
Martin Gross
cfefa1aad0 Remove be_NL again, again, again 2024-10-15 16:17:20 +02:00
Martin Gross
1d16049dc5 add "Cross" and "Selling" (as in: "Cross-Selling") to de/de_Informal wordlist 2024-10-15 14:48:25 +02:00
Martin Gross
8452899edd add "Cross-Selling" to de/de_Informal wordlist 2024-10-15 14:43:31 +02:00
pretix translation bot
d67ebc0f80 Update translations (#4529) 2024-10-15 14:03:10 +02:00
Mira Weller
0e87f03e1e Update po files
[CI skip]

Signed-off-by: Mira Weller <weller@rami.io>
2024-10-15 13:14:42 +02:00
Mira Weller
868408ea55 Fix typo 2024-10-15 13:13:43 +02:00
Mira Weller
fc75cd35f8 Update po files
[CI skip]

Signed-off-by: Mira Weller <weller@rami.io>
2024-10-15 13:06:53 +02:00
Mira
a3e2540331 Display "Normal category" instead of "None" in category list (#4526) 2024-10-15 12:14:15 +02:00
Raphael Michel
99ce7effde Add ticket renderer RPC API (Z#23165429) (#4525)
---------

Co-authored-by: Mira Weller <weller@rami.io>
2024-10-15 12:11:09 +02:00
Raphael Michel
0d645fc4c5 Hotfix broken query away 2024-10-14 18:17:30 +02:00
Mira
359df1f51e Cross selling (#4185)
Product categories can now be marked as "cross-selling categories", causing them to 
appear in the add-on checkout step as additional recommendations, depending on 
their cross-selling visibility (always, only if certain products are already in the cart, or 
only if they qualify for a discount according to discount rules).

---------

Co-authored-by: Raphael Michel <michel@rami.io>
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-10-14 14:39:49 +02:00
Raphael Michel
7607cc5d2f Fix indentation 2024-10-14 09:26:56 +02:00
Raphael Michel
40c8d014df Waiting list: Redirect to shop if no products can be awaited (Z#23168172) (#4517)
* Waiting list: Redirect to shop if no products can be awaited (Z#23168172)

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

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

---------

Co-authored-by: Mira <weller@rami.io>
2024-10-14 09:26:06 +02:00
Mira
c10efc692d Let organizers specify a helptext for the custom invoice field (Z#23167497) (#4520) 2024-10-14 09:22:58 +02:00
Raphael Michel
8f0a277c7b Fix tax rule calculation of negative amounts (PRETIXEU-ANN) 2024-10-11 15:28:07 +02:00
George Hickman
9dc38e42d8 Add device_changed signal (#4412)
This provides both the original and updated version of the Device so
subscribers can see the changes.
2024-10-11 11:08:23 +02:00
Raphael Michel
bfd88d1496 Docs: Fix wrong field name in example 2024-10-10 13:51:33 +02:00
Patrick Chilton
be6bd501bd Translations: Update Hungarian
Currently translated at 10.5% (608 of 5745 strings)

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

powered by weblate
2024-10-10 09:16:51 +02:00
Raphael Michel
d160c9fd67 Fix crash in checkin list action view (PRETIXEU-AN8) 2024-10-09 17:11:10 +02:00
Raphael Michel
221f14cc21 API: Fix crash PRETIXEU-AN5 2024-10-09 12:25:36 +02:00
Felix Schäfer
1dda2eb4fb Fix reauth loops with redirect style authentication plugins (#4512)
* Test reauth with redirect style auth #4498

* Fix reauth loops with redirect style auth #4498
2024-10-09 09:24:49 +02:00
Davide Manzella
30f2e99020 Translations: Update Italian
Currently translated at 22.2% (1279 of 5745 strings)

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

powered by weblate
2024-10-09 09:16:52 +02:00
Raphael Michel
8efe276ed0 Fix negative prices in bundles when tax rate is 0 (#4513) 2024-10-09 08:16:01 +02:00
Raphael Michel
61b25acdd2 Fix email confirm hash in templates 2024-10-07 17:54:40 +02:00
Raphael Michel
6cc9529d9a Authentication: Support for fallback secret keys in get_session_auth_hash (#4481)
* Authentication: Support for fallback secret keys in get_session_auth_hash

* Update src/pretix/presale/utils.py

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

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-10-07 16:58:37 +02:00
Raphael Michel
cdc5401dc2 Allow to set fallback secret keys (#4482) 2024-10-07 16:31:24 +02:00
Raphael Michel
1334a570e4 Generate email confirmation secret from tagged_secret (#4480) 2024-10-07 13:58:08 +02:00
Raphael Michel
7a66aea2cb Voucher update: Allow to remove seat 2024-10-07 11:42:28 +02:00
dependabot[bot]
ee77a5e447 Bump @rollup/plugin-node-resolve from 15.2.3 to 15.3.0 in /src/pretix/static/npm_dir (#4499)
Bumps [@rollup/plugin-node-resolve](https://github.com/rollup/plugins/tree/HEAD/packages/node-resolve) from 15.2.3 to 15.3.0.
- [Changelog](https://github.com/rollup/plugins/blob/master/packages/node-resolve/CHANGELOG.md)
- [Commits](https://github.com/rollup/plugins/commits/node-resolve-v15.3.0/packages/node-resolve)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-07 11:36:23 +02:00
Raphael Michel
827e127568 Fix #4365 -- Missing date in timeline 2024-10-04 16:17:52 +02:00
dependabot[bot]
ce0e0d7fd1 Update qrcode requirement from ==7.4.* to ==8.0 (#4500)
Updates the requirements on [qrcode](https://github.com/lincolnloop/python-qrcode) to permit the latest version.
- [Changelog](https://github.com/lincolnloop/python-qrcode/blob/main/CHANGES.rst)
- [Commits](https://github.com/lincolnloop/python-qrcode/compare/v7.4...v8.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-04 16:03:04 +02:00
dependabot[bot]
152a956dc5 Update sentry-sdk requirement from ==2.14.* to ==2.15.*
Updates the requirements on [sentry-sdk](https://github.com/getsentry/sentry-python) to permit the latest version.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.14.0...2.15.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-04 11:53:13 +02:00
Patrick Chilton
68e2c355e6 Translations: Update Hungarian
Currently translated at 10.3% (594 of 5745 strings)

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

powered by weblate
2024-10-04 11:48:19 +02:00
Patrick Chilton
171615558f Translations: Update Hungarian
Currently translated at 8.8% (506 of 5745 strings)

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

powered by weblate
2024-10-04 11:48:19 +02:00
Patrick Chilton
a1765910ea Translations: Update Hungarian
Currently translated at 45.2% (105 of 232 strings)

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

powered by weblate
2024-10-04 11:48:19 +02:00
Patrick Chilton
417277958b Translations: Update Hungarian
Currently translated at 8.7% (504 of 5745 strings)

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

powered by weblate
2024-10-04 11:48:19 +02:00
Patrick Chilton
0d50494e89 Translations: Update Hungarian
Currently translated at 43.9% (102 of 232 strings)

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

powered by weblate
2024-10-04 11:48:19 +02:00
Patrick Chilton
c6f634ce72 Translations: Update Hungarian
Currently translated at 7.9% (456 of 5745 strings)

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

powered by weblate
2024-10-04 11:48:19 +02:00
Patrick Chilton
adc78c14ab Translations: Update Hungarian
Currently translated at 2.9% (170 of 5745 strings)

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

powered by weblate
2024-10-04 11:48:19 +02:00
dependabot[bot]
b4ca2bdbb4 Update pycryptodome requirement from ==3.20.* to ==3.21.* (#4504)
Updates the requirements on [pycryptodome](https://github.com/Legrandin/pycryptodome) to permit the latest version.
- [Release notes](https://github.com/Legrandin/pycryptodome/releases)
- [Changelog](https://github.com/Legrandin/pycryptodome/blob/master/Changelog.rst)
- [Commits](https://github.com/Legrandin/pycryptodome/compare/v3.20.0x...v3.21.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-04 11:48:04 +02:00
dependabot[bot]
9a7ff592af Update redis requirement from ==5.0.* to ==5.1.* (#4491)
Updates the requirements on [redis](https://github.com/redis/redis-py) to permit the latest version.
- [Release notes](https://github.com/redis/redis-py/releases)
- [Changelog](https://github.com/redis/redis-py/blob/master/CHANGES)
- [Commits](https://github.com/redis/redis-py/compare/v5.0.0b1...v5.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 16:30:49 +02:00
Rosariocastellana
548b54cca6 Translations: Update Italian
Currently translated at 20.6% (1188 of 5745 strings)

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

powered by weblate
2024-09-30 13:51:23 +02:00
Anarion Dunedain
e736791446 Translations: Update Polish
Currently translated at 100.0% (5745 of 5745 strings)

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

powered by weblate
2024-09-30 13:51:23 +02:00
Raphael Michel
7bd945b2e6 Waiting list: Add warning when sold out products are hidden 2024-09-30 13:24:43 +02:00
Raphael Michel
a07d5aaf05 Add-on step: Minimal performance improvement 2024-09-27 13:26:30 +02:00
Raphael Michel
0cf1a32902 Bump version to 2024.10.0.dev0 2024-09-27 09:46:47 +02:00
Raphael Michel
be6aae8577 Bump version to 2024.9.0 2024-09-27 09:46:18 +02:00
Mira
fe80f5fb78 Utils for internal plugin (#4483)
* Add full_code property to OrderPosition

* Add inline "json_script" as supported data source for select2

* Use shorter OrderPosition.code
2024-09-26 19:29:33 +02:00
Raphael Michel
a2c15ad89e Check-in: Prevent duplicate auto check-outs (Z#23167003) (#4488) 2024-09-26 17:54:27 +02:00
Raphael Michel
cab0f37830 Translations: Update German
Currently translated at 100.0% (5745 of 5745 strings)

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

powered by weblate
2024-09-26 17:51:57 +02:00
Raphael Michel
0423980058 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5745 of 5745 strings)

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

powered by weblate
2024-09-26 17:51:57 +02:00
Raphael Michel
63983b1b68 Translations: Update spellcheck lists 2024-09-26 16:52:28 +02:00
Raphael Michel
61241c2a1e Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2024-09-26 13:23:02 +02:00
Raphael Michel
4069c61054 runperiodic: Allow to list all tasks 2024-09-26 10:32:53 +02:00
Raphael Michel
9bf4fb2d0f Sendmail: Fix incorrect filtering of subevents when mailing to attendees (Z#23166274) (#4474) 2024-09-25 17:55:33 +02:00
Raphael Michel
ff910f293f Item list: Show tax rate even for products with 0 price
Might be neccessary for price input, variations, etc
2024-09-25 12:42:45 +02:00
Raphael Michel
74f7bec617 Change wording in a headline to avoid double-using a term 2024-09-25 12:42:45 +02:00
Albizuri
467a35e353 Translations: Update Basque
Currently translated at 56.0% (3213 of 5737 strings)

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

powered by weblate
2024-09-24 10:13:22 +02:00
dependabot[bot]
770c13a4f0 Update pypdf requirement from ==4.3.* to ==5.0.* (#4471)
Updates the requirements on [pypdf](https://github.com/py-pdf/pypdf) to permit the latest version.
- [Release notes](https://github.com/py-pdf/pypdf/releases)
- [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/py-pdf/pypdf/compare/4.3.0...5.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-19 11:34:23 +02:00
dependabot[bot]
5373d4d8ba Bump django-bootstrap3 from 24.2 to 24.3
Bumps [django-bootstrap3](https://github.com/zostera/django-bootstrap3) from 24.2 to 24.3.
- [Changelog](https://github.com/zostera/django-bootstrap3/blob/main/CHANGELOG.md)
- [Commits](https://github.com/zostera/django-bootstrap3/compare/v24.2...v24.3)

---
updated-dependencies:
- dependency-name: django-bootstrap3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-19 11:28:15 +02:00
Raphael Michel
42e673b5f6 Fix missing JS file name 2024-09-19 09:39:49 +02:00
Albizuri
7af2f2a87b Translations: Update Basque
Currently translated at 56.0% (3213 of 5737 strings)

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

powered by weblate
2024-09-18 16:25:32 +02:00
Tinna Sandström
e408521769 Translations: Update Swedish
Currently translated at 99.1% (5686 of 5736 strings)

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

powered by weblate
2024-09-18 16:25:32 +02:00
Raphael Michel
8ed0d36346 Fix another wrong waiting_list_active reference 2024-09-18 10:28:41 +02:00
Raphael Michel
14cbe99667 Stripe: Fix a leftover } 2024-09-17 17:54:57 +02:00
Raphael Michel
b059995eff Widget: Fix waiting list disable time based on subevent 2024-09-17 17:39:22 +02:00
Raphael Michel
100e8d0a4b Fix #4454 -- Resolve deprecation warning from celery 2024-09-17 13:36:35 +02:00
Raphael Michel
eb92e4d8e6 Render progress info on non-javascript fallback page for celery tasks (#4452)
* Render progress info on non-javascript fallback page for celery tasks

* Review notes
2024-09-17 13:29:27 +02:00
Raphael Michel
32d6ded003 Stricten password validation to match PCI DSS requirements (#4467)
* Stricten password validation to match PCI DSS requirements

* Review fix

* Fix a file header
2024-09-17 13:29:17 +02:00
Raphael Michel
aa07533693 Order import: Allow to set check-in attention and text (Z#23165806) (#4469) 2024-09-17 11:50:19 +02:00
dependabot[bot]
e7d01f91a6 Bump @babel/preset-env from 7.25.3 to 7.25.4 in /src/pretix/static/npm_dir (#4438)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.25.3 to 7.25.4.
- [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.25.4/packages/babel-preset-env)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-17 11:42:33 +02:00
Raphael Michel
9616369f07 Formally deprecate eu_reverse_charge (#4470) 2024-09-17 11:39:32 +02:00
dependabot[bot]
af606090ba Update sentry-sdk requirement from ==2.13.* to ==2.14.* (#4455)
Updates the requirements on [sentry-sdk](https://github.com/getsentry/sentry-python) to permit the latest version.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.13.0...2.14.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-16 16:46:28 +02:00
Svyatoslav
931f3eca1b Translations: Update Estonian
Currently translated at 0.1% (1 of 5736 strings)

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

powered by weblate
2024-09-16 16:44:46 +02:00
Svyatoslav
36f306120e Translations: Update Latvian
Currently translated at 37.2% (2134 of 5736 strings)

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

powered by weblate
2024-09-16 16:44:46 +02:00
Svyatoslav
a3ba0c97e9 Translations: Update Russian
Currently translated at 19.1% (1097 of 5736 strings)

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

powered by weblate
2024-09-16 16:44:46 +02:00
Richard Schreiber
484d24b66c Translations: Add Estonian 2024-09-16 16:44:46 +02:00
Tinna Sandström
2d39d3cc8e Translations: Update Swedish
Currently translated at 99.1% (5686 of 5736 strings)

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

powered by weblate
2024-09-16 16:44:46 +02:00
Kristian Feldsam
78b1adf423 Translations: Update Slovak
Currently translated at 29.3% (68 of 232 strings)

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

powered by weblate
2024-09-16 16:44:46 +02:00
Kristian Feldsam
c3eedcc396 Translations: Update Slovak
Currently translated at 92.5% (5307 of 5736 strings)

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

powered by weblate
2024-09-16 16:44:46 +02:00
Tinna Sandström
682c328390 Translations: Update Swedish
Currently translated at 99.1% (5686 of 5736 strings)

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

powered by weblate
2024-09-16 16:44:46 +02:00
Wikinaut
5230827f5e Translations: Update German
Currently translated at 100.0% (5736 of 5736 strings)

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

powered by weblate
2024-09-16 16:44:46 +02:00
Arthur Nunes
dad9915435 Translations: Update Portuguese (Brazil)
Currently translated at 13.2% (759 of 5736 strings)

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

powered by weblate
2024-09-16 16:44:46 +02:00
Wikinaut
a9d2c1eb34 Translations: Update German
Currently translated at 100.0% (5736 of 5736 strings)

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

powered by weblate
2024-09-16 16:44:46 +02:00
Tinna Sandström
66fe45a478 Translations: Update Swedish
Currently translated at 86.2% (200 of 232 strings)

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

powered by weblate
2024-09-12 09:32:41 +02:00
Raphael Michel
24e2b1b9ab Import: Parse iso dates 2024-09-12 09:32:25 +02:00
Martin Gross
eebdce80cd OIDC: Respect token_endpoint_auth_methods_supported (Z#2164777) (#4459) 2024-09-11 16:40:42 +02:00
Kristian Feldsam
09af95ec20 Translations: Update Slovak
Currently translated at 92.1% (5283 of 5736 strings)

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

powered by weblate
2024-09-11 16:01:16 +02:00
Tinna Sandström
1ade674beb Translations: Update Swedish
Currently translated at 99.1% (5686 of 5736 strings)

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

powered by weblate
2024-09-11 16:01:16 +02:00
Kristian Feldsam
76ff59f9c2 Translations: Update Slovak
Currently translated at 6.4% (15 of 232 strings)

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

powered by weblate
2024-09-11 16:01:16 +02:00
Kristian Feldsam
0986522c2f Translations: Update Slovak
Currently translated at 6.0% (14 of 232 strings)

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

powered by weblate
2024-09-11 16:01:16 +02:00
Kristian Feldsam
91f4e731da Translations: Update Slovak
Currently translated at 92.0% (5280 of 5736 strings)

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

powered by weblate
2024-09-11 16:01:16 +02:00
Raphael Michel
98709286c6 Order cancellation: Fix crash on deleted order (PRETIXEU-AHP) 2024-09-10 14:00:11 +02:00
Raphael Michel
667c2555b2 AsyncMixin: Fix crash on invalid query string (PRETIXEU-AHG) 2024-09-10 14:00:08 +02:00
Raphael Michel
6f5acb1ca7 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (232 of 232 strings)

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

powered by weblate
2024-09-10 09:19:12 +02:00
Raphael Michel
65ec3e3fd6 Translations: Update German
Currently translated at 100.0% (232 of 232 strings)

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

powered by weblate
2024-09-10 09:19:12 +02:00
Richard Schreiber
1a8d0a973d Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5736 of 5736 strings)

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

powered by weblate
2024-09-10 09:19:12 +02:00
Richard Schreiber
3c94631405 Translations: Update German
Currently translated at 100.0% (5736 of 5736 strings)

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

powered by weblate
2024-09-10 09:19:12 +02:00
Kristian Feldsam
1dda7732a5 Translations: Update Slovak
Currently translated at 4.7% (11 of 232 strings)

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

powered by weblate
2024-09-10 09:19:12 +02:00
Kristian Feldsam
33accf5f99 Translations: Update Slovak
Currently translated at 92.0% (5279 of 5736 strings)

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

powered by weblate
2024-09-10 09:19:12 +02:00
Tinna Sandström
be2efd9df2 Translations: Update Swedish
Currently translated at 99.1% (5686 of 5736 strings)

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

powered by weblate
2024-09-10 09:19:12 +02:00
Ahmad AlHarthi
fe69137a4e Translations: Update Arabic
Currently translated at 65.1% (3736 of 5736 strings)

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

powered by weblate
2024-09-09 11:18:16 +02:00
Ayden Jahola
7ccfb3a27a Translations: Update Arabic
Currently translated at 65.1% (3736 of 5736 strings)

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

powered by weblate
2024-09-09 11:18:16 +02:00
Ayden Jahola
b7205622dc Translations: Update Arabic
Currently translated at 65.1% (3736 of 5736 strings)

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

powered by weblate
2024-09-09 11:18:16 +02:00
Ahmad AlHarthi
44da5b81b1 Translations: Update Arabic
Currently translated at 65.1% (3736 of 5736 strings)

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

powered by weblate
2024-09-09 11:18:16 +02:00
Martin Gross
5a058342a6 Translations: Update Portuguese (Portugal)
Currently translated at 56.8% (132 of 232 strings)

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

powered by weblate
2024-09-09 11:18:16 +02:00
Martin Gross
2d15dc7ce5 Translations: Update Portuguese (Portugal)
Currently translated at 56.8% (132 of 232 strings)

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

powered by weblate
2024-09-09 11:18:16 +02:00
Martin Gross
dd4ccc864e Translations: Update Portuguese (Portugal)
Currently translated at 86.7% (4977 of 5736 strings)

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

powered by weblate
2024-09-09 11:18:16 +02:00
Kristian Feldsam
b812f0affe Translations: Update Slovak
Currently translated at 3.8% (9 of 232 strings)

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

powered by weblate
2024-09-09 11:18:16 +02:00
Kristian Feldsam
2af4183ce6 Translations: Update Slovak
Currently translated at 92.0% (5279 of 5736 strings)

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

powered by weblate
2024-09-09 11:18:16 +02:00
Albizuri
8ac0b93ca5 Translations: Update Basque
Currently translated at 55.9% (3209 of 5737 strings)

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

powered by weblate
2024-09-09 11:18:16 +02:00
Ronisson Cabral
51a1193f32 Translations: Update Portuguese (Portugal)
Currently translated at 56.0% (130 of 232 strings)

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

powered by weblate
2024-09-09 11:18:16 +02:00
Ronisson Cabral
002da2c9b7 Translations: Update Portuguese (Portugal)
Currently translated at 86.2% (4948 of 5736 strings)

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

powered by weblate
2024-09-09 11:18:16 +02:00
Martin Gross
9a2ebe4e95 Refunds: Fix sortkey to work with not provided BICs (Z#23165187) (#4451) 2024-09-09 10:48:53 +02:00
Albizuri
bc6da2512a Translations: Update Basque
Currently translated at 48.4% (113 of 233 strings)

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

powered by weblate
2024-09-06 10:51:38 +02:00
Albizuri
6378dc69b8 Translations: Update Basque
Currently translated at 56.3% (3231 of 5737 strings)

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

powered by weblate
2024-09-06 10:51:38 +02:00
Richard Schreiber
2b53d04a19 Improve labels in mail settings for incomplete payments (#4444) 2024-09-06 08:53:08 +02:00
Richard Schreiber
7efe7b5ff7 Docs: fix typo for SSO provider instead of client 2024-09-05 13:02:20 +02:00
Alberto Ortega
ae5464d486 Translations: Update Spanish
Currently translated at 87.8% (5038 of 5736 strings)

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

powered by weblate
2024-09-05 11:09:47 +02:00
Tinna Sandström
67fec8d1f6 Translations: Update Swedish
Currently translated at 99.1% (5686 of 5736 strings)

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

powered by weblate
2024-09-05 11:09:47 +02:00
Albizuri
95a081676b Translations: Update Basque
Currently translated at 56.2% (3225 of 5737 strings)

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

powered by weblate
2024-08-30 09:17:49 +02:00
dependabot[bot]
7228a6304d Update protobuf requirement from ==5.27.* to ==5.28.* (#4430)
Updates the requirements on [protobuf](https://github.com/protocolbuffers/protobuf) to permit the latest version.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Changelog](https://github.com/protocolbuffers/protobuf/blob/main/protobuf_release.bzl)
- [Commits](https://github.com/protocolbuffers/protobuf/compare/v5.27.0-rc1...v5.28.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-30 09:11:23 +02:00
Raphael Michel
04b9134e36 Add Basque to languages 2024-08-29 16:52:54 +02:00
Albizuri
2e0769bc41 Translations: Update Basque
Currently translated at 54.4% (3126 of 5737 strings)

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

powered by weblate
2024-08-29 14:51:49 +02:00
Albizuri
4d2f854710 Translations: Update Basque
Currently translated at 48.0% (112 of 233 strings)

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

powered by weblate
2024-08-29 14:51:49 +02:00
Albizuri
b9ac9496d2 Translations: Update Basque
Currently translated at 52.7% (3026 of 5737 strings)

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

powered by weblate
2024-08-29 14:51:49 +02:00
Albizuri
a975f5dc50 Translations: Update Basque
Currently translated at 47.2% (110 of 233 strings)

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

powered by weblate
2024-08-29 14:51:49 +02:00
Albizuri
4ea1f6284a Translations: Update Basque
Currently translated at 47.5% (2726 of 5737 strings)

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

powered by weblate
2024-08-29 14:51:49 +02:00
Raphael Michel
a01d105829 Bank Transfer: Fix weird error code on duplicate match (Z#23164140) (#4428) 2024-08-29 13:58:37 +02:00
Raphael Michel
b1bfa1acee API: Provide useful device_id in checkins (Z#23163025) (#4427) 2024-08-29 12:36:27 +02:00
Raphael Michel
0b4e99c2d8 Fix inprecise wording 2024-08-28 16:41:37 +02:00
Raphael Michel
0cdce7a9cd Add payment_explanation to payment method change (Z#23164112) (#4424) 2024-08-28 15:12:16 +02:00
Richard Schreiber
464f625301 Seating: visually remove waitinglist headline for soldout seating 2024-08-28 14:36:30 +02:00
Richard Schreiber
0c1072503c A11y: improve/unify html-source for uncategorized products 2024-08-28 14:35:35 +02:00
Raphael Michel
9ead82839a API: Add show_dates_on_frontpage to device event settings 2024-08-28 13:47:24 +02:00
Albizuri
c346e3a7f4 Translations: Update Basque
Currently translated at 44.6% (2560 of 5737 strings)

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

powered by weblate
2024-08-28 12:10:11 +02:00
Tinna Sandström
a26f219faf Translations: Update Swedish
Currently translated at 99.0% (5684 of 5736 strings)

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

powered by weblate
2024-08-28 12:10:11 +02:00
Anarion Dunedain
74fb8e7d0c Translations: Update Polish
Currently translated at 100.0% (232 of 232 strings)

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

powered by weblate
2024-08-28 12:10:11 +02:00
Anarion Dunedain
b9dbeef1ef Translations: Update Polish
Currently translated at 100.0% (5736 of 5736 strings)

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

powered by weblate
2024-08-28 12:10:11 +02:00
Raphael Michel
54079797d2 Event list: Do not include old or inactive subevents in event list (Z#23163541) (#4415) 2024-08-28 09:28:35 +02:00
Raphael Michel
02a4ed4be2 Teams: Improve handling of revoked keys and team deletion (Z#23163674) (#4414) 2024-08-28 09:27:53 +02:00
Raphael Michel
7f7c95aedb Bump version to 2024.9.0.dev0 2024-08-28 09:18:59 +02:00
394 changed files with 176549 additions and 128762 deletions

View File

@@ -38,7 +38,7 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Install system dependencies
run: sudo apt update && sudo apt install gettext unzip
run: sudo apt update && sudo apt install -y gettext unzip
- name: Install Python dependencies
run: pip3 install -U setuptools build pip check-manifest
- name: Run check-manifest

View File

@@ -37,7 +37,7 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Install system packages
run: sudo apt update && sudo apt install enchant-2 hunspell aspell-en
run: sudo apt update && sudo apt install -y enchant-2 hunspell aspell-en
- name: Install Dependencies
run: pip3 install -Ur requirements.txt
working-directory: ./doc

View File

@@ -35,9 +35,9 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Install system packages
run: sudo apt update && sudo apt install gettext
run: sudo apt update && sudo apt -y install gettext
- name: Install Dependencies
run: pip3 install -e ".[dev]"
run: pip3 install uv && uv pip install --system -e ".[dev]"
- name: Compile messages
run: python manage.py compilemessages
working-directory: ./src
@@ -62,7 +62,7 @@ jobs:
- name: Install system packages
run: sudo apt update && sudo apt install enchant-2 hunspell hunspell-de-de aspell-en aspell-de
- name: Install Dependencies
run: pip3 install -e ".[dev]"
run: pip3 install uv && uv pip install --system -e ".[dev]"
- name: Spellcheck translations
run: potypo
working-directory: ./src

View File

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

View File

@@ -30,15 +30,21 @@ jobs:
python-version: "3.9"
- database: sqlite
python-version: "3.10"
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: pretix
options: >-
--health-cmd "pg_isready -U postgres -d pretix"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: harmon758/postgresql-action@v1
with:
postgresql version: '15'
postgresql db: 'pretix'
postgresql user: 'postgres'
postgresql password: 'postgres'
if: matrix.database == 'postgres'
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
@@ -50,9 +56,9 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Install system dependencies
run: sudo apt update && sudo apt install gettext
run: sudo apt update && sudo apt install -y gettext
- name: Install Python dependencies
run: pip3 install --ignore-requires-python -e ".[dev]" psycopg2-binary # We ignore that flake8 needs newer python as we don't run flake8 during tests
run: pip3 install uv && uv pip install --system -e ".[dev]" psycopg2-binary
- name: Run checks
run: python manage.py check
working-directory: ./src
@@ -64,15 +70,15 @@ jobs:
run: make all compress
- 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
run: PRETIX_CONFIG_FILE=tests/ci_${{ matrix.database }}.cfg py.test -n 3 -p no:sugar --cov=./ --cov-report=xml 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
run: PRETIX_CONFIG_FILE=tests/ci_${{ matrix.database }}.cfg py.test tests/concurrency_tests/ --reuse-db
if: matrix.database == 'postgres'
- name: Upload coverage
uses: codecov/codecov-action@v1
uses: codecov/codecov-action@v4
with:
file: src/coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
fail_ci_if_error: false
if: matrix.database == 'postgres' && matrix.python-version == '3.11'

View File

@@ -10,7 +10,7 @@ tests:
- cd src
- python manage.py check
- make all compress
- PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg py.test --reruns 3 -n 3 tests --maxfail=100
- PRETIX_CONFIG_FILE=tests/ci_sqlite.cfg py.test -n 3 tests --maxfail=100
except:
- pypi
pypi:

View File

@@ -288,17 +288,26 @@ Example::
[django]
secret=j1kjps5a5&4ilpn912s7a1!e2h!duz^i3&idu@_907s$wrz@x-
debug=off
passwords_argon2=on
``secret``
The secret to be used by Django for signing and verification purposes. If this
setting is not provided, pretix will generate a random secret on the first start
and will store it in the filesystem for later usage.
``secret_fallback0`` ... ``secret_fallback9``
Prior versions of the secret to be used by Django for signing and verification purposes that will still
be accepted but no longer be used for new signing.
``debug``
Whether or not to run in debug mode. Default is ``False``.
.. WARNING:: Never set this to ``True`` in production!
``passwords_argon``
Use the ``argon2`` algorithm for password hashing. Disable on systems with a small number of CPU cores (currently
less than 8).
``profile``
Enable code profiling for a random subset of requests. Disabled by default, see
:ref:`perf-monitoring` for details.

View File

@@ -231,11 +231,10 @@ The following snippet is an example on how to configure a nginx proxy for pretix
}
}
server {
listen 443 default_server;
listen [::]:443 ipv6only=on default_server;
listen 443 ssl default_server;
listen [::]:443 ipv6only=on ssl default_server;
server_name pretix.mydomain.com;
ssl on;
ssl_certificate /path/to/cert.chain.pem;
ssl_certificate_key /path/to/key.pem;

View File

@@ -216,11 +216,10 @@ The following snippet is an example on how to configure a nginx proxy for pretix
}
}
server {
listen 443 default_server;
listen [::]:443 ipv6only=on default_server;
listen 443 ssl default_server;
listen [::]:443 ipv6only=on ssl default_server;
server_name pretix.mydomain.com;
ssl on;
ssl_certificate /path/to/cert.chain.pem;
ssl_certificate_key /path/to/key.pem;

View File

@@ -71,7 +71,7 @@ Endpoints
"mode": "placed",
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"all_products": False,
"all_products": false,
"limit_products": [2, 3],
"limit_variations": [456],
"all_payment_methods": true,
@@ -113,7 +113,7 @@ Endpoints
"mode": "placed",
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"all_products": False,
"all_products": false,
"limit_products": [2, 3],
"limit_variations": [456],
"all_payment_methods": true,
@@ -146,7 +146,7 @@ Endpoints
"mode": "placed",
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"all_products": False,
"all_products": false,
"limit_products": [2, 3],
"limit_variations": [456],
"all_payment_methods": true,
@@ -167,7 +167,7 @@ Endpoints
"mode": "placed",
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"all_products": False,
"all_products": false,
"limit_products": [2, 3],
"limit_variations": [456],
"all_payment_methods": true,
@@ -216,7 +216,7 @@ Endpoints
"mode": "placed",
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"all_products": False,
"all_products": false,
"limit_products": [2, 3],
"limit_variations": [456],
"all_payment_methods": true,

View File

@@ -23,6 +23,22 @@ position integer An integer, use
is_addon boolean If ``true``, items within this category are not on sale
on their own but the category provides a source for
defining add-ons for other products.
cross_selling_mode string If ``null``, cross-selling is disabled for this category.
If ``"only"``, it is only visible in the cross-selling
step.
If ``"both"``, it is visible on the normal index page
as well.
Only available if ``is_addon`` is ``false``.
cross_selling_condition string Only relevant if ``cross_selling_mode`` is not ``null``.
If ``"always"``, always show in cross-selling step.
If ``"products"``, only show if the cart contains one of
the products listed in ``cross_selling_match_products``.
If ``"discounts"``, only show products that qualify for
a discount according to discount rules.
cross_selling_match_products list of integer Only relevant if ``cross_selling_condition`` is
``"products"``. Internal ID of the items of which at
least one needs to be in the cart for this category to
be shown.
===================================== ========================== =======================================================
@@ -60,7 +76,10 @@ Endpoints
"internal_name": "",
"description": {"en": "Tickets are what you need to get in."},
"position": 1,
"is_addon": false
"is_addon": false,
"cross_selling_mode": null,
"cross_selling_condition": null,
"cross_selling_match_products": []
}
]
}
@@ -102,7 +121,10 @@ Endpoints
"internal_name": "",
"description": {"en": "Tickets are what you need to get in."},
"position": 1,
"is_addon": false
"is_addon": false,
"cross_selling_mode": null,
"cross_selling_condition": null,
"cross_selling_match_products": []
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -130,7 +152,10 @@ Endpoints
"internal_name": "",
"description": {"en": "Tickets are what you need to get in."},
"position": 1,
"is_addon": false
"is_addon": false,
"cross_selling_mode": null,
"cross_selling_condition": null,
"cross_selling_match_products": []
}
**Example response**:
@@ -147,7 +172,10 @@ Endpoints
"internal_name": "",
"description": {"en": "Tickets are what you need to get in."},
"position": 1,
"is_addon": false
"is_addon": false,
"cross_selling_mode": null,
"cross_selling_condition": null,
"cross_selling_match_products": []
}
:param organizer: The ``slug`` field of the organizer of the event to create a category for
@@ -193,7 +221,10 @@ Endpoints
"internal_name": "",
"description": {"en": "Tickets are what you need to get in."},
"position": 1,
"is_addon": true
"is_addon": true,
"cross_selling_mode": null,
"cross_selling_condition": null,
"cross_selling_match_products": []
}
:param organizer: The ``slug`` field of the organizer to modify

View File

@@ -31,8 +31,6 @@ subevent integer ID of the date
position_count integer Number of tickets that match this list (read-only).
checkin_count integer Number of check-ins performed on this list (read-only).
include_pending boolean If ``true``, the check-in list also contains tickets from orders in pending state.
auto_checkin_sales_channels list of strings All items on the check-in list will be automatically marked as checked-in when purchased through any of the listed sales channels.
**Deprecated, will be removed in pretix 2024.10.** Use :ref:`rest-autocheckinrules`: instead.
allow_multiple_entries boolean If ``true``, subsequent scans of a ticket on this list should not show a warning but instead be stored as an additional check-in.
allow_entry_after_exit boolean If ``true``, subsequent scans of a ticket on this list are valid if the last scan of the ticket was an exit scan.
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.
@@ -91,10 +89,7 @@ Endpoints
"allow_entry_after_exit": true,
"exit_all_at": null,
"rules": {},
"addon_match": false,
"auto_checkin_sales_channels": [
"pretixpos"
]
"addon_match": false
}
]
}
@@ -146,10 +141,7 @@ Endpoints
"allow_entry_after_exit": true,
"exit_all_at": null,
"rules": {},
"addon_match": false,
"auto_checkin_sales_channels": [
"pretixpos"
]
"addon_match": false
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -246,10 +238,7 @@ Endpoints
"subevent": null,
"allow_multiple_entries": false,
"allow_entry_after_exit": true,
"addon_match": false,
"auto_checkin_sales_channels": [
"pretixpos"
]
"addon_match": false
}
**Example response**:
@@ -271,10 +260,7 @@ Endpoints
"subevent": null,
"allow_multiple_entries": false,
"allow_entry_after_exit": true,
"addon_match": false,
"auto_checkin_sales_channels": [
"pretixpos"
]
"addon_match": false
}
:param organizer: The ``slug`` field of the organizer of the event/item to create a list for
@@ -326,10 +312,7 @@ Endpoints
"subevent": null,
"allow_multiple_entries": false,
"allow_entry_after_exit": true,
"addon_match": false,
"auto_checkin_sales_channels": [
"pretixpos"
]
"addon_match": false
}
:param organizer: The ``slug`` field of the organizer to modify
@@ -342,7 +325,7 @@ Endpoints
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/checkinlist/(id)/
Delete a check-in list. Note that this also deletes the information on all check-ins performed via this list.
Delete a check-in list. **Note that this also deletes the information on all check-ins performed via this list.**
**Example request**:

View File

@@ -352,12 +352,12 @@ Fetching individual invoices
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param invoice_no: The ``invoice_no`` field of the invoice to fetch
:param number: The ``number`` field of the invoice 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)/invoices/(invoice_no)/download/
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/invoices/(number)/download/
Download an invoice in PDF format.
@@ -384,7 +384,7 @@ Fetching individual invoices
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param invoice_no: The ``invoice_no`` field of the invoice to fetch
:param number: The ``number`` field of the invoice 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.
@@ -397,7 +397,7 @@ 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/
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(number)/reissue/
Cancels the invoice and creates a new one.
@@ -419,13 +419,13 @@ Invoices cannot be edited directly, but the following actions can be triggered:
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param invoice_no: The ``invoice_no`` field of the invoice to reissue
:param number: The ``number`` field of the invoice to reissue
:statuscode 200: no error
:statuscode 400: The invoice has already been canceled
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(invoice_no)/regenerate/
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(number)/regenerate/
Re-generates the invoice from order data.
@@ -447,7 +447,7 @@ Invoices cannot be edited directly, but the following actions can be triggered:
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param invoice_no: The ``invoice_no`` field of the invoice to regenerate
:param number: The ``number`` field of the invoice to regenerate
:statuscode 200: no error
:statuscode 400: The invoice has already been canceled
:statuscode 401: Authentication failure

View File

@@ -104,6 +104,10 @@ url string The full URL to
payments list of objects List of payment processes (see below)
refunds list of objects List of refund processes (see below)
last_modified datetime Last modification of this object
cancellation_date datetime Time of order cancellation (or ``null``). **Note**:
Will not be set for partial cancellations and is not
reliable for orders that have been cancelled,
reactivated and cancelled again.
===================================== ========================== =======================================================
@@ -151,6 +155,9 @@ last_modified datetime Last modificati
The ``expires`` attribute can now be passed during order creation.
.. versionchanged:: 2024.11
The ``cancellation_date`` attribute has been added and can also be used as an ordering key.
.. _order-position-resource:
@@ -203,8 +210,20 @@ checkins list of objects List of **succe
├ datetime datetime Time of check-in
├ type string Type of scan (defaults to ``entry``)
├ gate integer Internal ID of the gate. Can be ``null``.
├ device integer Internal ID of the device. Can be ``null``.
├ device integer Internal ID of the device. Can be ``null``. **Deprecated**, since this ID is not otherwise used in the API and is therefore not very useful.
├ device_id integer Attribute ``device_id`` of the device. Can be ``null``.
└ auto_checked_in boolean Indicates if this check-in been performed automatically by the system
print_logs list of objects List of print jobs recorded e.g. by the pretix apps
├ id integer Internal ID of the print job
├ successful boolean Whether the print job successfully resulted in a print.
This is not expected to be 100 % reliable information (since
printer feedback is never perfect) and there is no guarantee
that unsuccessful jobs will be logged.
├ device_id integer Attribute ``device_id`` of the device that recorded the print. Can be ``null``.
├ datetime datetime Time of printing
├ source string Source of print job, e.g. name of the app used.
├ type string Type of print (currently ``badge``, ``ticket``, ``certificate``, or ``other``)
└ info object Additional data with client-dependent structure.
downloads list of objects List of ticket download options
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
└ url string Download URL
@@ -232,6 +251,10 @@ pdf_data object Data object req
The attributes ``blocked``, ``valid_from`` and ``valid_until`` have been added.
.. versionchanged:: 2024.9
The attribute ``print_logs`` has been added.
.. _order-payment-resource:
Order payment resource
@@ -398,10 +421,21 @@ List of all orders
"type": "entry",
"gate": null,
"device": 2,
"device_id": 1,
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}
],
"print_logs": [
{
"id": 1,
"type": "badge",
"datetime": "2017-12-25T12:45:23Z",
"device_id": 1,
"source": "pretixSCAN",
"info": {}
}
],
"answers": [
{
"question": 12,
@@ -437,14 +471,15 @@ List of all orders
"provider": "banktransfer"
}
],
"refunds": []
"refunds": [],
"cancellation_date": null
}
]
}
: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 ``datetime``, ``code``,
``last_modified``, and ``status``. Default: ``datetime``
``last_modified``, ``status`` and ``cancellation_date``. Default: ``datetime``
: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)
@@ -625,10 +660,22 @@ Fetching individual orders
"type": "entry",
"gate": null,
"device": 2,
"device_id": 1,
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}
],
"print_logs": [
{
"id": 1,
"type": "badge",
"successful": true,
"datetime": "2017-12-25T12:45:23Z",
"device_id": 1,
"source": "pretixSCAN",
"info": {}
}
],
"answers": [
{
"question": 12,
@@ -664,7 +711,8 @@ Fetching individual orders
"provider": "banktransfer"
}
],
"refunds": []
"refunds": [],
"cancellation_date": null
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -976,8 +1024,8 @@ Creating orders
* ``internal_reference``
* ``vat_id``
* ``vat_id_validated`` (optional) If you need support for reverse charge (rarely the case), you need to check
yourself if the passed VAT ID is a valid EU VAT ID. In that case, set this to ``true``. Only valid VAT IDs will
trigger reverse charge taxation. Don't forget to set ``is_business`` as well!
yourself if the passed VAT ID is a valid EU VAT ID. In that case, set this to ``true``. Only valid VAT IDs will
trigger reverse charge taxation. Don't forget to set ``is_business`` as well!
* ``positions``
@@ -1580,10 +1628,22 @@ List of all order positions
"type": "entry",
"gate": null,
"device": 2,
"device_id": 1,
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}
],
"print_logs": [
{
"id": 1,
"type": "badge",
"successful": true,
"datetime": "2017-12-25T12:45:23Z",
"device_id": 1,
"source": "pretixSCAN",
"info": {}
}
],
"answers": [
{
"question": 12,
@@ -1694,10 +1754,22 @@ Fetching individual positions
"type": "entry",
"gate": null,
"device": 2,
"device_id": 1,
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}
],
"print_logs": [
{
"id": 1,
"type": "badge",
"successful": true,
"datetime": "2017-12-25T12:45:23Z",
"device_id": 1,
"source": "pretixSCAN",
"info": {}
}
],
"answers": [
{
"question": 12,
@@ -1794,6 +1866,10 @@ Manipulating individual positions
The endpoints to manage blocks have been added.
.. versionchanged:: 2024.9
The API now supports logging ticket and badge prints.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/
Updates specific fields on an order position. Currently, only the following fields are supported:
@@ -2053,6 +2129,59 @@ Manipulating individual positions
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order position.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/printlog/
Creates a print log, stating that this ticket has been printed.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/printlog/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"datetime": "2024-09-19T13:37:00+02:00",
"source": "pretixPOS",
"type": "badge",
"info": {
"cashier": 1234
}
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/pdf
{
"id": 1234,
"device_id": null,
"datetime": "2024-09-19T13:37:00+02:00",
"source": "pretixPOS",
"type": "badge",
"info": {
"cashier": 1234
}
}
:param organizer: The ``slug`` field of the organizer to create a log for
:param event: The ``slug`` field of the event to create a log for
:param id: The ``id`` field of the order position to create a log for
:statuscode 201: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource
**or** downloads are not available for this order position at this time. The response content will
contain more details.
:statuscode 404: The requested order position or download provider does not exist.
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
seconds.
Changing order contents
-----------------------

View File

@@ -51,7 +51,7 @@ Endpoints
"results": [
{
"identifier": "web",
"name": {
"label": {
"en": "Online shop"
},
"type": "web",
@@ -88,7 +88,7 @@ Endpoints
{
"identifier": "web",
"name": {
"label": {
"en": "Online shop"
},
"type": "web",
@@ -116,7 +116,7 @@ Endpoints
{
"identifier": "api.custom",
"name": {
"label": {
"en": "Custom integration"
},
"type": "api",
@@ -133,7 +133,7 @@ Endpoints
{
"identifier": "api.custom",
"name": {
"label": {
"en": "Custom integration"
},
"type": "api",
@@ -178,7 +178,7 @@ Endpoints
{
"identifier": "web",
"name": {
"label": {
"en": "Online shop"
},
"type": "web",

View File

@@ -313,7 +313,7 @@ Endpoints for event exports
: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/
@@ -553,4 +553,4 @@ Endpoints for organizer exports
: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
.. _RFC 5545: https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.5.3

View File

@@ -1,4 +1,4 @@
.. _`rest-reusablemedia`:
.. _`rest-seats`:
Seats
=====
@@ -249,7 +249,7 @@ Endpoints
"orderposition": null,
"cartposition": null,
"voucher": null
},
}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
@@ -260,3 +260,114 @@ Endpoints
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to change this resource.
:statuscode 404: Seat does not exist; or the endpoint without subevent id was used for event with subevents, or vice versa.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/seats/bulk_block/
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/seats/bulk_block/
Set the ``blocked`` attribute to ``true`` for a large number of seats at once.
You can pass either a list of ``id`` values or a list of ``seat_guid`` values.
You can pass up to 10,000 seats in one request.
The endpoint will return an error if you pass a seat ID that does not exist.
However, it will not return an error if one of the passed seats is already blocked or sold.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/seats/bulk_block/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"ids": [12, 45, 56]
}
or
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/seats/bulk_block/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"seat_guids": ["6c0e29e5-05d6-421f-99f3-afd01478ecad", "c2899340-e2e7-4d05-8100-000a4b6d7cf4"]
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param subevent_id: The ``id`` field of the subevent to modify
:statuscode 200: no error
:statuscode 400: The seat could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to change this resource.
:statuscode 404: Seat does not exist; or the endpoint without subevent id was used for event with subevents, or vice versa.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/seats/bulk_unblock/
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/seats/bulk_unblock/
Set the ``blocked`` attribute to ``false`` for a large number of seats at once.
You can pass either a list of ``id`` values or a list of ``seat_guid`` values.
You can pass up to 10,000 seats in one request.
The endpoint will return an error if you pass a seat ID that does not exist.
However, it will not return an error if one of the passed seat is already unblocked or is sold.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/seats/bulk_unblock/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"ids": [12, 45, 56]
}
or
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/seats/bulk_unblock/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"seat_guids": ["6c0e29e5-05d6-421f-99f3-afd01478ecad", "c2899340-e2e7-4d05-8100-000a4b6d7cf4"]
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param subevent_id: The ``id`` field of the subevent to modify
:statuscode 200: no error
:statuscode 400: The seat could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to change this resource.
:statuscode 404: Seat does not exist; or the endpoint without subevent id was used for event with subevents, or vice versa.

View File

@@ -136,6 +136,7 @@ Endpoints
}
:query page: The page number in case of a multi-page result set, default is 1
:query is_public: If set to ``true``/``false``, only subevents with a matching value of ``is_public`` are returned.
:query active: If set to ``true``/``false``, only events with a matching value of ``active`` are returned.
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned.
@@ -467,6 +468,7 @@ Endpoints
}
:query page: The page number in case of a multi-page result set, default is 1
:query is_public: If set to ``true``/``false``, only subevents with a matching value of ``is_public`` are returned.
:query active: If set to ``true``/``false``, only events with a matching value of ``active`` are returned.
:query event__live: If set to ``true``/``false``, only events with a matching value of ``live`` on the parent event are returned.
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.

View File

@@ -20,8 +20,9 @@ internal_name string An optional nam
rate decimal (string) Tax rate in percent
price_includes_tax boolean If ``true`` (default), tax is assumed to be included in
the specified product price
eu_reverse_charge boolean If ``true``, EU reverse charge rules are applied. Will
be ignored if custom rules are set.
eu_reverse_charge boolean **DEPRECATED**. If ``true``, EU reverse charge rules
are applied. Will be ignored if custom rules are set.
Use custom rules instead.
home_country string Merchant country (required for reverse charge), can be
``null`` or empty string
keep_gross_if_rate_changes boolean If ``true``, changes of the tax rate based on custom

View File

@@ -17,6 +17,7 @@ First, you need to declare that you are using non-essential cookies by respondin
signal:
.. automodule:: pretix.presale.signals
:no-index:
:members: register_cookie_providers
You are expected to return a list of ``CookieProvider`` objects instantiated from the following class:

View File

@@ -14,7 +14,7 @@ Core
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification,
item_copy_data, register_sales_channel_types, register_global_settings, quota_availability, global_email_filter,
register_ticket_secret_generators, gift_card_transaction_display,
register_text_placeholders, register_mail_placeholders
register_text_placeholders, register_mail_placeholders, device_info_updated
Order events
""""""""""""
@@ -22,12 +22,14 @@ Order events
There are multiple signals that will be sent out in the ordering cycle:
.. automodule:: pretix.base.signals
:no-index:
:members: validate_cart, validate_cart_addons, validate_order, order_valid_if_pending, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
Check-ins
"""""""""
.. automodule:: pretix.base.signals
:no-index:
:members: checkin_created
@@ -39,18 +41,21 @@ Frontend
.. automodule:: pretix.presale.signals
:no-index:
:members: order_info, order_info_top, order_meta_from_request, order_api_meta_from_request
Request flow
""""""""""""
.. automodule:: pretix.presale.signals
:no-index:
:members: process_request, process_response
Vouchers
""""""""
.. automodule:: pretix.presale.signals
:no-index:
:members: voucher_redeem_info
Backend
@@ -62,24 +67,28 @@ Backend
item_formsets, order_search_filter_q, order_search_forms
.. automodule:: pretix.base.signals
:no-index:
:members: logentry_display, logentry_object_link, requiredaction_display, timeline_events, orderposition_blocked_display, customer_created, customer_signed_in
Vouchers
""""""""
.. automodule:: pretix.control.signals
:no-index:
:members: item_forms, voucher_form_class, voucher_form_html, voucher_form_validation
Dashboards
""""""""""
.. automodule:: pretix.control.signals
:no-index:
:members: event_dashboard_widgets, user_dashboard_widgets, event_dashboard_top
Ticket designs
""""""""""""""
.. automodule:: pretix.base.signals
:no-index:
:members: layout_text_variables, layout_image_variables
.. automodule:: pretix.plugins.ticketoutputpdf.signals
@@ -89,4 +98,9 @@ API
---
.. automodule:: pretix.base.signals
:no-index:
:members: validate_event_settings, api_event_settings_fields
.. automodule:: pretix.api.signals
:no-index:
:members: register_device_security_profile

View File

@@ -60,6 +60,7 @@ that we'll provide in this plugin:
Similar signals exist for other objects:
.. automodule:: pretix.base.signals
:no-index:
:members: voucher_import_columns

View File

@@ -84,8 +84,6 @@ convenient to you:
.. automethod:: _register_fonts
.. automethod:: _register_event_fonts
.. automethod:: _on_first_page
.. automethod:: _on_other_page

View File

@@ -86,7 +86,10 @@ Signals
-------
.. automodule:: pretix.base.signals
:no-index:
:members: register_text_placeholders
.. automodule:: pretix.base.signals
:no-index:
:members: register_mail_placeholders

View File

@@ -1,5 +1,5 @@
KulturPass
=========
==========
.. note::

View File

@@ -158,7 +158,7 @@ expects and - more importantly - supports.
for a sample configuration in an academic context.
Note, that you can have multiple attributes with the same ``friendlyName``
but different ``name``s. This is often used in systems, where the same
but different ``name`` value. This is often used in systems, where the same
information (for example a persons name) is saved in different fields -
for example because one institution is returning SAML 1.0 and other
institutions are returning SAML 2.0 style attributes. Typically, this only

View File

@@ -29,8 +29,8 @@ item_assignments list of objects Products this l
===================================== ========================== =======================================================
Endpoints
---------
Layout endpoints
----------------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/
@@ -268,5 +268,75 @@ Endpoints
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
Ticket rendering endpoint
-----------------------------
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/ticketpdfrenderer/render_batch/
With this API call, you can instruct the system to render a set of tickets into one combined PDF file. To specify
which tickets to render, you need to submit a list of "parts". For every part, the following fields are supported:
* ``orderposition`` (``integer``, required): The ID of the order position to render.
* ``override_channel`` (``string``, optional): The sales channel ID to be used for layout selection instead of the
original channel of the order.
* ``override_layout`` (``integer``, optional): The ticket layout ID to be used instead of the auto-selected one.
If your input parameters validate correctly, a ``202 Accepted`` status code is returned.
The body points you to the download URL of the result. Running a ``GET`` request on that result URL will
yield one of the following status codes:
* ``200 OK`` The export succeeded. The body will be your resulting file. Might be large!
* ``409 Conflict`` Your export is still running. The body will be JSON with the structure ``{"status": "running"}``. ``status`` can be ``waiting`` before the task is actually being processed. Please retry, but wait at least one second before you do.
* ``410 Gone`` Running the export has failed permanently. The body will be JSON with the structure ``{"status": "failed", "message": "Error message"}``
* ``404 Not Found`` The export does not exist / is expired.
.. warning:: This endpoint is considered **experimental**. It might change at any time without prior notice.
.. note:: To avoid performance issues, a maximum number of 1000 parts is currently allowed.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/ticketpdfrenderer/render_batch/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"parts": [
{
"orderposition": 55412
},
{
"orderposition": 55412,
"override_channel": "web"
},
{
"orderposition": 55412,
"override_layout": 56
}
]
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"download": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/ticketpdfrenderer/download/29891ede-196f-4942-9e26-d055a36e98b8/3f279f13-c198-4137-b49b-9b360ce9fcce/"
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:statuscode 202: no error
:statuscode 400: Invalid input options
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/schema/pdf-layout.schema.json

View File

@@ -175,7 +175,7 @@ without any special behavior.
Connecting SSO providers (pretix as the SSO client)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
To connect an external application as a SSO client, go to "Customer accounts" → "SSO providers" → "Create a new SSO provider"
To connect an external application as a SSO provider, go to "Customer accounts" → "SSO providers" → "Create a new SSO provider"
in your organizer account.
.. thumbnail:: ../../screens/organizer/customer_ssoprovider_add.png

View File

@@ -29,21 +29,21 @@ dependencies = [
"arabic-reshaper==3.0.0", # Support for Arabic in reportlab
"babel",
"BeautifulSoup4==4.12.*",
"bleach==5.0.*",
"bleach==6.2.*",
"celery==5.4.*",
"chardet==5.2.*",
"cryptography>=3.4.2",
"css-inline==0.14.*",
"defusedcsv>=1.1.0",
"Django[argon2]==4.2.*,>=4.2.15",
"django-bootstrap3==24.2",
"django-bootstrap3==24.3",
"django-compressor==4.5.1",
"django-countries==7.6.*",
"django-filter==24.3",
"django-formset-js-improved==0.5.0.3",
"django-formtools==2.5.1",
"django-hierarkey==1.2.*",
"django-hijack==3.6.*",
"django-hijack==3.7.*",
"django-i18nfield==1.9.*,>=1.9.4",
"django-libsass==0.9",
"django-localflavor==4.0",
@@ -53,9 +53,9 @@ dependencies = [
"django-phonenumber-field==7.3.*",
"django-redis==5.4.*",
"django-scopes==2.0.*",
"django-statici18n==2.5.*",
"django-statici18n==2.6.*",
"djangorestframework==3.15.*",
"dnspython==2.6.*",
"dnspython==2.7.*",
"drf_ujson2==1.7.*",
"geoip2==4.*",
"importlib_metadata==8.*", # Polyfill, we can probably drop this once we require Python 3.10+
@@ -74,55 +74,53 @@ dependencies = [
"paypal-checkout-serversdk==1.0.*",
"PyJWT==2.9.*",
"phonenumberslite==8.13.*",
"Pillow==10.4.*",
"Pillow==11.0.*",
"pretix-plugin-build",
"protobuf==5.27.*",
"protobuf==5.29.*",
"psycopg2-binary",
"pycountry",
"pycparser==2.22",
"pycryptodome==3.20.*",
"pypdf==4.3.*",
"pycryptodome==3.21.*",
"pypdf==5.1.*",
"python-bidi==0.6.*", # Support for Arabic in reportlab
"python-dateutil==2.9.*",
"pytz",
"pytz-deprecation-shim==0.1.*",
"pyuca",
"qrcode==7.4.*",
"redis==5.0.*",
"qrcode==8.0",
"redis==5.2.*",
"reportlab==4.2.*",
"requests==2.31.*",
"sentry-sdk==2.13.*",
"sentry-sdk==2.18.*",
"sepaxml==2.6.*",
"slimit",
"stripe==7.9.*",
"text-unidecode==1.*",
"tlds>=2020041600",
"tqdm==4.*",
"ua-parser==0.18.*",
"ua-parser==1.0.*",
"vat_moss_forked==2020.3.20.0.11.0",
"vobject==0.9.*",
"webauthn==2.2.*",
"zeep==4.2.*"
"webauthn==2.3.*",
"zeep==4.3.*"
]
[project.optional-dependencies]
memcached = ["pylibmc"]
dev = [
"aiohttp==3.10.*",
"aiohttp==3.11.*",
"coverage",
"coveralls",
"fakeredis==2.24.*",
"fakeredis==2.26.*",
"flake8==7.1.*",
"freezegun",
"isort==5.13.*",
"pep8-naming==0.14.*",
"potypo",
"pytest-asyncio",
"pytest-asyncio>=0.24",
"pytest-cache",
"pytest-cov",
"pytest-django==4.*",
"pytest-mock==3.14.*",
"pytest-rerunfailures==14.*",
"pytest-sugar",
"pytest-xdist==3.6.*",
"pytest==8.3.*",

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__ = "2024.8.0"
__version__ = "2024.12.0.dev0"

View File

@@ -80,6 +80,7 @@ ALL_LANGUAGES = [
('de', _('German')),
('de-informal', _('German (informal)')),
('ar', _('Arabic')),
('eu', _('Basque')),
('ca', _('Catalan')),
('zh-hans', _('Chinese (simplified)')),
('zh-hant', _('Chinese (traditional)')),

View File

@@ -27,7 +27,7 @@ from rest_framework import exceptions
from rest_framework.authentication import TokenAuthentication
from pretix.api.auth.devicesecurity import (
DEVICE_SECURITY_PROFILES, FullAccessSecurityProfile,
FullAccessSecurityProfile, get_all_security_profiles,
)
from pretix.base.models import Device
@@ -58,7 +58,8 @@ class DeviceTokenAuthentication(TokenAuthentication):
def authenticate(self, request):
r = super().authenticate(request)
if r and isinstance(r[1], Device):
profile = DEVICE_SECURITY_PROFILES.get(r[1].security_profile, FullAccessSecurityProfile)
profiles = get_all_security_profiles()
profile = profiles.get(r[1].security_profile, FullAccessSecurityProfile())
if not profile.is_allowed(request):
raise exceptions.PermissionDenied('Request denied by device security profile.')
return r

View File

@@ -20,13 +20,40 @@
# <https://www.gnu.org/licenses/>.
#
import logging
from collections import OrderedDict
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from pretix.api.signals import register_device_security_profile
logger = logging.getLogger(__name__)
_ALL_PROFILES = None
class FullAccessSecurityProfile:
class BaseSecurityProfile:
@property
def identifier(self) -> str:
"""
Unique identifier for this profile.
"""
raise NotImplementedError()
@property
def verbose_name(self) -> str:
"""
Human-readable name (can be a ``gettext_lazy`` object).
"""
raise NotImplementedError()
def is_allowed(self, request) -> bool:
"""
Return whether a given request should be allowed.
"""
raise NotImplementedError()
class FullAccessSecurityProfile(BaseSecurityProfile):
identifier = 'full'
verbose_name = _('Full device access (reading and changing orders and gift cards, reading of products and settings)')
@@ -34,7 +61,7 @@ class FullAccessSecurityProfile:
return True
class AllowListSecurityProfile:
class AllowListSecurityProfile(BaseSecurityProfile):
allowlist = ()
def is_allowed(self, request):
@@ -77,6 +104,7 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:blockedsecrets-list'),
('GET', 'api-v1:order-list'),
('GET', 'api-v1:orderposition-pdf_image'),
('POST', 'api-v1:orderposition-printlog'),
('GET', 'api-v1:event.settings'),
('POST', 'api-v1:upload'),
('POST', 'api-v1:checkinrpc.redeem'),
@@ -112,6 +140,7 @@ class PretixScanNoSyncNoSearchSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:revokedsecrets-list'),
('GET', 'api-v1:blockedsecrets-list'),
('GET', 'api-v1:orderposition-pdf_image'),
('POST', 'api-v1:orderposition-printlog'),
('GET', 'api-v1:event.settings'),
('POST', 'api-v1:upload'),
('POST', 'api-v1:checkinrpc.redeem'),
@@ -147,6 +176,7 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:revokedsecrets-list'),
('GET', 'api-v1:blockedsecrets-list'),
('GET', 'api-v1:orderposition-pdf_image'),
('POST', 'api-v1:orderposition-printlog'),
('GET', 'api-v1:event.settings'),
('POST', 'api-v1:upload'),
('POST', 'api-v1:checkinrpc.redeem'),
@@ -154,87 +184,28 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
)
class PretixPosSecurityProfile(AllowListSecurityProfile):
identifier = 'pretixpos'
verbose_name = _('pretixPOS')
allowlist = (
('GET', 'api-v1:version'),
('GET', 'api-v1:device.eventselection'),
('GET', 'api-v1:idempotency.query'),
('GET', 'api-v1:device.info'),
('POST', 'api-v1:device.update'),
('POST', 'api-v1:device.revoke'),
('POST', 'api-v1:device.roll'),
('GET', 'api-v1:event-list'),
('GET', 'api-v1:event-detail'),
('GET', 'api-v1:subevent-list'),
('GET', 'api-v1:subevent-detail'),
('GET', 'api-v1:itemcategory-list'),
('GET', 'api-v1:item-list'),
('GET', 'api-v1:question-list'),
('GET', 'api-v1:quota-list'),
('GET', 'api-v1:taxrule-list'),
('GET', 'api-v1:ticketlayout-list'),
('GET', 'api-v1:ticketlayoutitem-list'),
('GET', 'api-v1:badgelayout-list'),
('GET', 'api-v1:badgeitem-list'),
('GET', 'api-v1:voucher-list'),
('GET', 'api-v1:voucher-detail'),
('GET', 'api-v1:order-list'),
('POST', 'api-v1:order-list'),
('GET', 'api-v1:order-detail'),
('DELETE', 'api-v1:orderposition-detail'),
('PATCH', 'api-v1:orderposition-detail'),
('GET', 'api-v1:orderposition-list'),
('GET', 'api-v1:orderposition-answer'),
('GET', 'api-v1:orderposition-pdf_image'),
('POST', 'api-v1:order-mark-canceled'),
('POST', 'api-v1:orderpayment-list'),
('POST', 'api-v1:orderrefund-list'),
('POST', 'api-v1:orderrefund-done'),
('POST', 'api-v1:cartposition-list'),
('POST', 'api-v1:cartposition-bulk-create'),
('GET', 'api-v1:checkinlist-list'),
('POST', 'api-v1:checkinlistpos-redeem'),
('POST', 'plugins:pretix_posbackend:order.posprintlog'),
('POST', 'plugins:pretix_posbackend:order.poslock'),
('DELETE', 'plugins:pretix_posbackend:order.poslock'),
('DELETE', 'api-v1:cartposition-detail'),
('GET', 'api-v1:giftcard-list'),
('POST', 'api-v1:giftcard-transact'),
('PATCH', 'api-v1:giftcard-detail'),
('GET', 'plugins:pretix_posbackend:posclosing-list'),
('POST', 'plugins:pretix_posbackend:posreceipt-list'),
('POST', 'plugins:pretix_posbackend:posclosing-list'),
('POST', 'plugins:pretix_posbackend:posdebugdump-list'),
('POST', 'plugins:pretix_posbackend:posdebuglogentry-list'),
('POST', 'plugins:pretix_posbackend:posdebuglogentry-bulk-create'),
('GET', 'plugins:pretix_posbackend:poscashier-list'),
('POST', 'plugins:pretix_posbackend:stripeterminal.token'),
('POST', 'plugins:pretix_posbackend:stripeterminal.paymentintent'),
('PUT', 'plugins:pretix_posbackend:file.upload'),
('GET', 'api-v1:revokedsecrets-list'),
('GET', 'api-v1:blockedsecrets-list'),
('GET', 'api-v1:event.settings'),
('GET', 'plugins:pretix_seating:event.event'),
('GET', 'plugins:pretix_seating:event.event.subevent'),
('GET', 'plugins:pretix_seating:event.plan'),
('GET', 'plugins:pretix_seating:selection.simple'),
('POST', 'api-v1:upload'),
('POST', 'api-v1:checkinrpc.redeem'),
('GET', 'api-v1:checkinrpc.search'),
('POST', 'api-v1:reusablemedium-lookup'),
('GET', 'api-v1:reusablemedium-list'),
('POST', 'api-v1:reusablemedium-list'),
)
def get_all_security_profiles():
global _ALL_PROFILES
if _ALL_PROFILES:
return _ALL_PROFILES
types = OrderedDict()
for recv, ret in register_device_security_profile.send(None):
if isinstance(ret, (list, tuple)):
for r in ret:
types[r.identifier] = r
else:
types[ret.identifier] = ret
_ALL_PROFILES = types
return types
DEVICE_SECURITY_PROFILES = {
k.identifier: k() for k in (
FullAccessSecurityProfile,
PretixScanSecurityProfile,
PretixScanNoSyncSecurityProfile,
PretixScanNoSyncNoSearchSecurityProfile,
PretixPosSecurityProfile,
@receiver(register_device_security_profile, dispatch_uid="base_register_default_security_profiles")
def register_default_webhook_events(sender, **kwargs):
return (
FullAccessSecurityProfile(),
PretixScanSecurityProfile(),
PretixScanNoSyncSecurityProfile(),
PretixScanNoSyncNoSearchSecurityProfile(),
)
}

View File

@@ -88,16 +88,20 @@ class SalesChannelMigrationMixin:
}
if data.get("all_sales_channels") and set(data["sales_channels"]) != all_channels:
raise ValidationError(
"If 'all_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
"the list of all sales channels."
)
raise ValidationError({
"limit_sales_channels": [
"If 'all_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
"the list of all sales channels."
]
})
if data.get("limit_sales_channels") and set(data["sales_channels"]) != set(data["limit_sales_channels"]):
raise ValidationError(
"If 'limit_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
"the same list."
)
raise ValidationError({
"limit_sales_channels": [
"If 'limit_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
"the same list."
]
})
if data["sales_channels"] == all_channels:
data["all_sales_channels"] = True
@@ -106,6 +110,10 @@ class SalesChannelMigrationMixin:
data["all_sales_channels"] = False
data["limit_sales_channels"] = data["sales_channels"]
del data["sales_channels"]
if data.get("all_sales_channels"):
data["limit_sales_channels"] = []
return super().to_internal_value(data)
def to_representation(self, value):

View File

@@ -235,7 +235,7 @@ class CartPositionCreateSerializer(BaseCartPositionCreateSerializer):
return cid
def create(self, validated_data):
validated_data.pop('sales_channel')
validated_data.pop('sales_channel', None)
addons_data = validated_data.pop('addons', None)
bundled_data = validated_data.pop('bundled', None)

View File

@@ -26,31 +26,22 @@ from rest_framework.exceptions import ValidationError
from pretix.api.serializers.event import SubEventSerializer
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.media import MEDIA_TYPES
from pretix.base.models import Checkin, CheckinList, SalesChannel
from pretix.base.models import Checkin, CheckinList
class CheckinListSerializer(I18nAwareModelSerializer):
checkin_count = serializers.IntegerField(read_only=True)
position_count = serializers.IntegerField(read_only=True)
auto_checkin_sales_channels = serializers.SlugRelatedField(
slug_field="identifier",
queryset=SalesChannel.objects.none(),
required=False,
allow_empty=True,
many=True,
)
class Meta:
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',
'include_pending', 'allow_multiple_entries', 'allow_entry_after_exit',
'rules', 'exit_all_at', 'addon_match', 'ignore_in_statistics', 'consider_tickets_used')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['auto_checkin_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
if 'subevent' in self.context['request'].query_params.getlist('expand'):
self.fields['subevent'] = SubEventSerializer(read_only=True)

View File

@@ -772,6 +772,7 @@ class EventSettingsSerializer(SettingsSerializer):
'invoice_address_company_required',
'invoice_address_beneficiary',
'invoice_address_custom_field',
'invoice_address_custom_field_helptext',
'invoice_name_required',
'invoice_address_not_asked_free',
'invoice_show_payments',
@@ -896,6 +897,7 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
'locale',
'last_order_modification_date',
'show_quota_left',
'show_dates_on_frontpage',
'max_items_per_order',
'attendee_names_asked',
'attendee_names_required',
@@ -915,6 +917,7 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
'invoice_address_company_required',
'invoice_address_beneficiary',
'invoice_address_custom_field',
'invoice_address_custom_field_helptext',
'invoice_name_required',
'invoice_address_not_asked_free',
'invoice_address_from_name',
@@ -986,6 +989,40 @@ def prefetch_by_id(items, qs, id_attr, target_attr):
setattr(item, target_attr, result.get(getattr(item, id_attr)))
class SeatBulkBlockInputSerializer(serializers.Serializer):
ids = serializers.ListField(child=serializers.IntegerField(), required=False, allow_empty=True)
seat_guids = serializers.ListField(child=serializers.CharField(), required=False, allow_empty=True)
def to_internal_value(self, data):
data = super().to_internal_value(data)
if data.get("seat_guids") and data.get("ids"):
raise ValidationError("Please pass either seat_guids or ids.")
if data.get("seat_guids"):
seat_ids = data["seat_guids"]
if len(seat_ids) > 10000:
raise ValidationError({"seat_guids": ["Please do not pass over 10000 seats."]})
seats = {s.seat_guid: s for s in self.context["queryset"].filter(seat_guid__in=seat_ids)}
for s in seat_ids:
if s not in seats:
raise ValidationError({"seat_guids": [f"The seat '{s}' does not exist."]})
elif data.get("ids"):
seat_ids = data["ids"]
if len(seat_ids) > 10000:
raise ValidationError({"ids": ["Please do not pass over 10000 seats."]})
seats = self.context["queryset"].in_bulk(seat_ids)
for s in seat_ids:
if s not in seats:
raise ValidationError({"ids": [f"The seat '{s}' does not exist."]})
else:
raise ValidationError("Please pass either seat_guids or ids.")
return {"seats": seats.values()}
class SeatSerializer(I18nAwareModelSerializer):
orderposition = serializers.IntegerField(source='orderposition_id')
cartposition = serializers.IntegerField(source='cartposition_id')

View File

@@ -369,7 +369,7 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
require_membership_types = validated_data.pop('require_membership_types', [])
limit_sales_channels = validated_data.pop('limit_sales_channels', [])
item = Item.objects.create(**validated_data)
if limit_sales_channels:
if limit_sales_channels and not validated_data.get('all_sales_channels'):
item.limit_sales_channels.add(*limit_sales_channels)
if picture:
item.picture.save(os.path.basename(picture.name), picture)
@@ -441,7 +441,22 @@ class ItemCategorySerializer(I18nAwareModelSerializer):
class Meta:
model = ItemCategory
fields = ('id', 'name', 'internal_name', 'description', 'position', 'is_addon')
fields = (
'id', 'name', 'internal_name', 'description', 'position',
'is_addon', 'cross_selling_mode',
'cross_selling_condition', 'cross_selling_match_products'
)
def validate(self, data):
data = super().validate(data)
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
full_data.update(data)
if full_data.get('is_addon') and full_data.get('cross_selling_mode'):
raise ValidationError('is_addon and cross_selling_mode are mutually exclusive')
return data
class QuestionOptionSerializer(I18nAwareModelSerializer):

View File

@@ -55,7 +55,7 @@ from pretix.base.models import (
)
from pretix.base.models.orders import (
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
RevokedTicketSecret,
PrintLog, RevokedTicketSecret,
)
from pretix.base.pdf import get_images, get_variables
from pretix.base.services.cart import error_messages
@@ -273,9 +273,35 @@ class AnswerSerializer(I18nAwareModelSerializer):
class CheckinSerializer(I18nAwareModelSerializer):
device_id = serializers.SlugRelatedField(
source='device',
slug_field='device_id',
read_only=True,
)
class Meta:
model = Checkin
fields = ('id', 'datetime', 'list', 'auto_checked_in', 'gate', 'device', 'type')
fields = ('id', 'datetime', 'list', 'auto_checked_in', 'gate', 'device', 'device_id', 'type')
class PrintLogSerializer(serializers.ModelSerializer):
device_id = serializers.SlugRelatedField(
source='device',
slug_field='device_id',
read_only=True,
)
class Meta:
model = PrintLog
fields = (
"id",
"successful",
"datetime",
"source",
"type",
"device_id",
"info",
)
class FailedCheckinSerializer(I18nAwareModelSerializer):
@@ -470,6 +496,7 @@ class OrderPositionListSerializer(serializers.ListSerializer):
class OrderPositionSerializer(I18nAwareModelSerializer):
checkins = CheckinSerializer(many=True, read_only=True)
print_logs = PrintLogSerializer(many=True, read_only=True)
answers = AnswerSerializer(many=True)
downloads = PositionDownloadsField(source='*', read_only=True)
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
@@ -484,7 +511,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
'company', 'street', 'zipcode', 'city', 'country', 'state', 'discount',
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled',
'print_logs', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled',
'valid_from', 'valid_until', 'blocked', 'voucher_budget_use')
read_only_fields = (
'id', 'order', 'positionid', 'item', 'variation', 'price', 'voucher', 'tax_rate', 'tax_value', 'secret',
@@ -571,9 +598,9 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
'company', 'street', 'zipcode', 'city', 'country', 'state',
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'require_attention',
'order__status', 'order__valid_if_pending', 'order__require_approval', 'valid_from', 'valid_until',
'blocked')
'print_logs', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat',
'require_attention', 'order__status', 'order__valid_if_pending', 'order__require_approval',
'valid_from', 'valid_until', 'blocked')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -726,12 +753,12 @@ class OrderSerializer(I18nAwareModelSerializer):
'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', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
'url', 'customer', 'valid_if_pending', 'api_meta'
'url', 'customer', 'valid_if_pending', 'api_meta', 'cancellation_date'
)
read_only_fields = (
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
'payment_provider', 'fees', 'total', 'positions', 'downloads', 'customer',
'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel'
'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel', 'cancellation_date'
)
def __init__(self, *args, **kwargs):
@@ -1488,6 +1515,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
pos.answers = answers
pos.pseudonymization_id = "PREVIEW"
pos.checkins = []
pos.print_logs = []
pos_map[pos.positionid] = pos
else:
if pos.voucher:

View File

@@ -29,6 +29,7 @@ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from pretix.api.auth.devicesecurity import get_all_security_profiles
from pretix.api.serializers import AsymmetricField
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.order import CompatibleJSONField
@@ -297,6 +298,7 @@ class DeviceSerializer(serializers.ModelSerializer):
revoked = serializers.BooleanField(read_only=True)
initialized = serializers.DateTimeField(read_only=True)
initialization_token = serializers.DateTimeField(read_only=True)
security_profile = serializers.ChoiceField(choices=[], required=False, default="full")
class Meta:
model = Device
@@ -306,6 +308,10 @@ class DeviceSerializer(serializers.ModelSerializer):
'os_name', 'os_version', 'software_brand', 'software_version', 'security_profile'
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['security_profile'].choices = [(k, v.verbose_name) for k, v in get_all_security_profiles().items()]
class TeamInviteSerializer(serializers.ModelSerializer):
class Meta:

View File

@@ -32,10 +32,17 @@ from pretix.helpers.periodic import minimum_interval
register_webhook_events = Signal()
"""
This signal is sent out to get all known webhook events. Receivers should return an
instance of a subclass of pretix.api.webhooks.WebhookEvent or a list of such
instance of a subclass of ``pretix.api.webhooks.WebhookEvent`` or a list of such
instances.
"""
register_device_security_profile = Signal()
"""
This signal is sent out to get all known device security_profiles. Receivers should
return an instance of a subclass of ``pretix.api.auth.devicesecurity.BaseSecurityProfile``
or a list of such instances.
"""
@receiver(periodic_task)
@scopes_disabled()

View File

@@ -62,6 +62,7 @@ from pretix.base.models import (
CachedFile, Checkin, CheckinList, Device, Event, Order, OrderPosition,
Question, ReusableMedium, RevokedTicketSecret, TeamAPIToken,
)
from pretix.base.models.orders import PrintLog
from pretix.base.services.checkin import (
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
)
@@ -115,7 +116,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
if 'subevent' in self.request.query_params.getlist('expand'):
qs = qs.prefetch_related(
'subevent', 'subevent__event', 'subevent__subeventitem_set', 'subevent__subeventitemvariation_set',
'subevent__seat_category_mappings', 'subevent__meta_values', 'auto_checkin_sales_channels'
'subevent__seat_category_mappings', 'subevent__meta_values',
)
return qs
@@ -142,7 +143,9 @@ class CheckinListViewSet(viewsets.ModelViewSet):
data=self.request.data
)
@transaction.atomic
def perform_destroy(self, instance):
instance.checkins.all().delete()
instance.log_action(
'pretix.event.checkinlist.deleted',
user=self.request.user,
@@ -365,8 +368,9 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
qs = qs.prefetch_related(
Prefetch(
lookup='checkins',
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists])
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device')
),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'answers', 'answers__options', 'answers__question',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
@@ -377,7 +381,8 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
Prefetch(
'positions',
OrderPosition.objects.prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.all()),
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'item', 'variation', 'answers', 'answers__options', 'answers__question',
)
)
@@ -389,8 +394,9 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
qs = qs.prefetch_related(
Prefetch(
lookup='checkins',
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists])
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device')
),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'answers', 'answers__options', 'answers__question',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat')

View File

@@ -20,6 +20,7 @@
# <https://www.gnu.org/licenses/>.
#
import base64
import copy
import logging
from cryptography.hazmat.backends.openssl.backend import Backend
@@ -146,6 +147,8 @@ class InitializeView(APIView):
permission_classes = ()
def post(self, request, format=None):
from pretix.base.signals import device_info_updated
serializer = InitializationRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
@@ -160,6 +163,8 @@ class InitializeView(APIView):
if device.revoked:
raise ValidationError({'token': ['This initialization token has been revoked.']})
old_instance = copy.copy(device)
device.initialized = now()
device.hardware_brand = serializer.validated_data.get('hardware_brand')
device.hardware_model = serializer.validated_data.get('hardware_model')
@@ -174,6 +179,10 @@ class InitializeView(APIView):
device.log_action('pretix.device.initialized', data=serializer.validated_data, auth=device)
device_info_updated.send(
sender=Device, old_device=old_instance, new_device=device
)
serializer = DeviceSerializer(device)
return Response(serializer.data)
@@ -182,9 +191,12 @@ class UpdateView(APIView):
authentication_classes = (DeviceTokenAuthentication,)
def post(self, request, format=None):
from pretix.base.signals import device_info_updated
serializer = UpdateRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
device = request.auth
old_instance = copy.copy(device)
device.hardware_brand = serializer.validated_data.get('hardware_brand')
device.hardware_model = serializer.validated_data.get('hardware_model')
device.os_name = serializer.validated_data.get('os_name')
@@ -200,6 +212,10 @@ class UpdateView(APIView):
device.save()
device.log_action('pretix.device.updated', data=serializer.validated_data, auth=device)
device_info_updated.send(
sender=Device, old_device=old_instance, new_device=device
)
serializer = DeviceSerializer(device)
return Response(serializer.data)

View File

@@ -40,6 +40,7 @@ from django.utils.timezone import now
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import serializers, views, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import (
NotFound, PermissionDenied, ValidationError,
)
@@ -50,8 +51,9 @@ from pretix.api.auth.permission import EventCRUDPermission
from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.event import (
CloneEventSerializer, DeviceEventSettingsSerializer, EventSerializer,
EventSettingsSerializer, ItemMetaPropertiesSerializer, SeatSerializer,
SubEventSerializer, TaxRuleSerializer,
EventSettingsSerializer, ItemMetaPropertiesSerializer,
SeatBulkBlockInputSerializer, SeatSerializer, SubEventSerializer,
TaxRuleSerializer,
)
from pretix.api.views import ConditionalListView
from pretix.base.models import (
@@ -237,9 +239,9 @@ class EventViewSet(viewsets.ModelViewSet):
disabled = {m: 'disabled' for m in current_plugins_value if m not in updated_plugins_value}
changed = merge_dicts(enabled, disabled)
for module, action in changed.items():
for module, operation in changed.items():
serializer.instance.log_action(
'pretix.event.plugins.' + action,
'pretix.event.plugins.' + operation,
user=self.request.user,
auth=self.request.auth,
data={'plugin': module}
@@ -297,7 +299,8 @@ class EventViewSet(viewsets.ModelViewSet):
if 'all_sales_channels' in serializer.validated_data and 'sales_channels' in serializer.validated_data:
new_event.all_sales_channels = serializer.validated_data['all_sales_channels']
new_event.limit_sales_channels.set(serializer.validated_data['limit_sales_channels'])
if not new_event.all_sales_channels:
new_event.limit_sales_channels.set(serializer.validated_data['limit_sales_channels'])
else:
serializer.instance.set_defaults()
@@ -370,7 +373,7 @@ with scopes_disabled():
class Meta:
model = SubEvent
fields = ['active', 'event__live']
fields = ['is_public', 'active', 'event__live']
def ends_after_qs(self, queryset, name, value):
expr = Q(
@@ -743,3 +746,24 @@ class SeatViewSet(ConditionalListView, viewsets.ModelViewSet):
auth=self.request.auth,
data={"seats": [serializer.instance.pk]},
)
def bulk_change_blocked(self, blocked):
s = SeatBulkBlockInputSerializer(
data=self.request.data,
context={"event": self.request.event, "queryset": self.get_queryset()},
)
s.is_valid(raise_exception=True)
seats = s.validated_data["seats"]
for seat in seats:
seat.blocked = blocked
Seat.objects.bulk_update(seats, ["blocked"], batch_size=1000)
return Response({})
@action(methods=["POST"], detail=False)
def bulk_block(self, request, *args, **kwargs):
return self.bulk_change_blocked(True)
@action(methods=["POST"], detail=False)
def bulk_unblock(self, request, *args, **kwargs):
return self.bulk_change_blocked(False)

View File

@@ -42,6 +42,7 @@ from pretix.base.models import (
Checkin, GiftCard, GiftCardAcceptance, GiftCardTransaction, OrderPosition,
ReusableMedium,
)
from pretix.base.models.orders import PrintLog
from pretix.helpers import OF_SELF
from pretix.helpers.dicts import merge_dicts
@@ -78,7 +79,8 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
queryset=OrderPosition.objects.select_related(
'order', 'order__event', 'order__event__organizer', 'seat',
).prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.all()),
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'answers', 'answers__options', 'answers__question',
)
),

View File

@@ -57,7 +57,8 @@ from pretix.api.serializers.order import (
OrderPaymentCreateSerializer, OrderPaymentSerializer,
OrderPositionSerializer, OrderRefundCreateSerializer,
OrderRefundSerializer, OrderSerializer, PriceCalcSerializer,
RevokedTicketSecretSerializer, SimulatedOrderSerializer,
PrintLogSerializer, RevokedTicketSecretSerializer,
SimulatedOrderSerializer,
)
from pretix.api.serializers.orderchange import (
BlockNameSerializer, OrderChangeOperationSerializer,
@@ -75,7 +76,7 @@ from pretix.base.models import (
TeamAPIToken, generate_secret,
)
from pretix.base.models.orders import (
BlockedTicketSecret, QuestionAnswer, RevokedTicketSecret,
BlockedTicketSecret, PrintLog, QuestionAnswer, RevokedTicketSecret,
)
from pretix.base.payment import PaymentException
from pretix.base.pdf import get_images
@@ -214,7 +215,7 @@ class OrderViewSetMixin:
queryset = Order.objects.none()
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
ordering = ('datetime',)
ordering_fields = ('datetime', 'code', 'status', 'last_modified')
ordering_fields = ('datetime', 'code', 'status', 'last_modified', 'cancellation_date')
filterset_class = OrderFilter
lookup_field = 'code'
@@ -258,7 +259,8 @@ class OrderViewSetMixin:
return Prefetch(
'positions',
opq.all().prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.all()),
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
Prefetch('item', queryset=self.request.event.items.prefetch_related(
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'), to_attr='meta_values_cached')
)),
@@ -279,7 +281,8 @@ class OrderViewSetMixin:
return Prefetch(
'positions',
opq.all().prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.all()),
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'item', 'variation',
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options', 'question').order_by('question__position')),
'seat',
@@ -1092,7 +1095,8 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
'item_meta_properties',
)
qs = qs.prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.all()),
Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
Prefetch('item', queryset=self.request.event.items.prefetch_related(
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
to_attr='meta_values_cached')
@@ -1111,7 +1115,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
Prefetch(
'positions',
qs.prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.all()),
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
Prefetch('item', queryset=self.request.event.items.prefetch_related(
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
to_attr='meta_values_cached')
@@ -1135,7 +1139,8 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
)
else:
qs = qs.prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.all()),
Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'answers', 'answers__options', 'answers__question',
).select_related(
'item', 'order', 'order__event', 'order__event__organizer', 'seat'
@@ -1254,6 +1259,34 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
)
return resp
@action(detail=True, url_name="printlog", url_path="printlog", methods=["POST"])
def printlog(self, request, **kwargs):
pos = self.get_object()
serializer = PrintLogSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
with transaction.atomic():
serializer.save(
position=pos,
device=request.auth if isinstance(request.auth, Device) else None,
user=request.user if request.user.is_authenticated else None,
api_token=request.auth if isinstance(request.auth, TeamAPIToken) else None,
oauth_application=request.auth.application if isinstance(request.auth, OAuthAccessToken) else None,
)
pos.order.log_action(
"pretix.event.order.print",
data={
"position": pos.pk,
"positionid": pos.positionid,
**serializer.validated_data,
},
auth=request.auth,
user=request.user,
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
@action(detail=True, url_name='pdf_image', url_path=r'pdf_image/(?P<key>[^/]+)')
def pdf_image(self, request, key, **kwargs):
pos = self.get_object()

View File

@@ -32,13 +32,16 @@
# 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 string
from collections import OrderedDict
from importlib import import_module
from django import forms
from django.conf import settings
from django.contrib.auth import authenticate
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.hashers import check_password, make_password
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _, ngettext
def get_auth_backends():
@@ -149,7 +152,7 @@ class NativeAuthBackend(BaseAuthBackend):
to log in.
"""
d = OrderedDict([
('email', forms.EmailField(label=_("E-mail"), max_length=254,
('email', forms.EmailField(label=_("Email"), max_length=254,
widget=forms.EmailInput(attrs={'autofocus': 'autofocus'}))),
('password', forms.CharField(label=_("Password"), widget=forms.PasswordInput,
max_length=4096)),
@@ -160,3 +163,62 @@ class NativeAuthBackend(BaseAuthBackend):
u = authenticate(request=request, email=form_data['email'].lower(), password=form_data['password'])
if u and u.auth_backend == self.identifier:
return u
class NumericAndAlphabeticPasswordValidator:
def validate(self, password, user=None):
has_numeric = any(c in string.digits for c in password)
has_alpha = any(c in string.ascii_letters for c in password)
if not has_numeric or not has_alpha:
raise ValidationError(
_(
"Your password must contain both numeric and alphabetic characters.",
),
code="password_numeric_and_alphabetic",
)
def get_help_text(self):
return _(
"Your password must contain both numeric and alphabetic characters.",
)
class HistoryPasswordValidator:
def __init__(self, history_length=4):
self.history_length = history_length
def validate(self, password, user=None):
from pretix.base.models import User
if not user or not user.pk or not isinstance(user, User):
return
for hp in user.historic_passwords.order_by("-created")[:self.history_length]:
if check_password(password, hp.password):
raise ValidationError(
ngettext(
"Your password may not be the same as your previous password.",
"Your password may not be the same as one of your %(history_length)s previous passwords.",
self.history_length,
),
code="password_history",
params={"history_length": self.history_length},
)
def get_help_text(self):
return ngettext(
"Your password may not be the same as your previous password.",
"Your password may not be the same as one of your %(history_length)s previous passwords.",
self.history_length,
) % {"history_length": self.history_length}
def password_changed(self, password, user=None):
if not user:
pass
user.historic_passwords.create(password=make_password(password))
user.historic_passwords.filter(
pk__in=user.historic_passwords.order_by("-created")[self.history_length:].values_list("pk", flat=True),
).delete()

View File

@@ -46,6 +46,8 @@ This module contains utilities for implementing OpenID Connect for customer auth
as well as an OpenID Provider (OP).
"""
pretix_token_endpoint_auth_methods = ['client_secret_basic', 'client_secret_post']
def _urljoin(base, path):
if not base.endswith("/"):
@@ -127,6 +129,16 @@ def oidc_validate_and_complete_config(config):
fields=", ".join(provider_config.get("claims_supported", []))
))
if "token_endpoint_auth_methods_supported" in provider_config:
token_endpoint_auth_methods_supported = provider_config.get("token_endpoint_auth_methods_supported",
["client_secret_basic"])
if not any(x in pretix_token_endpoint_auth_methods for x in token_endpoint_auth_methods_supported):
raise ValidationError(
_(f'No supported Token Endpoint Auth Methods supported: {token_endpoint_auth_methods_supported}').format(
token_endpoint_auth_methods_supported=", ".join(token_endpoint_auth_methods_supported)
)
)
config['provider_config'] = provider_config
return config
@@ -147,6 +159,18 @@ def oidc_authorize_url(provider, state, redirect_uri):
def oidc_validate_authorization(provider, code, redirect_uri):
endpoint = provider.configuration['provider_config']['token_endpoint']
# Wall of shame and RFC ignorant IDPs
if endpoint == 'https://www.linkedin.com/oauth/v2/accessToken':
token_endpoint_auth_method = 'client_secret_post'
else:
token_endpoint_auth_methods = provider.configuration['provider_config'].get(
'token_endpoint_auth_methods_supported', ['client_secret_basic']
)
token_endpoint_auth_method = [
x for x in pretix_token_endpoint_auth_methods if x in token_endpoint_auth_methods
][0]
params = {
# https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
# https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
@@ -154,6 +178,11 @@ def oidc_validate_authorization(provider, code, redirect_uri):
'code': code,
'redirect_uri': redirect_uri,
}
if token_endpoint_auth_method == 'client_secret_post':
params['client_id'] = provider.configuration['client_id']
params['client_secret'] = provider.configuration['client_secret']
try:
resp = requests.post(
endpoint,
@@ -161,7 +190,10 @@ def oidc_validate_authorization(provider, code, redirect_uri):
headers={
'Accept': 'application/json',
},
auth=(provider.configuration['client_id'], provider.configuration['client_secret']),
auth=(
provider.configuration['client_id'],
provider.configuration['client_secret']
) if token_endpoint_auth_method == 'client_secret_basic' else None,
)
resp.raise_for_status()
data = resp.json()

View File

@@ -35,6 +35,7 @@ from django.utils.translation import get_language, gettext_lazy as _
from pretix.base.models import Event
from pretix.base.signals import register_html_mail_renderers
from pretix.base.templatetags.rich_text import markdown_compile_email
from pretix.helpers.format import SafeFormatter, format_map
from pretix.base.services.placeholders import ( # noqa
get_available_placeholders, PlaceholderContext
@@ -68,7 +69,7 @@ def test_custom_smtp_backend(backend: T, from_addr: str) -> None:
class BaseHTMLMailRenderer:
"""
This is the base class for all HTML e-mail renderers.
This is the base class for all HTML email renderers.
"""
def __init__(self, event: Event, organizer=None):
@@ -79,7 +80,7 @@ class BaseHTMLMailRenderer:
return self.identifier
def render(self, plain_body: str, plain_signature: str, subject: str, order=None,
position=None) -> str:
position=None, context=None) -> str:
"""
This method should generate the HTML part of the email.
@@ -88,6 +89,7 @@ class BaseHTMLMailRenderer:
:param subject: The email subject.
:param order: The order if this email is connected to one, otherwise ``None``.
:param position: The order position if this email is connected to one, otherwise ``None``.
:param context: Context to use to render placeholders in the plain body
:return: An HTML string
"""
raise NotImplementedError()
@@ -134,8 +136,10 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
def compile_markdown(self, plaintext):
return markdown_compile_email(plaintext)
def render(self, plain_body: str, plain_signature: str, subject: str, order, position) -> str:
def render(self, plain_body: str, plain_signature: str, subject: str, order, position, context) -> str:
body_md = self.compile_markdown(plain_body)
if context:
body_md = format_map(body_md, context=context, mode=SafeFormatter.MODE_RICH_TO_HTML)
htmlctx = {
'site': settings.PRETIX_INSTANCE_NAME,
'site_url': settings.SITE_URL,

View File

@@ -64,7 +64,7 @@ class CustomerListExporter(OrganizerLevelExportMixin, ListExporter):
_('Customer ID'),
_('SSO provider'),
_('External identifier'),
_('E-mail'),
_('Email'),
_('Phone number'),
_('Full name'),
]

View File

@@ -199,7 +199,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
_('Invoice number'),
_('Date'),
_('Order code'),
_('E-mail address'),
_('Email address'),
_('Invoice type'),
_('Cancellation of'),
_('Language'),
@@ -326,7 +326,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
_('Event start date'),
_('Date'),
_('Order code'),
_('E-mail address'),
_('Email address'),
_('Invoice type'),
_('Cancellation of'),
_('Invoice sender:') + ' ' + _('Name'),

View File

@@ -284,7 +284,7 @@ class OrderListExporter(MultiSheetListExporter):
headers.append(_('Comment'))
headers.append(_('Follow-up date'))
headers.append(_('Positions'))
headers.append(_('E-mail address verified'))
headers.append(_('Email address verified'))
headers.append(_('External customer ID'))
headers.append(_('Payment providers'))
if form_data.get('include_payment_amounts'):
@@ -655,7 +655,7 @@ class OrderListExporter(MultiSheetListExporter):
headers += [
_('Sales channel'),
_('Order locale'),
_('E-mail address verified'),
_('Email address verified'),
_('External customer ID'),
_('Check-in lists'),
_('Payment providers'),

View File

@@ -254,7 +254,7 @@ class PasswordRecoverForm(forms.Form):
class PasswordForgotForm(forms.Form):
email = forms.EmailField(
label=_('E-mail'),
label=_('Email'),
)
def __init__(self, *args, **kwargs):

View File

@@ -54,6 +54,7 @@ from django.core.validators import (
from django.db.models import QuerySet
from django.forms import Select, widgets
from django.forms.widgets import FILE_INPUT_CONTRADICTION
from django.urls import reverse
from django.utils.formats import date_format
from django.utils.html import escape
from django.utils.safestring import mark_safe
@@ -77,7 +78,7 @@ from pretix.base.i18n import (
get_babel_locale, get_language_without_region, language,
)
from pretix.base.models import InvoiceAddress, Item, Question, QuestionOption
from pretix.base.models.tax import VAT_ID_COUNTRIES, ask_for_vat_id
from pretix.base.models.tax import ask_for_vat_id
from pretix.base.services.tax import (
VATIDFinalError, VATIDTemporaryError, validate_vat_id,
)
@@ -602,6 +603,7 @@ class BaseQuestionsForm(forms.Form):
questions = pos.item.questions_to_ask
event = kwargs.pop('event')
self.all_optional = kwargs.pop('all_optional', False)
self.attendee_addresses_required = event.settings.attendee_addresses_required and not self.all_optional
super().__init__(*args, **kwargs)
@@ -676,7 +678,7 @@ class BaseQuestionsForm(forms.Form):
if item.ask_attendee_data and event.settings.attendee_addresses_asked:
add_fields['street'] = forms.CharField(
required=event.settings.attendee_addresses_required and not self.all_optional,
required=self.attendee_addresses_required,
label=_('Address'),
widget=forms.Textarea(attrs={
'rows': 2,
@@ -686,7 +688,7 @@ class BaseQuestionsForm(forms.Form):
initial=(cartpos.street if cartpos else orderpos.street),
)
add_fields['zipcode'] = forms.CharField(
required=event.settings.attendee_addresses_required and not self.all_optional,
required=False,
max_length=30,
label=_('ZIP code'),
initial=(cartpos.zipcode if cartpos else orderpos.zipcode),
@@ -695,7 +697,7 @@ class BaseQuestionsForm(forms.Form):
}),
)
add_fields['city'] = forms.CharField(
required=event.settings.attendee_addresses_required and not self.all_optional,
required=False,
label=_('City'),
max_length=255,
initial=(cartpos.city if cartpos else orderpos.city),
@@ -707,11 +709,12 @@ class BaseQuestionsForm(forms.Form):
add_fields['country'] = CountryField(
countries=CachedCountries
).formfield(
required=event.settings.attendee_addresses_required and not self.all_optional,
required=self.attendee_addresses_required,
label=_('Country'),
initial=country,
widget=forms.Select(attrs={
'autocomplete': 'country',
'data-country-information-url': reverse('js_helpers.states'),
}),
)
c = [('', pgettext_lazy('address', 'Select state'))]
@@ -946,9 +949,9 @@ class BaseQuestionsForm(forms.Form):
d = super().clean()
if self.address_validation:
self.cleaned_data = d = validate_address(d, True)
self.cleaned_data = d = validate_address(d, all_optional=not self.attendee_addresses_required)
if d.get('city') and d.get('country') and str(d['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
if d.get('street') and d.get('country') and str(d['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
if not d.get('state'):
self.add_error('state', _('This field is required.'))
@@ -1005,7 +1008,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
'street': forms.Textarea(attrs={
'rows': 2,
'placeholder': _('Street and Number'),
'autocomplete': 'street-address'
'autocomplete': 'street-address',
}),
'beneficiary': forms.Textarea(attrs={'rows': 3}),
'country': forms.Select(attrs={
@@ -1021,7 +1024,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
'data-display-dependency': '#id_is_business_1',
'autocomplete': 'organization',
}),
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1', 'data-countries-with-vat-id': ','.join(VAT_ID_COUNTRIES)}),
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
'internal_reference': forms.TextInput,
}
labels = {
@@ -1055,6 +1058,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
])
self.fields['country'].choices = CachedCountries()
self.fields['country'].widget.attrs['data-country-information-url'] = reverse('js_helpers.states')
c = [('', pgettext_lazy('address', 'Select state'))]
fprefix = self.prefix + '-' if self.prefix else ''
@@ -1083,6 +1087,10 @@ class BaseInvoiceAddressForm(forms.ModelForm):
)
self.fields['state'].widget.is_required = True
self.fields['street'].required = False
self.fields['zipcode'].required = False
self.fields['city'].required = False
# Without JavaScript the VAT ID field is not hidden, so we empty the field if a country outside the EU is selected.
if cc and not ask_for_vat_id(cc) and fprefix + 'vat_id' in self.data:
self.data = self.data.copy()
@@ -1122,6 +1130,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
if event.settings.invoice_address_custom_field:
self.fields['custom_field'].label = event.settings.invoice_address_custom_field
self.fields['custom_field'].help_text = event.settings.invoice_address_custom_field_helptext
else:
del self.fields['custom_field']
@@ -1134,6 +1143,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
validate_address # local import to prevent impact on startup time
data = self.cleaned_data
if not data.get('is_business'):
data['company'] = ''
data['vat_id'] = ''
@@ -1141,9 +1151,11 @@ class BaseInvoiceAddressForm(forms.ModelForm):
data['vat_id'] = ''
if self.event.settings.invoice_address_required:
if data.get('is_business') and not data.get('company'):
raise ValidationError(_('You need to provide a company name.'))
raise ValidationError({"company": _('You need to provide a company name.')})
if not data.get('is_business') and not data.get('name_parts'):
raise ValidationError(_('You need to provide your name.'))
if not self.all_optional and 'street' in self.fields and not data.get('street') and not data.get('zipcode') and not data.get('city'):
raise ValidationError({"street": _('This field is required.')})
if 'vat_id' in self.changed_data or not data.get('vat_id'):
self.instance.vat_id_validated = False

View File

@@ -48,10 +48,10 @@ from pretix.control.forms import SingleLanguageWidget
class UserSettingsForm(forms.ModelForm):
error_messages = {
'duplicate_identifier': _("There already is an account associated with this e-mail address. "
'duplicate_identifier': _("There already is an account associated with this email address. "
"Please choose a different one."),
'pw_current': _("Please enter your current password if you want to change your e-mail "
"address or password."),
'pw_current': _("Please enter your current password if you want to change your email address "
"or password."),
'pw_current_wrong': _("The current password you entered was not correct."),
'pw_mismatch': _("Please enter the same password twice"),
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),

View File

@@ -289,7 +289,7 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
def _clean_text(self, text, tags=None):
return self._normalize(bleach.clean(
text,
tags=tags or []
tags=set(tags) if tags else set()
).strip().replace('<br>', '<br />').replace('\n', '<br />\n'))
@@ -461,7 +461,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_event(self, canvas):
def shorten(txt):
txt = str(txt)
txt = bleach.clean(txt, tags=[]).strip()
txt = bleach.clean(txt, tags=set()).strip()
p = Paragraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
p_size = p.wrap(self.event_width, self.event_height)

View File

@@ -36,6 +36,7 @@ import time
import traceback
from django.conf import settings
from django.core.cache import cache
from django.core.management.base import BaseCommand
from django.dispatch.dispatcher import NO_RECEIVERS
@@ -50,17 +51,23 @@ class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('--tasks', action='store', type=str, help='Only execute the tasks with this name '
'(dotted path, comma separation)')
parser.add_argument('--list-tasks', action='store_true', help='Only list all tasks')
parser.add_argument('--exclude', action='store', type=str, help='Exclude the tasks with this name '
'(dotted path, comma separation)')
def handle(self, *args, **options):
verbosity = int(options['verbosity'])
cache.set("pretix_runperiodic_executed", True, 3600 * 12)
if not periodic_task.receivers or periodic_task.sender_receivers_cache.get(self) is NO_RECEIVERS:
return
for receiver in periodic_task._live_receivers(self):
name = f'{receiver.__module__}.{receiver.__name__}'
if options['list_tasks']:
print(name)
continue
if options.get('tasks'):
if name not in options.get('tasks').split(','):
continue
@@ -74,7 +81,7 @@ class Command(BaseCommand):
try:
r = receiver(signal=periodic_task, sender=self)
except Exception as err:
if isinstance(Exception, KeyboardInterrupt):
if isinstance(err, KeyboardInterrupt):
raise err
if settings.SENTRY_ENABLED:
from sentry_sdk import capture_exception

View File

@@ -37,6 +37,16 @@ class BaseMediaType:
def verbose_name(self):
raise NotImplementedError()
@property
def icon(self):
"""
This can be:
- The name of a Font Awesome icon to represent this channel type.
- The name of a SVG icon file that is resolvable through the static file system. We recommend to design for a size of 18x14 pixels.
"""
return "circle"
def generate_identifier(self, organizer):
if self.medium_created_by_server:
raise NotImplementedError()
@@ -59,6 +69,7 @@ class BaseMediaType:
class BarcodePlainMediaType(BaseMediaType):
identifier = 'barcode'
verbose_name = _('Barcode / QR-Code')
icon = 'qrcode'
medium_created_by_server = True
supports_giftcard = False
supports_orderposition = True
@@ -75,6 +86,7 @@ class BarcodePlainMediaType(BaseMediaType):
class NfcUidMediaType(BaseMediaType):
identifier = 'nfc_uid'
verbose_name = _('NFC UID-based')
icon = 'pretixbase/img/media/nfc_uid.svg'
medium_created_by_server = False
supports_giftcard = True
supports_orderposition = False
@@ -114,6 +126,7 @@ class NfcUidMediaType(BaseMediaType):
class NfcMf0aesMediaType(BaseMediaType):
identifier = 'nfc_mf0aes'
verbose_name = 'NFC Mifare Ultralight AES'
icon = 'pretixbase/img/media/nfc_secure.svg'
medium_created_by_server = False
supports_giftcard = True
supports_orderposition = False

View File

@@ -29,7 +29,7 @@ class Migration(migrations.Migration):
('password', models.CharField(verbose_name='password', max_length=128)),
('last_login', models.DateTimeField(verbose_name='last login', blank=True, null=True)),
('is_superuser', models.BooleanField(verbose_name='superuser status', default=False, help_text='Designates that this user has all permissions without explicitly assigning them.')),
('email', models.EmailField(max_length=191, blank=True, unique=True, verbose_name='E-mail', null=True,
('email', models.EmailField(max_length=191, blank=True, unique=True, verbose_name='Email', null=True,
db_index=True)),
('givenname', models.CharField(verbose_name='Given name', max_length=255, blank=True, null=True)),
('familyname', models.CharField(verbose_name='Family name', max_length=255, blank=True, null=True)),

View File

@@ -9,6 +9,7 @@ from decimal import Decimal
import django.core.validators
import django.db.models.deletion
import i18nfield.fields
from argon2.exceptions import HashingError
from django.conf import settings
from django.contrib.auth.hashers import make_password
from django.db import migrations, models
@@ -25,7 +26,14 @@ def initial_user(apps, schema_editor):
user = User(email='admin@localhost')
user.is_staff = True
user.is_superuser = True
user.password = make_password('admin')
try:
user.password = make_password('admin')
except HashingError:
raise Exception(
"Could not hash password of initial user with argon2id. If this is a system with less than 8 CPU cores, "
"you might need to disable argon2id by setting `passwords_argon2=off` in the `[django]` section of the "
"pretix.cfg configuration file."
)
user.save()
@@ -48,7 +56,7 @@ class Migration(migrations.Migration):
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('email', models.EmailField(blank=True, db_index=True, max_length=254, null=True, unique=True, verbose_name='E-mail')),
('email', models.EmailField(blank=True, db_index=True, max_length=254, null=True, unique=True, verbose_name='Email')),
('givenname', models.CharField(blank=True, max_length=255, null=True, verbose_name='Given name')),
('familyname', models.CharField(blank=True, max_length=255, null=True, verbose_name='Family name')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
@@ -232,7 +240,7 @@ class Migration(migrations.Migration):
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(max_length=16, verbose_name='Order code')),
('status', models.CharField(choices=[('n', 'pending'), ('p', 'paid'), ('e', 'expired'), ('c', 'cancelled'), ('r', 'refunded')], max_length=3, verbose_name='Status')),
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-mail')),
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email')),
('locale', models.CharField(blank=True, max_length=32, null=True, verbose_name='Locale')),
('secret', models.CharField(default=pretix.base.models.orders.generate_secret, max_length=32)),
('datetime', models.DateTimeField(verbose_name='Date')),

View File

@@ -187,7 +187,7 @@ class Migration(migrations.Migration):
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(max_length=16, verbose_name='Order code')),
('status', models.CharField(choices=[('n', 'pending'), ('p', 'paid'), ('e', 'expired'), ('c', 'cancelled'), ('r', 'refunded')], max_length=3, verbose_name='Status')),
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-mail')),
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email')),
('locale', models.CharField(blank=True, max_length=32, null=True, verbose_name='Locale')),
('secret', models.CharField(default=pretix.base.models.orders.generate_secret, max_length=32)),
('datetime', models.DateTimeField(verbose_name='Date')),

View File

@@ -20,7 +20,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='On waiting list since')),
('email', models.EmailField(max_length=254, verbose_name='E-mail address')),
('email', models.EmailField(max_length=254, verbose_name='Email address')),
('locale', models.CharField(default='en', max_length=190)),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Event', verbose_name='Event')),
('item', models.ForeignKey(help_text='The product the user waits for.', on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Item', verbose_name='Product')),

View File

@@ -35,7 +35,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='On waiting list since')),
('email', models.EmailField(max_length=254, verbose_name='E-mail address')),
('email', models.EmailField(max_length=254, verbose_name='Email address')),
('locale', models.CharField(default='en', max_length=190)),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Event', verbose_name='Event')),
('item', models.ForeignKey(help_text='The product the user waits for.', on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Item', verbose_name='Product')),

View File

@@ -163,7 +163,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('action_type', models.CharField(max_length=255)),
('method', models.CharField(choices=[('mail', 'E-mail')], max_length=255)),
('method', models.CharField(choices=[('mail', 'Email')], max_length=255)),
('event', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
to='pretixbase.Event')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),

View File

@@ -21,7 +21,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('action_type', models.CharField(max_length=255)),
('method', models.CharField(choices=[('mail', 'E-mail')], max_length=255)),
('method', models.CharField(choices=[('mail', 'Email')], max_length=255)),
('event', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Event')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('enabled', models.BooleanField(default=True)),

View File

@@ -0,0 +1,36 @@
# Generated by Django 4.2.15 on 2024-09-16 15:10
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0269_order_api_meta"),
]
operations = [
migrations.CreateModel(
name="HistoricPassword",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
("created", models.DateTimeField(auto_now_add=True)),
("password", models.CharField(max_length=128)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="historic_passwords",
to=settings.AUTH_USER_MODEL,
),
),
],
),
]

View File

@@ -0,0 +1,32 @@
# Generated by Django 4.2.11 on 2024-05-27 13:19
from django.db import migrations, models
import pretix.base.models.orders
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0270_historicpassword"),
]
operations = [
migrations.AddField(
model_name="itemcategory",
name="cross_selling_condition",
field=models.CharField(null=True, max_length=10),
),
migrations.AddField(
model_name="itemcategory",
name="cross_selling_mode",
field=models.CharField(null=True, max_length=5),
),
migrations.AddField(
model_name="itemcategory",
name="cross_selling_match_products",
field=models.ManyToManyField(
related_name="matched_by_cross_selling_categories", to="pretixbase.item"
),
),
]

View File

@@ -0,0 +1,79 @@
# Generated by Django 4.2.16 on 2024-09-19 10:41
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
("pretixbase", "0271_itemcategory_cross_selling"),
]
operations = [
migrations.CreateModel(
name="PrintLog",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
("datetime", models.DateTimeField(default=django.utils.timezone.now)),
("created", models.DateTimeField(auto_now_add=True, null=True)),
("successful", models.BooleanField(default=True)),
("source", models.CharField(max_length=255)),
("type", models.CharField(max_length=255)),
("info", models.JSONField(default=dict)),
(
"api_token",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="pretixbase.teamapitoken",
),
),
(
"device",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="print_logs",
to="pretixbase.device",
),
),
(
"oauth_application",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.PROTECT,
to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL,
),
),
(
"position",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="print_logs",
to="pretixbase.orderposition",
),
),
(
"user",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="print_logs",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ("-datetime",),
},
),
]

View File

@@ -0,0 +1,48 @@
# Generated by Django 4.2.16 on 2024-10-29 15:03
from django.db import migrations
def migrate_autocheckin(apps, schema_editor):
CheckinList = apps.get_model("pretixbase", "CheckinList")
AutoCheckinRule = apps.get_model("autocheckin", "AutoCheckinRule")
for cl in CheckinList.objects.filter(auto_checkin_sales_channels__isnull=False).select_related("event", "event__organizer"):
sales_channels = cl.auto_checkin_sales_channels.all()
all_sales_channels = cl.event.organizer.sales_channels.all()
if "pretix.plugins.autocheckin" not in cl.event.plugins:
cl.event.plugins = cl.event.plugins + ",pretix.plugins.autocheckin"
cl.event.save()
r = AutoCheckinRule.objects.get_or_create(
list=cl,
event=cl.event,
all_products=True,
all_payment_methods=True,
defaults=dict(
mode="placed",
all_sales_channels=len(sales_channels) == len(all_sales_channels),
)
)[0]
if len(sales_channels) != len(all_sales_channels):
r.limit_sales_channels.set(sales_channels)
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0272_printlog"),
("autocheckin", "0001_initial"),
]
operations = [
migrations.RunPython(
migrate_autocheckin,
migrations.RunPython.noop,
),
migrations.RemoveField(
model_name="checkinlist",
name="auto_checkin_sales_channels",
),
]

View File

@@ -213,7 +213,13 @@ class DatetimeColumnMixin:
except (ValueError, TypeError):
pass
else:
raise ValidationError(_("Could not parse {value} as a date and time.").format(value=value))
try:
d = datetime.datetime.fromisoformat(value)
if not d.tzinfo:
d = d.replace(tzinfo=self.timezone)
return d
except (ValueError, TypeError):
raise ValidationError(_("Could not parse {value} as a date and time.").format(value=value))
class DecimalColumnMixin:
@@ -250,6 +256,9 @@ class SubeventColumnMixin:
]
def clean(self, value, previous_values):
if not value:
return None
if value in self._subevent_cache:
return self._subevent_cache[value]

View File

@@ -40,8 +40,8 @@ from phonenumbers import SUPPORTED_REGIONS
from pretix.base.forms.questions import guess_country
from pretix.base.modelimport import (
DatetimeColumnMixin, DecimalColumnMixin, ImportColumn, SubeventColumnMixin,
i18n_flat,
BooleanColumnMixin, DatetimeColumnMixin, DecimalColumnMixin, ImportColumn,
SubeventColumnMixin, i18n_flat,
)
from pretix.base.models import (
Customer, ItemVariation, OrderPosition, Question, QuestionAnswer,
@@ -56,7 +56,7 @@ from pretix.base.signals import order_import_columns
class EmailColumn(ImportColumn):
identifier = 'email'
verbose_name = gettext_lazy('E-mail address')
verbose_name = gettext_lazy('Email address')
def clean(self, value, previous_values):
if value:
@@ -322,7 +322,7 @@ class AttendeeNamePart(ImportColumn):
class AttendeeEmail(ImportColumn):
identifier = 'attendee_email'
verbose_name = gettext_lazy('Attendee e-mail address')
verbose_name = gettext_lazy('Attendee email address')
def clean(self, value, previous_values):
if value:
@@ -604,6 +604,22 @@ class Comment(ImportColumn):
order.comment = value or ''
class CheckinAttentionColumn(BooleanColumnMixin, ImportColumn):
identifier = 'checkin_attention'
verbose_name = gettext_lazy('Requires special attention')
def assign(self, value, order, position, invoice_address, **kwargs):
order.checkin_attention = value
class CheckinTextColumn(ImportColumn):
identifier = 'checkin_text'
verbose_name = gettext_lazy('Check-in text')
def assign(self, value, order, position, invoice_address, **kwargs):
order.checkin_text = value
class QuestionColumn(ImportColumn):
def __init__(self, event, q):
self.q = q
@@ -742,6 +758,8 @@ def get_order_import_columns(event):
ValidUntil(event),
Locale(event),
Saleschannel(event),
CheckinAttentionColumn(event),
CheckinTextColumn(event),
Expires(event),
Comment(event),
]

View File

@@ -241,7 +241,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
REQUIRED_FIELDS = []
email = models.EmailField(unique=True, db_index=True, null=True, blank=True,
verbose_name=_('E-mail'), max_length=190)
verbose_name=_('Email'), max_length=190)
fullname = models.CharField(max_length=255, blank=True, null=True,
verbose_name=_('Full name'))
is_active = models.BooleanField(default=True,
@@ -571,13 +571,23 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
def get_session_auth_hash(self):
"""
Return an HMAC that needs to
Return an HMAC that needs to be the same throughout the session, used e.g. for forced
logout after every password change.
"""
return self._get_session_auth_hash(secret=settings.SECRET_KEY)
def get_session_auth_fallback_hash(self):
for fallback_secret in settings.SECRET_KEY_FALLBACKS:
yield self._get_session_auth_hash(secret=fallback_secret)
def _get_session_auth_hash(self, secret):
"""
"""
key_salt = "pretix.base.models.User.get_session_auth_hash"
payload = self.password
payload += self.email
payload += self.session_token
return salted_hmac(key_salt, payload).hexdigest()
return salted_hmac(key_salt, payload, secret=secret).hexdigest()
def update_session_token(self):
self.session_token = generate_session_token()
@@ -654,3 +664,9 @@ class WebAuthnDevice(Device):
@property
def webauthnpubkey(self):
return websafe_decode(self.pub_key)
class HistoricPassword(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="historic_passwords")
created = models.DateTimeField(auto_now_add=True)
password = models.CharField(verbose_name=_("Password"), max_length=128)

View File

@@ -99,14 +99,6 @@ class CheckinList(LoggedModel):
verbose_name=_('Automatically check out everyone at'),
null=True, blank=True
)
auto_checkin_sales_channels = models.ManyToManyField(
"SalesChannel",
verbose_name=_('Sales channels to automatically check in'),
help_text=_('This option is deprecated and will be removed in the next months. As a replacement, our new plugin '
'"Auto check-in" can be used. When we remove this option, we will automatically migrate your event '
'to use the new plugin.'),
blank=True,
)
rules = models.JSONField(default=dict, blank=True)
objects = ScopedManager(organizer='event__organizer')
@@ -141,7 +133,7 @@ class CheckinList(LoggedModel):
return self.positions_query(ignore_status=False)
@scopes_disabled()
def positions_inside_query(self, ignore_status=False, at_time=None):
def _filter_positions_inside(self, qs, at_time=None):
if at_time is None:
c_q = []
else:
@@ -149,7 +141,7 @@ class CheckinList(LoggedModel):
if "postgresql" not in settings.DATABASES["default"]["ENGINE"]:
# Use a simple approach that works on all databases
qs = self.positions_query(ignore_status=ignore_status).annotate(
qs = qs.annotate(
last_entry=Subquery(
Checkin.objects.filter(
*c_q,
@@ -202,7 +194,7 @@ class CheckinList(LoggedModel):
.values("position_id", "type", "datetime", "cnt_exists_after")
.query.sql_with_params()
)
return self.positions_query(ignore_status=ignore_status).filter(
return qs.filter(
pk__in=RawSQL(
f"""
SELECT "position_id"
@@ -214,6 +206,10 @@ class CheckinList(LoggedModel):
)
)
@scopes_disabled()
def positions_inside_query(self, ignore_status=False, at_time=None):
return self._filter_positions_inside(self.positions_query(ignore_status=ignore_status), at_time=at_time)
@property
def positions_inside(self):
return self.positions_inside_query(None)

View File

@@ -91,7 +91,7 @@ class Customer(LoggedModel):
),
],
)
email = models.EmailField(db_index=True, null=True, blank=False, verbose_name=_('E-mail'), max_length=190)
email = models.EmailField(db_index=True, null=True, blank=False, verbose_name=_('Email'), max_length=190)
phone = PhoneNumberField(null=True, blank=True, verbose_name=_('Phone number'))
password = models.CharField(verbose_name=_('Password'), max_length=128)
name_cached = models.CharField(max_length=255, verbose_name=_('Full name'), blank=True)
@@ -219,13 +219,24 @@ class Customer(LoggedModel):
return is_password_usable(self.password)
def get_session_auth_hash(self):
"""
Return an HMAC that needs to be the same throughout the session, used e.g. for forced
logout after every password change.
"""
return self._get_session_auth_hash(secret=settings.SECRET_KEY)
def get_session_auth_fallback_hash(self):
for fallback_secret in settings.SECRET_KEY_FALLBACKS:
yield self._get_session_auth_hash(secret=fallback_secret)
def _get_session_auth_hash(self, secret):
"""
Return an HMAC of the password field.
"""
key_salt = "pretix.base.models.customers.Customer.get_session_auth_hash"
payload = self.password
payload += self.email
return salted_hmac(key_salt, payload).hexdigest()
return salted_hmac(key_salt, payload, secret=secret).hexdigest()
def get_email_context(self):
from pretix.base.settings import get_name_parts_localized
@@ -381,7 +392,7 @@ class CustomerSSOClient(LoggedModel):
SCOPE_CHOICES = (
('openid', _('OpenID Connect access (required)')),
('profile', _('Profile data (name, addresses)')),
('email', _('E-mail address')),
('email', _('Email address')),
('phone', _('Phone number')),
)

View File

@@ -28,7 +28,6 @@ from django.utils.crypto import get_random_string
from django.utils.translation import gettext_lazy as _
from django_scopes import ScopedManager, scopes_disabled
from pretix.api.auth.devicesecurity import DEVICE_SECURITY_PROFILES
from pretix.base.models import LoggedModel
@@ -161,7 +160,6 @@ class Device(LoggedModel):
)
security_profile = models.CharField(
max_length=190,
choices=[(k, v.verbose_name) for k, v in DEVICE_SECURITY_PROFILES.items()],
default='full',
null=True,
blank=False

View File

@@ -20,11 +20,11 @@
# <https://www.gnu.org/licenses/>.
#
from collections import defaultdict
from collections import defaultdict, namedtuple
from decimal import Decimal
from itertools import groupby
from math import ceil
from typing import Dict, Optional, Tuple
from math import ceil, inf
from typing import Dict
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
@@ -36,6 +36,8 @@ from django_scopes import ScopedManager
from pretix.base.decimal import round_decimal
from pretix.base.models.base import LoggedModel
PositionInfo = namedtuple('PositionInfo', ['item_id', 'subevent_id', 'line_price_gross', 'is_addon_to', 'voucher_discount'])
class Discount(LoggedModel):
SUBEVENT_MODE_MIXED = 'mixed'
@@ -245,22 +247,26 @@ class Discount(LoggedModel):
return False
return True
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:
def _apply_min_value(self, positions, condition_idx_group, benefit_idx_group, result, collect_potential_discounts, subevent_id):
if self.condition_min_value and sum(positions[idx].line_price_gross 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 benefit_idx_group:
previous_price = positions[idx][2]
previous_price = positions[idx].line_price_gross
new_price = round_decimal(
previous_price * (Decimal('100.00') - self.benefit_discount_matching_percent) / Decimal('100.00'),
self.event.currency,
)
result[idx] = new_price
def _apply_min_count(self, positions, condition_idx_group, benefit_idx_group, result):
if collect_potential_discounts is not None:
for idx in condition_idx_group:
collect_potential_discounts[idx] = [(self, inf, -1, subevent_id)]
def _apply_min_count(self, positions, condition_idx_group, benefit_idx_group, result, collect_potential_discounts, subevent_id):
if len(condition_idx_group) < self.condition_min_count:
return
@@ -268,23 +274,53 @@ class Discount(LoggedModel):
raise ValueError('Validation invariant violated.')
if self.benefit_only_apply_to_cheapest_n_matches:
if not self.condition_min_count:
raise ValueError('Validation invariant violated.')
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
# sort by line_price
condition_idx_group = sorted(condition_idx_group, key=lambda idx: (positions[idx].line_price_gross, -idx))
benefit_idx_group = sorted(benefit_idx_group, key=lambda idx: (positions[idx].line_price_gross, -idx))
# Prevent over-consuming of items, i.e. if our discount is "buy 2, get 1 free", we only
# want to match multiples of 3
n_groups = min(len(condition_idx_group) // self.condition_min_count, ceil(len(benefit_idx_group) / self.benefit_only_apply_to_cheapest_n_matches))
# how many discount applications are allowed according to condition products in cart
possible_applications_cond = len(condition_idx_group) // self.condition_min_count
# how many discount applications are possible according to benefitting products in cart
possible_applications_benefit = ceil(len(benefit_idx_group) / self.benefit_only_apply_to_cheapest_n_matches)
n_groups = min(possible_applications_cond, possible_applications_benefit)
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]
if collect_potential_discounts is not None:
if n_groups * self.benefit_only_apply_to_cheapest_n_matches > len(benefit_idx_group):
# partially used discount ("for each 1 ticket you buy, get 50% on 2 t-shirts", cart content: 1 ticket
# but only 1 t-shirt) -> 1 shirt definitiv potential discount
for idx in consume_idx:
collect_potential_discounts[idx] = [
(self, n_groups * self.benefit_only_apply_to_cheapest_n_matches - len(benefit_idx_group), -1, subevent_id)
]
if possible_applications_cond * self.benefit_only_apply_to_cheapest_n_matches > len(benefit_idx_group):
# unused discount ("for each 1 ticket you buy, get 50% on 2 t-shirts", cart content: 1 ticket
# but 0 t-shirts) -> 2 shirt maybe potential discount (if the 1 ticket is not consumed by a later discount)
for i, idx in enumerate(condition_idx_group[
n_groups * self.condition_min_count:
possible_applications_cond * self.condition_min_count
]):
collect_potential_discounts[idx] += [
(self, self.benefit_only_apply_to_cheapest_n_matches, i // self.condition_min_count, subevent_id)
]
else:
consume_idx = condition_idx_group
benefit_idx = benefit_idx_group
if collect_potential_discounts is not None:
for idx in consume_idx:
collect_potential_discounts[idx] = [(self, inf, -1, subevent_id)]
for idx in benefit_idx:
previous_price = positions[idx][2]
previous_price = positions[idx].line_price_gross
new_price = round_decimal(
previous_price * (Decimal('100.00') - self.benefit_discount_matching_percent) / Decimal('100.00'),
self.event.currency,
@@ -292,15 +328,16 @@ class Discount(LoggedModel):
result[idx] = new_price
for idx in consume_idx:
result.setdefault(idx, positions[idx][2])
result.setdefault(idx, positions[idx].line_price_gross)
def apply(self, positions: Dict[int, Tuple[int, Optional[int], Decimal, bool, Decimal]]) -> Dict[int, Decimal]:
def apply(self, positions: Dict[int, PositionInfo],
collect_potential_discounts=None) -> Dict[int, Decimal]:
"""
Tries to apply this discount to a cart
:param positions: Dictionary mapping IDs to tuples of the form
``(item_id, subevent_id, line_price_gross, is_addon_to, voucher_discount)``.
:param positions: Dictionary mapping IDs to PositionInfo tuples.
Bundled positions may not be included.
:param collect_potential_discounts: For detailed description, see pretix.base.services.pricing.apply_discounts
:return: A dictionary mapping keys from the input dictionary to new prices. All positions
contained in this dictionary are considered "consumed" and should not be considered
@@ -342,13 +379,13 @@ class Discount(LoggedModel):
if self.subevent_mode == self.SUBEVENT_MODE_MIXED: # also applies to non-series events
if self.condition_min_count:
self._apply_min_count(positions, condition_candidates, benefit_candidates, result)
self._apply_min_count(positions, condition_candidates, benefit_candidates, result, collect_potential_discounts, None)
else:
self._apply_min_value(positions, condition_candidates, benefit_candidates, result)
self._apply_min_value(positions, condition_candidates, benefit_candidates, result, collect_potential_discounts, None)
elif self.subevent_mode == self.SUBEVENT_MODE_SAME:
def key(idx):
return positions[idx][1] or 0 # subevent_id
return positions[idx].subevent_id or 0
# Build groups of candidates with the same subevent, then apply our regular algorithm
# to each group
@@ -357,11 +394,11 @@ class Discount(LoggedModel):
candidate_groups = [(k, list(g)) for k, g in _groups]
for subevent_id, g in candidate_groups:
benefit_g = [idx for idx in benefit_candidates if positions[idx][1] == subevent_id]
benefit_g = [idx for idx in benefit_candidates if positions[idx].subevent_id == subevent_id]
if self.condition_min_count:
self._apply_min_count(positions, g, benefit_g, result)
self._apply_min_count(positions, g, benefit_g, result, collect_potential_discounts, subevent_id)
else:
self._apply_min_value(positions, g, benefit_g, result)
self._apply_min_value(positions, g, benefit_g, result, collect_potential_discounts, subevent_id)
elif self.subevent_mode == self.SUBEVENT_MODE_DISTINCT:
if self.condition_min_value or not self.benefit_same_products:
@@ -377,9 +414,9 @@ class Discount(LoggedModel):
# Build a list of subevent IDs in descending order of frequency
subevent_to_idx = defaultdict(list)
for idx, p in positions.items():
subevent_to_idx[p[1]].append(idx)
subevent_to_idx[p.subevent_id].append(idx)
for v in subevent_to_idx.values():
v.sort(key=lambda idx: positions[idx][2])
v.sort(key=lambda idx: positions[idx].line_price_gross)
subevent_order = sorted(list(subevent_to_idx.keys()), key=lambda s: len(subevent_to_idx[s]), reverse=True)
# Build groups of exactly condition_min_count distinct subevents
@@ -394,7 +431,7 @@ class Discount(LoggedModel):
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}:
if se not in {positions[idx].subevent_id for idx in current_group}:
candidates += l
cardinality = len(l)
@@ -403,7 +440,7 @@ class Discount(LoggedModel):
# Sort the list by prices, then pick one. For "buy 2 get 1 free" we apply a "pick 1 from the start
# and 2 from the end" scheme to optimize price distribution among groups
candidates = sorted(candidates, key=lambda idx: positions[idx][2])
candidates = sorted(candidates, key=lambda idx: positions[idx].line_price_gross)
if len(current_group) < (self.benefit_only_apply_to_cheapest_n_matches or 0):
candidate = candidates[0]
else:
@@ -415,14 +452,14 @@ class Discount(LoggedModel):
if len(current_group) >= max(self.condition_min_count, 1):
candidate_groups.append(current_group)
for c in current_group:
subevent_to_idx[positions[c][1]].remove(c)
subevent_to_idx[positions[c].subevent_id].remove(c)
current_group = []
# Distribute "leftovers"
for se in subevent_order:
if subevent_to_idx[se]:
for group in candidate_groups:
if se not in {positions[idx][1] for idx in group}:
if se not in {positions[idx].subevent_id for idx in group}:
group.append(subevent_to_idx[se].pop())
if not subevent_to_idx[se]:
break
@@ -432,6 +469,8 @@ class Discount(LoggedModel):
positions,
[idx for idx in g if idx in condition_candidates],
[idx for idx in g if idx in benefit_candidates],
result
result,
None,
None
)
return result

View File

@@ -823,6 +823,9 @@ class Event(EventMixin, LoggedModel):
self.save()
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
if hasattr(other, 'alternative_domain_assignment'):
other.alternative_domain_assignment.domain.event_assignments.create(event=self)
if not self.all_sales_channels:
self.limit_sales_channels.set(
self.organizer.sales_channels.filter(
@@ -870,10 +873,12 @@ class Event(EventMixin, LoggedModel):
for i in Item.objects.filter(event=other).prefetch_related(
'variations', 'limit_sales_channels', 'require_membership_types',
'variations__limit_sales_channels', 'variations__require_membership_types',
'matched_by_cross_selling_categories',
):
vars = list(i.variations.all())
require_membership_types = list(i.require_membership_types.all())
limit_sales_channels = list(i.limit_sales_channels.all())
matched_by_cross_selling_categories = list(i.matched_by_cross_selling_categories.all())
item_map[i.pk] = i
i.pk = None
i.event = self
@@ -911,6 +916,9 @@ class Event(EventMixin, LoggedModel):
if not v.all_sales_channels:
v.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels]))
if matched_by_cross_selling_categories:
i.matched_by_cross_selling_categories.set([category_map[c.pk] for c in matched_by_cross_selling_categories])
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()
@@ -1019,10 +1027,9 @@ class Event(EventMixin, LoggedModel):
checkin_list_map = {}
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related(
'limit_products', 'auto_checkin_sales_channels'
'limit_products'
):
items = list(cl.limit_products.all())
auto_checkin_sales_channels = list(cl.auto_checkin_sales_channels.all())
checkin_list_map[cl.pk] = cl
cl.pk = None
cl._prefetched_objects_cache = {}
@@ -1034,8 +1041,6 @@ class Event(EventMixin, LoggedModel):
cl.log_action('pretix.object.cloned')
for i in items:
cl.limit_products.add(item_map[i.pk])
if auto_checkin_sales_channels:
cl.auto_checkin_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in auto_checkin_sales_channels]))
if other.seating_plan:
if other.seating_plan.organizer_id == self.organizer_id:

View File

@@ -63,14 +63,13 @@ from django_countries.fields import Country
from django_scopes import ScopedManager
from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.media import MEDIA_TYPES
from pretix.base.models import Event, SubEvent
from pretix.base.models.base import LoggedModel
from pretix.base.models.fields import MultiStringField
from pretix.base.models.tax import TaxedPrice
from pretix.base.timemachine import time_machine_now
from ...helpers.images import ImageSizeValidator
from ..media import MEDIA_TYPES
from .event import Event, SubEvent
from pretix.helpers.images import ImageSizeValidator
class ItemCategory(LoggedModel):
@@ -111,6 +110,33 @@ class ItemCategory(LoggedModel):
'only be bought in combination with a product that has this category configured as a possible '
'source for add-ons.')
)
CROSS_SELLING_MODES = (
(None, _('Normal category')),
('both', _('Normal + cross-selling category')),
('only', _('Cross-selling category')),
)
cross_selling_mode = models.CharField(
choices=CROSS_SELLING_MODES,
null=True,
max_length=5
)
CROSS_SELLING_CONDITION = (
('always', _('Always show in cross-selling step')),
('discounts', _('Only show products that qualify for a discount according to discount rules')),
('products', _('Only show if the cart contains one of the following products')),
)
cross_selling_condition = models.CharField(
verbose_name=_("Cross-selling condition"),
choices=CROSS_SELLING_CONDITION,
null=True,
max_length=10,
)
cross_selling_match_products = models.ManyToManyField(
'pretixbase.Item',
blank=True,
verbose_name=_("Cross-selling condition products"),
related_name="matched_by_cross_selling_categories",
)
class Meta:
verbose_name = _("Product category")
@@ -119,19 +145,31 @@ class ItemCategory(LoggedModel):
def __str__(self):
name = self.internal_name or self.name
if self.is_addon:
return _('{category} (Add-On products)').format(category=str(name))
if self.category_type != 'normal':
return _('{category} ({category_type})').format(category=str(name),
category_type=self.get_category_type_display())
return str(name)
def get_category_type_display(self):
if self.is_addon:
return _('Add-On products')
return _('Add-on category')
elif self.cross_selling_mode:
return self.get_cross_selling_mode_display()
else:
return None
return _('Normal category')
@property
def category_type(self):
return 'addon' if self.is_addon else 'normal'
return 'addon' if self.is_addon else self.cross_selling_mode or 'normal'
@category_type.setter
def category_type(self, new_value):
if new_value == 'addon':
self.is_addon = True
self.cross_selling_mode = None
else:
self.is_addon = False
self.cross_selling_mode = None if new_value == 'normal' else new_value
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
@@ -270,7 +308,7 @@ class SubEventItemVariation(models.Model):
return True
def filter_available(qs, channel='web', voucher=None, allow_addons=False):
def filter_available(qs, channel='web', voucher=None, allow_addons=False, allow_cross_sell=False):
# Channel can currently be a SalesChannel or a str, since we need that compatibility, but a SalesChannel
# makes the query SIGNIFICANTLY faster
from .organizer import SalesChannel
@@ -291,6 +329,8 @@ def filter_available(qs, channel='web', voucher=None, allow_addons=False):
if not allow_addons:
q &= Q(Q(category__isnull=True) | Q(category__is_addon=False))
if not allow_cross_sell:
q &= Q(Q(category__isnull=True) | ~Q(category__cross_selling_mode='only'))
if voucher:
if voucher.item_id:
@@ -304,8 +344,8 @@ def filter_available(qs, channel='web', voucher=None, allow_addons=False):
class ItemQuerySet(models.QuerySet):
def filter_available(self, channel='web', voucher=None, allow_addons=False):
return filter_available(self, channel, voucher, allow_addons)
def filter_available(self, channel='web', voucher=None, allow_addons=False, allow_cross_sell=False):
return filter_available(self, channel, voucher, allow_addons, allow_cross_sell)
class ItemQuerySetManager(ScopedManager(organizer='event__organizer').__class__):
@@ -313,8 +353,8 @@ class ItemQuerySetManager(ScopedManager(organizer='event__organizer').__class__)
super().__init__()
self._queryset_class = ItemQuerySet
def filter_available(self, channel='web', voucher=None, allow_addons=False):
return filter_available(self.get_queryset(), channel, voucher, allow_addons)
def filter_available(self, channel='web', voucher=None, allow_addons=False, allow_cross_sell=False):
return filter_available(self.get_queryset(), channel, voucher, allow_addons, allow_cross_sell)
class Item(LoggedModel):
@@ -1078,13 +1118,12 @@ class ItemVariation(models.Model):
: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 variation can only be processed and paid after
approval by an administrator
approval by an administrator
:type require_approval: bool
:param all_sales_channels: A flag indicating that this variation is available on all channels and limit_sales_channels will be ignored.
:type all_sales_channels: bool
:param limit_sales_channels: A list of sales channel identifiers, that this variation is available for sale on.
:type limit_sales_channels: list
"""
item = models.ForeignKey(
Item,

View File

@@ -159,10 +159,24 @@ class Membership(models.Model):
de = date_format(self.date_end, 'SHORT_DATE_FORMAT')
return f'{self.membership_type.name}: {self.attendee_name} ({ds} {de})'
@property
def percentage_used(self):
if self.membership_type.max_usages and self.usages:
return int(self.usages / self.membership_type.max_usages * 100)
return 0
@property
def attendee_name(self):
return build_name(self.attendee_name_parts, fallback_scheme=lambda: self.customer.organizer.settings.name_scheme)
@property
def expired(self):
return time_machine_now() > self.date_end
@property
def not_yet_valid(self):
return time_machine_now() < self.date_start
def is_valid(self, ev=None, ticket_valid_from=None, valid_from_not_chosen=False):
if valid_from_not_chosen:
return not self.canceled and self.date_end >= time_machine_now()

View File

@@ -43,7 +43,7 @@ class NotificationSetting(models.Model):
:type enabled: bool
"""
CHANNELS = (
('mail', _('E-mail')),
('mail', _('Email')),
)
user = models.ForeignKey('User', on_delete=models.CASCADE,
related_name='notification_settings')

View File

@@ -40,6 +40,7 @@ import json
import logging
import operator
import string
import warnings
from collections import Counter
from datetime import datetime, time, timedelta
from decimal import Decimal
@@ -241,7 +242,7 @@ class Order(LockModel, LoggedModel):
)
email = models.EmailField(
null=True, blank=True,
verbose_name=_('E-mail')
verbose_name=_('Email')
)
phone = PhoneNumberField(
null=True, blank=True,
@@ -316,7 +317,7 @@ class Order(LockModel, LoggedModel):
)
email_known_to_work = models.BooleanField(
default=False,
verbose_name=_('E-mail address verified')
verbose_name=_('Email address verified')
)
invoice_dirty = models.BooleanField(
# Invoice needs to be re-issued when the order is paid again
@@ -381,8 +382,28 @@ class Order(LockModel, LoggedModel):
self.event.cache.delete('complain_testmode_orders')
self.delete()
def email_confirm_secret(self):
return self.tagged_secret("email_confirm", 9)
def email_confirm_hash(self):
return hashlib.sha256(settings.SECRET_KEY.encode() + self.secret.encode()).hexdigest()[:9]
warnings.warn('Use email_confirm_secret() instead of email_confirm_hash().',
DeprecationWarning)
return self.email_confirm_secret()
def check_email_confirm_secret(self, received_secret):
return (
hmac.compare_digest(
self.tagged_secret("email_confirm", 9),
received_secret[:9].lower()
) or any(
# TODO: remove this clause after a while (compatibility with old secrets currently in flight)
hmac.compare_digest(
hashlib.sha256(sk.encode() + self.secret.encode()).hexdigest()[:9],
received_secret
)
for sk in [settings.SECRET_KEY, *settings.SECRET_KEY_FALLBACKS]
)
)
def get_extended_status_display(self):
# Changes in this method should to be replicated in pretixcontrol/orders/fragment_order_status.html
@@ -2254,6 +2275,7 @@ class OrderFee(models.Model):
FEE_TYPE_SERVICE = "service"
FEE_TYPE_CANCELLATION = "cancellation"
FEE_TYPE_INSURANCE = "insurance"
FEE_TYPE_LATE = "late"
FEE_TYPE_OTHER = "other"
FEE_TYPE_GIFTCARD = "giftcard"
FEE_TYPES = (
@@ -2262,6 +2284,7 @@ class OrderFee(models.Model):
(FEE_TYPE_SERVICE, _("Service fee")),
(FEE_TYPE_CANCELLATION, _("Cancellation fee")),
(FEE_TYPE_INSURANCE, _("Insurance fee")),
(FEE_TYPE_LATE, _("Late fee")),
(FEE_TYPE_OTHER, _("Other fees")),
(FEE_TYPE_GIFTCARD, _("Gift card")),
)
@@ -2835,6 +2858,14 @@ class OrderPosition(AbstractPosition):
(self.order.event.settings.change_allow_user_addons and ItemAddOn.objects.filter(base_item_id__in=[op.item_id for op in positions]).exists())
)
@property
def code(self):
"""
A ticket code which is unique among all events of a single organizer,
built by the order code and the position number.
"""
return '{order_code}-{position}'.format(order_code=self.order.code, position=self.positionid)
class Transaction(models.Model):
"""
@@ -3175,9 +3206,9 @@ class InvoiceAddress(models.Model):
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'))
name_cached = models.CharField(max_length=255, verbose_name=_('Full name'), blank=True)
name_parts = models.JSONField(default=dict)
street = models.TextField(verbose_name=_('Address'), blank=False)
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=False)
city = models.CharField(max_length=255, verbose_name=_('City'), blank=False)
street = models.TextField(verbose_name=_('Address'), blank=True)
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=True)
city = models.CharField(max_length=255, verbose_name=_('City'), blank=True)
country_old = models.CharField(max_length=255, verbose_name=_('Country'), blank=False)
country = FastCountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'),
countries=CachedCountries)
@@ -3362,6 +3393,74 @@ class BlockedTicketSecret(models.Model):
unique_together = (('event', 'secret'),)
class PrintLog(models.Model):
"""
A print log object is created when a ticket or badge is printed with our apps.
"""
TYPE_BADGE = 'badge'
TYPE_TICKET = 'ticket'
TYPE_CERTIFICATE = 'certificate'
TYPE_OTHER = 'other'
PRINT_TYPES = (
(TYPE_BADGE, _('Badge')),
(TYPE_TICKET, _('Ticket')),
(TYPE_CERTIFICATE, _('Certificate')),
(TYPE_OTHER, _('Other')),
)
position = models.ForeignKey(
'pretixbase.OrderPosition',
related_name='print_logs',
on_delete=models.CASCADE,
)
successful = models.BooleanField(
default=True,
)
# Datetime of checkin, might be different from created if past scans are uploaded
datetime = models.DateTimeField(default=now)
# Datetime of creation on server
created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
# Who printed?
device = models.ForeignKey('Device', related_name='print_logs', null=True, blank=True, on_delete=models.PROTECT)
user = models.ForeignKey('User', related_name='print_logs', null=True, blank=True, on_delete=models.PROTECT)
api_token = models.ForeignKey('TeamAPIToken', null=True, blank=True, on_delete=models.PROTECT)
oauth_application = models.ForeignKey('pretixapi.OAuthApplication', null=True, blank=True, on_delete=models.PROTECT)
# Source = Tag field with undefined values, e.g. name of app ("pretixscan")
source = models.CharField(max_length=255)
# Type = Type of object printed ("badge", "ticket")
type = models.CharField(max_length=255, choices=PRINT_TYPES)
info = models.JSONField(default=dict)
objects = ScopedManager(organizer='position__order__event__organizer')
class Meta:
ordering = (('-datetime'),)
def __repr__(self):
return "<PrintLog: pos {} at {} from {}>".format(
self.position, self.datetime, self.source
)
def save(self, **kwargs):
super().save(**kwargs)
if self.position:
self.position.order.touch()
def delete(self, **kwargs):
super().delete(**kwargs)
self.position.order.touch()
@property
def is_late_upload(self):
return self.created and abs(self.created - self.datetime) > timedelta(minutes=2)
@receiver(post_delete, sender=CachedTicket)
def cachedticket_delete(sender, instance, **kwargs):
if instance.file:

View File

@@ -53,6 +53,30 @@ class SeatingPlanLayoutValidator:
e = str(e).replace('%', '%%')
raise ValidationError(_('Your layout file is not a valid seating plan. Error message: {}').format(e))
try:
seat_guids = set()
for z in val["zones"]:
for r in z["rows"]:
for s in r["seats"]:
if not s.get("seat_guid"):
raise ValidationError(
_("Seat with zone {zone}, row {row}, and number {number} has no seat ID.").format(
zone=z["name"],
row=r["row_number"],
number=s["seat_number"],
)
)
elif s["seat_guid"] in seat_guids:
raise ValidationError(
_("Multiple seats have the same ID: {id}").format(
id=s["seat_guid"],
)
)
seat_guids.add(s["seat_guid"])
except ValidationError as e:
raise ValidationError(_('Your layout file is not a valid seating plan. Error message: {}').format(", ".join(e.message for e in e.error_list)))
class SeatingPlan(LoggedModel):
"""

View File

@@ -29,6 +29,8 @@ from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.utils.deconstruct import deconstructible
from django.utils.formats import localize
from django.utils.functional import lazy
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _, pgettext
from i18nfield.fields import I18nCharField
from i18nfield.strings import LazyI18nString
@@ -120,6 +122,8 @@ EU_CURRENCIES = {
}
VAT_ID_COUNTRIES = EU_COUNTRIES | {'CH', 'NO'}
format_html_lazy = lazy(format_html, str)
def is_eu_country(cc):
cc = str(cc)
@@ -193,11 +197,17 @@ class TaxRule(LoggedModel):
eu_reverse_charge = models.BooleanField(
verbose_name=_("Use EU reverse charge taxation rules"),
default=False,
help_text=_("Not recommended. Most events will NOT be qualified for reverse charge since the place of "
"taxation is the location of the event. This option disables charging VAT for all customers "
"outside the EU and for business customers in different EU countries who entered a valid EU VAT "
"ID. Only enable this option after consulting a tax counsel. No warranty given for correct tax "
"calculation. USE AT YOUR OWN RISK.")
help_text=format_html_lazy(
'<span class="label label-warning" data-toggle="tooltip" title="{}">{}</span> {}',
_('This feature will be removed in the future as it does not handle VAT for non-business customers in '
'other EU countries in a way that works for all organizers. Use custom rules instead.'),
_('DEPRECATED'),
_("Not recommended. Most events will NOT be qualified for reverse charge since the place of "
"taxation is the location of the event. This option disables charging VAT for all customers "
"outside the EU and for business customers in different EU countries who entered a valid EU VAT "
"ID. Only enable this option after consulting a tax counsel. No warranty given for correct tax "
"calculation. USE AT YOUR OWN RISK.")
),
)
home_country = FastCountryField(
verbose_name=_('Merchant country'),
@@ -294,10 +304,24 @@ class TaxRule(LoggedModel):
subtract_from_gross = Decimal('0.00')
rate = adjust_rate
def _limit_subtract(base_price, subtract_from_gross):
if not subtract_from_gross:
return base_price
if base_price >= Decimal('0.00'):
# For positive prices, make sure they don't go negative because of bundles
return max(Decimal('0.00'), base_price - subtract_from_gross)
else:
# If the price is already negative, we don't really care any more
return base_price - subtract_from_gross
if rate == Decimal('0.00'):
gross = _limit_subtract(base_price, subtract_from_gross)
return TaxedPrice(
net=base_price - subtract_from_gross, gross=base_price - subtract_from_gross, tax=Decimal('0.00'),
rate=rate, name=self.name
net=gross,
gross=gross,
tax=Decimal('0.00'),
rate=rate,
name=self.name,
)
if base_price_is == 'auto':
@@ -307,19 +331,14 @@ class TaxRule(LoggedModel):
base_price_is = 'net'
if base_price_is == 'gross':
if base_price >= Decimal('0.00'):
# For positive prices, make sure they don't go negative because of bundles
gross = max(Decimal('0.00'), base_price - subtract_from_gross)
else:
# If the price is already negative, we don't really care any more
gross = base_price - subtract_from_gross
gross = _limit_subtract(base_price, subtract_from_gross)
net = round_decimal(gross - (gross * (1 - 100 / (100 + rate))),
currency)
elif base_price_is == 'net':
net = base_price
gross = round_decimal((net * (1 + rate / 100)), currency)
if subtract_from_gross:
gross -= subtract_from_gross
gross = _limit_subtract(gross, subtract_from_gross)
net = round_decimal(gross - (gross * (1 - 100 / (100 + rate))),
currency)
else:

View File

@@ -73,7 +73,7 @@ class WaitingListEntry(LoggedModel):
blank=True, default=dict
)
email = models.EmailField(
verbose_name=_("E-mail address")
verbose_name=_("Email address")
)
phone = PhoneNumberField(
null=True, blank=True,

View File

@@ -343,11 +343,13 @@ class CartManager:
err = error_messages['some_subevent_not_started']
cp.addons.all().delete()
cp.delete()
continue
if cp.subevent and cp.subevent.presale_end and time_machine_now(self.real_now_dt) > cp.subevent.presale_end:
err = error_messages['some_subevent_ended']
cp.addons.all().delete()
cp.delete()
continue
if cp.subevent:
tlv = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
@@ -360,6 +362,7 @@ class CartManager:
err = error_messages['some_subevent_ended']
cp.addons.all().delete()
cp.delete()
continue
return err
def _update_subevents_cache(self, se_ids: List[int]):
@@ -1542,10 +1545,9 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
"""
Removes a list of items from a user's cart.
:param event: The event ID in question
:param voucher: A voucher code
:param session: Session ID of a guest
:param cart_id: The cart ID of the cart to modify
"""
with language(locale), time_machine_now_assigned(override_now_dt):
try:
@@ -1566,10 +1568,10 @@ def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='e
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def remove_cart_position(self, event: Event, position: int, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
"""
Removes a list of items from a user's cart.
Removes an item specified by its position ID from a user's cart.
:param event: The event ID in question
:param position: A cart position ID
:param session: Session ID of a guest
:param cart_id: The cart ID of the cart to modify
"""
with language(locale), time_machine_now_assigned(override_now_dt):
try:
@@ -1590,9 +1592,9 @@ def remove_cart_position(self, event: Event, position: int, cart_id: str=None, l
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
"""
Removes a list of items from a user's cart.
Removes all items from a user's cart.
:param event: The event ID in question
:param session: Session ID of a guest
:param cart_id: The cart ID of the cart to modify
"""
with language(locale), time_machine_now_assigned(override_now_dt):
try:
@@ -1611,13 +1613,15 @@ def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def set_cart_addons(self, event: Event, addons: List[dict], cart_id: str=None, locale='en',
def set_cart_addons(self, event: Event, addons: List[dict], add_to_cart_items: List[dict], cart_id: str=None, locale='en',
invoice_address: int=None, sales_channel='web', override_now_dt: datetime=None) -> None:
"""
Removes a list of items from a user's cart.
Assigns addons to eligible products in a user's cart, adding and removing the addon products as necessary to
ensure the requested addon state.
:param event: The event ID in question
:param addons: A list of dicts with the keys addon_to, item, variation
:param session: Session ID of a guest
:param add_to_cart_items: A list of dicts with the keys item, variation, count, custom_price, voucher, seat ID
:param cart_id: The cart ID of the cart to modify
"""
with language(locale), time_machine_now_assigned(override_now_dt):
ia = False
@@ -1635,6 +1639,7 @@ def set_cart_addons(self, event: Event, addons: List[dict], cart_id: str=None, l
try:
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia, sales_channel=sales_channel)
cm.set_addons(addons)
cm.add_new_items(add_to_cart_items)
cm.commit()
except LockTimeoutException:
self.retry()

View File

@@ -57,7 +57,7 @@ from pretix.base.models import (
Checkin, CheckinList, Device, Event, Gate, Item, ItemVariation, Order,
OrderPosition, QuestionOption,
)
from pretix.base.signals import checkin_created, order_placed, periodic_task
from pretix.base.signals import checkin_created, periodic_task
from pretix.helpers import OF_SELF
from pretix.helpers.jsonlogic import Logic
from pretix.helpers.jsonlogic_boolalg import convert_to_dnf
@@ -1154,23 +1154,6 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
)
@receiver(order_placed, dispatch_uid="legacy_autocheckin_order_placed")
def order_placed(sender, **kwargs):
order = kwargs['order']
event = sender
cls = list(event.checkin_lists.filter(auto_checkin_sales_channels=order.sales_channel).prefetch_related(
'limit_products'))
if not cls:
return
for op in order.positions.all():
for cl in cls:
if cl.all_products or op.item_id in {i.pk for i in cl.limit_products.all()}:
if not cl.subevent_id or cl.subevent_id == op.subevent_id:
ci = Checkin.objects.create(position=op, list=cl, auto_checked_in=True, type=Checkin.TYPE_ENTRY)
checkin_created.send(event, checkin=ci)
@receiver(periodic_task, dispatch_uid="autocheckout_exit_all")
@scopes_disabled()
def process_exit_all(sender, **kwargs):
@@ -1182,10 +1165,11 @@ def process_exit_all(sender, **kwargs):
positions = cl.positions_inside_query(ignore_status=True, at_time=cl.exit_all_at)
for p in positions:
with scope(organizer=cl.event.organizer):
ci = Checkin.objects.create(
ci, created = Checkin.objects.get_or_create(
position=p, list=cl, auto_checked_in=True, type=Checkin.TYPE_EXIT, datetime=cl.exit_all_at
)
checkin_created.send(cl.event, checkin=ci)
if created:
checkin_created.send(cl.event, checkin=ci)
d = cl.exit_all_at.astimezone(cl.event.timezone)
if cl.event.settings.get(f'autocheckin_dst_hack_{cl.pk}'): # move time back if yesterday was DST switch
d -= timedelta(hours=1)

View File

@@ -0,0 +1,234 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from collections import defaultdict
from decimal import Decimal
from itertools import groupby
from math import inf
from typing import List
from django.utils.functional import cached_property
from pretix.base.models import CartPosition, ItemCategory, SalesChannel
from pretix.presale.views.event import get_grouped_items
class DummyCategory:
"""
Used to create fake category objects for displaying the same cross-selling category multiple times,
once for each subevent
"""
def __init__(self, category: ItemCategory, subevent):
self.id = category.id
self.name = str(category.name)
self.subevent_name = str(subevent)
self.description = category.description
class CrossSellingService:
def __init__(self, event, sales_channel: SalesChannel, cartpositions: List[CartPosition], customer):
self.event = event
self.sales_channel = sales_channel
self.cartpositions = cartpositions
self.customer = customer
def get_data(self):
if self.event.has_subevents:
subevents = set(pos.subevent for pos in self.cartpositions)
result = (
(DummyCategory(category, subevent),
self._prepare_items(subevent, items_qs, discount_info),
f'subevent_{subevent.pk}_')
for subevent in subevents
for (category, items_qs, discount_info) in self._applicable_categories(subevent.pk)
)
else:
result = (
(category,
self._prepare_items(None, items_qs, discount_info),
'')
for (category, items_qs, discount_info) in self._applicable_categories(0)
)
result = [(category, items, form_prefix) for (category, items, form_prefix) in result if len(items) > 0]
for category, items, form_prefix in result:
category.category_has_discount = any(item.original_price or (
item.has_variations and any(var.original_price for var in item.available_variations)
) for item in items)
return result
def _applicable_categories(self, subevent_id):
return [
(c, products_qs, discount_info) for (c, products_qs, discount_info) in
(
(c, *self._get_visible_items_for_category(subevent_id, c))
for c in self.event.categories.filter(cross_selling_mode__isnull=False).prefetch_related('items')
)
if products_qs is not None
]
def _get_visible_items_for_category(self, filter_subevent_id, category: ItemCategory):
"""
If this category should be visible in the cross-selling step for a given cart and sales_channel, this method
returns a queryset of the items that should be displayed, as well as a dict giving additional information on them.
:returns: (QuerySet<Item>, dict<(subevent_id, item_pk): (max_count, discount_rule)>)
max_count is `inf` if the item should not be limited
discount_rule is None if the item will not be discounted
"""
if category.cross_selling_mode is None:
return None, {}
if category.cross_selling_condition == 'always':
return category.items.all(), {}
if category.cross_selling_condition == 'products':
match = set(match.pk for match in category.cross_selling_match_products.only('pk')) # TODO prefetch this
return (category.items.all(), {}) if any(pos.item.pk in match for pos in self.cartpositions) else (None, {})
if category.cross_selling_condition == 'discounts':
my_item_pks = [item.id for item in category.items.all()]
potential_discount_items = {
item.pk: (max_count, discount_rule)
for subevent_id, item, max_count, discount_rule in self._potential_discounts_by_subevent_and_item_for_current_cart
if max_count > 0 and item.pk in my_item_pks and item.is_available() and (subevent_id == filter_subevent_id or subevent_id is None)
}
return category.items.filter(pk__in=potential_discount_items), potential_discount_items
@cached_property
def _potential_discounts_by_subevent_and_item_for_current_cart(self):
potential_discounts_by_cartpos = defaultdict(list)
from ..services.pricing import apply_discounts
self._discounted_prices = apply_discounts(
self.event,
self.sales_channel,
[
(cp.item_id, cp.subevent_id, cp.line_price_gross, bool(cp.addon_to), cp.is_bundled,
cp.listed_price - cp.price_after_voucher)
for cp in self.cartpositions
],
collect_potential_discounts=potential_discounts_by_cartpos
)
# flatten potential_discounts_by_cartpos (a dict of lists of potential discounts) into a set of potential discounts
# (which is technically stored as a dict, but we use it as an OrderedSet here)
potential_discount_set = dict.fromkeys(
info for lst in potential_discounts_by_cartpos.values() for info in lst)
# sum up the max_counts and pass them on (also pass on the discount_rules so we can calculate actual discounted prices later):
# group by benefit product
# - max_count for product: sum up max_counts
# - discount_rule for product: take first discount_rule
def discount_info(subevent_id, item, infos_for_item):
infos_for_item = list(infos_for_item)
return (
subevent_id,
item,
sum(max_count for (subevent_id, item, discount_rule, max_count, i) in infos_for_item),
next(discount_rule for (subevent_id, item, discount_rule, max_count, i) in infos_for_item),
)
return [
discount_info(subevent_id, item, infos_for_item) for (subevent_id, item), infos_for_item in
groupby(
sorted(
(
(subevent_id, item, discount_rule, max_count, i)
for (discount_rule, max_count, i, subevent_id) in potential_discount_set.keys()
for item in discount_rule.benefit_limit_products.all()
),
key=lambda tup: (tup[0], tup[1].pk)
),
lambda tup: (tup[0], tup[1]))
]
def _prepare_items(self, subevent, items_qs, discount_info):
items, _btn = get_grouped_items(
self.event,
subevent=subevent,
voucher=None,
channel=self.sales_channel,
base_qs=items_qs,
allow_addons=False,
allow_cross_sell=True,
memberships=(
self.customer.usable_memberships(
for_event=subevent or self.event,
testmode=self.event.testmode
)
if self.customer else None
),
)
new_items = list()
for item in items:
max_count = inf
if item.pk in discount_info:
(max_count, discount_rule) = discount_info[item.pk]
# only benefit_only_apply_to_cheapest_n_matches discounted items have a max_count, all others get 'inf'
if not max_count:
max_count = inf
# calculate discounted price
if discount_rule and discount_rule.benefit_discount_matching_percent > 0:
if not item.has_variations:
item.original_price = item.original_price or item.display_price
previous_price = item.display_price
new_price = (
previous_price * (
(Decimal('100.00') - discount_rule.benefit_discount_matching_percent) / Decimal('100.00'))
)
item.display_price = new_price
else:
# discounts always match "whole" items, not specific variations -> we apply the discount to all
# available variations of the item
for var in item.available_variations:
var.original_price = var.original_price or var.display_price
previous_price = var.display_price
new_price = (
previous_price * (
(Decimal('100.00') - discount_rule.benefit_discount_matching_percent) / Decimal('100.00'))
)
var.display_price = new_price
if not item.has_variations:
# reduce order_max by number of items already in cart (prevent recommending a product the user can't add anyway)
item.order_max = min(
item.order_max - sum(1 for pos in self.cartpositions if pos.item_id == item.pk),
max_count
)
if item.order_max > 0:
new_items.append(item)
else:
new_vars = list()
for var in item.available_variations:
# reduce order_max by number of items already in cart (prevent recommending a product the user can't add anyway)
var.order_max = min(
var.order_max - sum(1 for pos in self.cartpositions if pos.item_id == item.pk and pos.variation_id == var.pk),
max_count
)
if var.order_max > 0:
new_vars.append(var)
if len(new_vars):
item.available_variations = new_vars
new_items.append(item)
return new_items

View File

@@ -76,7 +76,7 @@ from pretix.base.services.tasks import TransactionAwareTask
from pretix.base.services.tickets import get_tickets_for_order
from pretix.base.signals import email_filter, global_email_filter
from pretix.celery_app import app
from pretix.helpers.format import format_map
from pretix.helpers.format import SafeFormatter, format_map
from pretix.helpers.hierarkey import clean_filename
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.presale.ical import get_private_icals
@@ -301,7 +301,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
order.event, 'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash()
'hash': order.email_confirm_secret()
}
)
)
@@ -311,11 +311,17 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
try:
if plain_text_only:
body_html = None
elif 'context' in inspect.signature(renderer.render).parameters:
body_html = renderer.render(content_plain, signature, raw_subject, order, position, context)
elif 'position' in inspect.signature(renderer.render).parameters:
# Backwards compatibility
warnings.warn('Email renderer called without context argument because context argument is not '
'supported.',
DeprecationWarning)
body_html = renderer.render(content_plain, signature, raw_subject, order, position)
else:
# Backwards compatibility
warnings.warn('E-mail renderer called without position argument because position argument is not '
warnings.warn('Email renderer called without position argument because position argument is not '
'supported.',
DeprecationWarning)
body_html = renderer.render(content_plain, signature, raw_subject, order)
@@ -323,6 +329,8 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
logger.exception('Could not render HTML body')
body_html = None
body_plain = format_map(body_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN)
send_task = mail_send_task.si(
to=[email] if isinstance(email, str) else list(email),
cc=cc,
@@ -655,7 +663,7 @@ def render_mail(template, context):
if isinstance(template, LazyI18nString):
body = str(template)
if context:
body = format_map(body, context)
body = format_map(body, context, mode=SafeFormatter.MODE_IGNORE_RICH)
else:
tpl = get_template(template)
body = tpl.render(context)

View File

@@ -26,6 +26,7 @@ from decimal import Decimal
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.html import escape
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
@@ -39,7 +40,8 @@ from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized
from pretix.base.signals import (
register_mail_placeholders, register_text_placeholders,
)
from pretix.helpers.format import SafeFormatter
from pretix.base.templatetags.rich_text import markdown_compile_email
from pretix.helpers.format import PlainHtmlAlternativeString, SafeFormatter
logger = logging.getLogger('pretix.base.services.placeholders')
@@ -107,6 +109,91 @@ class SimpleFunctionalTextPlaceholder(BaseTextPlaceholder):
return self._sample
class BaseRichTextPlaceholder(BaseTextPlaceholder):
"""
This is the base class for all placeholders which can render either to plain text
or to a rich HTML element.
"""
def __init__(self, identifier, args):
self._identifier = identifier
self._args = args
@property
def identifier(self):
return self._identifier
@property
def required_context(self):
return self._args
@property
def is_block(self):
return False
def render(self, context):
return PlainHtmlAlternativeString(
self.render_plain(**{k: context[k] for k in self._args}),
self.render_html(**{k: context[k] for k in self._args}),
self.is_block,
)
def render_html(self, **kwargs):
"""
HTML rendering of the placeholder. Should return "safe" HTML, i.e. everything needs to be
escaped.
"""
raise NotImplementedError
def render_plain(self, **kwargs):
"""
Plain text rendering of the placeholder.
"""
raise NotImplementedError
def render_sample(self, event):
return PlainHtmlAlternativeString(
self.render_sample_plain(event=event),
self.render_sample_html(event=event),
self.is_block,
)
def render_sample_html(self, event):
raise NotImplementedError
def render_sample_plain(self, event):
raise NotImplementedError
class SimpleButtonPlaceholder(BaseRichTextPlaceholder):
def __init__(self, identifier, args, url_func, text_func, sample_url_func, sample_text_func):
super().__init__(identifier, args)
self._url_func = url_func
self._text_func = text_func
self._sample_url_func = sample_url_func
self._sample_text_func = sample_text_func
def render_html(self, **context):
text = self._text_func(**{k: context[k] for k in self._args})
url = self._url_func(**{k: context[k] for k in self._args})
return f'<a href="{url}" class="button">{escape(text)}</a>'
def render_plain(self, **context):
text = self._text_func(**{k: context[k] for k in self._args})
url = self._url_func(**{k: context[k] for k in self._args})
return f'{text}: {url}'
def render_sample_html(self, event):
text = self._sample_text_func(event)
url = self._sample_url_func(event)
return f'<a href="{url}" class="button">{escape(text)}</a>'
def render_sample_plain(self, event):
text = self._sample_text_func(event)
url = self._sample_url_func(event)
return f'{text}: {url}'
class PlaceholderContext(SafeFormatter):
"""
Holds the contextual arguments and corresponding list of available placeholders for formatting
@@ -209,13 +296,24 @@ def get_best_name(position_or_address, parts=False):
def base_placeholders(sender, **kwargs):
from pretix.multidomain.urlreverse import build_absolute_uri
def _event_sample(event):
if event.has_subevents:
se = event.subevents.first()
if se:
return se.name
return event.name
ph = [
SimpleFunctionalTextPlaceholder(
'event', ['event'], lambda event: event.name, lambda event: event.name
),
SimpleFunctionalTextPlaceholder(
'event', ['event_or_subevent'], lambda event_or_subevent: event_or_subevent.name,
lambda event_or_subevent: event_or_subevent.name
_event_sample,
),
SimpleFunctionalTextPlaceholder(
'event_series_name', ['event', 'event_or_subevent'], lambda event, event_or_subevent: event.name,
lambda event: event.name
),
SimpleFunctionalTextPlaceholder(
'event_slug', ['event'], lambda event: event.slug, lambda event: event.slug
@@ -262,7 +360,7 @@ def base_placeholders(sender, **kwargs):
'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash()
'hash': order.email_confirm_secret()
}
), lambda event: build_absolute_uri(
event,
@@ -273,6 +371,27 @@ def base_placeholders(sender, **kwargs):
}
),
),
SimpleButtonPlaceholder(
'url_button', ['order', 'event'],
url_func=lambda order, event: build_absolute_uri(
event,
'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_secret()
}
),
text_func=lambda order, event: _("View order details"),
sample_url_func=lambda event: build_absolute_uri(
event,
'presale:event.order.open', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
'hash': '98kusd8ofsj8dnkd'
}
),
sample_text_func=lambda event: _("View order details"),
),
SimpleFunctionalTextPlaceholder(
'url_info_change', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
@@ -337,6 +456,27 @@ def base_placeholders(sender, **kwargs):
}
),
),
SimpleButtonPlaceholder(
'url_button', ['event', 'position'],
url_func=lambda event, position: build_absolute_uri(
event,
'presale:event.order.position', kwargs={
'order': position.order.code,
'secret': position.web_secret,
'position': position.positionid
}
),
text_func=lambda event, position: _("View registration details"),
sample_url_func=lambda event: build_absolute_uri(
event,
'presale:event.order.position', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
'position': '123'
}
),
sample_text_func=lambda event: _("View registration details"),
),
SimpleFunctionalTextPlaceholder(
'url_info_change', ['position', 'event'], lambda position, event: build_absolute_uri(
event,
@@ -443,7 +583,7 @@ def base_placeholders(sender, **kwargs):
'organizer': event.organizer.slug,
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash(),
'hash': order.email_confirm_secret(),
}),
)
for order in orders
@@ -592,8 +732,8 @@ def base_placeholders(sender, **kwargs):
class FormPlaceholderMixin:
def _set_field_placeholders(self, fn, base_parameters):
placeholders = get_available_placeholders(self.event, base_parameters)
def _set_field_placeholders(self, fn, base_parameters, rich=False):
placeholders = get_available_placeholders(self.event, base_parameters, rich=rich)
ht = format_placeholders_help_text(placeholders, self.event)
if self.fields[fn].help_text:
self.fields[fn].help_text += ' ' + str(ht)
@@ -604,7 +744,7 @@ class FormPlaceholderMixin:
)
def get_available_placeholders(event, base_parameters):
def get_available_placeholders(event, base_parameters, rich=False):
if 'order' in base_parameters:
base_parameters.append('invoice_address')
base_parameters.append('position_or_address')
@@ -613,6 +753,35 @@ def get_available_placeholders(event, base_parameters):
if not isinstance(val, (list, tuple)):
val = [val]
for v in val:
if isinstance(v, BaseRichTextPlaceholder) and not rich:
continue
if all(rp in base_parameters for rp in v.required_context):
params[v.identifier] = v
return params
def get_sample_context(event, context_parameters, rich=True):
context_dict = {}
lbl = _('This value will be replaced based on dynamic parameters.')
for k, v in get_available_placeholders(event, context_parameters, rich=rich).items():
sample = v.render_sample(event)
if isinstance(sample, PlainHtmlAlternativeString):
context_dict[k] = PlainHtmlAlternativeString(
sample.plain,
'<{el} class="placeholder placeholder-html" title="{title}">{html}</{el}>'.format(
el='div' if sample.is_block else 'span',
title=lbl,
html=sample.html,
)
)
elif str(sample).strip().startswith('* ') or str(sample).startswith(' '):
context_dict[k] = '<div class="placeholder" title="{}">{}</div>'.format(
lbl,
markdown_compile_email(str(sample))
)
else:
context_dict[k] = '<span class="placeholder" title="{}">{}</span>'.format(
lbl,
escape(sample)
)
return context_dict

View File

@@ -20,8 +20,9 @@
# <https://www.gnu.org/licenses/>.
#
import re
from collections import defaultdict
from decimal import Decimal
from typing import List, Optional, Tuple
from typing import List, Optional, Tuple, Union
from django import forms
from django.db.models import Q
@@ -31,6 +32,7 @@ from pretix.base.models import (
AbstractPosition, InvoiceAddress, Item, ItemAddOn, ItemVariation,
SalesChannel, Voucher,
)
from pretix.base.models.discount import Discount, PositionInfo
from pretix.base.models.event import Event, SubEvent
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
from pretix.base.timemachine import time_machine_now
@@ -155,14 +157,22 @@ def get_line_price(price_after_voucher: Decimal, custom_price_input: Decimal, cu
return price
def apply_discounts(event: Event, sales_channel: str,
positions: List[Tuple[int, Optional[int], Decimal, bool, bool]]) -> List[Decimal]:
def apply_discounts(event: Event, sales_channel: Union[str, SalesChannel],
positions: List[Tuple[int, Optional[int], Decimal, bool, bool, Decimal]],
collect_potential_discounts: Optional[defaultdict]=None) -> List[Tuple[Decimal, Optional[Discount]]]:
"""
Applies any dynamic discounts to a cart
:param event: Event the cart belongs to
:param sales_channel: Sales channel the cart was created with
:param positions: Tuple of the form ``(item_id, subevent_id, line_price_gross, is_addon_to, is_bundled, voucher_discount)``
:param collect_potential_discounts: If a `defaultdict(list)` is supplied, all discounts that could be applied to the cart
based on the "consumed" items, but lack matching "benefitting" items will be collected therein.
The dict will contain a mapping from index in the `positions` list of the item that could be consumed, to a list
of tuples describing the discounts that could be applied in the form `(discount, max_count, grouping_id)`.
`max_count` is either the maximum number of benefitting items that the discount would apply to, or `inf` if that number
is not limited. The `grouping_id` can be used to distinguish several occurrences of the same discount.
:return: A list of ``(new_gross_price, discount)`` tuples in the same order as the input
"""
if isinstance(sales_channel, SalesChannel):
@@ -177,10 +187,10 @@ def apply_discounts(event: Event, sales_channel: str,
).prefetch_related('condition_limit_products', 'benefit_limit_products').order_by('position', 'pk')
for discount in discount_qs:
result = discount.apply({
idx: (item_id, subevent_id, line_price_gross, is_addon_to, voucher_discount)
idx: PositionInfo(item_id, subevent_id, line_price_gross, is_addon_to, voucher_discount)
for idx, (item_id, subevent_id, line_price_gross, is_addon_to, is_bundled, voucher_discount) in enumerate(positions)
if not is_bundled and idx not in new_prices
})
}, collect_potential_discounts)
for k in result.keys():
result[k] = (result[k], discount)
new_prices.update(result)

View File

@@ -53,7 +53,7 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
v.tag = r.get('tag')
if v.comment:
v.comment += '\n\n'
v.comment = gettext('The voucher has been sent to {recipient}.').format(recipient=r['email'])
v.comment += gettext('The voucher has been sent to {recipient}.').format(recipient=r['email'])
logs.append(v.log_action(
'pretix.voucher.sent',
user=user,

View File

@@ -550,7 +550,7 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField,
'type': bool,
'form_kwargs': dict(
label=_("Require a business addresses"),
label=_("Require a business address"),
help_text=_('This will require users to enter a company name.'),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_invoice_address_required'}),
)
@@ -571,7 +571,7 @@ DEFAULTS = {
'form_class': I18nFormField,
'serializer_class': I18nField,
'form_kwargs': dict(
label=_("Custom recipient field"),
label=_("Custom recipient field label"),
widget=I18nTextInput,
help_text=_("If you want to add a custom text field, e.g. for a country-specific registration number, to "
"your invoice address form, please fill in the label here. This label will both be used for "
@@ -580,6 +580,18 @@ DEFAULTS = {
"The field will not be required.")
)
},
'invoice_address_custom_field_helptext': {
'default': '',
'type': LazyI18nString,
'form_class': I18nFormField,
'serializer_class': I18nField,
'form_kwargs': dict(
label=_("Custom recipient field help text"),
widget=I18nTextInput,
help_text=_("If you use the custom recipient field, you can specify a help text which will be displayed "
"underneath the field. It will not be displayed on the invoice.")
)
},
'invoice_address_vatid': {
'default': 'False',
'type': bool,
@@ -1295,7 +1307,8 @@ DEFAULTS = {
'form_kwargs': dict(
label=_("Show event times and dates on the ticket shop"),
help_text=_("If disabled, no date or time will be shown on the ticket shop's front page. This settings "
"does however not affect the display in other locations."),
"also affects a few other locations, however it should not be expected that the date of the "
"event is shown nowhere to users."),
)
},
'show_date_to': {

View File

@@ -287,9 +287,9 @@ class PhoneNumberShredder(BaseDataShredder):
class EmailAddressShredder(BaseDataShredder):
verbose_name = _('E-mails')
verbose_name = _('Emails')
identifier = 'order_emails'
description = _('This will remove all e-mail addresses from orders and attendees, as well as logged email '
description = _('This will remove all email addresses from orders and attendees, as well as logged email '
'contents. This will also remove the association to customer accounts.')
def generate_files(self) -> List[Tuple[str, str, str]]:

View File

@@ -367,7 +367,7 @@ validate_cart_addons = EventPluginSignal()
Arguments: ``addons``, ``base_position``, ``iao``
This signal is sent when a user tries to select a combination of addons. In contrast to
``validate_cart``, this is executed before the cart is actually modified. You are passed
``validate_cart``, this is executed before the cart is actually modified. You are passed
an argument ``addons`` containing a dict of ``(item, variation or None) → count`` tuples as well
as the ``ItemAddOn`` object as the argument ``iao`` and the base cart position as
``base_position``.
@@ -838,3 +838,12 @@ is given as the first argument.
The ``sender`` keyword argument will contain the organizer.
"""
device_info_updated = django.dispatch.Signal()
"""
Arguments: ``old_device``, ``new_device``
This signal is sent out each time the information for a Device is modified.
Both the original and updated versions of the Device are included to allow
receivers to see what has been updated.
"""

View File

@@ -131,6 +131,9 @@
text-align: left;
padding: 0;
}
.content table td.align-right {
text-align: right;
}
a.button {
display: inline-block;
@@ -178,6 +181,9 @@
pre, pre code {
white-space: pre-line;
}
.text-right, .content table td.text-right {
text-align: right;
}
{% if rtl %}
body {
@@ -186,6 +192,9 @@
.content {
text-align: right;
}
.text-right, .content table td.text-right {
text-align: left;
}
{% endif %}
{% block addcss %}{% endblock %}

View File

@@ -143,7 +143,7 @@
</tr>
</table>
<div class="order-button">
<a href="{% abseventurl event "presale:event.order.open" hash=order.email_confirm_hash order=order.code secret=order.secret %}" class="button">
<a href="{% abseventurl event "presale:event.order.open" hash=order.email_confirm_secret order=order.code secret=order.secret %}" class="button">
{% trans "View order details" %}
</a>
</div>

View File

@@ -0,0 +1,34 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from django import template
from django.utils.html import format_html
register = template.Library()
@register.simple_tag
def icon(key, *args, **kwargs):
return format_html(
'<span class="fa fa-{} {}" aria-hidden="true"></span>',
key,
kwargs["class"] if "class" in kwargs else "",
)

View File

@@ -52,12 +52,12 @@ def money_filter(value: Decimal, arg='', hide_currency=False):
# would make the numbers incorrect. If this branch executes, it's likely a bug in
# pretix, but we won't show wrong numbers!
if hide_currency:
return floatformat(value, 2)
return floatformat(value, "2g")
else:
return '{} {}'.format(arg, floatformat(value, 2))
return '{} {}'.format(arg, floatformat(value, "2g"))
if hide_currency:
return floatformat(value, places)
return floatformat(value, f"{places}g")
locale_parts = translation.get_language().split("-", 1)
locale = locale_parts[0]
@@ -70,7 +70,7 @@ def money_filter(value: Decimal, arg='', hide_currency=False):
try:
return format_currency(value, arg, locale=locale)
except:
return '{} {}'.format(arg, floatformat(value, places))
return '{} {}'.format(arg, floatformat(value, f"{places}g"))
@register.filter("money_numberfield")

View File

@@ -54,7 +54,7 @@ from tlds import tld_set
register = template.Library()
ALLOWED_TAGS_SNIPPET = [
ALLOWED_TAGS_SNIPPET = {
'a',
'abbr',
'acronym',
@@ -68,8 +68,8 @@ ALLOWED_TAGS_SNIPPET = [
'strike',
's',
# Update doc/user/markdown.rst if you change this!
]
ALLOWED_TAGS = ALLOWED_TAGS_SNIPPET + [
}
ALLOWED_TAGS = ALLOWED_TAGS_SNIPPET | {
'blockquote',
'li',
'ol',
@@ -91,7 +91,7 @@ ALLOWED_TAGS = ALLOWED_TAGS_SNIPPET + [
'h6',
'pre',
# Update doc/user/markdown.rst if you change this!
]
}
ALLOWED_ATTRIBUTES = {
'a': ['href', 'title', 'class'],
@@ -106,7 +106,7 @@ ALLOWED_ATTRIBUTES = {
# Update doc/user/markdown.rst if you change this!
}
ALLOWED_PROTOCOLS = ['http', 'https', 'mailto', 'tel']
ALLOWED_PROTOCOLS = {'http', 'https', 'mailto', 'tel'}
URL_RE = SimpleLazyObject(lambda: build_url_re(tlds=sorted(tld_set, key=len, reverse=True)))
@@ -211,9 +211,9 @@ class CleanPostprocessor(Postprocessor):
def run(self, text):
return bleach.clean(
text,
tags=self.tags,
tags=set(self.tags),
attributes=self.attributes,
protocols=self.protocols,
protocols=set(self.protocols),
strip=self.strip
)
@@ -305,10 +305,11 @@ def markdown_compile_email(source, allowed_tags=ALLOWED_TAGS, allowed_attributes
source,
extensions=[
'markdown.extensions.sane_lists',
'markdown.extensions.tables',
EmailNl2BrExtension(),
LinkifyAndCleanExtension(
linker,
tags=allowed_tags,
tags=set(allowed_tags),
attributes=allowed_attributes,
protocols=ALLOWED_PROTOCOLS,
strip=False,

View File

@@ -0,0 +1,42 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from django import template
from django.utils.html import format_html, mark_safe
register = template.Library()
@register.simple_tag
def textbubble(type, *args, **kwargs):
return format_html(
'<span class="textbubble-{}">{}',
type or "info",
"" if "icon" not in kwargs else format_html(
'<i class="fa fa-{}" aria-hidden="true"></i> ',
kwargs["icon"]
)
)
@register.simple_tag
def endtextbubble():
return mark_safe('</span>')

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