Compare commits

...
Author SHA1 Message Date
Martin Gross de1e7abc48 base/pdf: Only call get_fonts() once per generation 2024-03-08 11:23:31 +01:00
Martin Gross 5fa5810670 Invoices: Add support for event-fonts 2024-02-28 22:03:43 +01:00
Martin Gross 572fcf4751 Editor/webfonts.css: Do not create a font section for "pdf_only" 2024-02-28 21:46:33 +01:00
Martin Gross c11f718253 isort 2024-02-28 21:31:39 +01:00
Martin Gross 7949be15b7 Review Comments: Do not run signal for every single textbox on page 2024-02-28 21:30:03 +01:00
Martin Gross cc020f24a2 Review Comments: Check for // instead of https for web-webfonts 2024-02-28 21:21:52 +01:00
Martin Gross f0a76a3ee0 Add register_event_fonts signal and facilities for web-embedded webfonts 2024-02-13 10:20:12 +01:00
Phin WolkwitzandGitHub cb17d80b63 Widget: Fix variations collapse indicator placement (#3883) 2024-02-12 13:51:25 +01:00
Raphael MichelandGitHub 86b28b9b53 Get rid of unmaintained dependency python-u2flib-server (#3879) 2024-02-12 13:03:56 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fac404631c Update sentry-sdk requirement from ==1.15.* to ==1.40.* (#3850)
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/1.15.0...1.40.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-02-12 12:53:01 +01:00
Phin WolkwitzandRaphael Michel fd547014a9 Translations: Update German (informal) (de_Informal)
Currently translated at 99.1% (223 of 225 strings)

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

powered by weblate
2024-02-12 10:17:44 +01:00
Phin WolkwitzandRaphael Michel d23a625415 Translations: Update German
Currently translated at 99.1% (223 of 225 strings)

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

powered by weblate
2024-02-12 10:17:44 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
199416b904 Update django-redis requirement from ==5.2.* to ==5.4.* (#3877)
Updates the requirements on [django-redis](https://github.com/jazzband/django-redis) to permit the latest version.
- [Release notes](https://github.com/jazzband/django-redis/releases)
- [Changelog](https://github.com/jazzband/django-redis/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jazzband/django-redis/compare/5.2.0...5.4.0)

---
updated-dependencies:
- dependency-name: django-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-02-12 10:01:52 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0a7a113b4e Update pytest-rerunfailures requirement from ==11.* to ==13.* (#3878)
Updates the requirements on [pytest-rerunfailures](https://github.com/pytest-dev/pytest-rerunfailures) to permit the latest version.
- [Changelog](https://github.com/pytest-dev/pytest-rerunfailures/blob/master/CHANGES.rst)
- [Commits](https://github.com/pytest-dev/pytest-rerunfailures/compare/11.0...13.0)

---
updated-dependencies:
- dependency-name: pytest-rerunfailures
  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-02-12 09:47:04 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5978a715b5 Update aiohttp requirement from ==3.8.* to ==3.9.* (#3875)
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.8.0a0...v3.9.3)

---
updated-dependencies:
- dependency-name: aiohttp
  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-02-12 09:41:41 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
71beb54eb4 Bump django-formtools from 2.4.1 to 2.5.1 (#3876)
Bumps [django-formtools](https://github.com/jazzband/django-formtools) from 2.4.1 to 2.5.1.
- [Changelog](https://github.com/jazzband/django-formtools/blob/master/docs/changelog.rst)
- [Commits](https://github.com/jazzband/django-formtools/compare/2.4.1...2.5.1)

---
updated-dependencies:
- dependency-name: django-formtools
  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-02-12 09:35:10 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b92981353f Update django-statici18n requirement from ==2.3.* to ==2.4.* (#3874)
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/commits)

---
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-02-12 09:34:55 +01:00
Raphael Michel a5f7115e19 Allow dependabot to create more PRs (as we already have 5 long-running) 2024-02-12 09:08:12 +01:00
Raphael Michel f5e3d4b0bc API: Fix logging of event deletion 2024-02-09 17:27:08 +01:00
Raphael Michel 6fba080b8f Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2024-02-09 15:51:35 +01:00
365ccf159e Widget: Change text when expanding variations (Z#23141075) (#3852)
* Add text change to variation toggle
* Add svg caret
* Fix svg and css
* update caret-svg, add fill-link-color and animation
* Use computed property for link text
* Rename variable according to code review
* Improve texts

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-02-09 13:29:40 +01:00
Raphael Michel b40a41d742 OrderListExporter: Refactor querysets out of iterate methods 2024-02-09 12:03:26 +01:00
bf1081071b CheckinlistPDF: Export attendee company based on op; fallback to IA (Z#23144201) (#3869)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-02-09 10:10:45 +01:00
Phin WolkwitzandGitHub e261ce7554 Fix key error on copy_from (PRETIXEU-9NR) (#3868) 2024-02-08 17:34:24 +01:00
Raphael Michel 7cae0ceab8 Fix migration of old hide_without_voucher products 2024-02-08 13:08:13 +01:00
Mie FrydensbjergandRaphael Michel 064ee91225 Translations: Update Danish
Currently translated at 31.3% (1740 of 5550 strings)

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

powered by weblate
2024-02-08 12:55:10 +01:00
Raphael Michel 495b3ec770 API: Log ID of revoked device connection 2024-02-08 10:09:28 +01:00
Phin WolkwitzandGitHub 39f9329207 Quota form: Change item selection field per context (#3839)
* Change item checkbox select to multiselect widget

* Make item selection widget dependent on count

* Make item selection widget dependent on variable

* Adjust widget choices

* Fix widget choices

* Fix item variation key errors

* Simplify code

* Fix classname

* Improve argument name

* Fix widget name
2024-02-08 09:33:39 +01:00
Raphael Michel 2b72cfdaff Fix cration of variations 2024-02-07 10:58:56 +01:00
Raphael Michel 70d32ea1aa Voucher form: Add quota in help text 2024-02-07 09:59:32 +01:00
0871482681 Add product variation to addon-label when modifying order (#3864)
* Added Variation of Product by Additional Products

at Postion Name by Additional Products the Variant should missing. added it with the prefix -

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

---------

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
2024-02-07 09:48:01 +01:00
Raphael MichelandGitHub 5d4f3eab06 Stripe: Add link to Stripe App (#3860) 2024-02-06 17:48:13 +01:00
MiraandGitHub fa3265b1fb Let plugins prevent the download of individual tickets in an order (#3858)
* Let plugins allow/prevent the download of individual tickets in an order (#3836)

(extends the functionality of the allow_ticket_download signal)

(cherry picked from commit e20edab98f)

* fix bug where in some cases, only the first ticket could be downloaded
2024-02-06 17:35:59 +01:00
Richard Schreiber 92e6ffc7ef Fix widget unavailability_reason 2024-02-06 13:55:34 +01:00
22f91f7aa2 Improve UI to configure unavailable items handling (Z#23131828) (#3739)
* start impl of unavailability modes ui

* add db migration

* use new widget for more fields

* improve contrast

* use new widget for hide_without_voucher field

* improved wording

* rebase migration

* undo changes to require_membership_hidden

* code formatting

* move unavail_reason logic around

* enforce consistent state of hide_without_voucher / require_voucher

* annotate unavailability info in get_grouped_items

* remove MSIE6 compat

* add unavailability reasons to widget

* remove test output

* Apply suggestions from code review

text improvements

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

* add css fix for jumping items due to tooltip

* dynamically retrieve unavailability reason message

* widget: simplify logic conditions

* add available_{from,until}_mode to api and api docs

* rebase migration

* rebase migration

* add unavailable_*_mode to ItemVariation

* add available_*_mode to API docs for items

* fix wrong reference

* fix test cases

* add available_*_mode to item variation form

* apply unavailability modes to subevents and variations (presale)

* /o\

* apply unavailability modes to subevents and variations (widget)

* display unavailability mode in subevent product settings

* fix widget test

* fix api item tests

* copy available_*_mode when copying an item

* Apply suggestions from code review

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

* Add unavail mode indicator to bulk create and edit forms

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
Co-authored-by: Raphael Michel <michel@rami.io>
2024-02-06 12:27:19 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
43facd1e43 Bump @babel/core from 7.23.7 to 7.23.9 in /src/pretix/static/npm_dir (#3854)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.23.7 to 7.23.9.
- [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.23.9/packages/babel-core)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-06 11:54:57 +01:00
Martin GrossandGitHub 11242a2325 Docs/Widget: Add Payment Method Domain Registration Instructions (Z#23143782) (#3861)
* Docs/Widget: Add Payment Method Domain Registration Instructions

* Rephrase
2024-02-06 11:38:32 +01:00
Richard SchreiberandGitHub 28db9a5262 Fix item column overflow in order-overview PDF-report (#3857)
* Fix automatic line-break in order-overview PDF-report

* vertical align top
2024-02-06 11:37:57 +01:00
Raphael Michel 57e8c6aafd Fix #3856 -- Clarify label in sendmail form 2024-02-06 11:35:55 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fa47f63307 Bump @babel/preset-env from 7.23.7 to 7.23.9 in /src/pretix/static/npm_dir (#3855)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.23.7 to 7.23.9.
- [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.23.9/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-02-06 11:33:13 +01:00
bac673f3ab Allow template syntax in event text (Z#23140046) (#3815)
* remove duplicate context generation

* allow text templates in frontpage_text

* refactor: move placeholder functionality to separate file

* fix wrong class name, code style

* update year in license header

* undo license header update

* use new function name

* render only the placeholders that are actually used in the message

* refactoring

* add str(...) call

* Update doc/development/api/placeholder.rst

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

* rename register_mail_placeholders to register_template_placeholders
(deprecate old name)

* isort

* add signals to docs

---------

Co-authored-by: Raphael Michel <michel@rami.io>
2024-02-06 11:32:03 +01:00
Martin GrossandGitHub 45ac391998 BasePaymentProvider: skip country check if ia.country == '' (Z#23143749) (#3859) 2024-02-05 16:51:04 +01:00
Martin Gross fed5097708 CheckIn: Only run check_rules_qs conditionally on provided boolean 2024-02-05 14:01:42 +01:00
Martin Gross 9d115c30d7 Revert "Let plugins allow/prevent the download of individual tickets in an order (#3836)"
This reverts commit e20edab98f.
2024-02-02 16:09:42 +01:00
Martin Gross a769da62c7 Addresses: Add Federal Territories to state dropdown for MY. 2024-02-02 12:31:06 +01:00
MiraandGitHub e20edab98f Let plugins allow/prevent the download of individual tickets in an order (#3836)
(extends the functionality of the allow_ticket_download signal)
2024-02-01 17:45:58 +01:00
Pavle ErgovićandRaphael Michel 4f4fcb84ce Translations: Update Croatian
Currently translated at 1.3% (3 of 221 strings)

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

powered by weblate
2024-01-31 14:25:07 +01:00
Pavle ErgovićandRaphael Michel 6560d161c9 Translations: Update Croatian
Currently translated at 0.1% (4 of 5550 strings)

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

powered by weblate
2024-01-31 14:25:07 +01:00
Eduardo FernandezandRaphael Michel 7f23c590ca Translations: Update Spanish
Currently translated at 89.2% (4954 of 5550 strings)

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

powered by weblate
2024-01-31 14:25:07 +01:00
0ca33eddb1 Cart: More useful error message if some selected products are sold (#3848)
* Cart: More useful error message if some selected products are sold

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

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

---------

Co-authored-by: Mira <weller@rami.io>
2024-01-31 14:23:07 +01:00
Raphael MichelandRaphael Michel 45341c4a31 Translations: Update German
Currently translated at 100.0% (5550 of 5550 strings)

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

powered by weblate
2024-01-30 17:37:50 +01:00
Raphael MichelandRaphael Michel 5de5ae4ca2 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5550 of 5550 strings)

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

powered by weblate
2024-01-30 17:37:50 +01:00
Raphael Michel 03f71f3cdf Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2024-01-30 17:10:54 +01:00
f97ad66026 Add formats for en_CA (#3844)
* Create __init__.py

* Create formats.py for Canadian English

Referencing: https://github.com/pretix/pretix/discussions/3842


Based on:

- https://github.com/pretix/pretix/blob/master/src/pretix/helpers/formats/en_US/formats.py (for time formatting and structure)

- https://github.com/django/django/blob/main/django/conf/locale/fr_CA/formats.py (For dates)

* Update __init__.py

---------

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
2024-01-30 17:03:39 +01:00
Raphael MichelandGitHub 31392e5852 Add simplified invoice renderer (#3846) 2024-01-30 16:14:53 +01:00
Raphael MichelandGitHub 77a5a685f1 Bank transfer: Clarify instructions on pending payment (#3845) 2024-01-30 16:14:21 +01:00
Raphael Michel 2b77e59e0a Fix #3838 -- String not in translation 2024-01-30 12:16:09 +01:00
Raphael Michel 70f755599a Add "lead" to slug banlist 2024-01-30 10:10:47 +01:00
Raphael Michel 2a76b2a5dd Docs: Add new exhibitor API fields 2024-01-30 10:08:19 +01:00
Raphael Michel ffea243eae Bank transfer: Fix invoice address not existing (PRETIXEU-9M1) 2024-01-29 16:20:38 +01:00
Raphael Michel a4012e6100 Remove nl_BE *again* 2024-01-29 14:53:45 +01:00
Raphael Michel 9bd250f9fc Bump version to 2024.2.0.dev0 2024-01-29 13:47:00 +01:00
Raphael Michel 7f93fb6b4d Bump version to 2024.1.0 2024-01-29 13:46:05 +01:00
Raphael Michel 1b3f27852d Calendar: Fix week day header for incomplete months 2024-01-29 13:32:36 +01:00
Raphael Michel fab29088e6 Event calendar: Don't skip week if it is still sunday 2024-01-29 12:57:35 +01:00
Raphael MichelandRaphael Michel 9feea75bdb Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5546 of 5546 strings)

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

powered by weblate
2024-01-29 12:29:38 +01:00
Raphael MichelandRaphael Michel 52f08c818f Translations: Update German
Currently translated at 100.0% (5546 of 5546 strings)

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

powered by weblate
2024-01-29 12:29:38 +01:00
Raphael Michel e89483e3f2 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2024-01-29 12:17:07 +01:00
Raphael Michel 467469f26d Fix typo 2024-01-29 10:35:40 +01:00
MiraandGitHub aa9d526207 Allow consistent deep links to tabs (#3840) 2024-01-29 09:38:41 +01:00
Eduardo FernandezandRaphael Michel 3338858420 Translations: Update Spanish
Currently translated at 88.9% (4937 of 5548 strings)

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

powered by weblate
2024-01-29 09:32:59 +01:00
BMasterandRaphael Michel 3e758c6a1c Translations: Update Dutch
Currently translated at 83.2% (4617 of 5545 strings)

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

powered by weblate
2024-01-29 09:32:59 +01:00
RyoandRaphael Michel fc7e69523c Translations: Update Japanese
Currently translated at 3.1% (172 of 5545 strings)

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

powered by weblate
2024-01-29 09:32:59 +01:00
BerkieBbandRaphael Michel 3289b4411a Translations: Update Dutch
Currently translated at 83.1% (4613 of 5545 strings)

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

powered by weblate
2024-01-29 09:32:59 +01:00
AlainandRaphael Michel 5c98512241 Translations: Update Dutch
Currently translated at 83.1% (4613 of 5545 strings)

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

powered by weblate
2024-01-29 09:32:59 +01:00
Freek EngelbartsandRaphael Michel 3ca22933f4 Translations: Update Dutch
Currently translated at 83.1% (4613 of 5545 strings)

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

powered by weblate
2024-01-29 09:32:59 +01:00
BMasterandRaphael Michel 106d3f3c74 Translations: Update Dutch
Currently translated at 83.1% (4613 of 5545 strings)

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

powered by weblate
2024-01-29 09:32:59 +01:00
15d3fbc02e Fix auto-selected date in calendar with long-running events (Z#23141457) (#3829)
* Fix auto-selected date in calendar with long-running events (Z#23141457)

* Review fixes

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

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

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-01-26 19:22:45 +01:00
Raphael MichelandGitHub eaae7e9ea2 Allow to set amount and date when manually confirming a payment (#3828)
* Allow to set amount and date when manually confirming a payment

* Fix tests
2024-01-26 19:22:15 +01:00
Raphael MichelandGitHub 9eb1c5047b Subevent bulk creation: Handle available_from of items the same way as dates (#3822) 2024-01-26 19:20:34 +01:00
Raphael Michel 25d4b603be Docs: Add note on where to set up meta data 2024-01-26 11:48:46 +01:00
Raphael Michel eac88b5ef7 Widget: Fix language on first iframe request 2024-01-26 11:44:02 +01:00
Raphael Michel 734b376e05 Push missing file 2024-01-26 11:28:52 +01:00
Raphael Michel 2354affde7 API: Fix CSRF support for session-based usage 2024-01-26 11:20:56 +01:00
Martin Gross 11e922b1a9 Teams: Link email to user for superusers 2024-01-25 12:22:35 +01:00
Raphael Michel b7f0c3cc6c Fix cookie detection 2024-01-25 10:35:26 +01:00
Kian CrossandGitHub f9f629299b Ensure customer_signed_in signal is fired for popup login (#3835)
In a7f7c64, a `customer_signed_in` in signal was introduced. However,
when a user currently logs in using a popup, the signal is not fired.
This commit resolves this.
2024-01-25 10:03:50 +01:00
Raphael MichelandGitHub 645c9d5900 Add "Partitioned" flag to our cookies (#3830) 2024-01-25 09:46:04 +01:00
Raphael MichelandGitHub 6af2d38a98 Add __Host- prefix to CSRF and session cookie, remove cookie_domain (#3831)
* Add __Host- prefix to CSRF and session cookie, remove cookie_domain

* Fix tests
2024-01-25 09:45:56 +01:00
Raphael Michel dba8e80868 Delete nl_BE again 2024-01-24 17:48:53 +01:00
Raphael MichelandRaphael Michel 324ff2ab67 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5545 of 5545 strings)

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

powered by weblate
2024-01-24 16:48:09 +01:00
Raphael MichelandRaphael Michel 5ea09a5b0a Translations: Update German
Currently translated at 100.0% (5545 of 5545 strings)

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

powered by weblate
2024-01-24 16:48:09 +01:00
Eduardo FernandezandRaphael Michel c046caa894 Translations: Update Spanish
Currently translated at 88.4% (4898 of 5535 strings)

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

powered by weblate
2024-01-24 16:48:09 +01:00
Raphael Michel be8197825c Translations: Update wordlist 2024-01-24 16:30:38 +01:00
Raphael Michel d48c28bec7 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2024-01-24 14:03:24 +01:00
Raphael Michel b07c1802a0 Add limits to gift card length 2024-01-24 14:02:54 +01:00
Raphael MichelandGitHub 7f0cf1655a Stripe/PayPal: Add explanatory texts (Z#23127572) (#3826) 2024-01-24 14:02:44 +01:00
Phin WolkwitzandGitHub 0a154dc79f Presale: Fix booking period error message logic (Z#23115841) (#3832)
* Fix presale start and end error message logic

* Improve readability
2024-01-23 15:58:35 +01:00
Raphael Michel 7022e5bf49 Add missing entries to POS security profile 2024-01-23 11:40:41 +01:00
Raphael MichelandRaphael Michel 86ae18564d Translations: Update Chinese (Traditional)
Currently translated at 97.7% (214 of 219 strings)

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

powered by weblate
2024-01-22 18:11:08 +01:00
Raphael MichelandRaphael Michel a07a04d115 Translations: Update Chinese (Traditional)
Currently translated at 97.0% (5368 of 5533 strings)

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

powered by weblate
2024-01-22 18:11:08 +01:00
Eduardo FernandezandRaphael Michel 51e7c889ec Translations: Update Spanish
Currently translated at 86.5% (4790 of 5535 strings)

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

powered by weblate
2024-01-22 18:11:08 +01:00
Raphael Michel 4cd2381a5e Fix CartManager.apply_voucher to handle all_bundles_included 2024-01-22 17:55:32 +01:00
Raphael Michel 6474240d49 Fix display price for vouchers with all_bundles_included 2024-01-22 17:43:11 +01:00
Martin GrossandGitHub 0938bf3246 Stripe: Unidecode Statement Descriptor; unify allowed characters and symbols (#3825)
* Stripe: Unidecode Statement Descriptor; unify allowed characters and symbols

* Reverse str/unidecode order
2024-01-22 14:07:29 +01:00
Raphael Michel aad94f1b2a Add X-Mailer header on outgoing emails 2024-01-22 13:38:05 +01:00
Eduardo FernandezandRaphael Michel a677575eef Translations: Update Spanish
Currently translated at 85.6% (4742 of 5535 strings)

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

powered by weblate
2024-01-22 13:37:23 +01:00
Eduardo FernandezandRaphael Michel 9160e01333 Translations: Update Spanish
Currently translated at 85.3% (4722 of 5535 strings)

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

powered by weblate
2024-01-22 13:37:23 +01:00
Raphael MichelandRaphael Michel d2b9fe475b Translations: Delete Dutch (Belgium) 2024-01-22 13:37:23 +01:00
Martin Gross cff9ae6b18 Stripe: Filter statement_descriptor postfix for unacceptable characters 2024-01-22 12:15:21 +01:00
Raphael MichelandGitHub 4fb49820af Add upper limit on positions in an order (#3806)
* Add upper limit on positions in an order

* Fix form validation
2024-01-19 18:14:45 +01:00
chandiandRaphael Michel 1f465ddddb Translations: Update German
Currently translated at 100.0% (5533 of 5533 strings)

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

powered by weblate
2024-01-19 16:51:56 +01:00
Eduardo FernandezandRaphael Michel 1bda102d46 Translations: Update Spanish
Currently translated at 84.9% (4700 of 5535 strings)

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

powered by weblate
2024-01-19 16:51:56 +01:00
Clau LungandRaphael Michel 8066a7ed0f Translations: Update Chinese (Traditional)
Currently translated at 97.7% (214 of 219 strings)

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

powered by weblate
2024-01-19 16:51:56 +01:00
Eduardo FernandezandRaphael Michel bc960ffde1 Translations: Update Spanish
Currently translated at 84.7% (4690 of 5535 strings)

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

powered by weblate
2024-01-19 16:51:56 +01:00
Eduardo FernandezandRaphael Michel cbbbf44f1f Translations: Update Spanish
Currently translated at 84.3% (4669 of 5535 strings)

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

powered by weblate
2024-01-19 16:51:56 +01:00
Pedro PortelaandRaphael Michel 28ab0d118c Translations: Update Portuguese (Brazil)
Currently translated at 11.6% (646 of 5533 strings)

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

powered by weblate
2024-01-19 16:51:56 +01:00
Eduardo FernandezandRaphael Michel 15710cbec9 Translations: Update Spanish
Currently translated at 82.7% (4579 of 5535 strings)

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

powered by weblate
2024-01-19 16:51:56 +01:00
Eduardo FernandezandRaphael Michel 4aaeee64d0 Translations: Update Spanish
Currently translated at 82.6% (4575 of 5535 strings)

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

powered by weblate
2024-01-19 16:51:56 +01:00
BMasterandRaphael Michel ffd3882aa8 Translations: Update Dutch (informal) (nl_Informal)
Currently translated at 72.3% (4002 of 5533 strings)

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

powered by weblate
2024-01-19 16:51:56 +01:00
BMasterandRaphael Michel 36a90c4e76 Translations: Update Dutch
Currently translated at 83.2% (4607 of 5533 strings)

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

powered by weblate
2024-01-19 16:51:56 +01:00
Eduardo FernandezandRaphael Michel 098580ccf4 Translations: Update Spanish
Currently translated at 82.5% (4567 of 5535 strings)

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

powered by weblate
2024-01-19 16:51:56 +01:00
Eduardo FernandezandRaphael Michel c0f8c9b9e4 Translations: Update Spanish
Currently translated at 81.2% (4499 of 5535 strings)

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

powered by weblate
2024-01-19 16:51:56 +01:00
Raphael Michel a1a553eb52 Overview report: Fix usage of internal names 2024-01-19 16:38:32 +01:00
Richard SchreiberandGitHub 18e1e7716c Fix: remove duplicate repeat rule in subevent bulk creation (#3819) 2024-01-19 16:25:38 +01:00
Raphael Michel 081d3a73fa Fix crash in item form validation 2024-01-19 15:40:22 +01:00
Raphael Michel 4234a6440d Fix crash in discount computation when cloning series to single event 2024-01-19 15:40:22 +01:00
Richard SchreiberandGitHub a09275c582 Docs: fix typos in widget-docs 2024-01-19 10:40:39 +01:00
Raphael MichelandGitHub a8099a1284 Fix #3810 -- Stripe: Move to statement_descriptor_suffix (#3816) 2024-01-17 22:31:07 +01:00
Richard SchreiberandGitHub 2f19b48f97 Improve logging on general Ajax-errors 2024-01-17 09:43:08 +01:00
Richard SchreiberandGitHub abbf7bdd69 Widget: add support for html inside pretix-button 2024-01-17 09:39:33 +01:00
Raphael Michel 6cf8c7d4f0 Order overview PDF: Fix date filter 2024-01-15 11:58:54 +01:00
Raphael Michel c6196f9900 Order overview PDF: Fix date filter 2024-01-12 17:49:41 +01:00
Raphael Michel cd7850337b Add exporter for reusable media 2024-01-12 17:41:31 +01:00
Raphael MichelandGitHub 0220965ca9 Check-in: Add rule for number of days with entries since (#3808) 2024-01-12 17:09:51 +01:00
Raphael Michel bae1512235 Extended order search: Allow to search by quota 2024-01-12 15:55:34 +01:00
Raphael Michel 71f8a3ad3e Order overview PDF: Allow to filter by subevent date 2024-01-12 15:55:27 +01:00
Raphael Michel 24dd065839 Revert "Order overview PDF: Allow to filter by subevent date"
This reverts commit 08f0150177.
2024-01-12 15:54:59 +01:00
Raphael Michel 08f0150177 Order overview PDF: Allow to filter by subevent date 2024-01-12 15:53:38 +01:00
Raphael Michel 367a4fc64e Stripe: Fix compatibility with old payments 2024-01-12 12:45:53 +01:00
Raphael Michel 33ace8554b Check-in list export: Constant-memory implementation 2024-01-12 10:58:26 +01:00
94cbb19db0 Stripe: Convert all payment methods to intents except multibanco (#3780)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-01-12 10:23:00 +01:00
Raphael MichelandRaphael Michel ea33c7b1b9 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5533 of 5533 strings)

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

powered by weblate
2024-01-11 14:28:15 +01:00
Raphael MichelandRaphael Michel cba5fd4c27 Translations: Update German
Currently translated at 100.0% (5533 of 5533 strings)

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

powered by weblate
2024-01-11 14:28:15 +01:00
Raphael MichelandRaphael Michel 297999a00e Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5533 of 5533 strings)

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

powered by weblate
2024-01-11 14:28:15 +01:00
Raphael MichelandRaphael Michel cbbf13e9e2 Translations: Update German
Currently translated at 100.0% (5533 of 5533 strings)

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

powered by weblate
2024-01-11 14:28:15 +01:00
Eduardo FernandezandRaphael Michel 4437706494 Translations: Update Spanish
Currently translated at 78.7% (4357 of 5533 strings)

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

powered by weblate
2024-01-11 14:28:15 +01:00
Raphael Michel 3fff023a8a Check-in: Do not crash fully on invalid rule 2024-01-11 14:07:37 +01:00
Raphael Michel 583429b6d9 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2024-01-11 11:07:24 +01:00
Raphael Michel 61c12ca871 Checkout: Update text for missing membership 2024-01-11 10:58:01 +01:00
Raphael Michel e3145b79d5 Stop using deprecated dnspython method 2024-01-11 10:37:10 +01:00
Raphael Michel 1d8f789c21 Docs: Add DMARC recommendation 2024-01-11 10:33:59 +01:00
Raphael Michel eb8e36e1eb Fix imports in tests 2024-01-10 10:06:30 +01:00
Eduardo FernandezandRaphael Michel 21d8d93bed Translations: Update Spanish
Currently translated at 76.7% (4244 of 5533 strings)

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

powered by weblate
2024-01-10 10:02:00 +01:00
MiraandRaphael Michel f2e09c020b Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5533 of 5533 strings)

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

powered by weblate
2024-01-10 10:02:00 +01:00
MiraandRaphael Michel ebe5647066 Translations: Update German
Currently translated at 100.0% (5533 of 5533 strings)

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

powered by weblate
2024-01-10 10:02:00 +01:00
Raphael Michel 5b5e831e37 Order search: Add filter for pending without payment 2024-01-09 16:17:46 +01:00
8a155d5fe3 Name formats: Allow empty salutation (#3801)
* Name formats: Allow empty salutation

* Update src/pretix/base/forms/questions.py

Co-authored-by: Felix Rindt <felix@rindt.me>

---------

Co-authored-by: Felix Rindt <felix@rindt.me>
2024-01-09 15:29:00 +01:00
Raphael MichelandGitHub 2c67b82f4a Bank transfer: Allow using external IDs for deduplication (#3803)
* Bank transfer: Allow using external IDs for deduplication

* Do not use empty string in nullable field
2024-01-09 14:01:01 +01:00
Raphael MichelandRaphael Michel 7a2878657d Translations: Update Spanish
Currently translated at 75.2% (4165 of 5533 strings)

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

powered by weblate
2024-01-09 10:23:47 +01:00
Eduardo FernandezandRaphael Michel 90f9dd2bde Translations: Update Spanish
Currently translated at 75.2% (4165 of 5533 strings)

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

powered by weblate
2024-01-09 10:23:47 +01:00
Eduardo FernandezandRaphael Michel 6c292caa62 Translations: Update Spanish
Currently translated at 70.5% (3902 of 5533 strings)

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

powered by weblate
2024-01-09 10:23:47 +01:00
Eduardo FernandezandRaphael Michel d3e73c4fc2 Translations: Update Spanish
Currently translated at 70.2% (3885 of 5533 strings)

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

powered by weblate
2024-01-09 10:23:47 +01:00
MiraandGitHub bfe0b218de Add regexp compare for data-display-dependency-value (#3802) 2024-01-09 09:44:50 +01:00
Raphael Michel 0f44702640 Geo coding: Use always-ascii cache keys 2024-01-08 11:18:29 +01:00
Eduardo FernandezandRaphael Michel 8185175d14 Translations: Update Spanish
Currently translated at 70.0% (3878 of 5533 strings)

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

powered by weblate
2024-01-04 16:37:42 +01:00
Raphael Michel 8c24209e55 Allow image icons in info-row on start page 2024-01-04 16:22:55 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Raphael Michel
5c80b75d09 Bump vue from 2.7.15 to 2.7.16 in /src/pretix/static/npm_dir (#3795)
* Bump vue-template-compiler in /src/pretix/static/npm_dir

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

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

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

* Update vue as well

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Raphael Michel <michel@rami.io>
2024-01-03 15:01:00 +01:00
Eduardo FernandezandRaphael Michel 79c00dfa6c Translations: Update Spanish
Currently translated at 67.8% (3755 of 5533 strings)

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

powered by weblate
2024-01-03 13:09:53 +01:00
Raphael MichelandRaphael Michel 3793300d45 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5533 of 5533 strings)

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

powered by weblate
2024-01-03 13:09:53 +01:00
Raphael MichelandRaphael Michel a70cd792c8 Translations: Update German
Currently translated at 100.0% (5533 of 5533 strings)

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

powered by weblate
2024-01-03 13:09:53 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7c479c1df6 Bump @babel/core from 7.23.6 to 7.23.7 in /src/pretix/static/npm_dir (#3793)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.23.6 to 7.23.7.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.7/packages/babel-core)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-03 11:30:35 +01:00
Raphael Michel efd074a1ee Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2024-01-03 11:29:54 +01:00
RobertSF92andRaphael Michel 7d27440003 Translations: Update Dutch
Currently translated at 82.5% (4561 of 5528 strings)

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

powered by weblate
2024-01-03 11:29:24 +01:00
Eduardo FernandezandRaphael Michel 269f7bf789 Translations: Update Spanish
Currently translated at 67.4% (3730 of 5528 strings)

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

powered by weblate
2024-01-03 11:29:24 +01:00
Rubén VargasandRaphael Michel c96bfab70f Translations: Update Spanish
Currently translated at 64.8% (3586 of 5528 strings)

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

powered by weblate
2024-01-03 11:29:24 +01:00
Eduardo FernandezandRaphael Michel ec63456b9a Translations: Update Spanish
Currently translated at 64.8% (3586 of 5528 strings)

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

powered by weblate
2024-01-03 11:29:24 +01:00
Eduardo FernandezandRaphael Michel 3a39eb3815 Translations: Update Spanish
Currently translated at 62.3% (3449 of 5528 strings)

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

powered by weblate
2024-01-03 11:29:24 +01:00
Eduardo FernandezandRaphael Michel ce0ab8bd07 Translations: Update Spanish
Currently translated at 60.6% (3354 of 5528 strings)

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

powered by weblate
2024-01-03 11:29:24 +01:00
Christiaan de Die le ClercqandRaphael Michel 289496d6d2 Translations: Update Dutch
Currently translated at 82.4% (4560 of 5528 strings)

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

powered by weblate
2024-01-03 11:29:24 +01:00
Raphael Michel abe45bccae Fail on startup if an invalid language code is the default locale 2024-01-03 11:10:46 +01:00
Kian CrossandGitHub a8893bdc96 Fix incorrect order status on customer profile orders (#3785)
When a customer views the list of their orders on their profile,
orders which have been cancelled with a fee are displayed as 'paid'.
This commit fixes this by using the same logic from other places
where order status badges are displayed.
2024-01-03 11:03:13 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
cb76956887 Bump @babel/preset-env from 7.23.6 to 7.23.7 in /src/pretix/static/npm_dir (#3794)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.23.6 to 7.23.7.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.7/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-01-03 11:01:56 +01:00
Raphael Michel 809b971b4f Use custom error layout for shop offline 2024-01-03 11:01:19 +01:00
Raphael Michel d5f2f0e3af Item editor: Make dynamic validity more self-explanatory 2024-01-03 10:51:27 +01:00
Raphael Michel 301d9de18e Order email form: Fix missing placeholder validation 2024-01-03 10:19:06 +01:00
Raphael Michel dcb1d920eb Check-in rules: Do not use empty lists in SQL converted query 2024-01-03 09:56:59 +01:00
Kian CrossandGitHub fabe476397 Fix redirect loop caused when both 2fa and password change are forced (#3787) 2023-12-29 16:02:23 +01:00
Raphael Michel 3101660a3e PayPal: Fix error parsing mistake 2023-12-23 12:34:22 +01:00
Raphael MichelandRaphael Michel f8b677a2a9 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5528 of 5528 strings)

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

powered by weblate
2023-12-23 11:56:38 +01:00
Raphael MichelandRaphael Michel 15643f8dd5 Translations: Update German
Currently translated at 100.0% (5528 of 5528 strings)

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

powered by weblate
2023-12-23 11:56:38 +01:00
Raphael Michel fbaa0d667f Translations: Update word lists 2023-12-23 11:49:04 +01:00
Raphael Michel 50c5a2e8d9 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2023-12-23 11:30:38 +01:00
Raphael Michel 7bc559acab PayPal: Add communication messages on known issue 2023-12-23 11:29:20 +01:00
Raphael Michel c58a1aaa48 PayPal: Add idempotency ID as per https://developer.paypal.com/api/rest/reference/idempotency/ 2023-12-22 14:23:17 +01:00
Raphael Michel 385c8c6ef1 PayPal: Extend retry mechanism to POST and PATCH 2023-12-22 14:23:13 +01:00
Raphael Michel d3457ce5b6 PayPal: Fix log level 2023-12-22 13:58:18 +01:00
Raphael Michel 7087f783e6 PayPal: Add request retry logic 2023-12-22 13:37:06 +01:00
Raphael Michel 8c8650090d Improve logentry shredder function 2023-12-21 22:46:39 +01:00
Raphael Michel 12804ff1a0 PayPal: Improve logging for failures 2023-12-21 22:46:39 +01:00
Raphael Michel 4cb271868b Improve log entry shredding 2023-12-21 22:46:39 +01:00
Raphael Michel 9d546904e6 Docs: Recommend CNAME record for DKIM 2023-12-21 22:46:39 +01:00
Raphael Michel 092b51f8f3 Waiting list: Fix crash with invalid subevent id 2023-12-21 13:42:35 +01:00
Raphael MichelandGitHub 4a49519869 Rename LogEntry.organizer_link to LogEntry.organizer (#3762)
* Rename LogEntry.organizer_link to LogEntry.organizer

* isort fix
2023-12-20 13:52:20 +01:00
Raphael Michel aa121b900e Payment providers: Ignore case when sorting by name 2023-12-20 13:51:19 +01:00
Raphael MichelandGitHub 9e3ce4f1ec Bank transfer: Improve refund handling (#3769) 2023-12-20 13:50:50 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2e5385ca32 Bump @babel/preset-env from 7.23.2 to 7.23.6 in /src/pretix/static/npm_dir (#3776)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.23.2 to 7.23.6.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.6/packages/babel-preset-env)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-20 11:26:00 +01:00
RobertSF92andRaphael Michel f5dfe31fdd Translations: Update Dutch
Currently translated at 82.5% (4559 of 5520 strings)

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

powered by weblate
2023-12-20 10:51:37 +01:00
Felix HartnagelandRaphael Michel 032974e9ce Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5520 of 5520 strings)

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

powered by weblate
2023-12-20 10:51:37 +01:00
Felix HartnagelandRaphael Michel 31c086e2f0 Translations: Update German
Currently translated at 100.0% (5520 of 5520 strings)

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

powered by weblate
2023-12-20 10:51:37 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3c4de2aecd Bump @babel/core from 7.23.2 to 7.23.6 in /src/pretix/static/npm_dir (#3777)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.23.2 to 7.23.6.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.6/packages/babel-core)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-20 10:51:21 +01:00
8d9543c01e Stripe: Add Klarna (#3740)
* Stripe: Add Klarna

* Improve country detection

* Allow to select method

* Fix isort

---------

Co-authored-by: Raphael Michel <michel@rami.io>
2023-12-20 10:15:34 +01:00
Richard SchreiberandGitHub 608d82ce4f A11y: add date and time labels in MultiWidget (Z#23132744) (#3718)
* A11y: add date and time labels in MultiWidget

* fix code style issues
2023-12-20 09:10:59 +01:00
Raphael Michel 7f0ed374b5 API: Support expires attribute during order creation 2023-12-19 11:26:42 +01:00
Mattias AxellandRaphael Michel c7c9c95fbb Translations: Update Swedish
Currently translated at 18.5% (1024 of 5520 strings)

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

powered by weblate
2023-12-19 11:02:24 +01:00
5667da9ad2 PDF-Editor: fix controls when resizing multiple objects (#3764)
Co-authored-by: Mira <weller@rami.io>
2023-12-19 06:23:09 +01:00
Raphael Michel 558d0f1a4e Increase requests default timeout 2023-12-12 15:12:21 +01:00
Raphael Michel 0e832a7c41 Fix function signature of monkeypatched HTTP adapter 2023-12-12 14:37:34 +01:00
Raphael Michel 7f948bf263 Refunds in state "done" should always have an execution date 2023-12-12 14:20:59 +01:00
c0de-benderandRaphael Michel d4a32fd39d Translations: Update Polish
Currently translated at 40.4% (2234 of 5520 strings)

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

powered by weblate
2023-12-11 13:03:05 +01:00
c0de-benderandRaphael Michel 7bd5fc7ae0 Translations: Update Polish
Currently translated at 40.4% (2234 of 5520 strings)

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

powered by weblate
2023-12-11 13:03:05 +01:00
Fast128andRaphael Michel 36e8a4bf62 Translations: Update Polish
Currently translated at 40.4% (2234 of 5520 strings)

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

powered by weblate
2023-12-11 13:03:05 +01:00
c0de-benderandRaphael Michel 5ab01eb537 Translations: Update Polish
Currently translated at 40.1% (2218 of 5520 strings)

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

powered by weblate
2023-12-11 13:03:05 +01:00
c0de-benderandRaphael Michel 451ef2e734 Translations: Update Polish
Currently translated at 40.1% (2217 of 5520 strings)

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

powered by weblate
2023-12-11 13:03:05 +01:00
c0de-benderandRaphael Michel 2e657940bc Translations: Update Polish
Currently translated at 40.1% (2214 of 5520 strings)

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

powered by weblate
2023-12-11 13:03:05 +01:00
Fast128andRaphael Michel 6ef542837a Translations: Update Polish
Currently translated at 40.1% (2214 of 5520 strings)

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

powered by weblate
2023-12-11 13:03:05 +01:00
c0de-benderandRaphael Michel 619126bd24 Translations: Update Polish
Currently translated at 40.0% (2210 of 5520 strings)

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

powered by weblate
2023-12-11 13:03:05 +01:00
c0de-benderandRaphael Michel 1bdc49e087 Translations: Update Polish
Currently translated at 39.8% (2199 of 5520 strings)

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

powered by weblate
2023-12-11 13:03:05 +01:00
Fast128andRaphael Michel cf4ee53f0d Translations: Update Polish
Currently translated at 39.8% (2199 of 5520 strings)

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

powered by weblate
2023-12-11 13:03:05 +01:00
Joanna KochelandRaphael Michel d742aa2f48 Translations: Update Polish
Currently translated at 39.8% (2199 of 5520 strings)

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

powered by weblate
2023-12-11 13:03:05 +01:00
c0de-benderandRaphael Michel b7f693b934 Translations: Update Polish
Currently translated at 38.2% (2114 of 5520 strings)

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

powered by weblate
2023-12-11 13:03:05 +01:00
Michael StapelbergandGitHub c5ede32649 API: Prefetch customer in order list view (#3768)
Before this change, each order row needed an additional database query just to
resolve the customer id (integer) to customer identifier (string).

With this change, django does a database JOIN.
2023-12-11 13:02:40 +01:00
Raphael Michel f543cf2da5 Fix build settings 2023-12-11 12:31:55 +01:00
Raphael Michel 40cdb0c507 Fix favicon loading 2023-12-08 15:48:57 +01:00
Raphael Michel 7a8b3d08df Waiting list: Improve input parameter validation 2023-12-08 15:38:52 +01:00
Raphael Michel 12a898476e Replace redirect() with redirect_to_url() if we don't need Django's resolution 2023-12-08 15:38:50 +01:00
Raphael Michel 2acf043872 Subevent bulk creation: Add validate_min 2023-12-06 15:51:44 +01:00
Raphael Michel 062395dac1 Email: Retry after weird microsoft failure 2023-12-05 17:20:35 +01:00
Raphael Michel 6fc76d4122 Allow <s> in markdown 2023-12-05 15:31:20 +01:00
Raphael Michel 859df96afc Bump importlib_metadata to 7 2023-12-05 13:46:59 +01:00
Raphael Michel b95da484a0 Order import: Catch utf8 errors (PRETIXEU-9FP) 2023-12-05 13:40:00 +01:00
Raphael Michel d6d6b73a38 API: Fix cloning events with meta data (PRETIXEU-9FZ) 2023-12-05 12:57:14 +01:00
Raphael Michel c8b8fba171 Set timeout correctly 2023-12-02 14:49:31 +01:00
Raphael Michel 4580d6f710 Clean up monkeypatch code 2023-12-02 14:48:05 +01:00
Raphael Michel 2a19a423de Remove copy-paste artifact 2023-12-02 14:47:17 +01:00
Raphael Michel 409c1eef30 Add default timeout for HTTP requests 2023-12-02 14:45:28 +01:00
Raphael Michel 6c479808d0 Fix crash PRETIXEU-9FC 2023-11-30 13:49:27 +01:00
Raphael Michel bd14be485a Order change: Do not set invoice_dirty if invoicing is disabled 2023-11-30 11:51:41 +01:00
Raphael Michel fbf362a91f Export management command: Fix bug in exporter detection 2023-11-30 11:49:16 +01:00
Raphael Michel 82704b60c7 Voucher form: Fix quota check for partially redeemed vouchers 2023-11-29 16:09:04 +01:00
Raphael Michel b92feb382b Discounts: Fix scoping error with distinct subevents 2023-11-29 16:02:27 +01:00
Raphael Michel 66f934bba7 Bump version to 2023.11.0.dev0 2023-11-29 13:47:55 +01:00
Raphael Michel 13366b9985 Bump version to 2023.10.0 2023-11-29 13:47:08 +01:00
Raphael Michel e971733d51 Add missing signal to documentation 2023-11-28 17:17:57 +01:00
Raphael Michel 3bd491151b Add upgrade notes to docs 2023-11-28 17:17:20 +01:00
Raphael MichelandRaphael Michel 580bc65c3e Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (219 of 219 strings)

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

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

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

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

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

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

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

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

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

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

* Add change notes

* Rebase migration

* Add "expand" option to checkinrpc

* REmove accidental file

* Docs fixes

* REbase migration

* Rebase migration

* Fix typo

* REbase migration

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* display number of successful actions, use existing annotate method

* add tests, address review comments

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

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

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

* Fix condition

* Fix query count

* REbase migration

* Fix tests

---------

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

* Add tests

* isort fix

* Fix failures

* More tests

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Add and send signal for notifications

* Revert changes

* Fix typo

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

* Document parameters

---------

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

* move to own parapgraph

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix localization

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

* fix tests

---------

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

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

* add more test cases

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: Raphael Michel <michel@rami.io>
2023-11-13 16:14:52 +01:00
346 changed files with 206822 additions and 200105 deletions
+2
View File
@@ -10,7 +10,9 @@ updates:
schedule:
interval: "daily"
versioning-strategy: increase
open-pull-requests-limit: 10
- package-ecosystem: "npm"
directory: "/src/pretix/static/npm_dir"
schedule:
interval: "monthly"
open-pull-requests-limit: 5
-4
View File
@@ -42,7 +42,6 @@ Example::
currency=EUR
datadir=/data
plugins_default=pretix.plugins.sendmail,pretix.plugins.statistics
cookie_domain=.pretix.de
``instance_name``
The name of this installation. Default: ``pretix.de``
@@ -71,9 +70,6 @@ Example::
``auth_backends``
A comma-separated list of available auth backends. Defaults to ``pretix.base.auth.NativeAuthBackend``.
``cookie_domain``
The cookie domain to be set. Defaults to ``None``.
``registration``
Enables or disables the registration of new admin users. Defaults to ``off``.
+2 -1
View File
@@ -276,7 +276,8 @@ Restarting the service can take a few seconds, especially if the update requires
Replace ``stable`` above with a specific version number like ``1.0`` or with ``latest`` for the development
version, if you want to.
Make sure to also read :ref:`update_notes` and the release notes of the version you are updating to.
Make sure to also read :ref:`update_notes` and the release notes of the version you are updating to. Pay special
attention to the "Runtime and server environment" section of all release notes between your current and new version.
.. _`docker_plugininstall`:
+2 -1
View File
@@ -286,7 +286,8 @@ To upgrade to a new pretix release, pull the latest code changes and run the fol
(venv)$ python -m pretix updatestyles
# systemctl restart pretix-web pretix-worker
Make sure to also read :ref:`update_notes` and the release notes of the version you are updating to.
Make sure to also read :ref:`update_notes` and the release notes of the version you are updating to. Pay special
attention to the "Runtime and server environment" section of all release notes between your current and new version.
.. _`manual_plugininstall`:
+25
View File
@@ -47,5 +47,30 @@ Or, with a docker installation::
$ docker exec -it pretix.service pretix create_order_transactions
Upgrade to 2023.6.0 or newer
""""""""""""""""""""""""""""
MariaDB and MySQL are no longer supported.
Upgrade to 2023.8.0 or newer
""""""""""""""""""""""""""""
PostgreSQL 11 is now required.
Upgrade to 2023.9.0 or newer
""""""""""""""""""""""""""""
This release includes a migration that changes the `id` column of all core database tables from `integer`
to `bigint`. If you have a large database, the migration step of the upgrade might take significantly longer than
usual, so plan the update accordingly.
The default value for the `registration` setting in `pretix.cfg` has changed to `false`.
Upgrade to 2023.10.0 or newer
"""""""""""""""""""""""""""""
This release includes a migration that changes retroactively fills an `organizer` column in the table
`pretixbase_logentry`. If you have a large database, the migration step of the upgrade might take significantly
longer than usual, so plan the update accordingly.
.. _blog: https://pretix.eu/about/en/blog/
+9 -1
View File
@@ -31,6 +31,7 @@ Checking a ticket in
This endpoint supports passing multiple check-in lists to perform a multi-event scan. However, each check-in list
passed needs to be from a distinct event.
:query string expand: Expand a field inside the ``position`` object into a full object. Currently ``subevent``, ``item``, ``variation``, and ``answers.question`` are supported. Can be passed multiple times.
:<json string secret: Scanned QR code corresponding to the ``secret`` attribute of a ticket.
:<json string source_type: Type of source the ``secret`` was obtained form. Defaults to ``"barcode"``.
:<json array lists: List of check-in list IDs to search on. No two check-in lists may be from the same event.
@@ -63,6 +64,7 @@ Checking a ticket in
``checkin_attention`` flag set. (3) If ``attendee_name`` is empty, it may automatically fall
back to values from a parent product or from invoice addresses.
:>json boolean require_attention: Whether or not the ``require_attention`` flag is set on the item or order.
:>json list checkin_texts: List of additional texts to show to the user.
:>json object list: Excerpt of information about the matching :ref:`check-in list <rest-checkinlists>` (if any was found),
including the attributes ``id``, ``name``, ``event``, ``subevent``, and ``include_pending``.
:>json object questions: List of questions to be answered for check-in, only set on status ``"incomplete"``.
@@ -103,6 +105,7 @@ Checking a ticket in
},
"require_attention": false,
"checkin_texts": [],
"list": {
"id": 1,
"name": "Default check-in list",
@@ -125,6 +128,7 @@ Checking a ticket in
},
"require_attention": false,
"checkin_texts": [],
"list": {
"id": 1,
"name": "Default check-in list",
@@ -142,6 +146,7 @@ Checking a ticket in
"position": 1,
"identifier": "WY3TP9SL",
"ask_during_checkin": true,
"show_during_checkin": true,
"options": [
{
"id": 1,
@@ -178,7 +183,8 @@ Checking a ticket in
"status": "error",
"reason": "invalid",
"reason_explanation": null,
"require_attention": false
"require_attention": false,
"checkin_texts": []
}
**Example error response (known, but invalid ticket)**:
@@ -193,6 +199,7 @@ Checking a ticket in
"reason": "unpaid",
"reason_explanation": null,
"require_attention": false,
"checkin_texts": [],
"list": {
"id": 1,
"name": "Default check-in list",
@@ -217,6 +224,7 @@ Checking a ticket in
* ``rules`` - Check-in prevented by a user-defined rule.
* ``ambiguous`` - Multiple tickets match scan, rejected.
* ``revoked`` - Ticket code has been revoked.
* ``unapproved`` - Order has not yet been approved.
* ``error`` - Internal error.
In case of reason ``rules`` and ``invalid_time``, there might be an additional response field ``reason_explanation``
+5 -2
View File
@@ -498,7 +498,7 @@ Order position endpoints
``attendee_name,positionid``
:query string order: Only return positions of the order with the given order code
:query string search: Fuzzy search matching the attendee name, order code, invoice address name as well as to the beginning of the secret.
:query string expand: Expand a field into a full object. Currently only ``subevent``, ``item``, and ``variation`` are supported. Can be passed multiple times.
:query string expand: Expand a field into a full object. Currently ``subevent``, ``item``, ``variation``, and ``answers.question`` are supported. Can be passed multiple times.
:query integer item: Only return positions with the purchased item matching the given ID.
:query integer item__in: Only return positions with the purchased item matching one of the given comma-separated IDs.
:query integer variation: Only return positions with the purchased item variation matching the given ID.
@@ -632,7 +632,8 @@ Order position endpoints
set this to ``false``. In that case, questions will just be ignored. Defaults
to ``true``.
:<json boolean canceled_supported: When this parameter is set to ``true``, the response code ``canceled`` may be
returned. Otherwise, canceled orders will return ``unpaid``.
returned. Otherwise, canceled orders will return ``unpaid``. (**Deprecated**, in
the future, this will be ignored and ``canceled`` may always be returned.)
:<json datetime datetime: Specifies the datetime of the check-in. If not supplied, the current time will be used.
:<json boolean force: Specifies that the check-in should succeed regardless of revoked barcode, previous check-ins or required
questions that have not been filled. This is usually used to upload offline scans that already happened,
@@ -706,6 +707,7 @@ Order position endpoints
"position": 1,
"identifier": "WY3TP9SL",
"ask_during_checkin": true,
"show_during_checkin": true,
"options": [
{
"id": 1,
@@ -758,6 +760,7 @@ Order position endpoints
* ``rules`` - Check-in prevented by a user-defined rule.
* ``ambiguous`` - Multiple tickets match scan, rejected.
* ``revoked`` - Ticket code has been revoked.
* ``unapproved`` - Order has not yet been approved.
In case of reason ``rules`` or ``invalid_time``, there might be an additional response field ``reason_explanation``
with a human-readable description of the violated rules. However, that field can also be missing or be ``null``.
+4 -2
View File
@@ -36,6 +36,8 @@ geo_lon float Longitude of th
has_subevents boolean ``true`` if the event series feature is active for this
event. Cannot change after event is created.
meta_data object Values set for organizer-specific meta data parameters.
The allowed keys need to be set up as meta properties
in the organizer configuration.
plugins list A list of package names of the enabled plugins for this
event.
seating_plan integer If reserved seating is in use, the ID of a seating
@@ -343,8 +345,8 @@ Endpoints
Creates a new event with properties as set in the request body. The properties that are copied are: ``is_public``,
``testmode``, ``has_subevents``, settings, plugin settings, items, variations, add-ons, quotas, categories, tax rules, questions.
If the ``plugins``, ``has_subevents`` and/or ``is_public`` fields are present in the post body this will determine their
value. Otherwise their value will be copied from the existing event.
If the ``plugins``, ``has_subevents``, ``meta_data`` and/or ``is_public`` fields are present in the post body this will
determine their value. Otherwise their value will be copied from the existing event.
Please note that you can only copy from events under the same organizer this way. Use the ``clone_from`` parameter
when creating a new event for this instead.
+30
View File
@@ -29,6 +29,8 @@ position integer An integer, use
checkin_attention boolean If ``true``, the check-in app should show a warning
that this ticket requires special attention if such
a variation is being scanned.
checkin_text string Text that will be shown if a ticket of this type is
scanned (or ``null``).
require_approval boolean If ``true``, orders with this variation will need to be
approved by the event organizer before they can be
paid.
@@ -43,8 +45,16 @@ sales_channels list of strings Sales channels
available.
available_from datetime The first date time at which this variation can be bought
(or ``null``).
available_from_mode string If ``hide`` (the default), this variation is hidden in the shop
if unavailable due to the available_from setting.
If ``info``, the variation is visible, but can't be purchased,
and a note explaining the unavailability is displayed.
available_until datetime The last date time at which this variation can be bought
(or ``null``).
available_until_mode string If ``hide`` (the default), this variation is hidden in the shop
if unavailable due to the available_until setting.
If ``info``, the variation is visible, but can't be purchased,
and a note explaining the unavailability is displayed.
hide_without_voucher boolean If ``true``, this variation is only shown during the voucher
redemption process, but not in the normal shop
frontend.
@@ -58,6 +68,8 @@ meta_data object Values set for
.. versionchanged:: 2023.10
The ``free_price_suggestion`` attribute has been added.
The ``checkin_text`` attribute has been added.
Endpoints
---------
@@ -94,13 +106,16 @@ Endpoints
},
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": {
"en": "Test2"
@@ -119,13 +134,16 @@ Endpoints
},
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": {},
"position": 1,
@@ -179,13 +197,16 @@ Endpoints
"free_price_suggestion": null,
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"position": 0,
@@ -218,13 +239,16 @@ Endpoints
"default_price": "10.00",
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"position": 0,
@@ -248,13 +272,16 @@ Endpoints
"free_price_suggestion": null,
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"position": 0,
@@ -309,13 +336,16 @@ Endpoints
"free_price_suggestion": null,
"active": false,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"position": 1,
+66
View File
@@ -50,8 +50,16 @@ sales_channels list of strings Sales channel
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
available_from datetime The first date time at which this item can be bought
(or ``null``).
available_from_mode string If ``hide`` (the default), this item is hidden in the shop
if unavailable due to the ``available_from`` setting.
If ``info``, the item is visible, but can't be purchased,
and a note explaining the unavailability is displayed.
available_until datetime The last date time at which this item can be bought
(or ``null``).
available_until_mode string If ``hide`` (the default), this item is hidden in the shop
if unavailable due to the ``available_until`` setting.
If ``info``, the item is visible, but can't be purchased,
and a note explaining the unavailability is displayed.
hidden_if_available integer **DEPRECATED** The internal ID of a quota object, or ``null``. If
set, this item won't be shown publicly as long as this
quota is available.
@@ -74,6 +82,8 @@ max_per_order integer This product
checkin_attention boolean If ``true``, the check-in app should show a warning
that this ticket requires special attention if such
a product is being scanned.
checkin_text string Text that will be shown if a ticket of this type is
scanned (or ``null``).
original_price money (string) An original price, shown for comparison, not used
for price calculations (or ``null``).
require_approval boolean If ``true``, orders with this product will need to be
@@ -137,6 +147,8 @@ variations list of objects A list with o
├ checkin_attention boolean If ``true``, the check-in app should show a warning
that this ticket requires special attention if such
a variation is being scanned.
├ checkin_text string Text that will be shown if a ticket of this type is
scanned (or ``null``).
├ require_approval boolean If ``true``, orders with this variation will need to be
approved by the event organizer before they can be
paid.
@@ -152,8 +164,16 @@ variations list of objects A list with o
available.
├ available_from datetime The first date time at which this variation can be bought
(or ``null``).
├ available_from_mode string If ``hide`` (the default), this variation is hidden in the shop
if unavailable due to the ``available_from`` setting.
If ``info``, the variation is visible, but can't be purchased,
and a note explaining the unavailability is displayed.
├ available_until datetime The last date time at which this variation can be bought
(or ``null``).
├ available_until_mode string If ``hide`` (the default), this variation is hidden in the shop
if unavailable due to the ``available_until`` setting.
If ``info``, the variation is visible, but can't be purchased,
and a note explaining the unavailability is displayed.
├ hide_without_voucher boolean If ``true``, this variation is only shown during the voucher
redemption process, but not in the normal shop
frontend.
@@ -205,6 +225,7 @@ meta_data object Values set fo
.. versionchanged:: 2023.10
The ``checkin_text`` and ``variations[x].checkin_text`` attributes have been added.
The ``free_price_suggestion`` and ``variations[x].free_price_suggestion`` attributes have been added.
.. versionchanged:: 2023.10
@@ -274,7 +295,9 @@ Endpoints
"position": 0,
"picture": null,
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hidden_if_available": null,
"hidden_if_item_available": null,
"require_voucher": false,
@@ -283,6 +306,7 @@ Endpoints
"min_per_order": null,
"max_per_order": null,
"checkin_attention": false,
"checkin_text": null,
"has_variations": false,
"generate_tickets": null,
"allow_waitinglist": true,
@@ -312,12 +336,15 @@ Endpoints
"free_price_suggestion": null,
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"meta_data": {},
@@ -331,12 +358,15 @@ Endpoints
"free_price_suggestion": null,
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"meta_data": {},
@@ -409,7 +439,9 @@ Endpoints
"position": 0,
"picture": null,
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hidden_if_available": null,
"hidden_if_item_available": null,
"require_voucher": false,
@@ -421,6 +453,7 @@ Endpoints
"min_per_order": null,
"max_per_order": null,
"checkin_attention": false,
"checkin_text": null,
"has_variations": false,
"require_approval": false,
"require_bundling": false,
@@ -447,13 +480,16 @@ Endpoints
"free_price_suggestion": null,
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_types": [],
"description": null,
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"meta_data": {},
"position": 0
@@ -466,12 +502,15 @@ Endpoints
"free_price_suggestion": null,
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"meta_data": {},
@@ -525,7 +564,9 @@ Endpoints
"position": 0,
"picture": null,
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hidden_if_available": null,
"hidden_if_item_available": null,
"require_voucher": false,
@@ -537,6 +578,7 @@ Endpoints
"min_per_order": null,
"max_per_order": null,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_bundling": false,
"require_membership": false,
@@ -562,12 +604,15 @@ Endpoints
"free_price_suggestion": null,
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"meta_data": {},
@@ -581,12 +626,15 @@ Endpoints
"free_price_suggestion": null,
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"meta_data": {},
@@ -628,7 +676,9 @@ Endpoints
"position": 0,
"picture": null,
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hidden_if_available": null,
"hidden_if_item_available": null,
"require_voucher": false,
@@ -640,6 +690,7 @@ Endpoints
"allow_waitinglist": true,
"show_quota_left": null,
"checkin_attention": false,
"checkin_text": null,
"has_variations": true,
"require_approval": false,
"require_bundling": false,
@@ -666,12 +717,15 @@ Endpoints
"free_price_suggestion": null,
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"meta_data": {},
@@ -685,12 +739,15 @@ Endpoints
"free_price_suggestion": null,
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"meta_data": {},
@@ -763,7 +820,9 @@ Endpoints
"position": 0,
"picture": null,
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hidden_if_available": null,
"hidden_if_item_available": null,
"require_voucher": false,
@@ -775,6 +834,7 @@ Endpoints
"min_per_order": null,
"max_per_order": null,
"checkin_attention": false,
"checkin_text": null,
"has_variations": true,
"require_approval": false,
"require_bundling": false,
@@ -801,12 +861,15 @@ Endpoints
"free_price_suggestion": null,
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"meta_data": {},
@@ -820,12 +883,15 @@ Endpoints
"free_price_suggestion": null,
"active": true,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"meta_data": {},
+17
View File
@@ -46,6 +46,8 @@ custom_followup_at date Internal date f
checkin_attention boolean If ``true``, the check-in app should show a warning
that this ticket requires special attention if a ticket
of this order is scanned.
checkin_text string Text that will be shown if a ticket of this order is
scanned (or ``null``).
invoice_address object Invoice address information (can be ``null``)
├ last_modified datetime Last modification date of the address
├ company string Customer company name
@@ -139,6 +141,14 @@ last_modified datetime Last modificati
The ``customer`` query parameter has been added.
.. versionchanged:: 2023.10
The ``checkin_text`` attribute has been added.
.. versionchanged:: 2024.1
The ``expires`` attribute can now be passed during order creation.
.. _order-position-resource:
@@ -318,6 +328,7 @@ List of all orders
"comment": "",
"custom_followup_at": null,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"valid_if_pending": false,
"invoice_address": {
@@ -539,6 +550,7 @@ Fetching individual orders
"comment": "",
"custom_followup_at": null,
"checkin_attention": false,
"checkin_text": null,
"require_approval": false,
"valid_if_pending": false,
"invoice_address": {
@@ -709,6 +721,8 @@ Updating order fields
* ``checkin_attention``
* ``checkin_text``
* ``locale``
* ``comment``
@@ -719,6 +733,8 @@ Updating order fields
* ``valid_if_pending``
* ``expires``
**Example request**:
.. sourcecode:: http
@@ -924,6 +940,7 @@ Creating orders
* ``comment`` (optional)
* ``custom_followup_at`` (optional)
* ``checkin_attention`` (optional)
* ``checkin_text`` (optional)
* ``require_approval`` (optional)
* ``valid_if_pending`` (optional)
* ``invoice_address`` (optional)
+11
View File
@@ -44,6 +44,8 @@ identifier string An arbitrary st
ask_during_checkin boolean If ``true``, this question will not be asked while
buying the ticket, but will show up when redeeming
the ticket instead.
show_during_checkin boolean If ``true``, the answer to the question will be shown
during check-in (if the check-in client supports it).
hidden boolean If ``true``, the question will only be shown in the
backend.
print_on_invoice boolean If ``true``, the question will only be shown on
@@ -77,6 +79,10 @@ dependency_value string An old version
for one value. **Deprecated.**
===================================== ========================== =======================================================
.. versionchanged:: 2023.8
The ``show_during_checkin`` attribute has been added.
Endpoints
---------
@@ -115,6 +121,7 @@ Endpoints
"position": 1,
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"show_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"valid_number_min": null,
@@ -194,6 +201,7 @@ Endpoints
"position": 1,
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"show_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"valid_number_min": null,
@@ -257,6 +265,7 @@ Endpoints
"items": [1, 2],
"position": 1,
"ask_during_checkin": false,
"show_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"dependency_question": null,
@@ -293,6 +302,7 @@ Endpoints
"position": 1,
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"show_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"dependency_question": null,
@@ -376,6 +386,7 @@ Endpoints
"position": 2,
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"show_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"dependency_question": null,
+3 -2
View File
@@ -11,9 +11,10 @@ Core
----
.. automodule:: pretix.base.signals
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types,
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification,
item_copy_data, register_sales_channels, register_global_settings, quota_availability, global_email_filter,
register_ticket_secret_generators, gift_card_transaction_display
register_ticket_secret_generators, gift_card_transaction_display,
register_text_placeholders, register_mail_placeholders
Order events
""""""""""""
+2
View File
@@ -84,6 +84,8 @@ convenient to you:
.. automethod:: _register_fonts
.. automethod:: _register_event_fonts
.. automethod:: _on_first_page
.. automethod:: _on_other_page
+24 -15
View File
@@ -1,10 +1,11 @@
.. highlight:: python
:linenothreshold: 5
Writing an e-mail placeholder plugin
====================================
Writing a template placeholder plugin
=====================================
An email placeholder is a dynamic value that pretix users can use in their email templates.
A template placeholder is a dynamic value that pretix users can use in their email templates and in other
configurable texts.
Please read :ref:`Creating a plugin <pluginsetup>` first, if you haven't already.
@@ -12,31 +13,31 @@ Placeholder registration
------------------------
The placeholder API does not make a lot of usage from signals, however, it
does use a signal to get a list of all available email placeholders. Your plugin
should listen for this signal and return an instance of a subclass of ``pretix.base.email.BaseMailTextPlaceholder``:
does use a signal to get a list of all available placeholders. Your plugin
should listen for this signal and return an instance of a subclass of ``pretix.base.services.placeholders.BaseTextPlaceholder``:
.. code-block:: python
from django.dispatch import receiver
from pretix.base.signals import register_mail_placeholders
from pretix.base.signals import register_text_placeholders
@receiver(register_mail_placeholders, dispatch_uid="placeholder_custom")
def register_mail_renderers(sender, **kwargs):
from .email import MyPlaceholderClass
@receiver(register_text_placeholders, dispatch_uid="placeholder_custom")
def register_placeholder_renderers(sender, **kwargs):
from .placeholders import MyPlaceholderClass
return MyPlaceholder()
Context mechanism
-----------------
Emails are sent in different "contexts" within pretix. For example, many emails are sent in the
the context of an order, but some are not, such as the notification of a waiting list voucher.
Templates are used in different "contexts" within pretix. For example, many emails are rendered from
templates in the context of an order, but some are not, such as the notification of a waiting list voucher.
Not all placeholders make sense in every email, and placeholders usually depend some parameters
Not all placeholders make sense everywhere, and placeholders usually depend on some parameters
themselves, such as the ``Order`` object. Therefore, placeholders are expected to explicitly declare
what values they depend on and they will only be available in an email if all those dependencies are
what values they depend on and they will only be available in a context where all those dependencies are
met. Currently, placeholders can depend on the following context parameters:
* ``event``
@@ -51,7 +52,7 @@ There are a few more that are only to be used internally but not by plugins.
The placeholder class
---------------------
.. class:: pretix.base.email.BaseMailTextPlaceholder
.. class:: pretix.base.services.placeholders.BaseTextPlaceholder
.. autoattribute:: identifier
@@ -77,7 +78,15 @@ functions:
.. code-block:: python
placeholder = SimpleFunctionalMailTextPlaceholder(
placeholder = SimpleFunctionalTextPlaceholder(
'code', ['order'], lambda order: order.code, sample='F8VVL'
)
Signals
-------
.. automodule:: pretix.base.signals
:members: register_text_placeholders
.. automodule:: pretix.base.signals
:members: register_mail_placeholders
Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

After

Width:  |  Height:  |  Size: 287 KiB

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

Before

Width:  |  Height:  |  Size: 180 KiB

After

Width:  |  Height:  |  Size: 183 KiB

+12 -12
View File
@@ -42,23 +42,25 @@ endif
else
-down->[yes || force] "Is one or more block set on the ticket?"
--> if "" then
-right->[no && !force] "Return error BLOCKED"
-right->[yes && !force] "Return error BLOCKED"
else
-down->[yes || force] "If this is not an exit, is the valid_from/valid_until\nconstraint on the ticket fulfilled?"
-down->[no || force] "Is the order in status PENDING and not yet approved?"
--> if "" then
-right->[no && !force] "Return error INVALID_TIME"
-right->[yes && !force] "Return error UNAPPROVED"
else
-down->[yes || force] "Is the product part of the check-in list?"
-down->[no || force] "If this is not an exit, is the valid_from/valid_until\nconstraint on the ticket fulfilled?"
--> if "" then
-right->[no && !force] "Return error PRODUCT"
-right->[no && !force] "Return error INVALID_TIME"
else
-down->[yes || force] "Is the subevent part of the check-in list?"
-down->[yes || force] "Is the product part of the check-in list?"
--> if "" then
-right->[no && !force] "Return error PRODUCT "
-right->[no && !force] "Return error PRODUCT"
else
-down->[yes] "Is the order in status PAID?"
-down->[yes || force] "Is the subevent part of the check-in list?"
--> if "" then
-right->[no && !force] "Is Order.require_approval set?"
-right->[no && !force] "Return error PRODUCT "
else
-down->[yes] "Is the order in status PAID?"
--> if "" then
-->[no] "Is Order.valid_if_pending set?"
--> if "" then
@@ -77,10 +79,8 @@ else
endif
endif
else
-->[yes] "Return error UNPAID "
-down->[yes || force] "Is this an entry or exit?\nIs the upload forced?"
endif
else
-down->[yes || force] "Is this an entry or exit?\nIs the upload forced?"
endif
endif
endif
+3
View File
@@ -32,6 +32,7 @@ transactions list of objects Transactions in
├ checksum string Checksum computed from payer, reference, amount and
date
├ payer string Payment source
├ external_id string Unique ID of the payment from an external source
├ reference string Payment reference
├ amount string Payment amount
├ iban string Payment IBAN
@@ -85,6 +86,7 @@ Endpoints
"date": "26.06.2017",
"payer": "John Doe",
"order": null,
"external_id": null,
"iban": "",
"bic": "",
"checksum": "5de03a601644dfa63420dacfd285565f8375a8f2",
@@ -139,6 +141,7 @@ Endpoints
"iban": "",
"bic": "",
"order": null,
"external_id": null,
"checksum": "5de03a601644dfa63420dacfd285565f8375a8f2",
"reference": "GUTSCHRIFT\r\nSAMPLECONF-NAB12 EREF: SAMPLECONF-NAB12\r\nIBAN: DE1234556…",
"state": "nomatch",
+27 -7
View File
@@ -37,9 +37,12 @@ contact_email string Contact person
booth string Booth number (or ``null``). Maximum 100 characters.
locale string Locale for communication with the exhibitor.
access_code string Access code for the exhibitor to access their data or use the lead scanning app (read-only).
lead_scanning_access_code string Access code for the exhibitor to use the lead scanning app but not access data (read-only).
allow_lead_scanning boolean Enables lead scanning app
allow_lead_access boolean Enables access to data gathered by the lead scanning app
allow_voucher_access boolean Enables access to data gathered by exhibitor vouchers
lead_scanning_scope_by_device string Enables lead scanning to be handled as one lead per attendee
per scanning device, instead of only per exhibitor.
comment string Internal comment, not shown to exhibitor
===================================== ========================== =======================================================
@@ -62,6 +65,7 @@ data list of objects Attendee data s
except in a few cases where it contains an additional list of objects
with ``value`` and ``label`` keys (e.g. splitting of names).
device_name string User-defined name for the device used for scanning (or ``null``).
device_uuid string UUID of device used for scanning (or ``null``).
===================================== ========================== =======================================================
Endpoints
@@ -107,7 +111,9 @@ Endpoints
"contact_email": "johnson@as.example.org",
"booth": "A2",
"locale": "de",
"access_code": "VKHZ2FU8",
"access_code": "VKHZ2FU84",
"lead_scanning_access_code": "WVK2B8PZ",
"lead_scanning_scope_by_device": false,
"allow_lead_scanning": true,
"allow_lead_access": true,
"allow_voucher_access": true,
@@ -158,7 +164,9 @@ Endpoints
"contact_email": "johnson@as.example.org",
"booth": "A2",
"locale": "de",
"access_code": "VKHZ2FU8",
"access_code": "VKHZ2FU84",
"lead_scanning_access_code": "WVK2B8PZ",
"lead_scanning_scope_by_device": false,
"allow_lead_scanning": true,
"allow_lead_access": true,
"allow_voucher_access": true,
@@ -388,7 +396,9 @@ Endpoints
"contact_email": "johnson@as.example.org",
"booth": "A2",
"locale": "de",
"access_code": "VKHZ2FU8",
"access_code": "VKHZ2FU84",
"lead_scanning_access_code": "WVK2B8PZ",
"lead_scanning_scope_by_device": false,
"allow_lead_scanning": true,
"allow_lead_access": true,
"allow_voucher_access": true,
@@ -446,7 +456,9 @@ Endpoints
"contact_email": "johnson@as.example.org",
"booth": "A2",
"locale": "de",
"access_code": "VKHZ2FU8",
"access_code": "VKHZ2FU84",
"lead_scanning_access_code": "WVK2B8PZ",
"lead_scanning_scope_by_device": false,
"allow_lead_scanning": true,
"allow_lead_access": true,
"allow_voucher_access": true,
@@ -561,6 +573,7 @@ name string Exhibitor name
booth string Booth number (or ``null``)
event object Object describing the event
├ name multi-lingual string Event name
├ end_date datetime End date of the event. After this time, the app could show a warning that the event is over.
├ imprint_url string URL to legal notice page. If not ``null``, a button in the app should link to this page.
├ privacy_url string URL to privacy notice page. If not ``null``, a button in the app should link to this page.
├ help_url string URL to help page. If not ``null``, a button in the app should link to this page.
@@ -596,6 +609,7 @@ scan_types list of objects Only used for a
"booth": "A2",
"event": {
"name": {"en": "Sample conference", "de": "Beispielkonferenz"},
"end_date": "2017-12-28T10:00:00+00:00",
"slug": "bigevents",
"imprint_url": null,
"privacy_url": null,
@@ -634,6 +648,7 @@ On the request, you should set the following properties:
* ``tags`` with the list of selected tags
* ``rating`` with the rating assigned by the exhibitor
* ``device_name`` with a user-specified name of the device used for scanning (max. 190 characters), or ``null``
* ``device_uuid`` with a auto-generated UUID of the device used for scanning, or ``null``
If you submit ``tags`` and ``rating`` to be ``null`` and ``notes`` to be ``""``, the server
responds with the previously saved information and will not delete that information. If you
@@ -668,7 +683,8 @@ The request for this looks like this:
"scan_type": "lead",
"tags": ["foo"],
"rating": 4,
"device_name": "DEV1"
"device_name": "DEV1",
"device_uuid": "d8c2ec53-d602-4a08-882d-db4cf54344a2"
}
**Example response:**
@@ -701,7 +717,9 @@ The request for this looks like this:
},
"rating": 4,
"tags": ["foo"],
"notes": "Great customer, wants our newsletter"
"notes": "Great customer, wants our newsletter",
"device_name": "DEV1",
"device_uuid": "d8c2ec53-d602-4a08-882d-db4cf54344a2"
}
:statuscode 200: No error, leads was not scanned for the first time
@@ -756,7 +774,9 @@ You can also fetch existing leads (if you are authorized to do so):
},
"rating": 4,
"tags": ["foo"],
"notes": "Great customer, wants our newsletter"
"notes": "Great customer, wants our newsletter",
"device_name": "DEV1",
"device_uuid": "d8c2ec53-d602-4a08-882d-db4cf54344a2"
}
]
}
+9 -3
View File
@@ -194,17 +194,23 @@ A complete record could look like this::
v=spf1 a mx include:_spf.pretix.eu ~all
Make sure to read up on the `SPF specification`_. If you want to authenticate your emails with DKIM, set up a DNS TXT
record for the subdomain ``pretix._domainkey`` with the following contents::
Make sure to read up on the `SPF specification`_.
v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDXrDk6lwOWX00e2MbiiJac6huI+gnzLf9N4G1FnBv3PXq8fz3i2q1szH72OF5mAlKm3zXO4cl/uxx+lfidS1ERbX6Bn9BRstBTQUKWC4JFj8Yk9+fwT7LWehDURazLdTzfsIjJFudLLvxtOKSaOCtMhbPX05DIhziaqVCBqgz/NQIDAQAB
If you want to authenticate your emails with `DKIM`_, set up a ``CNAME`` record for the subdomain ``pretix._domainkey``
pointing to ``dkim.pretix.eu``::
pretix._domainkey.mydomain.com. CNAME dkim.pretix.eu.
Then, please contact support@pretix.eu and we will enable DKIM for your domain on our mail servers.
For senders with larger volumes, Google Mail also requires you to have a `DMARC`_ policy (that may however be ``p=none``).
.. note:: Many SMTP servers impose rate limits on the sent emails, such as a maximum number of emails sent per hour.
These SMTP servers are often not suitable for use with pretix, in case you want to send an email to many
hundreds or thousands of ticket buyers. Depending on how the rate limit is implemented, emails might be lost
in this case, as pretix only retries email delivery for a certain time period.
.. _DKIM: https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail
.. _Sender Policy Framework: https://en.wikipedia.org/wiki/Sender_Policy_Framework
.. _SPF specification: http://www.open-spf.org/SPF_Record_Syntax
.. _DMARC: https://en.wikipedia.org/wiki/DMARC
+23 -2
View File
@@ -138,7 +138,7 @@ the button-style of that checkbox with the one in the pretix shop, you can use t
.. note::
Due to compatibilty with existing widget installations, the default value for ``single-item-select``
Due to compatibility with existing widget installations, the default value for ``single-item-select``
is ``checkbox``. This might change in the future, so make sure, to set the attribute to
``single-item-select="checkbox"`` if you need it.
@@ -196,7 +196,7 @@ settings. For example, if you set up a meta data property called "Promoted" that
<pretix-widget event="https://pretix.eu/demo/series/" list-type="list" filter="attr[Promoted]=Yes"></pretix-widget>
If you have enabled public filters in your meta data attribute configuration, a filter formshows up. To disable, use::
If you have enabled public filters in your meta data attribute configuration, a filter-form shows up. To disable, use::
<pretix-widget event="https://pretix.eu/demo/democon/" disable-filters></pretix-widget>
@@ -429,4 +429,25 @@ Hosted or pretix Enterprise are active, you can pass the following fields:
});
</script>
Offering wallet payments (Apple Pay, Google Pay) within the widget
------------------------------------------------------------------
Some payment providers (such as Stripe) also offer Apple or Google Pay. But in order to use them, the domain of the
payment needs to be approved first. As of right now, pretix will take care of the domain verification process for you
automatically, when using Stripe. However, pretix can only validate the domain that is being used for your default,
"stand-alone" shop (such as https://pretix.eu/demo/democon/ ).
When embedding the widget on your website, the domain of the embedding page will also need to be validated in order to
be able to use it for wallet payments.
The details might vary from payment provider to payment provider, but generally speaking, it will either involve just
telling your payment provider the domain name and (for Apple Pay) placing an
``apple-developer-merchantid-domain-association``-file into the ``.well-known``-directory of your domain.
Further reading:
* `Stripe Payment Method Domain registration`_
.. _Let's Encrypt: https://letsencrypt.org/
.. _Stripe Payment Method Domain registration: https://stripe.com/docs/payments/payment-methods/pmd-registration
+1 -1
View File
@@ -145,7 +145,7 @@ to get a better plain text representation of your text. Note however, that for
security reasons you can only use the following HTML elements::
a, abbr, acronym, b, br, code, div, em, h1, h2,
h3, h4, h5, h6, hr, i, li, ol, p, pre, span, strong,
h3, h4, h5, h6, hr, i, li, ol, p, pre, s, span, strong,
table, tbody, td, thead, tr, ul
Additionally, only the following attributes are allowed on them::
+8 -9
View File
@@ -42,7 +42,7 @@ dependencies = [
"django-countries==7.5.*",
"django-filter==23.2",
"django-formset-js-improved==0.5.0.3",
"django-formtools==2.4.1",
"django-formtools==2.5.1",
"django-hierarkey==1.1.*",
"django-hijack==3.3.*",
"django-i18nfield==1.9.*,>=1.9.4",
@@ -52,14 +52,14 @@ dependencies = [
"django-oauth-toolkit==2.2.*",
"django-otp==1.2.*",
"django-phonenumber-field==7.1.*",
"django-redis==5.2.*",
"django-redis==5.4.*",
"django-scopes==2.0.*",
"django-statici18n==2.3.*",
"django-statici18n==2.4.*",
"djangorestframework==3.14.*",
"dnspython==2.3.*",
"drf_ujson2==1.7.*",
"geoip2==4.*",
"importlib_metadata==6.*", # Polyfill, we can probably drop this once we require Python 3.10+
"importlib_metadata==7.*", # Polyfill, we can probably drop this once we require Python 3.10+
"isoweek",
"jsonschema",
"kombu==5.3.*",
@@ -85,7 +85,6 @@ dependencies = [
"pypdf==3.9.*",
"python-bidi==0.4.*", # Support for Arabic in reportlab
"python-dateutil==2.8.*",
"python-u2flib-server==4.*",
"pytz",
"pytz-deprecation-shim==0.1.*",
"pyuca",
@@ -93,11 +92,11 @@ dependencies = [
"redis==4.6.*",
"reportlab==4.0.*",
"requests==2.31.*",
"sentry-sdk==1.15.*",
"sentry-sdk==1.40.*",
"sepaxml==2.6.*",
"slimit",
"static3==0.7.*",
"stripe==5.4.*",
"stripe==7.9.*",
"text-unidecode==1.*",
"tlds>=2020041600",
"tqdm==4.*",
@@ -110,7 +109,7 @@ dependencies = [
[project.optional-dependencies]
memcached = ["pylibmc"]
dev = [
"aiohttp==3.8.*",
"aiohttp==3.9.*",
"coverage",
"coveralls",
"fakeredis==2.18.*",
@@ -126,7 +125,7 @@ dev = [
"pytest-cov",
"pytest-django==4.*",
"pytest-mock==3.10.*",
"pytest-rerunfailures==11.*",
"pytest-rerunfailures==13.*",
"pytest-sugar",
"pytest-xdist==3.3.*",
"pytest==7.3.*",
+1 -1
View File
@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "2023.10.0.dev0"
__version__ = "2024.2.0.dev0"
+6 -3
View File
@@ -109,7 +109,7 @@ LANGUAGES_RTL = {
'ar', 'hw'
}
LANGUAGES_INCUBATING = {
'pl', 'fi', 'pt-br', 'gl',
'fi', 'pt-br', 'gl',
}
LOCALE_PATHS = [
os.path.join(os.path.dirname(__file__), 'locale'),
@@ -267,9 +267,10 @@ CACHE_LARGE_VALUES_ALIAS = 'default'
FILE_UPLOAD_EXTENSIONS_IMAGE = (".png", ".jpg", ".gif", ".jpeg")
PILLOW_FORMATS_IMAGE = ('PNG', 'GIF', 'JPEG')
FILE_UPLOAD_EXTENSIONS_FAVICON = (".ico", ".png", "jpg", ".gif", ".jpeg")
FILE_UPLOAD_EXTENSIONS_FAVICON = (".ico", ".png", ".jpg", ".gif", ".jpeg")
PILLOW_FORMATS_QUESTIONS_FAVICON = ('PNG', 'GIF', 'JPEG', 'ICO')
FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE = (".png", "jpg", ".gif", ".jpeg", ".bmp", ".tif", ".tiff", ".jfif")
FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE = (".png", ".jpg", ".gif", ".jpeg", ".bmp", ".tif", ".tiff", ".jfif")
PILLOW_FORMATS_QUESTIONS_IMAGE = ('PNG', 'GIF', 'JPEG', 'BMP', 'TIFF')
FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT = (
@@ -278,3 +279,5 @@ FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT = (
".bmp", ".tif", ".tiff"
)
FILE_UPLOAD_EXTENSIONS_OTHER = FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT
PRETIX_MAX_ORDER_SIZE = 500
+1
View File
@@ -38,6 +38,7 @@ MAIL_FROM_ORGANIZERS = 'invalid@invalid'
FILE_UPLOAD_MAX_SIZE_EMAIL_AUTO_ATTACHMENT = 10
FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT = 10
FILE_UPLOAD_MAX_SIZE_IMAGE = 10
FILE_UPLOAD_MAX_SIZE_FAVICON = 10
DEFAULT_CURRENCY = 'EUR'
SECRET_KEY = "build-time-secret-key"
HAS_REDIS = False
+5
View File
@@ -19,6 +19,8 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import logging
from django.contrib.auth.models import AnonymousUser
from django_scopes import scopes_disabled
from rest_framework import exceptions
@@ -29,6 +31,8 @@ from pretix.api.auth.devicesecurity import (
)
from pretix.base.models import Device
logger = logging.getLogger(__name__)
class DeviceTokenAuthentication(TokenAuthentication):
model = Device
@@ -46,6 +50,7 @@ class DeviceTokenAuthentication(TokenAuthentication):
raise exceptions.AuthenticationFailed('Device has not been initialized.')
if device.revoked:
logging.warning(f'Connection attempt of revoked device {device.pk}.')
raise exceptions.AuthenticationFailed('Device access has been revoked.')
return AnonymousUser(), device
+2
View File
@@ -185,6 +185,7 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
('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'),
@@ -223,6 +224,7 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
('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'),
)
+49
View File
@@ -0,0 +1,49 @@
#
# 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 rest_framework import exceptions
from rest_framework.authentication import (
SessionAuthentication as BaseSessionAuthentication,
)
from pretix.multidomain.middlewares import CsrfViewMiddleware
class CustomCSRFCheck(CsrfViewMiddleware):
def _reject(self, request, reason):
# Return the failure reason instead of an HttpResponse
return reason
class SessionAuthentication(BaseSessionAuthentication):
# Override from DRF to user our custom CSRF middleware
def enforce_csrf(self, request):
def dummy_get_response(request): # pragma: no cover
return None
check = CustomCSRFCheck(dummy_get_response)
# populates request.META['CSRF_COOKIE'], which is used in process_view()
check.process_request(request)
reason = check.process_view(request, None, (), {})
if reason:
# CSRF failed, bail with explicit error message
raise exceptions.PermissionDenied('CSRF Failed: %s' % reason)
+1 -1
View File
@@ -54,7 +54,7 @@ class IdempotencyMiddleware:
auth_hash_parts = '{}:{}'.format(
request.headers.get('Authorization', ''),
request.COOKIES.get(settings.SESSION_COOKIE_NAME, '')
request.COOKIES.get('__Host-' + settings.SESSION_COOKIE_NAME, request.COOKIES.get(settings.SESSION_COOKIE_NAME, ''))
)
auth_hash = sha1(auth_hash_parts.encode()).hexdigest()
idempotency_key = request.headers.get('X-Idempotency-Key', '')
+3 -1
View File
@@ -424,7 +424,7 @@ class CloneEventSerializer(EventSerializer):
new_event = super().create({**validated_data, 'plugins': None})
event = Event.objects.filter(slug=self.context['event'], organizer=self.context['organizer'].pk).first()
new_event.copy_data_from(event)
new_event.copy_data_from(event, skip_meta_data='meta_data' in validated_data)
if plugins is not None:
new_event.set_active_plugins(plugins)
@@ -796,6 +796,8 @@ class EventSettingsSerializer(SettingsSerializer):
'cancel_allow_user_paid_refund_as_giftcard',
'cancel_allow_user_paid_require_approval',
'cancel_allow_user_paid_require_approval_fee_unknown',
'cancel_terms_paid',
'cancel_terms_unpaid',
'change_allow_user_variation',
'change_allow_user_addons',
'change_allow_user_until',
+11 -5
View File
@@ -61,7 +61,8 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
fields = ('id', 'value', 'active', 'description',
'position', 'default_price', 'price', 'original_price', 'free_price_suggestion', 'require_approval',
'require_membership', 'require_membership_types', 'require_membership_hidden',
'checkin_attention', 'available_from', 'available_until',
'checkin_attention', 'checkin_text',
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
'sales_channels', 'hide_without_voucher', 'meta_data')
def __init__(self, *args, **kwargs):
@@ -85,7 +86,8 @@ class ItemVariationSerializer(I18nAwareModelSerializer):
fields = ('id', 'value', 'active', 'description',
'position', 'default_price', 'price', 'original_price', 'free_price_suggestion', 'require_approval',
'require_membership', 'require_membership_types', 'require_membership_hidden',
'checkin_attention', 'available_from', 'available_until',
'checkin_attention', 'checkin_text',
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
'sales_channels', 'hide_without_voucher', 'meta_data')
def __init__(self, *args, **kwargs):
@@ -235,9 +237,10 @@ class ItemSerializer(I18nAwareModelSerializer):
model = Item
fields = ('id', 'category', 'name', 'internal_name', 'active', 'sales_channels', 'description',
'default_price', 'free_price', 'free_price_suggestion', 'tax_rate', 'tax_rule', 'admission',
'personalized', 'position', 'picture', 'available_from', 'available_until',
'personalized', 'position', 'picture',
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
'min_per_order', 'max_per_order', 'checkin_attention', 'checkin_text', 'has_variations', 'variations',
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
'show_quota_left', 'hidden_if_available', 'hidden_if_item_available', 'allow_waitinglist',
'issue_giftcard', 'meta_data',
@@ -440,7 +443,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
class Meta:
model = Question
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
'ask_during_checkin', 'show_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
'hidden', 'dependency_value', 'print_on_invoice', 'help_text', 'valid_number_min',
'valid_number_max', 'valid_date_min', 'valid_date_max', 'valid_datetime_min', 'valid_datetime_max',
'valid_string_length_max', 'valid_file_portrait')
@@ -486,6 +489,9 @@ class QuestionSerializer(I18nAwareModelSerializer):
if full_data.get('ask_during_checkin') and full_data.get('type') in Question.ASK_DURING_CHECKIN_UNSUPPORTED:
raise ValidationError(_('This type of question cannot be asked during check-in.'))
if full_data.get('show_during_checkin') and full_data.get('type') in Question.SHOW_DURING_CHECKIN_UNSUPPORTED:
raise ValidationError(_('This type of question cannot be shown during check-in.'))
Question.clean_items(event, full_data.get('items'))
return data
+22 -8
View File
@@ -44,7 +44,7 @@ from pretix.api.serializers import CompatibleJSONField
from pretix.api.serializers.event import SubEventSerializer
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.item import (
InlineItemVariationSerializer, ItemSerializer,
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
)
from pretix.base.channels import get_all_sales_channels
from pretix.base.decimal import round_decimal
@@ -585,6 +585,9 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
if 'variation' in self.context['expand']:
self.fields['variation'] = InlineItemVariationSerializer(read_only=True)
if 'answers.question' in self.context['expand']:
self.fields['answers'].child.fields['question'] = QuestionSerializer(read_only=True)
class OrderPaymentTypeField(serializers.Field):
# TODO: Remove after pretix 2.2
@@ -715,7 +718,7 @@ class OrderSerializer(I18nAwareModelSerializer):
fields = (
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
'url', 'customer', 'valid_if_pending'
)
read_only_fields = (
@@ -771,8 +774,8 @@ class OrderSerializer(I18nAwareModelSerializer):
def update(self, instance, validated_data):
# Even though all fields that shouldn't be edited are marked as read_only in the serializer
# (hopefully), we'll be extra careful here and be explicit about the model fields we update.
update_fields = ['comment', 'custom_followup_at', 'checkin_attention', 'email', 'locale', 'phone',
'valid_if_pending']
update_fields = ['comment', 'custom_followup_at', 'checkin_attention', 'checkin_text', 'email', 'locale',
'phone', 'valid_if_pending']
if 'invoice_address' in validated_data:
iadata = validated_data.pop('invoice_address')
@@ -1032,13 +1035,14 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
super().__init__(*args, **kwargs)
self.fields['positions'].child.fields['voucher'].queryset = self.context['event'].vouchers.all()
self.fields['customer'].queryset = self.context['event'].organizer.customers.all()
self.fields['expires'].required = False
class Meta:
model = Order
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts',
'force', 'send_email', 'simulate', 'customer', 'custom_followup_at', 'require_approval',
'valid_if_pending')
'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date',
'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at',
'require_approval', 'valid_if_pending', 'expires')
def validate_payment_provider(self, pp):
if pp is None:
@@ -1047,6 +1051,11 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
raise ValidationError('The given payment provider is not known.')
return pp
def validate_expires(self, expires):
if expires < now():
raise ValidationError('Expiration date must be in the future.')
return expires
def validate_sales_channel(self, channel):
if channel not in get_all_sales_channels():
raise ValidationError('Unknown sales channel.')
@@ -1068,6 +1077,10 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
raise ValidationError(
'An order cannot be empty.'
)
if len(data) > settings.PRETIX_MAX_ORDER_SIZE:
raise ValidationError(
'Orders cannot have more than %(max)s positions.' % {'max': settings.PRETIX_MAX_ORDER_SIZE}
)
errs = [{} for p in data]
if any([p.get('positionid') for p in data]):
if not all([p.get('positionid') for p in data]):
@@ -1353,7 +1366,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
if validated_data.get('locale', None) is None:
validated_data['locale'] = self.context['event'].settings.locale
order = Order(event=self.context['event'], **validated_data)
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
if not validated_data.get('expires'):
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
order.meta_info = "{}"
order.total = Decimal('0.00')
if validated_data.get('require_approval') is not None:
+9
View File
@@ -285,6 +285,8 @@ with scopes_disabled():
return queryset.filter(last_checked_in__isnull=not value)
def check_rules_qs(self, queryset, name, value):
if not value:
return queryset
if not self.checkinlist.rules:
return queryset
return queryset.filter(
@@ -536,6 +538,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'reason': Checkin.REASON_ALREADY_REDEEMED,
'reason_explanation': None,
'require_attention': False,
'checkin_texts': [],
'__warning': 'Compatibility hack active due to detected old pretixSCAN version',
}, status=400)
except: # we don't care e.g. about invalid version numbers
@@ -547,6 +550,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'reason': Checkin.REASON_INVALID,
'reason_explanation': None,
'require_attention': False,
'checkin_texts': [],
'list': MiniCheckinListSerializer(checkinlists[0]).data,
}, status=404)
elif revoked_matches and force:
@@ -576,6 +580,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'reason': Checkin.REASON_REVOKED,
'reason_explanation': None,
'require_attention': False,
'checkin_texts': [],
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, revoked_matches[
0].event)).data,
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
@@ -631,6 +636,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'reason': Checkin.REASON_AMBIGUOUS,
'reason_explanation': None,
'require_attention': op.require_checkin_attention,
'checkin_texts': op.checkin_texts,
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
}, status=400)
@@ -679,6 +685,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
return Response({
'status': 'incomplete',
'require_attention': op.require_checkin_attention,
'checkin_texts': op.checkin_texts,
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
'questions': [
QuestionSerializer(q).data for q in e.questions
@@ -709,6 +716,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'reason': e.code,
'reason_explanation': e.reason,
'require_attention': op.require_checkin_attention,
'checkin_texts': op.checkin_texts,
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
}, status=400)
@@ -716,6 +724,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
return Response({
'status': 'ok',
'require_attention': op.require_checkin_attention,
'checkin_texts': op.checkin_texts,
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
}, status=201)
+2 -2
View File
@@ -254,7 +254,7 @@ class EventViewSet(viewsets.ModelViewSet):
new_event = serializer.save(organizer=self.request.organizer)
if copy_from:
new_event.copy_data_from(copy_from)
new_event.copy_data_from(copy_from, skip_meta_data='meta_data' in serializer.validated_data)
if plugins is not None:
new_event.set_active_plugins(plugins)
@@ -291,7 +291,7 @@ class EventViewSet(viewsets.ModelViewSet):
try:
with transaction.atomic():
instance.organizer.log_action(
'pretix.event.deleted', user=self.request.user,
'pretix.event.deleted', user=self.request.user, auth=self.request.auth,
data={
'event_id': instance.pk,
'name': str(instance.name),
+1 -1
View File
@@ -42,7 +42,7 @@ class IdempotencyQueryView(APIView):
idempotency_key = request.GET.get("key")
auth_hash_parts = '{}:{}'.format(
request.headers.get('Authorization', ''),
request.COOKIES.get(settings.SESSION_COOKIE_NAME, '')
request.COOKIES.get('__Host-' + settings.SESSION_COOKIE_NAME, request.COOKIES.get(settings.SESSION_COOKIE_NAME, ''))
)
auth_hash = sha1(auth_hash_parts.encode()).hexdigest()
if not idempotency_key:
+12
View File
@@ -222,6 +222,8 @@ class OrderViewSetMixin:
qs = qs.prefetch_related('refunds', 'refunds__payment')
if 'invoice_address' not in self.request.GET.getlist('exclude'):
qs = qs.select_related('invoice_address')
if 'customer' not in self.request.GET.getlist('exclude'):
qs = qs.select_related('customer')
qs = qs.prefetch_related(self._positions_prefetch(self.request))
return qs
@@ -827,6 +829,16 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
}
)
if 'checkin_text' in self.request.data and serializer.instance.checkin_text != self.request.data.get('checkin_text'):
serializer.instance.log_action(
'pretix.event.order.checkin_text',
user=self.request.user,
auth=self.request.auth,
data={
'new_value': self.request.data.get('checkin_text')
}
)
if 'valid_if_pending' in self.request.data and serializer.instance.valid_if_pending != self.request.data.get('valid_if_pending'):
serializer.instance.log_action(
'pretix.event.order.valid_if_pending',
+1 -1
View File
@@ -384,7 +384,7 @@ def register_default_webhook_events(sender, **kwargs):
def notify_webhooks(logentry_ids: list):
if not isinstance(logentry_ids, list):
logentry_ids = [logentry_ids]
qs = LogEntry.all.select_related('event', 'event__organizer').filter(id__in=logentry_ids)
qs = LogEntry.all.select_related('event', 'event__organizer', 'organizer').filter(id__in=logentry_ids)
_org, _at, webhooks = None, None, None
for logentry in qs:
if not logentry.organizer:
+11 -504
View File
@@ -19,10 +19,7 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import inspect
import logging
from datetime import timedelta
from decimal import Decimal
from itertools import groupby
from smtplib import SMTPResponseException
from typing import TypeVar
@@ -33,21 +30,21 @@ from django.core.mail.backends.smtp import EmailBackend
from django.db.models import Count
from django.dispatch import receiver
from django.template.loader import get_template
from django.utils.formats import date_format
from django.utils.timezone import now
from django.utils.translation import get_language, gettext_lazy as _
from pretix.base.i18n import (
LazyCurrencyNumber, LazyDate, LazyExpiresDate, LazyNumber,
)
from pretix.base.models import Event
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized
from pretix.base.signals import (
register_html_mail_renderers, register_mail_placeholders,
)
from pretix.base.signals import register_html_mail_renderers
from pretix.base.templatetags.rich_text import markdown_compile_email
from pretix.base.services.placeholders import ( # noqa
get_available_placeholders, PlaceholderContext
)
from pretix.base.services.placeholders import ( # noqa
BaseTextPlaceholder as BaseMailTextPlaceholder,
SimpleFunctionalTextPlaceholder as SimpleFunctionalMailTextPlaceholder,
)
from pretix.base.settings import get_name_parts_localized # noqa
logger = logging.getLogger('pretix.base.email')
T = TypeVar("T", bound=EmailBackend)
@@ -217,495 +214,5 @@ def base_renderers(sender, **kwargs):
return [ClassicMailRenderer, UnembellishedMailRenderer]
class BaseMailTextPlaceholder:
"""
This is the base class for for all email text placeholders.
"""
@property
def required_context(self):
"""
This property should return a list of all attribute names that need to be
contained in the base context so that this placeholder is available. By default,
it returns a list containing the string "event".
"""
return ["event"]
@property
def identifier(self):
"""
This should return the identifier of this placeholder in the email.
"""
raise NotImplementedError()
def render(self, context):
"""
This method is called to generate the actual text that is being
used in the email. You will be passed a context dictionary with the
base context attributes specified in ``required_context``. You are
expected to return a plain-text string.
"""
raise NotImplementedError()
def render_sample(self, event):
"""
This method is called to generate a text to be used in email previews.
This may only depend on the event.
"""
raise NotImplementedError()
class SimpleFunctionalMailTextPlaceholder(BaseMailTextPlaceholder):
def __init__(self, identifier, args, func, sample):
self._identifier = identifier
self._args = args
self._func = func
self._sample = sample
@property
def identifier(self):
return self._identifier
@property
def required_context(self):
return self._args
def render(self, context):
return self._func(**{k: context[k] for k in self._args})
def render_sample(self, event):
if callable(self._sample):
return self._sample(event)
else:
return self._sample
def get_available_placeholders(event, base_parameters):
if 'order' in base_parameters:
base_parameters.append('invoice_address')
base_parameters.append('position_or_address')
params = {}
for r, val in register_mail_placeholders.send(sender=event):
if not isinstance(val, (list, tuple)):
val = [val]
for v in val:
if all(rp in base_parameters for rp in v.required_context):
params[v.identifier] = v
return params
def get_email_context(**kwargs):
from pretix.base.models import InvoiceAddress
event = kwargs['event']
if 'position' in kwargs:
kwargs.setdefault("position_or_address", kwargs['position'])
if 'order' in kwargs:
try:
if not kwargs.get('invoice_address'):
kwargs['invoice_address'] = kwargs['order'].invoice_address
except InvoiceAddress.DoesNotExist:
kwargs['invoice_address'] = InvoiceAddress(order=kwargs['order'])
finally:
kwargs.setdefault("position_or_address", kwargs['invoice_address'])
ctx = {}
for r, val in register_mail_placeholders.send(sender=event):
if not isinstance(val, (list, tuple)):
val = [val]
for v in val:
if all(rp in kwargs for rp in v.required_context):
try:
ctx[v.identifier] = v.render(kwargs)
except:
ctx[v.identifier] = '(error)'
logger.exception(f'Failed to process email placeholder {v.identifier}.')
return ctx
def _placeholder_payments(order, payments):
d = []
for payment in payments:
if 'payment' in inspect.signature(payment.payment_provider.order_pending_mail_render).parameters:
d.append(str(payment.payment_provider.order_pending_mail_render(order, payment)))
else:
d.append(str(payment.payment_provider.order_pending_mail_render(order)))
d = [line for line in d if line.strip()]
if d:
return '\n\n'.join(d)
else:
return ''
def get_best_name(position_or_address, parts=False):
"""
Return the best name we got for either an invoice address or an order position, falling back to the respective other
"""
from pretix.base.models import InvoiceAddress, OrderPosition
if isinstance(position_or_address, InvoiceAddress):
if position_or_address.name:
return position_or_address.name_parts if parts else position_or_address.name
elif position_or_address.order:
position_or_address = position_or_address.order.positions.exclude(attendee_name_cached="").exclude(attendee_name_cached__isnull=True).first()
if isinstance(position_or_address, OrderPosition):
if position_or_address.attendee_name:
return position_or_address.attendee_name_parts if parts else position_or_address.attendee_name
elif position_or_address.order:
try:
return position_or_address.order.invoice_address.name_parts if parts else position_or_address.order.invoice_address.name
except InvoiceAddress.DoesNotExist:
pass
return {} if parts else ""
@receiver(register_mail_placeholders, dispatch_uid="pretixbase_register_mail_placeholders")
def base_placeholders(sender, **kwargs):
from pretix.multidomain.urlreverse import build_absolute_uri
ph = [
SimpleFunctionalMailTextPlaceholder(
'event', ['event'], lambda event: event.name, lambda event: event.name
),
SimpleFunctionalMailTextPlaceholder(
'event', ['event_or_subevent'], lambda event_or_subevent: event_or_subevent.name,
lambda event_or_subevent: event_or_subevent.name
),
SimpleFunctionalMailTextPlaceholder(
'event_slug', ['event'], lambda event: event.slug, lambda event: event.slug
),
SimpleFunctionalMailTextPlaceholder(
'code', ['order'], lambda order: order.code, 'F8VVL'
),
SimpleFunctionalMailTextPlaceholder(
'total', ['order'], lambda order: LazyNumber(order.total), lambda event: LazyNumber(Decimal('42.23'))
),
SimpleFunctionalMailTextPlaceholder(
'currency', ['event'], lambda event: event.currency, lambda event: event.currency
),
SimpleFunctionalMailTextPlaceholder(
'order_email', ['order'], lambda order: order.email, 'john@example.org'
),
SimpleFunctionalMailTextPlaceholder(
'invoice_number', ['invoice'],
lambda invoice: invoice.full_invoice_no,
f'{sender.settings.invoice_numbers_prefix or (sender.slug.upper() + "-")}00000'
),
SimpleFunctionalMailTextPlaceholder(
'refund_amount', ['event_or_subevent', 'refund_amount'],
lambda event_or_subevent, refund_amount: LazyCurrencyNumber(refund_amount, event_or_subevent.currency),
lambda event_or_subevent: LazyCurrencyNumber(Decimal('42.23'), event_or_subevent.currency)
),
SimpleFunctionalMailTextPlaceholder(
'pending_sum', ['event', 'pending_sum'],
lambda event, pending_sum: LazyCurrencyNumber(pending_sum, event.currency),
lambda event: LazyCurrencyNumber(Decimal('42.23'), event.currency)
),
SimpleFunctionalMailTextPlaceholder(
'total_with_currency', ['event', 'order'], lambda event, order: LazyCurrencyNumber(order.total,
event.currency),
lambda event: LazyCurrencyNumber(Decimal('42.23'), event.currency)
),
SimpleFunctionalMailTextPlaceholder(
'expire_date', ['event', 'order'], lambda event, order: LazyExpiresDate(order.expires.astimezone(event.timezone)),
lambda event: LazyDate(now() + timedelta(days=15))
),
SimpleFunctionalMailTextPlaceholder(
'url', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash()
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.open', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
'hash': '98kusd8ofsj8dnkd'
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url_info_change', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.modify', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.modify', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url_products_change', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.change', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.change', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url_cancel', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.cancel', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.cancel', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url', ['event', 'position'], lambda event, position: build_absolute_uri(
event,
'presale:event.order.position',
kwargs={
'order': position.order.code,
'secret': position.web_secret,
'position': position.positionid
}
),
lambda event: build_absolute_uri(
event,
'presale:event.order.position', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
'position': '123'
}
),
),
SimpleFunctionalMailTextPlaceholder(
'order_modification_deadline_date_and_time', ['order', 'event'],
lambda order, event:
date_format(order.modify_deadline.astimezone(event.timezone), 'SHORT_DATETIME_FORMAT')
if order.modify_deadline
else '',
lambda event: date_format(
event.settings.get(
'last_order_modification_date', as_type=RelativeDateWrapper
).datetime(event).astimezone(event.timezone),
'SHORT_DATETIME_FORMAT'
) if event.settings.get('last_order_modification_date') else '',
),
SimpleFunctionalMailTextPlaceholder(
'event_location', ['event_or_subevent'], lambda event_or_subevent: str(event_or_subevent.location or ''),
lambda event: str(event.location or ''),
),
SimpleFunctionalMailTextPlaceholder(
'event_admission_time', ['event_or_subevent'],
lambda event_or_subevent:
date_format(event_or_subevent.date_admission.astimezone(event_or_subevent.timezone), 'TIME_FORMAT')
if event_or_subevent.date_admission
else '',
lambda event: date_format(event.date_admission.astimezone(event.timezone), 'TIME_FORMAT') if event.date_admission else '',
),
SimpleFunctionalMailTextPlaceholder(
'subevent', ['waiting_list_entry', 'event'],
lambda waiting_list_entry, event: str(waiting_list_entry.subevent or event),
lambda event: str(event if not event.has_subevents or not event.subevents.exists() else event.subevents.first())
),
SimpleFunctionalMailTextPlaceholder(
'subevent_date_from', ['waiting_list_entry', 'event'],
lambda waiting_list_entry, event: (waiting_list_entry.subevent or event).get_date_from_display(),
lambda event: (event if not event.has_subevents or not event.subevents.exists() else event.subevents.first()).get_date_from_display()
),
SimpleFunctionalMailTextPlaceholder(
'url_remove', ['waiting_list_voucher', 'event'],
lambda waiting_list_voucher, event: build_absolute_uri(
event, 'presale:event.waitinglist.remove'
) + '?voucher=' + waiting_list_voucher.code,
lambda event: build_absolute_uri(
event,
'presale:event.waitinglist.remove',
) + '?voucher=68CYU2H6ZTP3WLK5',
),
SimpleFunctionalMailTextPlaceholder(
'url', ['waiting_list_voucher', 'event'],
lambda waiting_list_voucher, event: build_absolute_uri(
event, 'presale:event.redeem'
) + '?voucher=' + waiting_list_voucher.code,
lambda event: build_absolute_uri(
event,
'presale:event.redeem',
) + '?voucher=68CYU2H6ZTP3WLK5',
),
SimpleFunctionalMailTextPlaceholder(
'invoice_name', ['invoice_address'], lambda invoice_address: invoice_address.name or '',
_('John Doe')
),
SimpleFunctionalMailTextPlaceholder(
'invoice_company', ['invoice_address'], lambda invoice_address: invoice_address.company or '',
_('Sample Corporation')
),
SimpleFunctionalMailTextPlaceholder(
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
'* {} - {}'.format(
order.full_code,
build_absolute_uri(event, 'presale:event.order.open', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash(),
}),
)
for order in orders
), lambda event: '\n' + '\n\n'.join(
'* {} - {}'.format(
'{}-{}'.format(event.slug.upper(), order['code']),
build_absolute_uri(event, 'presale:event.order.open', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'order': order['code'],
'secret': order['secret'],
'hash': order['hash'],
}),
)
for order in [
{'code': 'F8VVL', 'secret': '6zzjnumtsx136ddy', 'hash': 'abcdefghi'},
{'code': 'HIDHK', 'secret': '98kusd8ofsj8dnkd', 'hash': 'jklmnopqr'},
{'code': 'OPKSB', 'secret': '09pjdksflosk3njd', 'hash': 'stuvwxy2z'}
]
),
),
SimpleFunctionalMailTextPlaceholder(
'hours', ['event', 'waiting_list_entry'], lambda event, waiting_list_entry:
event.settings.waiting_list_hours,
lambda event: event.settings.waiting_list_hours
),
SimpleFunctionalMailTextPlaceholder(
'product', ['waiting_list_entry'], lambda waiting_list_entry: waiting_list_entry.item.name,
_('Sample Admission Ticket')
),
SimpleFunctionalMailTextPlaceholder(
'code', ['waiting_list_voucher'], lambda waiting_list_voucher: waiting_list_voucher.code,
'68CYU2H6ZTP3WLK5'
),
SimpleFunctionalMailTextPlaceholder(
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
'voucher_list', ['voucher_list'], lambda voucher_list: ' \n'.join(voucher_list),
' 68CYU2H6ZTP3WLK5\n 7MB94KKPVEPSMVF2'
),
SimpleFunctionalMailTextPlaceholder(
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
'voucher_url_list', ['event', 'voucher_list'],
lambda event, voucher_list: ' \n'.join([
build_absolute_uri(
event, 'presale:event.redeem'
) + '?voucher=' + c
for c in voucher_list
]),
lambda event: ' \n'.join([
build_absolute_uri(
event, 'presale:event.redeem'
) + '?voucher=' + c
for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2']
]),
),
SimpleFunctionalMailTextPlaceholder(
'url', ['event', 'voucher_list'], lambda event, voucher_list: build_absolute_uri(event, 'presale:event.index', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
}), lambda event: build_absolute_uri(event, 'presale:event.index', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
})
),
SimpleFunctionalMailTextPlaceholder(
'name', ['name'], lambda name: name,
_('John Doe')
),
SimpleFunctionalMailTextPlaceholder(
'comment', ['comment'], lambda comment: comment,
_('An individual text with a reason can be inserted here.'),
),
SimpleFunctionalMailTextPlaceholder(
'payment_info', ['order', 'payments'], _placeholder_payments,
_('The amount has been charged to your card.'),
),
SimpleFunctionalMailTextPlaceholder(
'payment_info', ['payment_info'], lambda payment_info: payment_info,
_('Please transfer money to this bank account: 9999-9999-9999-9999'),
),
SimpleFunctionalMailTextPlaceholder(
'attendee_name', ['position'], lambda position: position.attendee_name,
_('John Doe'),
),
SimpleFunctionalMailTextPlaceholder(
'positionid', ['position'], lambda position: str(position.positionid),
'1'
),
SimpleFunctionalMailTextPlaceholder(
'name', ['position_or_address'],
get_best_name,
_('John Doe'),
),
]
name_scheme = PERSON_NAME_SCHEMES[sender.settings.name_scheme]
if "concatenation_for_salutation" in name_scheme:
concatenation_for_salutation = name_scheme["concatenation_for_salutation"]
else:
concatenation_for_salutation = name_scheme["concatenation"]
ph.append(SimpleFunctionalMailTextPlaceholder(
"name_for_salutation", ["waiting_list_entry"],
lambda waiting_list_entry: concatenation_for_salutation(waiting_list_entry.name_parts),
_("Mr Doe"),
))
ph.append(SimpleFunctionalMailTextPlaceholder(
"name", ["waiting_list_entry"],
lambda waiting_list_entry: waiting_list_entry.name or "",
_("Mr Doe"),
))
ph.append(SimpleFunctionalMailTextPlaceholder(
"name_for_salutation", ["position_or_address"],
lambda position_or_address: concatenation_for_salutation(get_best_name(position_or_address, parts=True)),
_("Mr Doe"),
))
for f, l, w in name_scheme['fields']:
if f == 'full_name':
continue
ph.append(SimpleFunctionalMailTextPlaceholder(
'name_%s' % f, ['waiting_list_entry'], lambda waiting_list_entry, f=f: get_name_parts_localized(waiting_list_entry.name_parts, f),
name_scheme['sample'][f]
))
ph.append(SimpleFunctionalMailTextPlaceholder(
'attendee_name_%s' % f, ['position'], lambda position, f=f: get_name_parts_localized(position.attendee_name_parts, f),
name_scheme['sample'][f]
))
ph.append(SimpleFunctionalMailTextPlaceholder(
'name_%s' % f, ['position_or_address'],
lambda position_or_address, f=f: get_name_parts_localized(get_best_name(position_or_address, parts=True), f),
name_scheme['sample'][f]
))
for k, v in sender.meta_data.items():
ph.append(SimpleFunctionalMailTextPlaceholder(
'meta_%s' % k, ['event'], lambda event, k=k: event.meta_data[k],
v
))
ph.append(SimpleFunctionalMailTextPlaceholder(
'meta_%s' % k, ['event_or_subevent'], lambda event_or_subevent, k=k: event_or_subevent.meta_data[k],
v
))
return ph
return PlaceholderContext(**kwargs).render_all()
+1
View File
@@ -28,4 +28,5 @@ from .items import * # noqa
from .json import * # noqa
from .mail import * # noqa
from .orderlist import * # noqa
from .reusablemedia import * # noqa
from .waitinglist import * # noqa
+3
View File
@@ -88,6 +88,7 @@ class ItemDataExporter(ListExporter):
_("Minimum amount per order"),
_("Maximum amount per order"),
_("Requires special attention"),
_("Check-in text"),
_("Original price"),
_("This product is a gift card"),
_("Require a valid membership"),
@@ -162,6 +163,7 @@ class ItemDataExporter(ListExporter):
i.min_per_order if i.min_per_order is not None else "",
i.max_per_order if i.max_per_order is not None else "",
_("Yes") if i.checkin_attention else "",
i.checkin_text or "",
v.original_price or i.original_price or "",
_("Yes") if i.issue_giftcard else "",
_("Yes") if i.require_membership or v.require_membership else "",
@@ -206,6 +208,7 @@ class ItemDataExporter(ListExporter):
i.min_per_order if i.min_per_order is not None else "",
i.max_per_order if i.max_per_order is not None else "",
_("Yes") if i.checkin_attention else "",
i.checkin_text or "",
i.original_price or "",
_("Yes") if i.issue_giftcard else "",
_("Yes") if i.require_membership else "",
+3
View File
@@ -96,6 +96,7 @@ class JSONExporter(BaseExporter):
'min_per_order': item.min_per_order,
'max_per_order': item.max_per_order,
'checkin_attention': item.checkin_attention,
'checkin_text': item.checkin_text,
'original_price': item.original_price,
'issue_giftcard': item.issue_giftcard,
'meta_data': item.meta_data,
@@ -110,6 +111,7 @@ class JSONExporter(BaseExporter):
'description': str(variation.description),
'position': variation.position,
'checkin_attention': variation.checkin_attention,
'checkin_text': variation.checkin_text,
'require_approval': variation.require_approval,
'require_membership': variation.require_membership,
'sales_channels': variation.sales_channels,
@@ -164,6 +166,7 @@ class JSONExporter(BaseExporter):
'custom_followup_at': order.custom_followup_at,
'require_approval': order.require_approval,
'checkin_attention': order.checkin_attention,
'checkin_text': order.checkin_text,
'sales_channel': order.sales_channel,
'expires': order.expires,
'datetime': order.datetime,
+27 -12
View File
@@ -209,7 +209,7 @@ class OrderListExporter(MultiSheetListExporter):
return qs.annotate(**annotations).filter(**filters)
return qs
def iterate_orders(self, form_data: dict):
def orders_qs(self, form_data):
p_date = OrderPayment.objects.filter(
order=OuterRef('pk'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
@@ -250,6 +250,10 @@ class OrderListExporter(MultiSheetListExporter):
if form_data['paid_only']:
qs = qs.filter(status=Order.STATUS_PAID)
return qs
def iterate_orders(self, form_data: dict):
qs = self.orders_qs(form_data)
tax_rates = self._get_all_tax_rates(qs)
headers = [
@@ -275,6 +279,7 @@ class OrderListExporter(MultiSheetListExporter):
headers.append(_('Invoice numbers'))
headers.append(_('Sales channel'))
headers.append(_('Requires special attention'))
headers.append(_('Check-in text'))
headers.append(_('Comment'))
headers.append(_('Follow-up date'))
headers.append(_('Positions'))
@@ -332,7 +337,7 @@ class OrderListExporter(MultiSheetListExporter):
self.event_object_cache[order.event_id].slug,
order.code,
order.total,
order.get_status_display(),
order.get_extended_status_display(),
order.email,
str(order.phone) if order.phone else '',
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
@@ -384,6 +389,7 @@ class OrderListExporter(MultiSheetListExporter):
row.append(order.invoice_numbers)
row.append(order.sales_channel)
row.append(_('Yes') if order.checkin_attention else _('No'))
row.append(order.checkin_text or "")
row.append(order.comment or "")
row.append(order.custom_followup_at.strftime("%Y-%m-%d") if order.custom_followup_at else "")
row.append(order.pcnt)
@@ -404,7 +410,7 @@ class OrderListExporter(MultiSheetListExporter):
row += self.event_object_cache[order.event_id].meta_data.values()
yield row
def iterate_fees(self, form_data: dict):
def fees_qs(self, form_data):
p_providers = OrderPayment.objects.filter(
order=OuterRef('order'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
@@ -423,6 +429,10 @@ class OrderListExporter(MultiSheetListExporter):
qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False)
qs = self._date_filter(qs, form_data, rel='order__')
return qs
def iterate_fees(self, form_data: dict):
qs = self.fees_qs(form_data)
headers = [
_('Event slug'),
@@ -463,7 +473,7 @@ class OrderListExporter(MultiSheetListExporter):
row = [
self.event_object_cache[order.event_id].slug,
order.code,
_("canceled") if op.canceled else order.get_status_display(),
_("canceled") if op.canceled else order.get_extended_status_display(),
order.email,
str(order.phone) if order.phone else '',
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
@@ -504,7 +514,19 @@ class OrderListExporter(MultiSheetListExporter):
row += self.event_object_cache[order.event_id].meta_data.values()
yield row
def positions_qs(self, form_data: dict):
qs = OrderPosition.all.filter(
order__event__in=self.events,
)
if form_data['paid_only']:
qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False)
qs = self._date_filter(qs, form_data, rel='order__')
return qs
def iterate_positions(self, form_data: dict):
base_qs = self.positions_qs(form_data)
p_providers = OrderPayment.objects.filter(
order=OuterRef('order'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
@@ -514,9 +536,6 @@ class OrderListExporter(MultiSheetListExporter):
).values(
'm'
).order_by()
base_qs = OrderPosition.all.filter(
order__event__in=self.events,
)
qs = base_qs.annotate(
payment_providers=Subquery(p_providers, output_field=CharField()),
).select_related(
@@ -526,10 +545,6 @@ class OrderListExporter(MultiSheetListExporter):
'subevent', 'subevent__meta_values',
'answers', 'answers__question', 'answers__options'
)
if form_data['paid_only']:
qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False)
qs = self._date_filter(qs, form_data, rel='order__')
has_subevents = self.events.filter(has_subevents=True).exists()
@@ -638,7 +653,7 @@ class OrderListExporter(MultiSheetListExporter):
self.event_object_cache[order.event_id].slug,
order.code,
op.positionid,
_("canceled") if op.canceled else order.get_status_display(),
_("canceled") if op.canceled else order.get_extended_status_display(),
order.email,
str(order.phone) if order.phone else '',
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
@@ -0,0 +1,78 @@
#
# 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.dispatch import receiver
from django.utils.formats import date_format
from django.utils.translation import gettext_lazy as _, pgettext, pgettext_lazy
from ..exporter import ListExporter, OrganizerLevelExportMixin
from ..models import ReusableMedium
from ..signals import register_multievent_data_exporters
class ReusableMediaExporter(OrganizerLevelExportMixin, ListExporter):
identifier = 'reusablemedia'
verbose_name = _('Reusable media')
category = pgettext_lazy('export_category', 'Reusable media')
description = _('Download a spread sheet with the data of all reusable medias on your account.')
def iterate_list(self, form_data):
media = ReusableMedium.objects.filter(
organizer=self.organizer,
).select_related(
'customer', 'linked_orderposition', 'linked_giftcard',
).order_by('created')
headers = [
pgettext('reusable_medium', 'Media type'),
pgettext('reusable_medium', 'Identifier'),
_('Active'),
_('Expiration date'),
_('Customer account'),
_('Linked ticket'),
_('Linked gift card'),
_('Notes'),
]
yield headers
yield self.ProgressSetTotal(total=media.count())
for medium in media.iterator(chunk_size=1000):
row = [
medium.type,
medium.identifier,
_('Yes') if medium.active else _('No'),
date_format(medium.expires, 'SHORT_DATETIME_FORMAT') if medium.expires else '',
medium.customer.identifier if medium.customer_id else '',
f"{medium.linked_orderposition.order.code}-{medium.linked_orderposition.positionid}" if medium.linked_orderposition_id else '',
medium.linked_giftcard.secret if medium.linked_giftcard_id else '',
medium.notes,
]
yield row
def get_filename(self):
return f'{self.organizer.slug}_media'
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_reusablemedia")
def register_multievent_i_reusable_media_exporter(sender, **kwargs):
return ReusableMediaExporter
+11 -4
View File
@@ -125,7 +125,7 @@ class NamePartsWidget(forms.MultiWidget):
if fname == 'title' and self.titles:
widgets.append(Select(attrs=a, choices=[('', '')] + [(d, d) for d in self.titles[1]]))
elif fname == 'salutation':
widgets.append(Select(attrs=a, choices=[('', '---')] + PERSON_NAME_SALUTATIONS))
widgets.append(Select(attrs=a, choices=[('', '---'), ('empty', '')] + PERSON_NAME_SALUTATIONS))
else:
widgets.append(self.widget(attrs=a))
super().__init__(widgets, attrs)
@@ -136,7 +136,10 @@ class NamePartsWidget(forms.MultiWidget):
data = []
for i, field in enumerate(self.scheme['fields']):
fname, label, size = field
data.append(value.get(fname, ""))
fval = value.get(fname, "")
if fname == "salutation" and fname in value and fval == "":
fval = "empty"
data.append(fval)
if '_legacy' in value and not data[-1]:
data[-1] = value.get('_legacy', '')
elif not any(d for d in data) and '_scheme' in value:
@@ -190,7 +193,8 @@ class NamePartsFormField(forms.MultiValueField):
data = {}
data['_scheme'] = self.scheme_name
for i, value in enumerate(data_list):
data[self.scheme['fields'][i][0]] = value or ''
key = self.scheme['fields'][i][0]
data[key] = value or ''
return data
def __init__(self, *args, **kwargs):
@@ -239,7 +243,7 @@ class NamePartsFormField(forms.MultiValueField):
d.pop('validators', None)
field = forms.ChoiceField(
**d,
choices=[('', '---')] + PERSON_NAME_SALUTATIONS
choices=[('', '---'), ('empty', '')] + PERSON_NAME_SALUTATIONS
)
else:
field = forms.CharField(**defaults)
@@ -265,6 +269,9 @@ class NamePartsFormField(forms.MultiValueField):
if sum(len(v) for v in value.values() if v) > 250:
raise forms.ValidationError(_('Please enter a shorter name.'), code='max_length')
if value.get("salutation") == "empty":
value["salutation"] = ""
return value
+4 -1
View File
@@ -209,7 +209,10 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
date_attrs['placeholder'] = lazy(date_placeholder, str)
time_attrs['placeholder'] = lazy(time_placeholder, str)
date_attrs['aria-label'] = _('Date')
time_attrs['aria-label'] = _('Time')
if 'aria-label' in attrs:
del attrs['aria-label']
widgets = (
forms.DateInput(attrs=date_attrs, format=date_format),
forms.TimeInput(attrs=time_attrs, format=time_format),
+34 -3
View File
@@ -182,7 +182,7 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
pdfmetrics.registerFontFamily('OpenSans', normal='OpenSans', bold='OpenSansBd',
italic='OpenSansIt', boldItalic='OpenSansBI')
for family, styles in get_fonts().items():
for family, styles in get_fonts(event=self.event, pdf_support_required=True).items():
if family == self.event.settings.invoice_renderer_font:
pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype'])))
self.font_regular = family
@@ -855,7 +855,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
class Modern1Renderer(ClassicInvoiceRenderer):
identifier = 'modern1'
verbose_name = gettext_lazy('Modern Invoice Renderer (pretix 2.7)')
verbose_name = gettext_lazy('Default invoice renderer (European-style letter)')
bottom_margin = 16.9 * mm
top_margin = 16.9 * mm
right_margin = 20 * mm
@@ -989,6 +989,37 @@ class Modern1Renderer(ClassicInvoiceRenderer):
canvas.drawText(textobject)
class Modern1SimplifiedRenderer(Modern1Renderer):
identifier = 'modern1simplified'
verbose_name = gettext_lazy('Simplified invoice renderer')
logo_left = Modern1Renderer.left_margin
logo_width = pagesizes.A4[0] - Modern1Renderer.right_margin - logo_left
logo_height = 25 * mm
logo_top = 13 * mm
logo_anchor = 'nw'
def _draw_invoice_from(self, canvas):
super(Modern1Renderer, self)._draw_invoice_from(canvas)
def _draw_event(self, canvas):
pass
def _get_intro(self):
i = []
if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage:
i.append(Paragraph(
pgettext('invoice', 'Event date: {date_range}').format(
date_range=self.invoice.event.get_date_range_display(),
),
self.stylesheet['Normal'],
))
i.append(Spacer(2 * mm, 2 * mm))
return i + super()._get_intro()
@receiver(register_invoice_renderers, dispatch_uid="invoice_renderer_classic")
def recv_classic(sender, **kwargs):
return [ClassicInvoiceRenderer, Modern1Renderer]
return [ClassicInvoiceRenderer, Modern1Renderer, Modern1SimplifiedRenderer]
@@ -104,7 +104,7 @@ class Command(BaseCommand):
with language(locale), override(timezone):
for receiver, response in signal_result:
if not response:
return None
continue
ex = response(e, o, report_status)
if ex.identifier == options['export_provider']:
params = json.loads(options.get('parameters') or '{}')
+43 -16
View File
@@ -20,7 +20,7 @@
# <https://www.gnu.org/licenses/>.
#
from collections import OrderedDict
from urllib.parse import urlsplit
from urllib.parse import urlparse, urlsplit
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from django.conf import settings
@@ -40,10 +40,30 @@ from pretix.base.settings import global_settings_object
from pretix.multidomain.urlreverse import (
get_event_domain, get_organizer_domain,
)
from pretix.presale.style import get_fonts
_supported = None
def get_supported_language(requested_language, allowed_languages, default_language):
language = requested_language
if language not in allowed_languages:
firstpart = language.split('-')[0]
if firstpart in allowed_languages:
language = firstpart
else:
language = default_language
for lang in allowed_languages:
if lang.startswith(firstpart + '-'):
language = lang
break
if language not in allowed_languages:
# This seems redundant, but can happen in the rare edge case that settings.locale is (wrongfully)
# not part of settings.locales
language = allowed_languages[0]
return language
class LocaleMiddleware(MiddlewareMixin):
"""
@@ -65,20 +85,11 @@ class LocaleMiddleware(MiddlewareMixin):
settings_holder = None
if settings_holder:
if language not in settings_holder.settings.locales:
firstpart = language.split('-')[0]
if firstpart in settings_holder.settings.locales:
language = firstpart
else:
language = settings_holder.settings.locale
for lang in settings_holder.settings.locales:
if lang.startswith(firstpart + '-'):
language = lang
break
if language not in settings_holder.settings.locales:
# This seems redundant, but can happen in the rare edge case that settings.locale is (wrongfully)
# not part of settings.locales
language = settings_holder.settings.locales[0]
language = get_supported_language(
language,
settings_holder.settings.locales,
settings_holder.settings.locale,
)
if '-' not in language and settings_holder.settings.region:
language += '-' + settings_holder.settings.region
else:
@@ -230,6 +241,14 @@ class SecurityMiddleware(MiddlewareMixin):
)
def process_response(self, request, resp):
def nested_dict_values(d):
for v in d.values():
if isinstance(v, dict):
yield from nested_dict_values(v)
else:
if isinstance(v, str):
yield v
url = resolve(request.path_info)
if settings.DEBUG and resp.status_code >= 400:
@@ -249,6 +268,14 @@ class SecurityMiddleware(MiddlewareMixin):
if gs.settings.leaflet_tiles:
img_src.append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*"))
font_src = set()
if hasattr(request, 'event'):
for font in get_fonts(request.event, pdf_support_required=False).values():
for path in list(nested_dict_values(font)):
font_location = urlparse(path)
if font_location.scheme and font_location.netloc:
font_src.add('{}://{}'.format(font_location.scheme, font_location.netloc))
h = {
'default-src': ["{static}"],
'script-src': ['{static}'],
@@ -257,7 +284,7 @@ class SecurityMiddleware(MiddlewareMixin):
'style-src': ["{static}", "{media}"],
'connect-src': ["{dynamic}", "{media}"],
'img-src': ["{static}", "{media}", "data:"] + img_src,
'font-src': ["{static}"],
'font-src': ["{static}"] + list(font_src),
'media-src': ["{static}", "data:"],
# form-action is not only used to match on form actions, but also on URLs
# form-actions redirect to. In the context of e.g. payment providers or
@@ -0,0 +1,17 @@
# Generated by Django 4.2.4 on 2023-11-13 16:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0250_eventmetaproperty_filter_public"),
]
operations = [
migrations.AddField(
model_name="order",
name="invoice_dirty",
field=models.BooleanField(default=False),
),
]
@@ -0,0 +1,56 @@
# Generated by Django 4.2.4 on 2023-11-20 12:38
import django.db.models.deletion
from django.db import migrations, models
from django.db.models import F, OuterRef, Subquery
def backfill_organizer(apps, schema_editor):
LogEntry = apps.get_model("pretixbase", "LogEntry")
Event = apps.get_model("pretixbase", "Event")
ContentType = apps.get_model("contenttypes", "ContentType")
LogEntry.objects.filter(
organizer_link__isnull=True, event__isnull=False
).update(organizer_link_id=Subquery(
Event.objects.filter(pk=OuterRef('event_id')).values('organizer_id'),
)
)
for ct in ContentType.objects.all():
try:
model = apps.get_model(ct.app_label, ct.model)
except LookupError:
continue
if "organizer" in model._meta.fields:
LogEntry.objects.filter(
organizer_link__isnull=True, event__isnull=True, content_type=ct,
).update(
organizer_link_id=Subquery(model.objects.filter(pk=OuterRef('object_id')).values('organizer_id'))
)
elif "event" in model._meta.fields:
LogEntry.objects.filter(
organizer_link__isnull=True, event__isnull=True, content_type=ct,
).update(
organizer_link_id=Subquery(model.objects.filter(pk=OuterRef('object_id')).values('event__organizer_id'))
)
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0251_order_invoice_dirty"),
]
operations = [
migrations.AddField(
model_name="logentry",
name="organizer_link",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="pretixbase.organizer",
),
),
migrations.RunPython(
backfill_organizer,
)
]
@@ -0,0 +1,32 @@
# Generated by Django 4.2.4 on 2023-09-06 09:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0252_logentry_organizer"),
]
operations = [
migrations.AddField(
model_name="item",
name="checkin_text",
field=models.TextField(null=True),
),
migrations.AddField(
model_name="itemvariation",
name="checkin_text",
field=models.TextField(null=True),
),
migrations.AddField(
model_name="order",
name="checkin_text",
field=models.TextField(null=True),
),
migrations.AddField(
model_name="question",
name="show_during_checkin",
field=models.BooleanField(default=False),
),
]
@@ -0,0 +1,28 @@
# Generated by Django 4.2.4 on 2023-12-06 14:53
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0253_checkin_info"),
]
operations = [
migrations.AlterField(
model_name="logentry",
name="organizer_link",
field=models.ForeignKey(
db_column="organizer_link_id",
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="pretixbase.organizer",
),
),
migrations.RenameField(
model_name="logentry",
old_name="organizer_link",
new_name="organizer",
),
]
@@ -0,0 +1,22 @@
# Generated by Django 4.2.4 on 2023-11-22 20:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0254_alter_logentry_organizer_link_and_more"),
]
operations = [
migrations.AddField(
model_name="item",
name="available_from_mode",
field=models.CharField(default="hide", max_length=16),
),
migrations.AddField(
model_name="item",
name="available_until_mode",
field=models.CharField(default="hide", max_length=16),
)
]
@@ -0,0 +1,22 @@
# Generated by Django 4.2.4 on 2024-01-11 15:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0255_item_unavail_modes"),
]
operations = [
migrations.AddField(
model_name="itemvariation",
name="available_from_mode",
field=models.CharField(default="hide", max_length=16),
),
migrations.AddField(
model_name="itemvariation",
name="available_until_mode",
field=models.CharField(default="hide", max_length=16),
),
]
+1 -3
View File
@@ -53,13 +53,11 @@ from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django_otp.models import Device
from django_scopes import scopes_disabled
from u2flib_server.utils import (
pub_key_from_der, websafe_decode, websafe_encode,
)
from pretix.base.i18n import language
from pretix.helpers.urls import build_absolute_uri
from ...helpers.u2f import pub_key_from_der, websafe_decode, websafe_encode
from .base import LoggingMixin
+12 -3
View File
@@ -84,13 +84,21 @@ class LoggingMixin:
from .devices import Device
from .event import Event
from .log import LogEntry
from .organizer import TeamAPIToken
from .organizer import Organizer, TeamAPIToken
event = None
if isinstance(self, Event):
organizer_id = None
if isinstance(self, Organizer):
organizer_id = self.pk
elif isinstance(self, Event):
event = self
organizer_id = self.organizer_id
elif hasattr(self, 'event'):
event = self.event
organizer_id = self.event.organizer_id
elif hasattr(self, 'organizer_id'):
organizer_id = self.organizer_id
if user and not user.is_authenticated:
user = None
@@ -106,7 +114,8 @@ class LoggingMixin:
elif isinstance(api_token, TeamAPIToken):
kwargs['api_token'] = api_token
logentry = LogEntry(content_object=self, user=user, action_type=action, event=event, **kwargs)
logentry = LogEntry(content_object=self, user=user, action_type=action, event=event,
organizer_id=organizer_id, **kwargs)
if isinstance(data, dict):
sensitivekeys = ['password', 'secret', 'api_key']
+5 -2
View File
@@ -280,7 +280,8 @@ class CheckinList(LoggedModel):
'<', '<=', '>', '>=', '==', '!=', 'inList', 'isBefore', 'isAfter', 'or', 'and'
}
allowed_operators = top_level_operators | {
'buildTime', 'objectList', 'lookup', 'var', 'entries_since', 'entries_before'
'buildTime', 'objectList', 'lookup', 'var', 'entries_since', 'entries_before', 'entries_days_since',
'entries_days_before',
}
allowed_vars = {
'product', 'variation', 'now', 'now_isoweekday', 'entries_number', 'entries_today', 'entries_days',
@@ -309,7 +310,7 @@ class CheckinList(LoggedModel):
raise ValidationError(f'Logic variable "{values[0]}" is currently not allowed.')
return rules
if operator in ('entries_since', 'entries_before'):
if operator in ('entries_since', 'entries_before', 'entries_days_since', 'entries_days_before'):
if len(values) != 1 or "buildTime" not in values[0]:
raise ValidationError(f'Operator "{operator}" takes exactly one "buildTime" argument.')
@@ -352,6 +353,7 @@ class Checkin(models.Model):
REASON_AMBIGUOUS = 'ambiguous'
REASON_ERROR = 'error'
REASON_BLOCKED = 'blocked'
REASON_UNAPPROVED = 'unapproved'
REASON_INVALID_TIME = 'invalid_time'
REASONS = (
(REASON_CANCELED, _('Order canceled')),
@@ -365,6 +367,7 @@ class Checkin(models.Model):
(REASON_AMBIGUOUS, _('Ticket code is ambiguous on list')),
(REASON_ERROR, _('Server error')),
(REASON_BLOCKED, _('Ticket blocked')),
(REASON_UNAPPROVED, _('Order not approved')),
(REASON_INVALID_TIME, _('Ticket not valid at this time')),
)
+7 -2
View File
@@ -344,7 +344,7 @@ class Discount(LoggedModel):
elif self.subevent_mode == self.SUBEVENT_MODE_SAME:
def key(idx):
return positions[idx][1] # subevent_id
return positions[idx][1] or 0 # subevent_id
# Build groups of candidates with the same subevent, then apply our regular algorithm
# to each group
@@ -424,5 +424,10 @@ class Discount(LoggedModel):
break
for g in candidate_groups:
self._apply_min_count(positions, g, g, result)
self._apply_min_count(
positions,
[idx for idx in g if idx in condition_candidates],
[idx for idx in g if idx in benefit_candidates],
result
)
return result
+8 -2
View File
@@ -775,7 +775,7 @@ class Event(EventMixin, LoggedModel):
time(hour=23, minute=59, second=59)
), tz)
def copy_data_from(self, other):
def copy_data_from(self, other, skip_meta_data=False):
from pretix.presale.style import regenerate_css
from ..signals import event_copy_data
@@ -798,6 +798,12 @@ class Event(EventMixin, LoggedModel):
self.save()
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
if not skip_meta_data:
for emv in EventMetaValue.objects.filter(event=other):
emv.pk = None
emv.event = self
emv.save(force_insert=True)
for fl in EventFooterLink.objects.filter(event=other):
fl.pk = None
fl.event = self
@@ -1058,7 +1064,7 @@ class Event(EventMixin, LoggedModel):
providers[pp.identifier] = pp
self._cached_payment_providers = OrderedDict(sorted(
providers.items(), key=lambda v: (-v[1].priority, str(v[1].verbose_name))
providers.items(), key=lambda v: (-v[1].priority, str(v[1].verbose_name).title())
))
return self._cached_payment_providers
+86 -3
View File
@@ -263,8 +263,8 @@ def filter_available(qs, channel='web', voucher=None, allow_addons=False):
# IMPORTANT: If this is updated, also update the ItemVariation query
# in models/event.py: EventMixin.annotated()
Q(active=True)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()) | Q(available_from_mode='info'))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()) | Q(available_until_mode='info'))
& Q(sales_channels__contains=channel) & Q(require_bundling=False)
)
if not allow_addons:
@@ -336,6 +336,8 @@ class Item(LoggedModel):
:type min_per_order: int
:param checkin_attention: Requires special attention at check-in
:type checkin_attention: bool
:param checkin_text: Additional text to show at check-in
:type checkin_text: bool
:param original_price: The item's "original" price. Will not be used for any calculations, will just be shown.
:type original_price: decimal.Decimal
:param require_approval: If set to ``True``, orders containing this product can only be processed and paid after approved by an administrator
@@ -372,6 +374,13 @@ class Item(LoggedModel):
(VALIDITY_MODE_DYNAMIC, _('Dynamic validity')),
)
UNAVAIL_MODE_HIDDEN = "hide"
UNAVAIL_MODE_INFO = "info"
UNAVAIL_MODES = (
(UNAVAIL_MODE_HIDDEN, _("Hide product if unavailable")),
(UNAVAIL_MODE_INFO, _("Show info text if unavailable")),
)
MEDIA_POLICY_REUSE = 'reuse'
MEDIA_POLICY_NEW = 'new'
MEDIA_POLICY_REUSE_OR_NEW = 'reuse_or_new'
@@ -485,11 +494,21 @@ class Item(LoggedModel):
null=True, blank=True,
help_text=_('This product will not be sold before the given date.')
)
available_from_mode = models.CharField(
choices=UNAVAIL_MODES,
default=UNAVAIL_MODE_HIDDEN,
max_length=16,
)
available_until = models.DateTimeField(
verbose_name=_("Available until"),
null=True, blank=True,
help_text=_('This product will not be sold after the given date.')
)
available_until_mode = models.CharField(
choices=UNAVAIL_MODES,
default=UNAVAIL_MODE_HIDDEN,
max_length=16,
)
hidden_if_available = models.ForeignKey(
'Quota',
null=True, blank=True,
@@ -566,6 +585,11 @@ class Item(LoggedModel):
'attention. You can use this for example for student tickets to indicate to the person at '
'check-in that the student ID card still needs to be checked.')
)
checkin_text = models.TextField(
verbose_name=_('Check-in text'),
null=True, blank=True,
help_text=_('This text will be shown by the check-in app if a ticket of this type is scanned.')
)
original_price = models.DecimalField(
verbose_name=_('Original price'),
blank=True, null=True,
@@ -624,7 +648,7 @@ class Item(LoggedModel):
null=True, blank=True, max_length=16,
verbose_name=_('Validity'),
help_text=_(
'When setting up a regular event, or an event series with time slots, you typically to NOT need to change '
'When setting up a regular event, or an event series with time slots, you typically do NOT need to change '
'this value. The default setting means that the validity time of tickets will not be decided by the '
'product, but by the event and check-in configuration. Only use the other options if you need them to '
'realize e.g. a booking of a year-long ticket with a dynamic start date. Note that the validity will be '
@@ -696,6 +720,8 @@ class Item(LoggedModel):
return str(self.internal_name or self.name)
def save(self, *args, **kwargs):
if self.hide_without_voucher:
self.require_voucher = True
super().save(*args, **kwargs)
if self.event:
self.event.cache.clear()
@@ -773,6 +799,24 @@ class Item(LoggedModel):
return False
return True
def unavailability_reason(self, now_dt: datetime=None, has_voucher=False, subevent=None) -> Optional[str]:
now_dt = now_dt or now()
subevent_item = subevent and subevent.item_overrides.get(self.pk)
if not self.active:
return 'active'
elif self.available_from and self.available_from > now_dt:
return 'available_from'
elif self.available_until and self.available_until < now_dt:
return 'available_until'
elif (self.require_voucher or self.hide_without_voucher) and not has_voucher:
return 'require_voucher'
elif subevent_item and subevent_item.available_from and subevent_item.available_from > now_dt:
return 'available_from'
elif subevent_item and subevent_item.available_until and subevent_item.available_until < now_dt:
return 'available_until'
else:
return None
def _get_quotas(self, ignored_quotas=None, subevent=None):
check_quotas = set(getattr(
self, '_subevent_quotas', # Utilize cache in product list
@@ -1071,11 +1115,21 @@ class ItemVariation(models.Model):
null=True, blank=True,
help_text=_('This variation will not be sold before the given date.')
)
available_from_mode = models.CharField(
choices=Item.UNAVAIL_MODES,
default=Item.UNAVAIL_MODE_HIDDEN,
max_length=16,
)
available_until = models.DateTimeField(
verbose_name=_("Available until"),
null=True, blank=True,
help_text=_('This variation will not be sold after the given date.')
)
available_until_mode = models.CharField(
choices=Item.UNAVAIL_MODES,
default=Item.UNAVAIL_MODE_HIDDEN,
max_length=16,
)
sales_channels = fields.MultiStringField(
verbose_name=_('Sales channels'),
default=_all_sales_channels_identifiers,
@@ -1096,6 +1150,11 @@ class ItemVariation(models.Model):
'attention. You can use this for example for student tickets to indicate to the person at '
'check-in that the student ID card still needs to be checked.')
)
checkin_text = models.TextField(
verbose_name=_('Check-in text'),
null=True, blank=True,
help_text=_('This text will be shown by the check-in app if a ticket of this type is scanned.')
)
objects = ScopedManager(organizer='item__event__organizer')
@@ -1248,6 +1307,22 @@ class ItemVariation(models.Model):
return False
return True
def unavailability_reason(self, now_dt: datetime=None, has_voucher=False, subevent=None) -> Optional[str]:
now_dt = now_dt or now()
subevent_var = subevent and subevent.var_overrides.get(self.pk)
if not self.active:
return 'active'
elif self.available_from and self.available_from > now_dt:
return 'available_from'
elif self.available_until and self.available_until < now_dt:
return 'available_until'
elif subevent_var and subevent_var.available_from and subevent_var.available_from > now_dt:
return 'available_from'
elif subevent_var and subevent_var.available_until and subevent_var.available_until < now_dt:
return 'available_until'
else:
return None
@property
def meta_data(self):
data = self.item.meta_data
@@ -1450,6 +1525,8 @@ class Question(LoggedModel):
:param items: A set of ``Items`` objects that this question should be applied to
:param ask_during_checkin: Whether to ask this question during check-in instead of during check-out.
:type ask_during_checkin: bool
:param show_during_checkin: Whether to show the answer to this question during check-in.
:type show_during_checkin: bool
:param hidden: Whether to only show the question in the backend
:type hidden: bool
:param identifier: An arbitrary, internal identifier
@@ -1487,6 +1564,7 @@ class Question(LoggedModel):
)
UNLOCALIZED_TYPES = [TYPE_DATE, TYPE_TIME, TYPE_DATETIME]
ASK_DURING_CHECKIN_UNSUPPORTED = []
SHOW_DURING_CHECKIN_UNSUPPORTED = [TYPE_FILE]
event = models.ForeignKey(
Event,
@@ -1538,6 +1616,11 @@ class Question(LoggedModel):
help_text=_('Not supported by all check-in apps for all question types.'),
default=False
)
show_during_checkin = models.BooleanField(
verbose_name=_('Show answer during check-in'),
help_text=_('Not supported by all check-in apps for all question types.'),
default=False
)
hidden = models.BooleanField(
verbose_name=_('Hidden question'),
help_text=_('This question will only show up in the backend.'),
+1 -14
View File
@@ -78,6 +78,7 @@ class LogEntry(models.Model):
device = models.ForeignKey('Device', null=True, blank=True, on_delete=models.PROTECT)
oauth_application = models.ForeignKey('pretixapi.OAuthApplication', null=True, blank=True, on_delete=models.PROTECT)
event = models.ForeignKey('Event', null=True, blank=True, on_delete=models.SET_NULL)
organizer = models.ForeignKey('Organizer', null=True, blank=True, on_delete=models.PROTECT, db_column='organizer_link_id')
action_type = models.CharField(max_length=255)
data = models.TextField(default='{}')
visible = models.BooleanField(default=True)
@@ -122,20 +123,6 @@ class LogEntry(models.Model):
typepath = typepath.rsplit('.', 1)[0]
return no_type
@cached_property
def organizer(self):
from .organizer import Organizer
if self.event:
return self.event.organizer
elif hasattr(self.content_object, 'event'):
return self.content_object.event.organizer
elif hasattr(self.content_object, 'organizer'):
return self.content_object.organizer
elif isinstance(self.content_object, Organizer):
return self.content_object
return None
@cached_property
def display_object(self):
from . import (
+58 -14
View File
@@ -44,7 +44,7 @@ from datetime import datetime, time, timedelta
from decimal import Decimal
from functools import reduce
from time import sleep
from typing import Any, Dict, List, Union
from typing import Any, Dict, Iterable, List, Union
from zoneinfo import ZoneInfo
import dateutil
@@ -79,7 +79,7 @@ from pretix.base.i18n import language
from pretix.base.models import Customer, User
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import order_gracefully_delete
from pretix.base.signals import allow_ticket_download, order_gracefully_delete
from ...helpers import OF_SELF
from ...helpers.countries import CachedCountries, FastCountryField
@@ -244,6 +244,11 @@ class Order(LockModel, LoggedModel):
'special attention. This will not show any details or custom message, so you need to brief your '
'check-in staff how to handle these cases.')
)
checkin_text = models.TextField(
verbose_name=_('Check-in text'),
null=True, blank=True,
help_text=_('This text will be shown by the check-in app if a ticket of this order is scanned.')
)
expiry_reminder_sent = models.BooleanField(
default=False
)
@@ -266,6 +271,10 @@ class Order(LockModel, LoggedModel):
default=False,
verbose_name=_('E-mail address verified')
)
invoice_dirty = models.BooleanField(
# Invoice needs to be re-issued when the order is paid again
default=False,
)
objects = ScopedManager(organizer='event__organizer')
@@ -325,6 +334,18 @@ class Order(LockModel, LoggedModel):
def email_confirm_hash(self):
return hashlib.sha256(settings.SECRET_KEY.encode() + self.secret.encode()).hexdigest()[:9]
def get_extended_status_display(self):
# Changes in this method should to be replicated in pretixcontrol/orders/fragment_order_status.html
# and pretixpresale/event/fragment_order_status.html
if self.status == Order.STATUS_PENDING:
if self.require_approval:
return _("approval pending")
elif self.valid_if_pending:
return pgettext_lazy("order state", "pending (confirmed)")
elif self.status == Order.STATUS_PAID and self.count_positions == 0:
return _("canceled (paid fee)")
return self.get_status_display()
@property
def fees(self):
"""
@@ -1069,9 +1090,6 @@ class Order(LockModel, LoggedModel):
if not self.email and not (position and position.attendee_email):
return
for k, v in self.event.meta_data.items():
context['meta_' + k] = v
with language(self.locale, self.event.settings.region):
recipient = self.email
if position and position.attendee_email:
@@ -1116,12 +1134,19 @@ class Order(LockModel, LoggedModel):
attach_tickets=True,
)
@property
def positions_with_tickets_ignoring_plugins(self):
return (op for op in self.positions.select_related('item') if op.generate_ticket)
@property
def positions_with_tickets(self):
for op in self.positions.select_related('item'):
if not op.generate_ticket:
continue
yield op
signal_response = allow_ticket_download.send(self.event, order=self)
if all([r is True for rr, r in signal_response]):
return self.positions_with_tickets_ignoring_plugins
elif any([r is False for rr, r in signal_response]):
return []
else:
return set.intersection(set(self.positions_with_tickets_ignoring_plugins), *[set(r) for rr, r in signal_response if isinstance(r, Iterable)])
def create_transactions(self, is_new=False, positions=None, fees=None, dt_now=None, migrated=False,
_backfill_before_cancellation=False, save=True):
@@ -1823,7 +1848,7 @@ class OrderPayment(models.Model):
def _mark_order_paid(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text='',
ignore_date=False, lock=True, payment_refund_sum=0, allow_generate_invoice=True):
from pretix.base.services.invoices import (
generate_invoice, invoice_qualified,
generate_cancellation, generate_invoice, invoice_qualified,
)
from pretix.base.services.locking import LOCK_TRUST_WINDOW
@@ -1841,9 +1866,14 @@ class OrderPayment(models.Model):
cancellations = self.order.invoices.filter(is_cancellation=True).count()
gen_invoice = (
(invoices == 0 and self.order.event.settings.get('invoice_generate') in ('True', 'paid')) or
0 < invoices <= cancellations
0 < invoices <= cancellations or
self.order.invoice_dirty
)
if gen_invoice:
if invoices:
last_i = self.order.invoices.filter(is_cancellation=False).last()
if not last_i.canceled:
generate_cancellation(last_i)
invoice = generate_invoice(
self.order,
trigger_pdf=not send_mail or not self.order.event.settings.invoice_email_attachment
@@ -2117,6 +2147,12 @@ class OrderRefund(models.Model):
self.local_id = (self.order.refunds.aggregate(m=Max('local_id'))['m'] or 0) + 1
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'local_id'}.union(kwargs['update_fields'])
if self.state == OrderRefund.REFUND_STATE_DONE and not self.execution_date:
self.execution_date = now()
if 'update_fields' in kwargs:
kwargs['update_fields'] = {'execution_date'}.union(kwargs['update_fields'])
super().save(*args, **kwargs)
@@ -2404,6 +2440,17 @@ class OrderPosition(AbstractPosition):
return True
return False
@cached_property
def checkin_texts(self):
texts = []
if self.order.checkin_text:
texts.append(self.order.checkin_text)
if self.variation_id and self.variation.checkin_text:
texts.append(self.variation.checkin_text)
if self.item.checkin_text:
texts.append(self.item.checkin_text)
return texts
@property
def checkins(self):
"""
@@ -2607,9 +2654,6 @@ class OrderPosition(AbstractPosition):
if not self.attendee_email:
return
for k, v in self.event.meta_data.items():
context['meta_' + k] = v
with language(self.order.locale, self.order.event.settings.region):
recipient = self.attendee_email
try:
+2 -1
View File
@@ -251,7 +251,8 @@ class Voucher(LoggedModel):
null=True, blank=True,
on_delete=models.PROTECT, # We use a fake version of SET_NULL in Item.delete()
help_text=_(
"This product is added to the user's cart if the voucher is redeemed."
"This product is added to the user's cart if the voucher is redeemed. Instead of a specific product, you "
"can also select a quota. In this case, all products assigned to this quota can be selected."
)
)
variation = models.ForeignKey(
-3
View File
@@ -259,9 +259,6 @@ class WaitingListEntry(LoggedModel):
if not self.email:
return
for k, v in self.event.meta_data.items():
context['meta_' + k] = v
with language(self.locale, self.event.settings.region):
recipient = self.email
+1 -1
View File
@@ -849,7 +849,7 @@ class BasePaymentProvider:
except InvoiceAddress.DoesNotExist:
pass
else:
if str(ia.country) not in restricted_countries:
if str(ia.country) != '' and str(ia.country) not in restricted_countries:
return False
if order.sales_channel not in self.settings.get('_restrict_to_sales_channels', as_type=list, default=['web']):
+10 -3
View File
@@ -78,7 +78,7 @@ from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import Paragraph
from pretix.base.i18n import language
from pretix.base.models import Order, OrderPosition, Question
from pretix.base.models import Event, Order, OrderPosition, Question
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import layout_image_variables, layout_text_variables
from pretix.base.templatetags.money import money_filter
@@ -738,9 +738,10 @@ class Renderer:
else:
self.bg_bytes = None
self.bg_pdf = None
self.event_fonts = list(get_fonts(event, pdf_support_required=True).keys()) + ['Open Sans']
@classmethod
def _register_fonts(cls):
def _register_fonts(cls, event: Event = None):
if hasattr(cls, '_fonts_registered'):
return
pdfmetrics.registerFont(TTFont('Open Sans', finders.find('fonts/OpenSans-Regular.ttf')))
@@ -748,7 +749,7 @@ class Renderer:
pdfmetrics.registerFont(TTFont('Open Sans B', finders.find('fonts/OpenSans-Bold.ttf')))
pdfmetrics.registerFont(TTFont('Open Sans B I', finders.find('fonts/OpenSans-BoldItalic.ttf')))
for family, styles in get_fonts().items():
for family, styles in get_fonts(event, pdf_support_required=True).items():
pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype'])))
if 'italic' in styles:
pdfmetrics.registerFont(TTFont(family + ' I', finders.find(styles['italic']['truetype'])))
@@ -939,6 +940,12 @@ class Renderer:
if o['italic']:
font += ' I'
# Since pdfmetrics.registerFont is global, we want to make sure that no one tries to sneak in a font, they
# should not have access to.
if font not in self.event_fonts:
logger.warning(f'Unauthorized use of font "{font}"')
font = 'Open Sans'
try:
ad = getAscentDescent(font, float(o['fontsize']))
except KeyError: # font not known, fall back
+53 -8
View File
@@ -41,6 +41,7 @@ from typing import List, Optional
from celery.exceptions import MaxRetriesExceededError
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import DatabaseError, transaction
from django.db.models import Count, Exists, IntegerField, OuterRef, Q, Value
@@ -112,6 +113,15 @@ error_messages = {
'Some of the products you selected are no longer available in '
'the quantity you selected. Please see below for details.'
),
'unavailable_listed': gettext_lazy(
'Some of the products you selected are no longer available. '
'The following products are affected and have not been added to your cart: %s'
),
'in_part_listed': gettext_lazy(
'Some of the products you selected are no longer available in '
'the quantity you selected. The following products are affected and have not '
'been added to your cart: %s'
),
'max_items': ngettext_lazy(
"You cannot select more than %s item per order.",
"You cannot select more than %s items per order."
@@ -378,8 +388,9 @@ class CartManager:
cartsize += sum([op.count for op in self._operations if isinstance(op, self.AddOperation) and not op.addon_to])
cartsize -= len([1 for op in self._operations if isinstance(op, self.RemoveOperation) if
not op.position.addon_to_id])
if cartsize > int(self.event.settings.max_items_per_order):
raise CartError(error_messages['max_items'] % self.event.settings.max_items_per_order)
limit = min(int(self.event.settings.max_items_per_order), settings.PRETIX_MAX_ORDER_SIZE)
if cartsize > limit:
raise CartError(error_messages['max_items'] % limit)
def _check_item_constraints(self, op, current_ops=[]):
if isinstance(op, (self.AddOperation, self.ExtendOperation)):
@@ -1103,6 +1114,8 @@ class CartManager:
if 'sleep-after-quota-check' in debugflags_var.get():
sleep(2)
err_unavailable_products = []
for iop, op in enumerate(self._operations):
if isinstance(op, self.RemoveOperation):
if op.position.expires > self.now_dt:
@@ -1130,9 +1143,15 @@ class CartManager:
voucher_available_count = min(voucher_available_count, vouchers_ok[op.voucher])
if quota_available_count < 1:
err = err or error_messages['unavailable']
err = err or error_messages['unavailable_listed']
err_unavailable_products.append(
f'{op.item.name} {op.variation}' if op.variation else op.item.name
)
elif quota_available_count < requested_count:
err = err or error_messages['in_part']
err = err or error_messages['in_part_listed']
err_unavailable_products.append(
f'{op.item.name} {op.variation}' if op.variation else op.item.name
)
if voucher_available_count < 1:
if op.voucher in self._voucher_depend_on_cart:
@@ -1149,16 +1168,25 @@ class CartManager:
b_quotas = list(b.quotas)
if not b_quotas:
if not op.voucher or not op.voucher.allow_ignore_quota:
err = err or error_messages['unavailable']
err = err or error_messages['unavailable_listed']
err_unavailable_products.append(
f'{op.item.name} {op.variation}' if op.variation else op.item.name
)
available_count = 0
continue
b_quota_available_count = min(available_count * b.count, min(quotas_ok[q] for q in b_quotas))
if b_quota_available_count < b.count:
err = err or error_messages['unavailable']
err = err or error_messages['unavailable_listed']
err_unavailable_products.append(
f'{op.item.name} {op.variation}' if op.variation else op.item.name
)
available_count = 0
elif b_quota_available_count < available_count * b.count:
err = err or error_messages['in_part']
err = err or error_messages['in_part_listed']
available_count = b_quota_available_count // b.count
err_unavailable_products.append(
f'{op.item.name} {op.variation}' if op.variation else op.item.name
)
for q in b_quotas:
quotas_ok[q] -= available_count * b.count
# TODO: is this correct?
@@ -1297,10 +1325,23 @@ class CartManager:
op.position.price_after_voucher = op.price_after_voucher
op.position.voucher = op.voucher
# op.posiiton.price will be set in recompute_final_prices_and_taxes
# op.position.price will be set in recompute_final_prices_and_taxes
op.position.save(update_fields=['price_after_voucher', 'voucher'])
vouchers_ok[op.voucher] -= 1
if op.voucher.all_bundles_included or op.voucher.all_addons_included:
for a in op.position.addons.all():
if a.is_bundled and op.voucher.all_bundles_included and a.price:
a.listed_price = Decimal("0.00")
a.price_after_voucher = Decimal("0.00")
# a.price will be set in recompute_final_prices_and_taxes
a.save(update_fields=['listed_price', 'price_after_voucher'])
elif not a.is_bundled and op.voucher.all_addons_included and a.price and not a.custom_price_input:
a.listed_price = Decimal("0.00")
a.price_after_voucher = Decimal("0.00")
# op.positon.price will be set in recompute_final_prices_and_taxes
a.save(update_fields=['listed_price', 'price_after_voucher'])
for p in new_cart_positions:
if getattr(p, '_answers', None):
if not p.pk: # We stored some to the database already before
@@ -1310,6 +1351,10 @@ class CartManager:
if 'sleep-before-commit' in debugflags_var.get():
sleep(2)
if err in (error_messages['unavailable_listed'], error_messages['in_part_listed']):
err = err % ', '.join(str(p) for p in err_unavailable_products)
return err
def recompute_final_prices_and_taxes(self):
+95 -13
View File
@@ -31,6 +31,7 @@
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import logging
import os
from datetime import datetime, timedelta, timezone
from functools import partial, reduce
@@ -65,6 +66,8 @@ from pretix.helpers.jsonlogic_query import (
MinutesSince, tolerance,
)
logger = logging.getLogger(__name__)
def _build_time(t=None, value=None, ev=None, now_dt=None):
now_dt = now_dt or now()
@@ -199,7 +202,7 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
'var': values[0]["var"],
'rhs': values[1:],
}
elif "entries_since" in values[0] or "entries_before" in values[0]:
elif any(t in values[0] for t in ("entries_since", "entries_before", "entries_days_since", "entries_days_before")):
_var_explanations[new_var_name] = {
'operator': operator,
'var': values[0],
@@ -277,11 +280,13 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
var_weights[vname] = (500, 0)
var_texts[vname] = _('Wrong entrance gate')
elif var in ('entries_number', 'entries_today', 'entries_days', 'minutes_since_last_entry', 'minutes_since_first_entry', 'now_isoweekday') \
or (isinstance(var, dict) and ("entries_since" in var or "entries_before" in var)):
or (isinstance(var, dict) and any(t in var for t in ("entries_since", "entries_before", "entries_days_since", "entries_days_before"))):
w = {
'minutes_since_first_entry': 80,
'minutes_since_last_entry': 90,
'entries_days': 100,
'entries_days_since': 105,
'entries_days_before': 105,
'entries_since': 110,
'entries_before': 110,
'entries_number': 120,
@@ -304,10 +309,12 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
'entries_today': _('number of entries today'),
'entries_since': _('number of entries since {datetime}'),
'entries_before': _('number of entries before {datetime}'),
'entries_days_since': _('number of days with an entry since {datetime}'),
'entries_days_before': _('number of days with an entry before {datetime}'),
'now_isoweekday': _('week day'),
}
if isinstance(var, dict) and ("entries_since" in var or "entries_before" in var):
if isinstance(var, dict) and any(t in var for t in ("entries_since", "entries_before", "entries_days_since", "entries_days_before")):
varname = list(var.keys())[0]
cutoff = _build_time(*var[varname][0]['buildTime'], ev=ev, now_dt=now_dt).astimezone(ev.timezone)
if abs(now_dt - cutoff) < timedelta(hours=12):
@@ -407,6 +414,8 @@ def _get_logic_environment(ev, rule_data, now_dt):
logic.add_operation('isAfter', lambda t1, t2, tol=None: is_before(t2, t1, tol))
logic.add_operation('entries_since', lambda t1: rule_data.entries_since(t1))
logic.add_operation('entries_before', lambda t1: rule_data.entries_before(t1))
logic.add_operation('entries_days_since', lambda t1: rule_data.entries_days_since(t1))
logic.add_operation('entries_days_before', lambda t1: rule_data.entries_days_before(t1))
return logic
@@ -464,6 +473,32 @@ class LazyRuleVars:
self.__cache['entries_before', cutoff] = self._position.checkins.filter(type=Checkin.TYPE_ENTRY, list=self._clist, datetime__lt=cutoff).count()
return self.__cache['entries_before', cutoff]
def entries_days_since(self, cutoff):
tz = self._clist.event.timezone
with override(tz):
if ('entries_days_since', cutoff) not in self.__cache:
self.__cache['entries_days_since', cutoff] = self._position.checkins.filter(
type=Checkin.TYPE_ENTRY,
list=self._clist,
datetime__gte=cutoff
).annotate(
day=TruncDate('datetime', tzinfo=tz)
).values('day').distinct().count()
return self.__cache['entries_days_since', cutoff]
def entries_days_before(self, cutoff):
tz = self._clist.event.timezone
with override(tz):
if ('entries_days_before', cutoff) not in self.__cache:
self.__cache['entries_days_before', cutoff] = self._position.checkins.filter(
type=Checkin.TYPE_ENTRY,
list=self._clist,
datetime__lt=cutoff
).annotate(
day=TruncDate('datetime', tzinfo=tz)
).values('day').distinct().count()
return self.__cache['entries_days_before', cutoff]
@cached_property
def entries_days(self):
tz = self._clist.event.timezone
@@ -530,7 +565,8 @@ class SQLLogic:
"isBefore": partial(self.comparison_to_q, operator=LowerThan, modifier=partial(tolerance, sign=1)),
"isAfter": partial(self.comparison_to_q, operator=GreaterThan, modifier=partial(tolerance, sign=-1)),
}
self.expression_ops = {'buildTime', 'objectList', 'lookup', 'var', 'entries_since', 'entries_before'}
self.expression_ops = {'buildTime', 'objectList', 'lookup', 'var', 'entries_since', 'entries_before',
'entries_days_since', 'entries_days_before'}
def operation_to_expression(self, rule):
if not isinstance(rule, dict):
@@ -608,6 +644,42 @@ class SQLLogic:
Value(0),
output_field=IntegerField()
)
elif operator == 'entries_days_since':
tz = self.list.event.timezone
return Coalesce(
Subquery(
Checkin.objects.filter(
position_id=OuterRef('pk'),
type=Checkin.TYPE_ENTRY,
list_id=self.list.pk,
datetime__gte=self.operation_to_expression(values[0]),
).annotate(
day=TruncDate('datetime', tzinfo=tz)
).values('position_id').order_by().annotate(
c=Count('day', distinct=True)
).values('c')
),
Value(0),
output_field=IntegerField()
)
elif operator == 'entries_days_before':
tz = self.list.event.timezone
return Coalesce(
Subquery(
Checkin.objects.filter(
position_id=OuterRef('pk'),
type=Checkin.TYPE_ENTRY,
list_id=self.list.pk,
datetime__lt=self.operation_to_expression(values[0]),
).annotate(
day=TruncDate('datetime', tzinfo=tz)
).values('position_id').order_by().annotate(
c=Count('day', distinct=True)
).values('c')
),
Value(0),
output_field=IntegerField()
)
elif operator == 'var':
if values[0] == 'now':
return Value(now().astimezone(timezone.utc))
@@ -874,6 +946,15 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
'blocked'
)
if op.order.status == Order.STATUS_PENDING and op.order.require_approval:
if force:
force_used = True
else:
raise CheckInError(
_('This order is not yet approved.'),
'unapproved',
)
if type != Checkin.TYPE_EXIT and op.valid_from and op.valid_from > dt:
if force:
force_used = True
@@ -941,14 +1022,6 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
'product'
)
if op.order.status != Order.STATUS_PAID and op.order.require_approval:
if force:
force_used = True
else:
raise CheckInError(
_('This order is not yet approved.'),
'unpaid'
)
elif op.order.status != Order.STATUS_PAID and not op.order.valid_if_pending and not (
ignore_unpaid and clist.include_pending and op.order.status == Order.STATUS_PENDING
):
@@ -963,7 +1036,16 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
if type == Checkin.TYPE_ENTRY and clist.rules:
rule_data = LazyRuleVars(op, clist, dt, gate=gate)
logic = _get_logic_environment(op.subevent or clist.event, rule_data, now_dt=dt)
if not logic.apply(clist.rules, rule_data):
try:
logic_result = logic.apply(clist.rules, rule_data)
except Exception:
logger.exception("Check-in rule evaluation failed")
raise CheckInError(
_('Evaluation of custom rules has failed.'),
'rules',
)
if not logic_result:
if force:
force_used = True
else:
+4
View File
@@ -395,6 +395,10 @@ def generate_invoice(order: Order, trigger_pdf=True):
if order.status == Order.STATUS_CANCELED:
generate_cancellation(invoice, trigger_pdf)
if order.invoice_dirty:
order.invoice_dirty = False
order.save(update_fields=['invoice_dirty'])
return invoice
+2 -1
View File
@@ -100,7 +100,8 @@ def lock_objects(objects, *, shared_lock_objects=None, replace_exclusive_with_sh
if 'postgresql' in settings.DATABASES['default']['ENGINE']:
shared_keys = set(pg_lock_key(obj) for obj in shared_lock_objects) if shared_lock_objects else set()
exclusive_keys = set(pg_lock_key(obj) for obj in objects)
if replace_exclusive_with_shared_when_exclusive_are_more_than and len(exclusive_keys) > replace_exclusive_with_shared_when_exclusive_are_more_than:
if replace_exclusive_with_shared_when_exclusive_are_more_than and shared_keys and \
len(exclusive_keys) > replace_exclusive_with_shared_when_exclusive_are_more_than:
exclusive_keys = shared_keys
keys = sorted(list(shared_keys | exclusive_keys))
calls = ", ".join([
+6 -6
View File
@@ -183,12 +183,9 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
if auto_email:
headers['X-Auto-Response-Suppress'] = 'OOF, NRN, AutoReply, RN'
headers['Auto-Submitted'] = 'auto-generated'
headers.setdefault('X-Mailer', 'pretix')
with language(locale):
if isinstance(context, dict) and event:
for k, v in event.meta_data.items():
context['meta_' + k] = v
if isinstance(context, dict) and order:
try:
context.update({
@@ -573,8 +570,11 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
except smtplib.SMTPRecipientsRefused as e:
smtp_codes = [a[0] for a in e.recipients.values()]
if not any(c >= 500 for c in smtp_codes):
# Not a permanent failure (mailbox full, service unavailable), retry later, but with large intervals
if not any(c >= 500 for c in smtp_codes) or any(b'Message is too large' in a[1] for a in e.recipients.values()):
# This is not a permanent failure (mailbox full, service unavailable), retry later, but with large
# intervals. One would think that "Message is too lage" is a permanent failure, but apparently it is not.
# We have documented cases of emails to Microsoft returning the error occasionally and then later
# allowing the very same email.
try:
self.retry(max_retries=5, countdown=[60, 300, 600, 1200, 1800, 1800][self.request.retries])
except MaxRetriesExceededError:
@@ -30,6 +30,7 @@ from pretix.base.models import LogEntry, NotificationSetting, User
from pretix.base.notifications import Notification, get_all_notification_types
from pretix.base.services.mail import mail_send_task
from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask
from pretix.base.signals import notification
from pretix.celery_app import app
from pretix.helpers.urls import build_absolute_uri
@@ -90,6 +91,8 @@ def notify(logentry_ids: list):
if enabled and um not in notify_specific:
send_notification.apply_async(args=(logentry.id, notification_type.action_type, user.pk, method))
notification.send(logentry.event, logentry_id=logentry.id, notification_type=notification_type.action_type)
@app.task(base=ProfiledTask, acks_late=True, max_retries=9, default_retry_delay=900)
def send_notification(logentry_id: int, action_type: str, user_id: int, method: str):
+15 -1
View File
@@ -23,6 +23,7 @@ import csv
import io
from decimal import Decimal
from django.conf import settings as django_settings
from django.core.exceptions import ValidationError
from django.db import transaction
from django.utils.timezone import now
@@ -91,7 +92,15 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user,
user = User.objects.get(pk=user)
with language(locale, event.settings.region):
cols = get_all_columns(event)
parsed = parse_csv(cf.file, charset=charset)
try:
parsed = parse_csv(cf.file, charset=charset)
except UnicodeDecodeError as e:
raise DataImportError(
_(
'Error decoding special characters in your file: {message}').format(
message=str(e)
)
)
orders = []
order = None
data = []
@@ -116,6 +125,11 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user,
)
data.append(values)
if settings['orders'] == 'one' and len(data) > django_settings.PRETIX_MAX_ORDER_SIZE:
raise DataImportError(
_('Orders cannot have more than %(max)s positions.') % {'max': django_settings.PRETIX_MAX_ORDER_SIZE}
)
# Prepare model objects. Yes, this might consume lots of RAM, but allows us to make the actual SQL transaction
# shorter. We'll see what works better in reality…
lock_seats = []
+47 -25
View File
@@ -98,10 +98,9 @@ from pretix.base.services.pricing import (
from pretix.base.services.quotas import QuotaAvailability
from pretix.base.services.tasks import ProfiledEventTask, ProfiledTask
from pretix.base.signals import (
allow_ticket_download, order_approved, order_canceled, order_changed,
order_denied, order_expired, order_fee_calculation, order_paid,
order_placed, order_split, order_valid_if_pending, periodic_task,
validate_order,
order_approved, order_canceled, order_changed, order_denied, order_expired,
order_fee_calculation, order_paid, order_placed, order_split,
order_valid_if_pending, periodic_task, validate_order,
)
from pretix.celery_app import app
from pretix.helpers import OF_SELF
@@ -1408,23 +1407,16 @@ def send_download_reminders(sender, **kwargs):
if o.download_reminder_sent:
# Race condition
continue
if not all([r for rr, r in allow_ticket_download.send(event, order=o)]):
positions = list(o.positions_with_tickets)
if not positions:
continue
if not o.ticket_download_available:
continue
positions = o.positions.select_related('item')
if o.status != Order.STATUS_PAID:
if o.status != Order.STATUS_PENDING or o.require_approval or (not o.valid_if_pending and not o.event.settings.ticket_download_pending):
continue
send = False
for p in positions:
if p.generate_ticket:
send = True
break
if not send:
continue
with language(o.locale, o.event.settings.region):
o.download_reminder_sent = True
@@ -1442,10 +1434,7 @@ def send_download_reminders(sender, **kwargs):
logger.exception('Reminder email could not be sent')
if event.settings.mail_send_download_reminder_attendee:
for p in o.positions.all():
if not p.generate_ticket:
continue
for p in positions:
if p.subevent_id:
reminder_date = (p.subevent.date_from - timedelta(days=days)).replace(
hour=0, minute=0, second=0, microsecond=0
@@ -1512,6 +1501,7 @@ class OrderChangeManager:
"You need to select at least %(min)s items of the product %(product)s.",
"min"
),
'max_order_size': gettext_lazy('Orders cannot have more than %(max)s positions.'),
}
ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation'))
SubeventOperation = namedtuple('SubeventOperation', ('position', 'subevent'))
@@ -2599,6 +2589,14 @@ class OrderChangeManager:
self.order.total = total + payment_fee
self.order.save()
def _check_order_size(self):
if (len(self.order.positions.all()) + len([op for op in self._operations if isinstance(op, self.AddOperation)])) > settings.PRETIX_MAX_ORDER_SIZE:
raise OrderError(
self.error_messages['max_order_size'] % {
'max': settings.PRETIX_MAX_ORDER_SIZE,
}
)
def _payment_fee_diff(self):
total = self.order.total + self._totaldiff
if self.open_payment:
@@ -2619,14 +2617,37 @@ class OrderChangeManager:
def _reissue_invoice(self):
i = self.order.invoices.filter(is_cancellation=False).last()
if self.reissue_invoice and self._invoice_dirty:
if i and not i.refered.exists():
self._invoices.append(generate_cancellation(i))
if invoice_qualified(self.order) and \
(i or
self.event.settings.invoice_generate == 'True' or (
self.open_payment is not None and self.event.settings.invoice_generate == 'paid' and
self.open_payment.payment_provider.requires_invoice_immediately)):
self._invoices.append(generate_invoice(self.order))
order_now_qualified = invoice_qualified(self.order)
invoice_should_be_generated_now = (
self.event.settings.invoice_generate == "True" or (
self.event.settings.invoice_generate == "paid" and
self.open_payment is not None and
self.open_payment.payment_provider.requires_invoice_immediately
) or (
self.event.settings.invoice_generate == "paid" and
self.order.status == Order.STATUS_PAID
) or (
# Backwards-compatible behaviour
self.event.settings.invoice_generate not in ("True", "paid") and
i and
not i.canceled
)
)
invoice_should_be_generated_later = not invoice_should_be_generated_now and (
self.event.settings.invoice_generate in ("True", "paid")
)
if order_now_qualified:
if invoice_should_be_generated_now:
if i and not i.canceled:
self._invoices.append(generate_cancellation(i))
self._invoices.append(generate_invoice(self.order))
elif invoice_should_be_generated_later:
self.order.invoice_dirty = True
self.order.save(update_fields=["invoice_dirty"])
else:
if i and not i.canceled:
self._invoices.append(generate_cancellation(i))
def _check_complete_cancel(self):
current = self.order.positions.count()
@@ -2716,6 +2737,7 @@ class OrderChangeManager:
# finally, incorporate difference in payment fees
self._payment_fee_diff()
self._check_order_size()
with transaction.atomic():
locked_instance = Order.objects.select_for_update(of=OF_SELF).get(pk=self.order.pk)
+584
View File
@@ -0,0 +1,584 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import inspect
import logging
from datetime import timedelta
from decimal import Decimal
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from pretix.base.forms import PlaceholderValidator
from pretix.base.forms.widgets import format_placeholders_help_text
from pretix.base.i18n import (
LazyCurrencyNumber, LazyDate, LazyExpiresDate, LazyNumber,
)
from pretix.base.reldate import RelativeDateWrapper
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
logger = logging.getLogger('pretix.base.services.placeholders')
class BaseTextPlaceholder:
"""
This is the base class for all email text placeholders.
"""
@property
def required_context(self):
"""
This property should return a list of all attribute names that need to be
contained in the base context so that this placeholder is available. By default,
it returns a list containing the string "event".
"""
return ["event"]
@property
def identifier(self):
"""
This should return the identifier of this placeholder in the email.
"""
raise NotImplementedError()
def render(self, context):
"""
This method is called to generate the actual text that is being
used in the email. You will be passed a context dictionary with the
base context attributes specified in ``required_context``. You are
expected to return a plain-text string.
"""
raise NotImplementedError()
def render_sample(self, event):
"""
This method is called to generate a text to be used in email previews.
This may only depend on the event.
"""
raise NotImplementedError()
class SimpleFunctionalTextPlaceholder(BaseTextPlaceholder):
def __init__(self, identifier, args, func, sample):
self._identifier = identifier
self._args = args
self._func = func
self._sample = sample
@property
def identifier(self):
return self._identifier
@property
def required_context(self):
return self._args
def render(self, context):
return self._func(**{k: context[k] for k in self._args})
def render_sample(self, event):
if callable(self._sample):
return self._sample(event)
else:
return self._sample
class PlaceholderContext(SafeFormatter):
"""
Holds the contextual arguments and corresponding list of available placeholders for formatting
an email or other templated text.
Example:
context = PlaceholderContext(event=my_event, order=my_order)
formatted_doc = context.format(input_doc)
"""
def __init__(self, **kwargs):
super().__init__({})
self.context_args = kwargs
self._extend_context_args()
self.placeholders = {}
self.cache = {}
event = kwargs['event']
for r, val in [
*register_mail_placeholders.send(sender=event),
*register_text_placeholders.send(sender=event)
]:
if not isinstance(val, (list, tuple)):
val = [val]
for v in val:
if all(rp in kwargs for rp in v.required_context):
self.placeholders[v.identifier] = v
def _extend_context_args(self):
from pretix.base.models import InvoiceAddress
if 'position' in self.context_args:
self.context_args.setdefault("position_or_address", self.context_args['position'])
if 'order' in self.context_args:
try:
if not self.context_args.get('invoice_address'):
self.context_args['invoice_address'] = self.context_args['order'].invoice_address
except InvoiceAddress.DoesNotExist:
self.context_args['invoice_address'] = InvoiceAddress(order=self.context_args['order'])
finally:
self.context_args.setdefault("position_or_address", self.context_args['invoice_address'])
def render_placeholder(self, placeholder):
try:
return self.cache[placeholder.identifier]
except KeyError:
try:
value = self.cache[placeholder.identifier] = placeholder.render(self.context_args)
return value
except:
logger.exception(f'Failed to process template placeholder {placeholder.identifier}.')
return '(error)'
def render_all(self):
return {identifier: self.render_placeholder(placeholder)
for (identifier, placeholder) in self.placeholders.items()}
def get_value(self, key, args, kwargs):
if key not in self.placeholders:
return '{' + str(key) + '}'
return self.render_placeholder(self.placeholders[key])
def _placeholder_payments(order, payments):
d = []
for payment in payments:
if 'payment' in inspect.signature(payment.payment_provider.order_pending_mail_render).parameters:
d.append(str(payment.payment_provider.order_pending_mail_render(order, payment)))
else:
d.append(str(payment.payment_provider.order_pending_mail_render(order)))
d = [line for line in d if line.strip()]
if d:
return '\n\n'.join(d)
else:
return ''
def get_best_name(position_or_address, parts=False):
"""
Return the best name we got for either an invoice address or an order position, falling back to the respective other
"""
from pretix.base.models import InvoiceAddress, OrderPosition
if isinstance(position_or_address, InvoiceAddress):
if position_or_address.name:
return position_or_address.name_parts if parts else position_or_address.name
elif position_or_address.order:
position_or_address = position_or_address.order.positions.exclude(attendee_name_cached="").exclude(attendee_name_cached__isnull=True).first()
if isinstance(position_or_address, OrderPosition):
if position_or_address.attendee_name:
return position_or_address.attendee_name_parts if parts else position_or_address.attendee_name
elif position_or_address.order:
try:
return position_or_address.order.invoice_address.name_parts if parts else position_or_address.order.invoice_address.name
except InvoiceAddress.DoesNotExist:
pass
return {} if parts else ""
@receiver(register_text_placeholders, dispatch_uid="pretixbase_register_text_placeholders")
def base_placeholders(sender, **kwargs):
from pretix.multidomain.urlreverse import build_absolute_uri
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
),
SimpleFunctionalTextPlaceholder(
'event_slug', ['event'], lambda event: event.slug, lambda event: event.slug
),
SimpleFunctionalTextPlaceholder(
'code', ['order'], lambda order: order.code, 'F8VVL'
),
SimpleFunctionalTextPlaceholder(
'total', ['order'], lambda order: LazyNumber(order.total), lambda event: LazyNumber(Decimal('42.23'))
),
SimpleFunctionalTextPlaceholder(
'currency', ['event'], lambda event: event.currency, lambda event: event.currency
),
SimpleFunctionalTextPlaceholder(
'order_email', ['order'], lambda order: order.email, 'john@example.org'
),
SimpleFunctionalTextPlaceholder(
'invoice_number', ['invoice'],
lambda invoice: invoice.full_invoice_no,
f'{sender.settings.invoice_numbers_prefix or (sender.slug.upper() + "-")}00000'
),
SimpleFunctionalTextPlaceholder(
'refund_amount', ['event_or_subevent', 'refund_amount'],
lambda event_or_subevent, refund_amount: LazyCurrencyNumber(refund_amount, event_or_subevent.currency),
lambda event_or_subevent: LazyCurrencyNumber(Decimal('42.23'), event_or_subevent.currency)
),
SimpleFunctionalTextPlaceholder(
'pending_sum', ['event', 'pending_sum'],
lambda event, pending_sum: LazyCurrencyNumber(pending_sum, event.currency),
lambda event: LazyCurrencyNumber(Decimal('42.23'), event.currency)
),
SimpleFunctionalTextPlaceholder(
'total_with_currency', ['event', 'order'], lambda event, order: LazyCurrencyNumber(order.total,
event.currency),
lambda event: LazyCurrencyNumber(Decimal('42.23'), event.currency)
),
SimpleFunctionalTextPlaceholder(
'expire_date', ['event', 'order'], lambda event, order: LazyExpiresDate(order.expires.astimezone(event.timezone)),
lambda event: LazyDate(now() + timedelta(days=15))
),
SimpleFunctionalTextPlaceholder(
'url', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash()
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.open', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
'hash': '98kusd8ofsj8dnkd'
}
),
),
SimpleFunctionalTextPlaceholder(
'url_info_change', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.modify', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.modify', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
}
),
),
SimpleFunctionalTextPlaceholder(
'url_products_change', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.change', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.change', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
}
),
),
SimpleFunctionalTextPlaceholder(
'url_cancel', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.cancel', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.cancel', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
}
),
),
SimpleFunctionalTextPlaceholder(
'url', ['event', 'position'], lambda event, position: build_absolute_uri(
event,
'presale:event.order.position',
kwargs={
'order': position.order.code,
'secret': position.web_secret,
'position': position.positionid
}
),
lambda event: build_absolute_uri(
event,
'presale:event.order.position', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
'position': '123'
}
),
),
SimpleFunctionalTextPlaceholder(
'order_modification_deadline_date_and_time', ['order', 'event'],
lambda order, event:
date_format(order.modify_deadline.astimezone(event.timezone), 'SHORT_DATETIME_FORMAT')
if order.modify_deadline
else '',
lambda event: date_format(
event.settings.get(
'last_order_modification_date', as_type=RelativeDateWrapper
).datetime(event).astimezone(event.timezone),
'SHORT_DATETIME_FORMAT'
) if event.settings.get('last_order_modification_date') else '',
),
SimpleFunctionalTextPlaceholder(
'event_location', ['event_or_subevent'], lambda event_or_subevent: str(event_or_subevent.location or ''),
lambda event: str(event.location or ''),
),
SimpleFunctionalTextPlaceholder(
'event_admission_time', ['event_or_subevent'],
lambda event_or_subevent:
date_format(event_or_subevent.date_admission.astimezone(event_or_subevent.timezone), 'TIME_FORMAT')
if event_or_subevent.date_admission
else '',
lambda event: date_format(event.date_admission.astimezone(event.timezone), 'TIME_FORMAT') if event.date_admission else '',
),
SimpleFunctionalTextPlaceholder(
'subevent', ['waiting_list_entry', 'event'],
lambda waiting_list_entry, event: str(waiting_list_entry.subevent or event),
lambda event: str(event if not event.has_subevents or not event.subevents.exists() else event.subevents.first())
),
SimpleFunctionalTextPlaceholder(
'subevent_date_from', ['waiting_list_entry', 'event'],
lambda waiting_list_entry, event: (waiting_list_entry.subevent or event).get_date_from_display(),
lambda event: (event if not event.has_subevents or not event.subevents.exists() else event.subevents.first()).get_date_from_display()
),
SimpleFunctionalTextPlaceholder(
'url_remove', ['waiting_list_voucher', 'event'],
lambda waiting_list_voucher, event: build_absolute_uri(
event, 'presale:event.waitinglist.remove'
) + '?voucher=' + waiting_list_voucher.code,
lambda event: build_absolute_uri(
event,
'presale:event.waitinglist.remove',
) + '?voucher=68CYU2H6ZTP3WLK5',
),
SimpleFunctionalTextPlaceholder(
'url', ['waiting_list_voucher', 'event'],
lambda waiting_list_voucher, event: build_absolute_uri(
event, 'presale:event.redeem'
) + '?voucher=' + waiting_list_voucher.code,
lambda event: build_absolute_uri(
event,
'presale:event.redeem',
) + '?voucher=68CYU2H6ZTP3WLK5',
),
SimpleFunctionalTextPlaceholder(
'invoice_name', ['invoice_address'], lambda invoice_address: invoice_address.name or '',
_('John Doe')
),
SimpleFunctionalTextPlaceholder(
'invoice_company', ['invoice_address'], lambda invoice_address: invoice_address.company or '',
_('Sample Corporation')
),
SimpleFunctionalTextPlaceholder(
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
'* {} - {}'.format(
order.full_code,
build_absolute_uri(event, 'presale:event.order.open', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash(),
}),
)
for order in orders
), lambda event: '\n' + '\n\n'.join(
'* {} - {}'.format(
'{}-{}'.format(event.slug.upper(), order['code']),
build_absolute_uri(event, 'presale:event.order.open', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'order': order['code'],
'secret': order['secret'],
'hash': order['hash'],
}),
)
for order in [
{'code': 'F8VVL', 'secret': '6zzjnumtsx136ddy', 'hash': 'abcdefghi'},
{'code': 'HIDHK', 'secret': '98kusd8ofsj8dnkd', 'hash': 'jklmnopqr'},
{'code': 'OPKSB', 'secret': '09pjdksflosk3njd', 'hash': 'stuvwxy2z'}
]
),
),
SimpleFunctionalTextPlaceholder(
'hours', ['event', 'waiting_list_entry'], lambda event, waiting_list_entry:
event.settings.waiting_list_hours,
lambda event: event.settings.waiting_list_hours
),
SimpleFunctionalTextPlaceholder(
'product', ['waiting_list_entry'], lambda waiting_list_entry: waiting_list_entry.item.name,
_('Sample Admission Ticket')
),
SimpleFunctionalTextPlaceholder(
'code', ['waiting_list_voucher'], lambda waiting_list_voucher: waiting_list_voucher.code,
'68CYU2H6ZTP3WLK5'
),
SimpleFunctionalTextPlaceholder(
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
'voucher_list', ['voucher_list'], lambda voucher_list: ' \n'.join(voucher_list),
' 68CYU2H6ZTP3WLK5\n 7MB94KKPVEPSMVF2'
),
SimpleFunctionalTextPlaceholder(
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
'voucher_url_list', ['event', 'voucher_list'],
lambda event, voucher_list: ' \n'.join([
build_absolute_uri(
event, 'presale:event.redeem'
) + '?voucher=' + c
for c in voucher_list
]),
lambda event: ' \n'.join([
build_absolute_uri(
event, 'presale:event.redeem'
) + '?voucher=' + c
for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2']
]),
),
SimpleFunctionalTextPlaceholder(
'url', ['event', 'voucher_list'], lambda event, voucher_list: build_absolute_uri(event, 'presale:event.index', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
}), lambda event: build_absolute_uri(event, 'presale:event.index', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
})
),
SimpleFunctionalTextPlaceholder(
'name', ['name'], lambda name: name,
_('John Doe')
),
SimpleFunctionalTextPlaceholder(
'comment', ['comment'], lambda comment: comment,
_('An individual text with a reason can be inserted here.'),
),
SimpleFunctionalTextPlaceholder(
'payment_info', ['order', 'payments'], _placeholder_payments,
_('The amount has been charged to your card.'),
),
SimpleFunctionalTextPlaceholder(
'payment_info', ['payment_info'], lambda payment_info: payment_info,
_('Please transfer money to this bank account: 9999-9999-9999-9999'),
),
SimpleFunctionalTextPlaceholder(
'attendee_name', ['position'], lambda position: position.attendee_name,
_('John Doe'),
),
SimpleFunctionalTextPlaceholder(
'positionid', ['position'], lambda position: str(position.positionid),
'1'
),
SimpleFunctionalTextPlaceholder(
'name', ['position_or_address'],
get_best_name,
_('John Doe'),
),
]
name_scheme = PERSON_NAME_SCHEMES[sender.settings.name_scheme]
if "concatenation_for_salutation" in name_scheme:
concatenation_for_salutation = name_scheme["concatenation_for_salutation"]
else:
concatenation_for_salutation = name_scheme["concatenation"]
ph.append(SimpleFunctionalTextPlaceholder(
"name_for_salutation", ["waiting_list_entry"],
lambda waiting_list_entry: concatenation_for_salutation(waiting_list_entry.name_parts),
_("Mr Doe"),
))
ph.append(SimpleFunctionalTextPlaceholder(
"name", ["waiting_list_entry"],
lambda waiting_list_entry: waiting_list_entry.name or "",
_("Mr Doe"),
))
ph.append(SimpleFunctionalTextPlaceholder(
"name_for_salutation", ["position_or_address"],
lambda position_or_address: concatenation_for_salutation(get_best_name(position_or_address, parts=True)),
_("Mr Doe"),
))
for f, l, w in name_scheme['fields']:
if f == 'full_name':
continue
ph.append(SimpleFunctionalTextPlaceholder(
'name_%s' % f, ['waiting_list_entry'], lambda waiting_list_entry, f=f: get_name_parts_localized(waiting_list_entry.name_parts, f),
name_scheme['sample'][f]
))
ph.append(SimpleFunctionalTextPlaceholder(
'attendee_name_%s' % f, ['position'], lambda position, f=f: get_name_parts_localized(position.attendee_name_parts, f),
name_scheme['sample'][f]
))
ph.append(SimpleFunctionalTextPlaceholder(
'name_%s' % f, ['position_or_address'],
lambda position_or_address, f=f: get_name_parts_localized(get_best_name(position_or_address, parts=True), f),
name_scheme['sample'][f]
))
for k, v in sender.meta_data.items():
ph.append(SimpleFunctionalTextPlaceholder(
'meta_%s' % k, ['event'], lambda event, k=k: event.meta_data[k],
v
))
ph.append(SimpleFunctionalTextPlaceholder(
'meta_%s' % k, ['event_or_subevent'], lambda event_or_subevent, k=k: event_or_subevent.meta_data[k],
v
))
return ph
class FormPlaceholderMixin:
def _set_field_placeholders(self, fn, base_parameters):
placeholders = get_available_placeholders(self.event, base_parameters)
ht = format_placeholders_help_text(placeholders, self.event)
if self.fields[fn].help_text:
self.fields[fn].help_text += ' ' + str(ht)
else:
self.fields[fn].help_text = ht
self.fields[fn].validators.append(
PlaceholderValidator(['{%s}' % p for p in placeholders.keys()])
)
def get_available_placeholders(event, base_parameters):
if 'order' in base_parameters:
base_parameters.append('invoice_address')
base_parameters.append('position_or_address')
params = {}
for r, val in [*register_mail_placeholders.send(sender=event), *register_text_placeholders.send(sender=event)]:
if not isinstance(val, (list, tuple)):
val = [val]
for v in val:
if all(rp in base_parameters for rp in v.required_context):
params[v.identifier] = v
return params
+7 -2
View File
@@ -112,7 +112,7 @@ def dictsum(*dicts) -> dict:
def order_overview(
event: Event, subevent: SubEvent=None, date_filter='', date_from=None, date_until=None, fees=False,
admission_only=False, base_qs=None, base_fees_qs=None,
admission_only=False, base_qs=None, base_fees_qs=None, subevent_date_from=None, subevent_date_until=None
) -> Tuple[List[Tuple[ItemCategory, List[Item]]], Dict[str, Tuple[Decimal, Decimal]]]:
items = event.items.all().select_related(
'category', # for re-grouping
@@ -125,6 +125,11 @@ def order_overview(
qs = qs.filter(subevent__in=subevent)
elif subevent:
qs = qs.filter(subevent=subevent)
if subevent_date_from:
qs = qs.filter(subevent__date_from__gte=subevent_date_from)
if subevent_date_until:
qs = qs.filter(subevent__date_from__lt=subevent_date_until)
if admission_only:
qs = qs.filter(item__admission=True)
items = items.filter(admission=True)
@@ -232,7 +237,7 @@ def order_overview(
payment_cat_obj.name = _('Fees')
payment_items = []
if subevent is None and fees:
if subevent is None and not subevent_date_from and not subevent_date_until and fees:
qs = OrderFee.all if base_fees_qs is None else base_fees_qs
qs = qs.filter(
order__event=event
+3 -6
View File
@@ -34,7 +34,7 @@ from pretix.base.models import (
)
from pretix.base.services.tasks import EventTask, ProfiledTask
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import allow_ticket_download, register_ticket_outputs
from pretix.base.signals import register_ticket_outputs
from pretix.celery_app import app
from pretix.helpers.database import rolledback_transaction
@@ -124,8 +124,8 @@ def preview(event: int, provider: str):
def get_tickets_for_order(order, base_position=None):
can_download = all([r for rr, r in allow_ticket_download.send(order.event, order=order)])
if not can_download:
positions = list(order.positions_with_tickets)
if not positions:
return []
if not order.ticket_download_available:
return []
@@ -135,10 +135,8 @@ def get_tickets_for_order(order, base_position=None):
for receiver, response
in register_ticket_outputs.send(order.event)
]
tickets = []
positions = list(order.positions_with_tickets)
if base_position:
# Only the given position and its children
positions = [
@@ -202,7 +200,6 @@ def get_tickets_for_order(order, base_position=None):
))
except:
logger.exception('Failed to generate ticket.')
return tickets
+55 -2
View File
@@ -89,7 +89,7 @@ def primary_font_kwargs():
choices = [('Open Sans', 'Open Sans')]
choices += sorted([
(a, {"title": a, "data": v}) for a, v in get_fonts().items() if not v.get('pdf_only', False)
(a, {"title": a, "data": v}) for a, v in get_fonts(pdf_support_required=False).items()
], key=lambda a: a[0])
return {
'choices': choices,
@@ -306,9 +306,11 @@ DEFAULTS = {
'serializer_class': serializers.IntegerField,
'serializer_kwargs': dict(
min_value=1,
max_value=settings.PRETIX_MAX_ORDER_SIZE,
),
'form_kwargs': dict(
min_value=1,
max_value=settings.PRETIX_MAX_ORDER_SIZE,
required=True,
label=_("Maximum number of items per order"),
help_text=_("Add-on products will not be counted.")
@@ -1936,6 +1938,32 @@ DEFAULTS = {
label=_("Do not allow cancellations after"),
)
},
'cancel_terms_paid': {
'default': None,
'type': LazyI18nString,
'serializer_class': I18nField,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Terms of cancellation"),
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown when cancellation is allowed for a paid order. Leave empty if you "
"want pretix to automatically generate the terms of cancellation based on your settings.")
)
},
'cancel_terms_unpaid': {
'default': None,
'type': LazyI18nString,
'serializer_class': I18nField,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Terms of cancellation"),
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown when cancellation is allowed for an unpaid or free order. Leave empty "
"if you want pretix to automatically generate the terms of cancellation based on your settings.")
)
},
'contact_mail': {
'default': None,
'type': str,
@@ -2884,6 +2912,25 @@ Your {organizer} team""")) # noqa: W291
label=_('Use header image also for events without an individually uploaded logo'),
)
},
'favicon': {
'default': None,
'type': File,
'form_class': ExtFileField,
'form_kwargs': dict(
label=_('Favicon'),
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_FAVICON,
max_size=settings.FILE_UPLOAD_MAX_SIZE_FAVICON,
help_text=_('If you provide a favicon, we will show it instead of the default pretix icon. '
'We recommend a size of at least 200x200px to accommodate most devices.')
),
'serializer_class': UploadedFileField,
'serializer_kwargs': dict(
allowed_types=[
'image/png', 'image/jpeg', 'image/gif', 'image/x-icon', 'image/vnd.microsoft.icon',
],
max_size=settings.FILE_UPLOAD_MAX_SIZE_FAVICON,
)
},
'og_image': {
'default': None,
'type': File,
@@ -3182,6 +3229,12 @@ Your {organizer} team""")) # noqa: W291
label=_('Length of gift card codes'),
help_text=_('The system generates by default {}-character long gift card codes. However, if a different length '
'is required, it can be set here.'.format(settings.ENTROPY['giftcard_secret'])),
min_value=6,
max_value=64,
),
'serializer_kwargs': dict(
min_value=6,
max_value=64,
)
},
'giftcard_expiry_years': {
@@ -3610,7 +3663,7 @@ COUNTRIES_WITH_STATE_IN_ADDRESS = {
'BR': (['State'], 'short'),
'CA': (['Province', 'Territory'], 'short'),
# 'CN': (['Province', 'Autonomous region', 'Munincipality'], 'long'),
'MY': (['State'], 'long'),
'MY': (['State', 'Federal territory'], 'long'),
'MX': (['State', 'Federal district'], 'short'),
'US': (['State', 'Outlying area', 'District'], 'short'),
}
+39 -11
View File
@@ -153,19 +153,47 @@ class BaseDataShredder:
def shred_log_fields(logentry, banlist=None, whitelist=None):
def _shred(d, banlist, whitelist):
shredded = False
if whitelist:
for k, v in d.items():
if k not in whitelist:
if isinstance(d[k], list):
newlist = []
for i in d[k]:
if isinstance(i, dict):
_shred(i, None, [None])
else:
i = ''
newlist.append(i)
d[k] = newlist
elif isinstance(d[k], dict):
_shred(d[k], None, [None])
elif d[k]:
d[k] = ''
shredded = True
elif banlist:
for k in banlist:
if k in d:
if isinstance(d[k], list):
newlist = []
for i in d[k]:
if isinstance(i, dict):
_shred(i, None, [None])
else:
i = ''
newlist.append(i)
d[k] = newlist
elif isinstance(d[k], dict):
_shred(d[k], None, [None])
elif d[k]:
d[k] = ''
shredded = True
return shredded
d = logentry.parsed_data
initial_data = copy.copy(d)
shredded = False
if whitelist:
for k, v in d.items():
if k not in whitelist:
d[k] = ''
shredded = True
elif banlist:
for f in banlist:
if f in d:
d[f] = ''
shredded = True
shredded = _shred(d, banlist, whitelist)
if d != initial_data:
logentry.data = json.dumps(d)
logentry.shredded = logentry.shredded or shredded
+19 -4
View File
@@ -220,14 +220,20 @@ subclass of pretix.base.payment.BasePaymentProvider or a list of these
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
register_mail_placeholders = EventPluginSignal()
register_text_placeholders = EventPluginSignal()
"""
This signal is sent out to get all known email text placeholders. Receivers should return
an instance of a subclass of pretix.base.email.BaseMailTextPlaceholder or a list of these.
This signal is sent out to get all known text placeholders. Receivers should return
an instance of a subclass of pretix.base.services.placeholders.BaseTextPlaceholder or a
list of these.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
register_mail_placeholders = EventPluginSignal()
"""
**DEPRECATED**: This signal has a new name, please use ``register_text_placeholders`` instead.
"""
register_html_mail_renderers = EventPluginSignal()
"""
This signal is sent out to get all known HTML email renderers. Receivers should return a
@@ -279,6 +285,15 @@ however for this signal, the ``sender`` **may also be None** to allow creating t
notification settings!
"""
notification = EventPluginSignal()
"""
Arguments: ``logentry_id``, ``notification_type``
This signal is sent out when a notification is sent.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
register_sales_channels = django.dispatch.Signal()
"""
This signal is sent out to get all known sales channels types. Receivers should return an
@@ -637,7 +652,7 @@ allow_ticket_download = EventPluginSignal()
Arguments: ``order``
This signal is sent out to check if tickets for an order can be downloaded. If any receiver returns false,
a download will not be offered.
a download will not be offered. If a receiver returns a list of OrderPositions, only those will be downloadable.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
@@ -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
register = template.Library()
@register.filter("iter_weekdays")
def iter_weekdays(weeks):
for i in range(7):
for w in weeks:
if i <= len(w) and w[i]:
yield w[i]['date']
break
+10 -14
View File
@@ -48,11 +48,18 @@ def money_filter(value: Decimal, arg='', hide_currency=False):
places = settings.CURRENCY_PLACES.get(arg, 2)
rounded = value.quantize(Decimal('1') / 10 ** places, ROUND_HALF_UP)
if places < 2 and rounded != value:
places = 2
# We display decimal places even if we shouldn't for this currency if rounding
# 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)
else:
return '{} {}'.format(arg, floatformat(value, 2))
if hide_currency:
return floatformat(value, places)
locale_parts = translation.get_language().split('-', 1)
locale_parts = translation.get_language().split("-", 1)
locale = locale_parts[0]
if len(locale_parts) > 1 and len(locale_parts[1]) == 2:
try:
@@ -61,20 +68,9 @@ def money_filter(value: Decimal, arg='', hide_currency=False):
pass
try:
if rounded != value:
# We display decimal places even if we shouldn't for this currency if rounding
# would make the numbers incorrect. If this branch executes, it's likely a bug in
# pretix, but we won't show wrong numbers!
return '{} {}'.format(
arg,
floatformat(value, 2)
)
return format_currency(value, arg, locale=locale)
except:
return '{} {}'.format(
arg,
floatformat(value, places)
)
return '{} {}'.format(arg, floatformat(value, places))
@register.filter("money_numberfield")
@@ -65,6 +65,8 @@ ALLOWED_TAGS_SNIPPET = [
'i',
'strong',
'span',
'strike',
's',
# Update doc/user/markdown.rst if you change this!
]
ALLOWED_TAGS = ALLOWED_TAGS_SNIPPET + [
+4 -1
View File
@@ -96,6 +96,9 @@ class BaseTicketOutput:
"""
raise NotImplementedError()
def get_tickets_to_print(self, order):
return order.positions_with_tickets
def generate_order(self, order: Order) -> Tuple[str, str, str]:
"""
This method is the same as order() but should not generate one file per order position
@@ -116,7 +119,7 @@ class BaseTicketOutput:
"""
with tempfile.TemporaryDirectory() as d:
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
for pos in order.positions_with_tickets:
for pos in self.get_tickets_to_print(order):
fname, __, content = self.generate(pos)
zipf.writestr('{}-{}{}'.format(
order.code, pos.positionid, os.path.splitext(fname)[1]
+4 -2
View File
@@ -24,6 +24,7 @@ from datetime import datetime, time, timedelta
from django.db.models import Q
from django.urls import reverse
from django.utils.text import format_lazy
from django.utils.timezone import make_aware
from django.utils.translation import pgettext_lazy
@@ -89,9 +90,10 @@ def timeline_for_event(event, subevent=None):
datetime=(
ev.presale_end or ev.date_to or ev.date_from.astimezone(ev.timezone).replace(hour=23, minute=59, second=59)
),
description='{}{}'.format(
description=format_lazy(
'{} ({})',
pgettext_lazy('timeline', 'End of ticket sales'),
f" ({pgettext_lazy('timeline', 'automatically because the event is over and no end of presale has been configured')})" if not ev.presale_end else ""
pgettext_lazy('timeline', 'automatically because the event is over and no end of presale has been configured') if not ev.presale_end else ""
),
edit_url=ev_edit_url
))
+2
View File
@@ -72,6 +72,7 @@ class EventSlugBanlistValidator(BanlistValidator):
'widget',
'customer',
'account',
'lead',
]
@@ -92,6 +93,7 @@ class OrganizerSlugBanlistValidator(BanlistValidator):
'api',
'csp_report',
'widget',
'lead',
]
+7 -6
View File
@@ -34,7 +34,7 @@ from django.core.exceptions import PermissionDenied, ValidationError
from django.core.files.uploadedfile import UploadedFile
from django.db import transaction
from django.http import HttpResponse, JsonResponse, QueryDict
from django.shortcuts import redirect, render
from django.shortcuts import render
from django.test import RequestFactory
from django.utils import timezone, translation
from django.utils.datastructures import MultiValueDict
@@ -47,6 +47,7 @@ from redis import ResponseError
from pretix.base.models import CachedFile, User
from pretix.base.services.tasks import ProfiledEventTask
from pretix.celery_app import app
from pretix.helpers.http import redirect_to_url
logger = logging.getLogger('pretix.base.tasks')
@@ -152,7 +153,7 @@ class AsyncMixin:
'redirect': self.get_success_url(value),
'message': str(self.get_success_message(value))
})
return redirect(self.get_success_url(value))
return redirect_to_url(self.get_success_url(value))
def error(self, exception):
if isinstance(exception, PermissionDenied):
@@ -165,7 +166,7 @@ class AsyncMixin:
'redirect': self.get_error_url(),
'message': str(self.get_error_message(exception))
})
return redirect(self.get_error_url())
return redirect_to_url(self.get_error_url())
def get_error_message(self, exception):
if isinstance(exception, dict) and exception['exc_type'] in self.known_errortypes:
@@ -203,7 +204,7 @@ class AsyncAction(AsyncMixin):
return self.success(res.info)
else:
return self.error(res.info)
return redirect(self.get_check_url(res.id, False))
return redirect_to_url(self.get_check_url(res.id, False))
def get(self, request, *args, **kwargs):
if 'async_id' in request.GET and settings.HAS_CELERY:
@@ -375,7 +376,7 @@ class AsyncFormView(AsyncMixin, FormView):
return self.success(res.info)
else:
return self.error(res.info)
return redirect(self.get_check_url(res.id, False))
return redirect_to_url(self.get_check_url(res.id, False))
class AsyncPostView(AsyncMixin, View):
@@ -478,4 +479,4 @@ class AsyncPostView(AsyncMixin, View):
return self.success(res.info)
else:
return self.error(res.info)
return redirect(self.get_check_url(res.id, False))
return redirect_to_url(self.get_check_url(res.id, False))
+14
View File
@@ -415,3 +415,17 @@ class FontSelect(forms.RadioSelect):
class ItemMultipleChoiceField(SafeModelMultipleChoiceField):
def label_from_instance(self, obj):
return str(obj) if obj.active else mark_safe(f'<strike class="text-muted">{escape(obj)}</strike>')
class ButtonGroupRadioSelect(forms.RadioSelect):
template_name = 'pretixcontrol/button_group_radio.html'
option_template_name = 'pretixcontrol/button_group_radio_option.html'
def __init__(self, *args, **kwargs):
self.option_icons = kwargs.pop('option_icons')
super().__init__(*args, **kwargs)
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
attrs['icon'] = self.option_icons[value]
opt = super().create_option(name, value, label, selected, index, subindex, attrs)
return opt
+19 -15
View File
@@ -60,12 +60,11 @@ from i18nfield.forms import (
from pytz import common_timezones
from pretix.base.channels import get_all_sales_channels
from pretix.base.email import get_available_placeholders
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
from pretix.base.forms.widgets import format_placeholders_help_text
from pretix.base.models import Event, Organizer, TaxRule, Team
from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from pretix.base.services.placeholders import FormPlaceholderMixin
from pretix.base.settings import (
COUNTRIES_WITH_STATE_IN_ADDRESS, DEFAULTS, PERSON_NAME_SCHEMES,
PERSON_NAME_TITLE_GROUPS, validate_event_settings,
@@ -80,6 +79,7 @@ from pretix.helpers.countries import CachedCountries
from pretix.multidomain.models import KnownDomain
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.plugins.banktransfer.payment import BankTransfer
from pretix.presale.style import get_fonts
class EventWizardFoundationForm(forms.Form):
@@ -503,7 +503,7 @@ class EventSettingsValidationMixin:
del self.cleaned_data[field]
class EventSettingsForm(EventSettingsValidationMixin, SettingsForm):
class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, SettingsForm):
timezone = forms.ChoiceField(
choices=((a, a) for a in common_timezones),
label=_("Event timezone"),
@@ -593,6 +593,10 @@ class EventSettingsForm(EventSettingsValidationMixin, SettingsForm):
'og_image',
]
base_context = {
'frontpage_text': ['event'],
}
def _resolve_virtual_keys_input(self, data, prefix=''):
# set all dependants of virtual_keys and
# delete all virtual_fields to prevent them from being saved
@@ -648,6 +652,9 @@ class EventSettingsForm(EventSettingsValidationMixin, SettingsForm):
del self.fields['event_list_available_only']
del self.fields['event_list_filters']
del self.fields['event_calendar_future_only']
self.fields['primary_font'].choices += [
(a, {"title": a, "data": v}) for a, v in get_fonts(self.event, pdf_support_required=False).items()
]
# create "virtual" fields for better UX when editing <name>_asked and <name>_required fields
self.virtual_keys = []
@@ -682,6 +689,9 @@ class EventSettingsForm(EventSettingsValidationMixin, SettingsForm):
else:
self.initial[virtual_key] = 'do_not_ask'
for k, v in self.base_context.items():
self._set_field_placeholders(k, v)
@cached_property
def changed_data(self):
data = []
@@ -730,6 +740,8 @@ class CancelSettingsForm(SettingsForm):
'cancel_allow_user_paid_refund_as_giftcard',
'cancel_allow_user_paid_require_approval',
'cancel_allow_user_paid_require_approval_fee_unknown',
'cancel_terms_paid',
'cancel_terms_unpaid',
'change_allow_user_variation',
'change_allow_user_price',
'change_allow_user_until',
@@ -923,6 +935,9 @@ class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm):
)
)
self.fields['invoice_generate'].choices = generate_choices
self.fields['invoice_renderer_font'].choices += [
(a, a) for a in get_fonts(event, pdf_support_required=True).keys()
]
def contains_web_channel_validate(val):
@@ -930,7 +945,7 @@ def contains_web_channel_validate(val):
raise ValidationError(_("The online shop must be selected to receive these emails."))
class MailSettingsForm(SettingsForm):
class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
auto_fields = [
'mail_prefix',
'mail_from_name',
@@ -1342,17 +1357,6 @@ class MailSettingsForm(SettingsForm):
'mail_attach_ical_description': ['event', 'event_or_subevent'],
}
def _set_field_placeholders(self, fn, base_parameters):
placeholders = get_available_placeholders(self.event, base_parameters)
ht = format_placeholders_help_text(placeholders, self.event)
if self.fields[fn].help_text:
self.fields[fn].help_text += ' ' + str(ht)
else:
self.fields[fn].help_text = ht
self.fields[fn].validators.append(
PlaceholderValidator(['{%s}' % p for p in placeholders.keys()])
)
def __init__(self, *args, **kwargs):
self.event = event = kwargs.get('obj')
super().__init__(*args, **kwargs)
+34 -5
View File
@@ -57,7 +57,7 @@ from pretix.base.forms.widgets import (
from pretix.base.models import (
Checkin, CheckinList, Device, Event, EventMetaProperty, EventMetaValue,
Gate, Invoice, InvoiceAddress, Item, Order, OrderPayment, OrderPosition,
OrderRefund, Organizer, Question, QuestionAnswer, SubEvent,
OrderRefund, Organizer, Question, QuestionAnswer, Quota, SubEvent,
SubEventMetaValue, Team, TeamAPIToken, TeamInvite, Voucher,
)
from pretix.base.signals import register_payment_providers
@@ -230,6 +230,7 @@ class OrderFilterForm(FilterForm):
('partially_paid', _('Partially paid')),
('underpaid', _('Underpaid (but confirmed)')),
('pendingpaid', _('Pending (but fully paid)')),
('pendingnopayment', _('Pending (but no current payment)')),
)),
(_('Approval process'), (
('na', _('Approved, payment pending')),
@@ -327,6 +328,18 @@ class OrderFilterForm(FilterForm):
Q(status__in=(Order.STATUS_EXPIRED, Order.STATUS_PENDING)) & Q(pending_sum_t__lte=0)
& Q(require_approval=False)
)
elif s == 'pendingnopayment':
qs = qs.exclude(
Exists(
OrderPayment.objects.filter(
order=OuterRef('pk'),
state__in=(OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING)
)
)
).filter(
status=Order.STATUS_PENDING,
require_approval=False,
)
elif s == 'partially_paid':
qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True)
qs = qs.filter(
@@ -578,11 +591,10 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
widget=FilterNullBooleanSelect,
label=_('At least one ticket with check-in'),
)
checkin_attention = forms.NullBooleanField(
quota = SafeModelChoiceField(
queryset=Quota.objects.none(),
label=_('Affected quota'),
required=False,
widget=FilterNullBooleanSelect,
label=_('Requires special attention'),
help_text=_('Only matches orders with the attention checkbox set directly for the order, not based on the product.'),
)
def __init__(self, *args, **kwargs):
@@ -667,6 +679,17 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
label=_('Ticket secret'),
required=False
)
self.fields['quota'].queryset = self.event.quotas.all()
self.fields['quota'].widget = Select2(
attrs={
'data-model-select2': 'generic',
'data-select2-url': reverse('control:event.items.quotas.select2', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
}),
}
)
self.fields['quota'].widget.choices = self.fields['quota'].choices
for q in self.event.questions.all():
self.fields['question_{}'.format(q.pk)] = forms.CharField(
label=q.question,
@@ -760,6 +783,12 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
qs = qs.filter(
all_positions__secret__icontains=fdata.get('ticket_secret')
).distinct()
if fdata.get('quota'):
quota = fdata['quota']
qs = qs.filter(
Q(all_positions__item__in=quota.items.all(), all_positions__variation__isnull=True) |
Q(all_positions__variation__in=quota.variations.all())
).distinct()
for q in self.event.questions.all():
if fdata.get(f'question_{q.pk}'):
answers = QuestionAnswer.objects.filter(
+103 -11
View File
@@ -64,10 +64,10 @@ from pretix.base.models import (
from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
from pretix.base.signals import item_copy_data
from pretix.control.forms import (
ItemMultipleChoiceField, SizeValidationMixin, SplitDateTimeField,
SplitDateTimePickerWidget,
ButtonGroupRadioSelect, ItemMultipleChoiceField, SizeValidationMixin,
SplitDateTimeField, SplitDateTimePickerWidget,
)
from pretix.control.forms.widgets import Select2
from pretix.control.forms.widgets import Select2, Select2ItemVarMulti
from pretix.helpers.models import modelcopy
from pretix.helpers.money import change_decimal_field
@@ -133,6 +133,14 @@ class QuestionForm(I18nModelForm):
return val
def clean_show_during_checkin(self):
val = self.cleaned_data.get('show_during_checkin')
if val and self.cleaned_data.get('type') in Question.SHOW_DURING_CHECKIN_UNSUPPORTED:
raise ValidationError(_('This type of question cannot be shown during check-in.'))
return val
def clean_identifier(self):
val = self.cleaned_data.get('identifier')
Question._clean_identifier(self.instance.event, val, self.instance)
@@ -155,6 +163,7 @@ class QuestionForm(I18nModelForm):
'type',
'required',
'ask_during_checkin',
'show_during_checkin',
'hidden',
'identifier',
'items',
@@ -198,14 +207,20 @@ class QuestionOptionForm(I18nModelForm):
class QuotaForm(I18nModelForm):
itemvars = forms.MultipleChoiceField(
label=_("Products"),
required=True,
)
def __init__(self, **kwargs):
self.instance = kwargs.get('instance', None)
self.event = kwargs.get('event')
items = kwargs.pop('items', None) or self.event.items.prefetch_related('variations')
searchable_selection = kwargs.pop('searchable_selection', None)
self.original_instance = modelcopy(self.instance) if self.instance else None
initial = kwargs.get('initial', {})
if self.instance and self.instance.pk and 'itemvars' not in initial:
initial['itemvars'] = [str(i.pk) for i in self.instance.items.all()] + [
initial['itemvars'] = [str(i.pk) for i in self.instance.items.all() if (len(i.variations.all()) == 0)] + [
'{}-{}'.format(v.item_id, v.pk) for v in self.instance.variations.all()
]
kwargs['initial'] = initial
@@ -222,12 +237,22 @@ class QuotaForm(I18nModelForm):
else:
choices.append(('{}'.format(item.pk), str(item) if item.active else mark_safe(f'<strike class="text-muted">{escape(item)}</strike>')))
self.fields['itemvars'] = forms.MultipleChoiceField(
label=_('Products'),
required=True,
choices=choices,
widget=forms.CheckboxSelectMultiple
)
if searchable_selection:
self.fields['itemvars'].widget = Select2ItemVarMulti(
attrs={
'data-model-select2': 'generic',
'data-select2-url': reverse('control:event.items.itemvars.select2', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
}),
'data-placeholder': _('No products')
},
choices=choices,
)
else:
self.fields['itemvars'].widget = forms.CheckboxSelectMultiple()
self.fields['itemvars'].choices = choices
if self.event.has_subevents:
self.fields['subevent'].queryset = self.event.subevents.all()
@@ -371,7 +396,9 @@ class ItemCreateForm(I18nModelForm):
'description',
'active',
'available_from',
'available_from_mode',
'available_until',
'available_until_mode',
'require_voucher',
'hide_without_voucher',
'allow_cancel',
@@ -379,6 +406,7 @@ class ItemCreateForm(I18nModelForm):
'max_per_order',
'generate_tickets',
'checkin_attention',
'checkin_text',
'free_price',
'original_price',
'sales_channels',
@@ -552,6 +580,34 @@ class ItemUpdateForm(I18nModelForm):
)
change_decimal_field(self.fields['default_price'], self.event.currency)
self.fields['available_from_mode'].widget = ButtonGroupRadioSelect(
choices=self.fields['available_from_mode'].choices,
option_icons={
Item.UNAVAIL_MODE_HIDDEN: 'eye-slash',
Item.UNAVAIL_MODE_INFO: 'info'
}
)
self.fields['available_until_mode'].widget = ButtonGroupRadioSelect(
choices=self.fields['available_until_mode'].choices,
option_icons={
Item.UNAVAIL_MODE_HIDDEN: 'eye-slash',
Item.UNAVAIL_MODE_INFO: 'info'
}
)
self.fields['hide_without_voucher'].widget = ButtonGroupRadioSelect(
choices=(
(True, _("Hide product if unavailable")),
(False, _("Show product with info on why its unavailable")),
),
option_icons={
True: 'eye-slash',
False: 'info'
},
attrs={'data-checkbox-dependency': '#id_require_voucher'}
)
if self.instance.hidden_if_available_id:
self.fields['hidden_if_available'].queryset = self.event.quotas.all()
self.fields['hidden_if_available'].help_text = format_html(
@@ -604,6 +660,15 @@ class ItemUpdateForm(I18nModelForm):
self.fields['free_price_suggestion'].widget.attrs['data-display-dependency'] = '#id_free_price'
self.fields['validity_dynamic_start_choice'] = forms.TypedChoiceField(
label=_("Start of validity"),
choices=(
("False", _("Purchase date")),
("True", _("Date chosen by customer")),
),
coerce=lambda x: x == 'True',
)
qs = self.event.organizer.membership_types.all()
if qs:
self.fields['require_membership_types'].queryset = qs
@@ -637,6 +702,9 @@ class ItemUpdateForm(I18nModelForm):
)
)
if not d.get('require_voucher'):
d['hide_without_voucher'] = False
if d.get('require_membership') and not d.get('require_membership_types'):
self.add_error(
'require_membership_types',
@@ -651,7 +719,7 @@ class ItemUpdateForm(I18nModelForm):
if d.get('grant_membership_type'):
if not d['grant_membership_type'].transferable and not d['personalized']:
self.add_error(
'personalized' if d['admission'] else 'admission',
'personalized' if d.get('admission') else 'admission',
_("Your product grants a non-transferable membership and should therefore be a personalized "
"admission ticket. Otherwise customers might not be able to use the membership later. If you "
"want the membership to be non-personalized, set the membership type to be transferable.")
@@ -694,7 +762,9 @@ class ItemUpdateForm(I18nModelForm):
'free_price_suggestion',
'tax_rule',
'available_from',
'available_from_mode',
'available_until',
'available_until_mode',
'require_voucher',
'require_approval',
'hide_without_voucher',
@@ -703,6 +773,7 @@ class ItemUpdateForm(I18nModelForm):
'max_per_order',
'min_per_order',
'checkin_attention',
'checkin_text',
'generate_tickets',
'original_price',
'require_bundling',
@@ -751,6 +822,7 @@ class ItemUpdateForm(I18nModelForm):
'show_quota_left': ShowQuotaNullBooleanSelect(),
'max_per_order': forms.widgets.NumberInput(attrs={'min': 0}),
'min_per_order': forms.widgets.NumberInput(attrs={'min': 0}),
'checkin_text': forms.TextInput(),
}
@@ -829,6 +901,22 @@ class ItemVariationForm(I18nModelForm):
self.fields['free_price_suggestion'].widget.attrs['data-display-dependency'] = '#id_free_price'
self.fields['available_from_mode'].widget = ButtonGroupRadioSelect(
choices=self.fields['available_from_mode'].choices,
option_icons={
Item.UNAVAIL_MODE_HIDDEN: 'eye-slash',
Item.UNAVAIL_MODE_INFO: 'info'
}
)
self.fields['available_until_mode'].widget = ButtonGroupRadioSelect(
choices=self.fields['available_until_mode'].choices,
option_icons={
Item.UNAVAIL_MODE_HIDDEN: 'eye-slash',
Item.UNAVAIL_MODE_INFO: 'info'
}
)
self.meta_fields = []
meta_defaults = {}
if self.instance.pk:
@@ -869,8 +957,11 @@ class ItemVariationForm(I18nModelForm):
'require_membership_hidden',
'require_membership_types',
'checkin_attention',
'checkin_text',
'available_from',
'available_from_mode',
'available_until',
'available_until_mode',
'sales_channels',
'hide_without_voucher',
]
@@ -884,6 +975,7 @@ class ItemVariationForm(I18nModelForm):
'require_membership_types': forms.CheckboxSelectMultiple(attrs={
'class': 'scrolling-multiple-choice'
}),
'checkin_text': forms.TextInput(),
}
def clean(self):
+10 -19
View File
@@ -61,6 +61,7 @@ from pretix.base.models import (
TaxRule,
)
from pretix.base.models.event import SubEvent
from pretix.base.services.placeholders import FormPlaceholderMixin
from pretix.base.services.pricing import get_price
from pretix.control.forms import SplitDateTimeField
from pretix.control.forms.widgets import Select2
@@ -151,10 +152,6 @@ class ForceQuotaConfirmationForm(forms.Form):
del self.fields['force']
class ConfirmPaymentForm(ForceQuotaConfirmationForm):
pass
class ReactivateOrderForm(ForceQuotaConfirmationForm):
pass
@@ -220,10 +217,11 @@ class DenyForm(forms.Form):
)
class MarkPaidForm(ConfirmPaymentForm):
class MarkPaidForm(ForceQuotaConfirmationForm):
send_email = forms.BooleanField(
required=False,
label=_('Notify customer by email'),
help_text=_('A mail will only be sent if the order is fully paid after this.'),
initial=True
)
amount = forms.DecimalField(
@@ -240,9 +238,10 @@ class MarkPaidForm(ConfirmPaymentForm):
)
def __init__(self, *args, **kwargs):
payment = kwargs.pop('payment', None)
super().__init__(*args, **kwargs)
change_decimal_field(self.fields['amount'], self.instance.event.currency)
self.fields['amount'].initial = max(Decimal('0.00'), self.instance.pending_sum)
self.fields['amount'].initial = max(Decimal('0.00'), payment.amount if payment else self.instance.pending_sum)
class ExporterForm(forms.Form):
@@ -265,12 +264,13 @@ class ExporterForm(forms.Form):
class CommentForm(I18nModelForm):
class Meta:
model = Order
fields = ['comment', 'checkin_attention', 'custom_followup_at']
fields = ['comment', 'checkin_attention', 'checkin_text', 'custom_followup_at']
widgets = {
'comment': forms.Textarea(attrs={
'rows': 3,
'class': 'helper-width-100',
}),
'checkin_text': forms.TextInput(),
'custom_followup_at': DatePickerWidget(),
}
@@ -704,6 +704,7 @@ class OrderMailForm(forms.Form):
)
self.fields['attach_invoices'].queryset = order.invoices.all()
self._set_field_placeholders('message', ['event', 'order'])
self._set_field_placeholders('subject', ['event', 'order'])
class OrderPositionMailForm(OrderMailForm):
@@ -719,6 +720,7 @@ class OrderPositionMailForm(OrderMailForm):
initial=self.order.event.settings.mail_text_order_custom_mail.localize(self.order.locale),
)
self._set_field_placeholders('message', ['event', 'order', 'position'])
self._set_field_placeholders('subject', ['event', 'order'])
class OrderRefundForm(forms.Form):
@@ -766,7 +768,7 @@ class OrderRefundForm(forms.Form):
return data
class EventCancelForm(forms.Form):
class EventCancelForm(FormPlaceholderMixin, forms.Form):
subevent = forms.ModelChoiceField(
SubEvent.objects.none(),
label=pgettext_lazy('subevent', 'Date'),
@@ -866,17 +868,6 @@ class EventCancelForm(forms.Form):
send_waitinglist_subject = forms.CharField()
send_waitinglist_message = forms.CharField()
def _set_field_placeholders(self, fn, base_parameters):
placeholders = get_available_placeholders(self.event, base_parameters)
ht = format_placeholders_help_text(placeholders, self.event)
if self.fields[fn].help_text:
self.fields[fn].help_text += ' ' + str(ht)
else:
self.fields[fn].help_text = ht
self.fields[fn].validators.append(
PlaceholderValidator(['{%s}' % p for p in placeholders.keys()])
)
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event')
kwargs.setdefault('initial', {})
+1 -8
View File
@@ -423,6 +423,7 @@ class OrganizerSettingsForm(SettingsForm):
'organizer_link_back',
'organizer_logo_image_large',
'organizer_logo_image_inherit',
'favicon',
'giftcard_length',
'giftcard_expiry_years',
'locales',
@@ -464,14 +465,6 @@ class OrganizerSettingsForm(SettingsForm):
'can increase the size with the setting below. We recommend not using small details on the picture '
'as it will be resized on smaller screens.')
)
favicon = ExtFileField(
label=_('Favicon'),
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_FAVICON,
required=False,
max_size=settings.FILE_UPLOAD_MAX_SIZE_FAVICON,
help_text=_('If you provide a favicon, we will show it instead of the default pretix icon. '
'We recommend a size of at least 200x200px to accommodate most devices.')
)
def __init__(self, *args, **kwargs):
is_admin = kwargs.pop('is_admin', False)
+37 -3
View File
@@ -20,7 +20,7 @@
# <https://www.gnu.org/licenses/>.
#
from bootstrap3.text import text_value
from django.forms import CheckboxInput
from django.forms import CheckboxInput, CheckboxSelectMultiple, RadioSelect
from django.forms.utils import flatatt
from django.utils.html import format_html
from django.utils.safestring import mark_safe
@@ -30,7 +30,7 @@ from i18nfield.forms import I18nFormField
from pretix.base.forms.renderers import FieldRenderer, InlineFieldRenderer
def render_label(content, label_for=None, label_class=None, label_title='', optional=False):
def render_label(content, label_for=None, label_class=None, label_title='', label_id='', optional=False):
"""
Render a label with content
"""
@@ -41,6 +41,8 @@ def render_label(content, label_for=None, label_class=None, label_title='', opti
attrs['class'] = label_class
if label_title:
attrs['title'] = label_title
if label_id:
attrs['id'] = label_id
if text_value(content) == '&#160;':
# Empty label, e.g. checkbox
@@ -61,6 +63,7 @@ class ControlFieldRenderer(FieldRenderer):
def __init__(self, *args, **kwargs):
kwargs['layout'] = 'horizontal'
super().__init__(*args, **kwargs)
self.is_group_widget = isinstance(self.widget, (CheckboxSelectMultiple, RadioSelect, )) or (self.is_multi_widget and len(self.widget.widgets) > 1)
def add_label(self, html):
label = self.get_label()
@@ -73,14 +76,45 @@ class ControlFieldRenderer(FieldRenderer):
else:
required = self.field.field.required
if self.is_group_widget:
label_for = ""
label_id = "legend-{}".format(self.field.html_name)
else:
label_for = self.field.id_for_label
label_id = ""
html = render_label(
label,
label_for=self.field.id_for_label,
label_for=label_for,
label_class=self.get_label_class(),
label_id=label_id,
optional=not required and not isinstance(self.widget, CheckboxInput)
) + html
return html
def wrap_label_and_field(self, html):
if self.is_group_widget:
attrs = ' role="group" aria-labelledby="legend-{}"'.format(self.field.html_name)
else:
attrs = ''
return '<div class="{klass}"{attrs}>{html}</div>'.format(klass=self.get_form_group_class(), html=html, attrs=attrs)
class ControlFieldWithVisibilityRenderer(ControlFieldRenderer):
def __init__(self, *args, **kwargs):
kwargs['layout'] = 'horizontal'
kwargs['horizontal_field_class'] = 'col-md-7'
self.visibility_field = kwargs['visibility_field']
super().__init__(*args, **kwargs)
def render_visibility_field(self):
return self.visibility_field.as_widget(attrs=self.visibility_field.field.widget.attrs)
def wrap_field(self, html):
html = super().wrap_field(html)
html += '<div class="col-md-2 text-right">' + self.render_visibility_field() + '</div>'
return html
class BulkEditMixin:
+8 -2
View File
@@ -260,6 +260,8 @@ class SubEventItemForm(SubEventItemOrVariationFormMixin, forms.ModelForm):
super().__init__(*args, **kwargs)
self.fields['price'].widget.attrs['placeholder'] = money_filter(self.item.default_price, self.item.event.currency, hide_currency=True)
self.fields['price'].label = str(self.item)
self.available_from_mode = self.item.available_from_mode
self.available_until_mode = self.item.available_until_mode
class Meta:
model = SubEventItem
@@ -287,6 +289,8 @@ class SubEventItemVariationForm(SubEventItemOrVariationFormMixin, forms.ModelFor
super().__init__(*args, **kwargs)
self.fields['price'].widget.attrs['placeholder'] = money_filter(self.variation.price, self.item.event.currency, hide_currency=True)
self.fields['price'].label = '{} {}'.format(str(self.item), self.variation.value)
self.available_from_mode = self.variation.available_from_mode
self.available_until_mode = self.variation.available_until_mode
class Meta:
model = SubEventItemVariation
@@ -356,6 +360,7 @@ class BulkSubEventItemVariationForm(SubEventItemVariationForm):
class QuotaFormSet(I18nInlineFormSet):
def __init__(self, *args, **kwargs):
self.searchable_selection = kwargs.pop('searchable_selection', None)
self.event = kwargs.pop('event', None)
self.locales = self.event.settings.get('locales')
super().__init__(*args, **kwargs)
@@ -368,7 +373,7 @@ class QuotaFormSet(I18nInlineFormSet):
kwargs['locales'] = self.locales
kwargs['event'] = self.event
kwargs['items'] = self.items
kwargs['items'] = self.items
kwargs['searchable_selection'] = self.searchable_selection
return super()._construct_form(i, **kwargs)
@property
@@ -463,7 +468,8 @@ class RRuleFormSetForm(RRuleForm):
RRuleFormSet = formset_factory(
RRuleFormSetForm,
can_order=False, can_delete=True, extra=1
min_num=1, validate_min=True,
can_order=False, can_delete=True, extra=0
)
+4 -1
View File
@@ -63,7 +63,8 @@ class VoucherForm(I18nModelForm):
itemvar = FakeChoiceField(
label=_("Product"),
help_text=_(
"This product is added to the user's cart if the voucher is redeemed."
"This product is added to the user's cart if the voucher is redeemed. Instead of a specific product, you "
"can also select a quota. In this case, all products assigned to this quota can be selected."
),
required=True
)
@@ -201,6 +202,8 @@ class VoucherForm(I18nModelForm):
cnt = len(data['codes']) * data.get('max_usages', 0)
else:
cnt = data.get('max_usages', 0)
if self.instance and self.instance.pk:
cnt -= self.instance.redeemed # these do not need quota any more
Voucher.clean_item_properties(
data, self.instance.event,
+16
View File
@@ -77,3 +77,19 @@ class Select2ItemVarQuotaMixin(Select2Mixin):
class Select2ItemVarQuota(Select2ItemVarQuotaMixin, forms.Select):
pass
class Select2ItemVarMulti(Select2Mixin, forms.SelectMultiple):
def options(self, name, value, attrs=None):
# we need this for multi-selection without a queryset for the selection of items and variations
for i, v in enumerate(value):
yield self.create_option(
None,
v,
dict(self.choices)[v],
True,
i,
subindex=None,
attrs=attrs
)
return

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