Compare commits

...
Author SHA1 Message Date
Raphael Michel 9ebd5d83ca RejectInvalidInputMiddleware: Ignore invalid charsets on body 2026-06-25 17:42:42 +02:00
Raphael Michel a8997f8971 [SECURITY] Properly escape HTML tags in PDF generation (CVE-2026-57535) 2026-06-25 16:46:13 +02:00
Raphael MichelandRaphael Michel eb068f524c [SECURITY] Prevent reading of any local files in reportlab (CVE-2026-57535) 2026-06-25 16:46:13 +02:00
Raphael MichelandRaphael Michel f615595547 [SECURITY] Disable outbound and file access for reportlab (CVE-2026-57535) 2026-06-25 16:46:13 +02:00
Mira WellerandRaphael Michel 8bd78eefcf [SECURITY] Fix reflected XSS in redirection page (CVE-2026-57533) 2026-06-25 16:46:13 +02:00
Mira WellerandRaphael Michel 848f7fa0e5 [SECURITY] Fix stored XSS in ticket confirmation page (CVE-2026-13225) 2026-06-25 16:46:13 +02:00
Mira WellerandRaphael Michel 3442a543c8 [SECURITY] Hardening: Don't use |safe on confirm_messages 2026-06-25 16:46:13 +02:00
Mira WellerandRaphael Michel f88c24863d [SECURITY] Fix XSS in ticket layout JSON (CVE-2026-57532) 2026-06-25 16:46:13 +02:00
Lukas BockstallerandGitHub 79c5160b57 Revert "Update django-countries requirement from ==8.2.* to ==9.0.* (#6269)" (#6310)
This reverts commit 1e301da26c.
2026-06-24 10:42:56 +02:00
Richard SchreiberandGitHub e8492cad3c Seating: fix handling optional position attribute (#6303) 2026-06-24 09:47:08 +02:00
Richard SchreiberandGitHub 7ea5a2b59e PDF: add placeholder invoice_custom_field (#6298) 2026-06-24 09:46:43 +02:00
luelistaandGitHub 4c373518d0 Fix event meta property handling when cloning across organizers (Z#23231419) (#6306) 2026-06-23 19:01:32 +02:00
luelistaandGitHub 1521c0cfcd Fix URL matching in EventQRCode (Z#23237781) (#6304) 2026-06-23 18:34:31 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3432e62e4f Update css-inline requirement from ==0.20.* to ==0.21.* (#6302)
Updates the requirements on [css-inline](https://github.com/Stranger6667/css-inline) to permit the latest version.
- [Release notes](https://github.com/Stranger6667/css-inline/releases)
- [Changelog](https://github.com/Stranger6667/css-inline/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stranger6667/css-inline/compare/c-v0.20.0...c-v0.21.0)

---
updated-dependencies:
- dependency-name: css-inline
  dependency-version: 0.21.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 13:32:50 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
736fa38ca7 Update sentry-sdk requirement from ==2.62.* to ==2.63.* (#6301)
Updates the requirements on [sentry-sdk](https://github.com/getsentry/sentry-python) to permit the latest version.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.62.0...2.63.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 13:32:39 +02:00
d14dc4c5ff Test order deletion: Improve bulk performance (Z#23237160) (#6274)
* Test order deletion: Improve bulk performance (Z#23237160)

* Apply suggestion from @pajowu

Co-authored-by: pajowu <engelhardt@pretix.eu>

* Fix style issue

---------

Co-authored-by: pajowu <engelhardt@pretix.eu>
2026-06-22 09:49:27 +02:00
Raphael MichelandGitHub 5db1a5b8af Rename confusingly named helpers for URL generation (#6280)
* Rename confusingly named helpers for URL generation

* new name

* fix old call

* Revert "new name"

This reverts commit a6e9a488b6.

* New name
2026-06-22 09:11:55 +02:00
Nikita MitasovandRaphael Michel 86a51afe9e Translations: Update Russian
Currently translated at 19.0% (1199 of 6302 strings)

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

powered by weblate
2026-06-22 09:11:47 +02:00
NikolaiandRaphael Michel ea9c85a1b4 Translations: Update Danish
Currently translated at 62.5% (3945 of 6302 strings)

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

powered by weblate
2026-06-22 09:11:47 +02:00
Nikita MitasovandRaphael Michel 226ff9b044 Translations: Update Russian
Currently translated at 18.7% (1183 of 6302 strings)

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

powered by weblate
2026-06-22 09:11:47 +02:00
Szurofka MártonandRaphael Michel d8991d8138 Translations: Update Hungarian
Currently translated at 64.6% (119 of 184 strings)

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

powered by weblate
2026-06-22 09:11:47 +02:00
Nikita MitasovandRaphael Michel 83642ec9f3 Translations: Update Russian
Currently translated at 18.6% (1174 of 6302 strings)

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

powered by weblate
2026-06-22 09:11:47 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9f0ce28ce4 Bump vite from 8.0.12 to 8.0.16 (#6294)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.12 to 8.0.16.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.16/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.16
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 08:57:41 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
28722fecbd Update cryptography requirement from >=48.0.1 to >=49.0.0 (#6289)
Updates the requirements on [cryptography](https://github.com/pyca/cryptography) to permit the latest version.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/48.0.1...49.0.0)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 49.0.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 08:57:33 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
10a5d4ac68 Update pytest requirement from ==9.0.* to ==9.1.* (#6290)
Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/9.0.0...9.1.0)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 9.1.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 08:57:10 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ba36f6d2ce Update webauthn requirement from ==2.7.* to ==2.8.* (#6293)
Updates the requirements on [webauthn](https://github.com/duo-labs/py_webauthn) to permit the latest version.
- [Release notes](https://github.com/duo-labs/py_webauthn/releases)
- [Changelog](https://github.com/duo-labs/py_webauthn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/duo-labs/py_webauthn/compare/v2.7.0...v2.8.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 08:56:57 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1e301da26c Update django-countries requirement from ==8.2.* to ==9.0.* (#6269)
Updates the requirements on [django-countries](https://github.com/SmileyChris/django-countries) to permit the latest version.
- [Changelog](https://github.com/SmileyChris/django-countries/blob/main/CHANGES.md)
- [Commits](https://github.com/SmileyChris/django-countries/compare/v8.2.0...v9.0.0)

---
updated-dependencies:
- dependency-name: django-countries
  dependency-version: 9.0.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 08:56:41 +02:00
luelistaandGitHub d0307b9936 Use OrderPosition.all instead of .objects in metrics (#6285) 2026-06-21 12:46:07 +02:00
Martin Gross c083ce904a Checkin API: Provide 'reason' for RequiredMediaExchangeError (PRETIXEU-DHW) 2026-06-19 12:41:32 +02:00
694b915d89 include errors.js by default and make it coop with async_task_replace_page (Z#23236752) (#6284)
* load errors.js as standard and make it coop with async_task_replace_page

* scope down event

* Update src/pretix/static/pretixbase/js/asynctask.js

Co-authored-by: pajowu <engelhardt@pretix.eu>

* drop the jquery dependency for error.js

Co-authored-by: pajowu <pajowu@pajowu.de>

* include errors.js in error.html

* include errors.js in control base.html

* Update src/pretix/static/pretixbase/js/asynctask.js

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>

* put errors.js in an IIFE call

---------

Co-authored-by: pajowu <engelhardt@pretix.eu>
Co-authored-by: pajowu <pajowu@pajowu.de>
Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
2026-06-16 15:18:19 +02:00
Raphael MichelandGitHub ea928ea7d4 Widget: Fix handling of HTTP-429 errors 2026-06-16 09:11:33 +02:00
Richard SchreiberandGitHub 36d49fbd77 Question detail: Fix crash in filter 2026-06-15 08:00:55 +02:00
Raphael Michelandpajowu f0cb451c34 LocaleMiddleware: Correctly reset region for backend views 2026-06-12 15:30:06 +02:00
2c7fcd0599 Accounting report: Correctly split subevents with same label (Z#23237301) (#6275)
* Accounting report: Correctly split subevents with same label (Z#23237301)

* Accountingreport: Fix crash for single events

---------

Co-authored-by: Kara Engelhardt <engelhardt@pretix.eu>
2026-06-12 14:36:10 +02:00
Raphael Michel 034722fa39 Skip e2e tests on gitlab for now 2026-06-12 14:07:42 +02:00
dependabot[bot]andRaphael Michel 5a6870fce1 Update cryptography requirement from >=48.0.0 to >=48.0.1
Updates the requirements on [cryptography](https://github.com/pyca/cryptography) to permit the latest version.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/48.0.0...48.0.1)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 48.0.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-12 08:23:03 +02:00
MiraandRaphael Michel de28425993 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (6302 of 6302 strings)

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

powered by weblate
2026-06-11 18:59:48 +02:00
MiraandRaphael Michel f3eb0d2dba Translations: Update German
Currently translated at 100.0% (6302 of 6302 strings)

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

powered by weblate
2026-06-11 18:59:48 +02:00
Sébastien BRUNEAUandRaphael Michel 0630e05d50 Translations: Update French
Currently translated at 100.0% (6302 of 6302 strings)

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

powered by weblate
2026-06-11 18:59:48 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f868507670 Update beautifulsoup4 requirement from ==4.14.* to ==4.15.* (#6257)
Updates the requirements on [beautifulsoup4](https://www.crummy.com/software/BeautifulSoup/bs4/) to permit the latest version.

---
updated-dependencies:
- dependency-name: beautifulsoup4
  dependency-version: 4.15.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-11 16:29:49 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
04032078c1 Update sentry-sdk requirement from ==2.61.* to ==2.62.* (#6256)
Updates the requirements on [sentry-sdk](https://github.com/getsentry/sentry-python) to permit the latest version.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.61.0...2.62.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-11 16:29:40 +02:00
Martin WeineltandGitHub 8af2714a04 Prune wheel and setuptools-rust from build-system (#6268)
For wheel the setuptools documentation notes:

> Historically this documentation has unnecessarily listed wheel in
> the requires list, and many projects still do that. This is not
> recommended, as the backend no longer requires the wheel package,
> and listing it explicitly causes it to be unnecessarily required for
> source distribution builds.

https://setuptools.pypa.io/en/latest/userguide/quickstart.html#basic-use

For setuptools-rust I could not find any Rust extension that need to be
built. The introduction goes back to c132ccd14, where css-inline, a rust
component, was added as a dependency.
2026-06-11 16:29:32 +02:00
luelistaandGitHub c4b9cc4143 Allow search by partial giftcard secret with organizer.giftcards:read (#6263) 2026-06-11 16:26:05 +02:00
8c132d8342 Teams: Add a note to the degree of isolation between permissions (#6258)
* Teams: Add a note to the degree of isolation between permissions

* Update src/pretix/control/templates/pretixcontrol/organizers/team_edit.html

Co-authored-by: pajowu <engelhardt@pretix.eu>

---------

Co-authored-by: pajowu <engelhardt@pretix.eu>
2026-06-11 16:25:58 +02:00
pajowuandGitHub 8e63fafc62 Devex: Fix vite devserver capturing stdin (#6267)
Pass DEVNULL as stdin to vite, otherwise the vite devserver captures parts of stdin, making things like pasting during debugging impossible
2026-06-11 16:25:48 +02:00
luelistaandGitHub 63ebe16fd3 Fix count in order bulk delete success message (#6270) 2026-06-11 16:25:38 +02:00
775fdd1ccb Check-in API: Add reusable media exchange (#6115)
* Add Reusable Media Exchange to Checkin API

* isort

* Remove debugging leftover

* Apply suggestions from code review

Co-authored-by: robbi5 <maxi@richt.name>

* Add media_exchange_supported to CheckinRPCRedeemInputSerializer

* SecurityProfiles: Add api-v1:reusablemedia-lookup and -detail for SCAN

* Simplify media exchange checks

* Apply suggestions from code review

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>

* Wording: re-usable --> reusable

* Deny checkins if media-exchange is required but device does not support it.

* Remove media_exchange_supported-Flag: Checkin will always be denied if media needs to be exchanged; apps will fall back to explanation text

* CheckinRPC: Also perform media exchange

* Use media_policy from item, not as a checkinrpc parameter

* my own review notes

* Fixes, cleanup, rebase

* block expired media

* Fix query

* add logging

* Refactor link_action into media policy, gift card support

* Block illegal policy-type combination

* Drop add_to_reusable_medium, decide all by policy

* Fix test failure

* fix test on postgres

* Expose reusable_media_usage_enforced to devies

* Explicitly set update view

---------

Co-authored-by: robbi5 <maxi@richt.name>
Co-authored-by: Maximilian Richt <richt@pretix.eu>
Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
Co-authored-by: Raphael Michel <michel@rami.io>
Co-authored-by: Raphael Michel <michel@pretix.eu>
2026-06-11 16:25:13 +02:00
luelistaandGitHub 784577d86f Fix markup of error template (#6265) 2026-06-10 14:16:46 +02:00
112 changed files with 2159 additions and 1147 deletions
+1 -2
View File
@@ -10,8 +10,7 @@ tests:
- cd src - cd src
- python manage.py check - python manage.py check
- make all compress - make all compress
- playwright install - PRETIX_CONFIG_FILE=tests/ci_sqlite.cfg py.test -n 3 tests --ignore=tests/e2e --maxfail=100
- PRETIX_CONFIG_FILE=tests/ci_sqlite.cfg py.test -n 3 tests --maxfail=100
except: except:
- '/^v.*$/' - '/^v.*$/'
pypi: pypi:
+1 -2
View File
@@ -57,8 +57,7 @@ COPY vite.config.ts /pretix/vite.config.ts
RUN pip3 install -U \ RUN pip3 install -U \
pip \ pip \
setuptools \ setuptools && \
wheel && \
cd /pretix && \ cd /pretix && \
PRETIX_DOCKER_BUILD=TRUE pip3 install \ PRETIX_DOCKER_BUILD=TRUE pip3 install \
-e ".[memcached]" \ -e ".[memcached]" \
+8 -1
View File
@@ -46,12 +46,14 @@ Checking a ticket in
this request twice with the same nonce, the second request will also succeed but will always this request twice with the same nonce, the second request will also succeed but will always
create only one check-in object even when the previous request was successful as well. This create only one check-in object even when the previous request was successful as well. This
allows for a certain level of idempotency and enables you to re-try after a connection failure. allows for a certain level of idempotency and enables you to re-try after a connection failure.
:<json string exchange_medium_type: To perform an exchange to a reusable medium, pass the type of the new reusable medium
:<json string exchange_medium_identifier: To perform an exchange to a reusable media, pass the identifier of the new medium
:<json boolean use_order_locale: Specifies that pretix should use the customer's language (``locale`` field from the :<json boolean use_order_locale: Specifies that pretix should use the customer's language (``locale`` field from the
order) when building texts (currently only the ``reason_explanation`` response field). order) when building texts (currently only the ``reason_explanation`` response field).
Defaults to ``false`` in which case the server will determine the language (currently Defaults to ``false`` in which case the server will determine the language (currently
the event default language, might change in the future with support for the the event default language, might change in the future with support for the
``Accept-Language`` header). ``Accept-Language`` header).
:>json string status: ``"ok"``, ``"incomplete"``, or ``"error"`` :>json string status: ``"ok"``, ``"incomplete"``, ``"exchange"``, or ``"error"``
:>json string reason: Reason code, only set on status ``"error"``, see below for possible values. :>json string reason: Reason code, only set on status ``"error"``, see below for possible values.
:>json string reason_explanation: Human-readable explanation, only set on status ``"error"`` and reason ``"rules"``, can be null. :>json string reason_explanation: Human-readable explanation, only set on status ``"error"`` and reason ``"rules"``, can be null.
:>json object position: Copy of the matching order position (if any was found). The contents are the same as the :>json object position: Copy of the matching order position (if any was found). The contents are the same as the
@@ -67,6 +69,8 @@ Checking a ticket in
:>json object list: Excerpt of information about the matching :ref:`check-in list <rest-checkinlists>` (if any was found), :>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``. 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"``. :>json object questions: List of questions to be answered for check-in, only set on status ``"incomplete"``.
:>json object media_policy: Reusable media policy (see documentation on items), only set on status ``"exchange"``.
:>json object media_type: Reusable media type (see documentation on items), only set on status ``"exchange"``.
**Example request**: **Example request**:
@@ -224,6 +228,9 @@ Checking a ticket in
* ``ambiguous`` - Multiple tickets match scan, rejected. * ``ambiguous`` - Multiple tickets match scan, rejected.
* ``revoked`` - Ticket code has been revoked. * ``revoked`` - Ticket code has been revoked.
* ``unapproved`` - Order has not yet been approved. * ``unapproved`` - Order has not yet been approved.
* ``already_exchanged`` - Ticket already has been exchanged for a reusable medium that must now be used for check-in.
* ``medium_invalid`` - Reusable medium identifier given was not found or is not valid.
* ``medium_exists`` - Reusable medium identifier already exists, but expected to be new.
* ``error`` - Internal error. * ``error`` - Internal error.
In case of reason ``rules`` and ``invalid_time``, there might be an additional response field ``reason_explanation`` In case of reason ``rules`` and ``invalid_time``, there might be an additional response field ``reason_explanation``
+5 -1
View File
@@ -602,7 +602,8 @@ Order position endpoints
We no longer recommend using this API if you're building a ticket scanning application, as it has a few design We no longer recommend using this API if you're building a ticket scanning application, as it has a few design
flaws that can lead to `security issues`_ or compatibility issues due to barcode content characters that are not flaws that can lead to `security issues`_ or compatibility issues due to barcode content characters that are not
URL-safe. We recommend to use our new :ref:`check-in API <rest-checkin>` instead. URL-safe. We recommend to use our new :ref:`check-in API <rest-checkin>` instead. Advanced features like medium
exchange are only supported on the new API.
:query boolean untrusted_input: If set to true, the lookup parameter is **always** interpreted as a ``secret``, never :query boolean untrusted_input: If set to true, the lookup parameter is **always** interpreted as a ``secret``, never
as an ``id``. This should be always set if you are passing through untrusted, scanned as an ``id``. This should be always set if you are passing through untrusted, scanned
@@ -741,6 +742,9 @@ Order position endpoints
* ``ambiguous`` - Multiple tickets match scan, rejected. * ``ambiguous`` - Multiple tickets match scan, rejected.
* ``revoked`` - Ticket code has been revoked. * ``revoked`` - Ticket code has been revoked.
* ``unapproved`` - Order has not yet been approved. * ``unapproved`` - Order has not yet been approved.
* ``already_exchanged`` - Ticket already has been exchanged for a reusable medium that must now be used for check-in.
* ``medium_invalid`` - Reusable medium identifier given was not found and could not be automatically created.
* ``medium_exists`` - Reusable medium identifier already exists, but expected to be new.
In case of reason ``rules`` or ``invalid_time``, there might be an additional response field ``reason_explanation`` 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``. with a human-readable description of the violated rules. However, that field can also be missing or be ``null``.
+1 -1
View File
@@ -131,7 +131,7 @@ allow_waitinglist boolean If ``false``,
product when it is sold out. product when it is sold out.
issue_giftcard boolean If ``true``, buying this product will yield a gift card. issue_giftcard boolean If ``true``, buying this product will yield a gift card.
media_policy string Policy on how to handle reusable media (experimental feature). media_policy string Policy on how to handle reusable media (experimental feature).
Possible values are ``null``, ``"new"``, ``"reuse"``, and ``"reuse_or_new"``. Possible values are ``null``, ``"new"``, ``"reuse"``, ``"reuse_or_new"``, ``"append"``, and ``"append_or_new"``.
media_type string Type of reusable media to work on (experimental feature). See :ref:`rest-reusablemedia` for possible choices. media_type string Type of reusable media to work on (experimental feature). See :ref:`rest-reusablemedia` for possible choices.
show_quota_left boolean Publicly show how many tickets are still available. show_quota_left boolean Publicly show how many tickets are still available.
If this is ``null``, the event default is used. If this is ``null``, the event default is used.
+1 -2
View File
@@ -1069,8 +1069,7 @@ Creating orders
* ``valid_from`` (optional, if both ``valid_from`` and ``valid_until`` are **missing** (not ``null``) the availability will be computed from the given product) * ``valid_from`` (optional, if both ``valid_from`` and ``valid_until`` are **missing** (not ``null``) the availability will be computed from the given product)
* ``valid_until`` (optional, if both ``valid_from`` and ``valid_until`` are **missing** (not ``null``) the availability will be computed from the given product) * ``valid_until`` (optional, if both ``valid_from`` and ``valid_until`` are **missing** (not ``null``) the availability will be computed from the given product)
* ``requested_valid_from`` (optional, can be set **instead** of ``valid_from`` and ``valid_until`` to signal a user choice for the start time that may or may not be respected) * ``requested_valid_from`` (optional, can be set **instead** of ``valid_from`` and ``valid_until`` to signal a user choice for the start time that may or may not be respected)
* ``use_reusable_medium`` (optional, causes the new ticket to take over the given reusable medium, identified by its ID) * ``use_reusable_medium`` (optional, causes the new ticket to be connected to the given reusable medium, identified by its ID)
* ``add_to_reusable_medium`` (optional, causes the new ticket to be added to the given reusable medium, identified by its ID)
* ``discount`` (optional, only possible if ``price`` is set; attention: if this is set to not-``null`` on any position, automatic calculation of discounts will not run) * ``discount`` (optional, only possible if ``price`` is set; attention: if this is set to not-``null`` on any position, automatic calculation of discounts will not run)
* ``answers`` * ``answers``
+1 -1
View File
@@ -81,7 +81,7 @@ is a python method that emulates a behavior similar to ``reverse``:
If you need to communicate the URL externally, you can use a different method to ensure that it is always an absolute URL: If you need to communicate the URL externally, you can use a different method to ensure that it is always an absolute URL:
.. autofunction:: pretix.multidomain.urlreverse.build_absolute_uri .. autofunction:: pretix.multidomain.urlreverse.eventreverse_absolute
In addition, there is a template tag that works similar to ``url`` but takes an event or organizer object In addition, there is a template tag that works similar to ``url`` but takes an event or organizer object
as its first argument and can be used like this:: as its first argument and can be used like this::
+109 -91
View File
@@ -370,14 +370,14 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@napi-rs/wasm-runtime": { "node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.4", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@tybys/wasm-util": "^0.10.1" "@tybys/wasm-util": "^0.10.2"
}, },
"funding": { "funding": {
"type": "github", "type": "github",
@@ -427,9 +427,9 @@
} }
}, },
"node_modules/@oxc-project/types": { "node_modules/@oxc-project/types": {
"version": "0.129.0", "version": "0.133.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.129.0.tgz", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
"integrity": "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==", "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@@ -758,9 +758,9 @@
} }
}, },
"node_modules/@rolldown/binding-android-arm64": { "node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
"integrity": "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==", "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -775,9 +775,9 @@
} }
}, },
"node_modules/@rolldown/binding-darwin-arm64": { "node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
"integrity": "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==", "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -792,9 +792,9 @@
} }
}, },
"node_modules/@rolldown/binding-darwin-x64": { "node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
"integrity": "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==", "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -809,9 +809,9 @@
} }
}, },
"node_modules/@rolldown/binding-freebsd-x64": { "node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
"integrity": "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==", "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -826,9 +826,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm-gnueabihf": { "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
"integrity": "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==", "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -843,13 +843,16 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm64-gnu": { "node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
"integrity": "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==", "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -860,13 +863,16 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm64-musl": { "node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
"integrity": "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==", "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -877,13 +883,16 @@
} }
}, },
"node_modules/@rolldown/binding-linux-ppc64-gnu": { "node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
"integrity": "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==", "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -894,13 +903,16 @@
} }
}, },
"node_modules/@rolldown/binding-linux-s390x-gnu": { "node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
"integrity": "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==", "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -911,13 +923,16 @@
} }
}, },
"node_modules/@rolldown/binding-linux-x64-gnu": { "node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
"integrity": "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==", "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -928,13 +943,16 @@
} }
}, },
"node_modules/@rolldown/binding-linux-x64-musl": { "node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
"integrity": "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==", "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -945,9 +963,9 @@
} }
}, },
"node_modules/@rolldown/binding-openharmony-arm64": { "node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
"integrity": "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==", "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -962,9 +980,9 @@
} }
}, },
"node_modules/@rolldown/binding-wasm32-wasi": { "node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
"integrity": "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==", "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
"cpu": [ "cpu": [
"wasm32" "wasm32"
], ],
@@ -981,9 +999,9 @@
} }
}, },
"node_modules/@rolldown/binding-win32-arm64-msvc": { "node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
"integrity": "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==", "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -998,9 +1016,9 @@
} }
}, },
"node_modules/@rolldown/binding-win32-x64-msvc": { "node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
"integrity": "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==", "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -3158,9 +3176,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -3334,9 +3352,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.14", "version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -3353,7 +3371,7 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.12",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"source-map-js": "^1.2.1" "source-map-js": "^1.2.1"
}, },
@@ -3610,14 +3628,14 @@
} }
}, },
"node_modules/rolldown": { "node_modules/rolldown": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0.tgz", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
"integrity": "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==", "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@oxc-project/types": "=0.129.0", "@oxc-project/types": "=0.133.0",
"@rolldown/pluginutils": "1.0.0" "@rolldown/pluginutils": "^1.0.0"
}, },
"bin": { "bin": {
"rolldown": "bin/cli.mjs" "rolldown": "bin/cli.mjs"
@@ -3626,27 +3644,27 @@
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.0", "@rolldown/binding-android-arm64": "1.0.3",
"@rolldown/binding-darwin-arm64": "1.0.0", "@rolldown/binding-darwin-arm64": "1.0.3",
"@rolldown/binding-darwin-x64": "1.0.0", "@rolldown/binding-darwin-x64": "1.0.3",
"@rolldown/binding-freebsd-x64": "1.0.0", "@rolldown/binding-freebsd-x64": "1.0.3",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0", "@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
"@rolldown/binding-linux-arm64-gnu": "1.0.0", "@rolldown/binding-linux-arm64-gnu": "1.0.3",
"@rolldown/binding-linux-arm64-musl": "1.0.0", "@rolldown/binding-linux-arm64-musl": "1.0.3",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0", "@rolldown/binding-linux-ppc64-gnu": "1.0.3",
"@rolldown/binding-linux-s390x-gnu": "1.0.0", "@rolldown/binding-linux-s390x-gnu": "1.0.3",
"@rolldown/binding-linux-x64-gnu": "1.0.0", "@rolldown/binding-linux-x64-gnu": "1.0.3",
"@rolldown/binding-linux-x64-musl": "1.0.0", "@rolldown/binding-linux-x64-musl": "1.0.3",
"@rolldown/binding-openharmony-arm64": "1.0.0", "@rolldown/binding-openharmony-arm64": "1.0.3",
"@rolldown/binding-wasm32-wasi": "1.0.0", "@rolldown/binding-wasm32-wasi": "1.0.3",
"@rolldown/binding-win32-arm64-msvc": "1.0.0", "@rolldown/binding-win32-arm64-msvc": "1.0.3",
"@rolldown/binding-win32-x64-msvc": "1.0.0" "@rolldown/binding-win32-x64-msvc": "1.0.3"
} }
}, },
"node_modules/rolldown/node_modules/@rolldown/pluginutils": { "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
"integrity": "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==", "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@@ -4325,9 +4343,9 @@
} }
}, },
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.16", "version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -4465,17 +4483,17 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "8.0.12", "version": "8.0.16",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.12.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
"integrity": "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg==", "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"lightningcss": "^1.32.0", "lightningcss": "^1.32.0",
"picomatch": "^4.0.4", "picomatch": "^4.0.4",
"postcss": "^8.5.14", "postcss": "^8.5.15",
"rolldown": "1.0.0", "rolldown": "1.0.3",
"tinyglobby": "^0.2.16" "tinyglobby": "^0.2.17"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"
+6 -8
View File
@@ -29,12 +29,12 @@ classifiers = [
dependencies = [ dependencies = [
"arabic-reshaper==3.0.1", # Support for Arabic in reportlab "arabic-reshaper==3.0.1", # Support for Arabic in reportlab
"babel", "babel",
"BeautifulSoup4==4.14.*", "BeautifulSoup4==4.15.*",
"bleach==6.4.*", "bleach==6.4.*",
"celery==5.6.*", "celery==5.6.*",
"chardet==5.2.*", "chardet==5.2.*",
"cryptography>=48.0.0", "cryptography>=49.0.0",
"css-inline==0.20.*", "css-inline==0.21.*",
"defusedcsv>=3.0.0", "defusedcsv>=3.0.0",
"dnspython==2.*", "dnspython==2.*",
"Django[argon2]==5.2.*", "Django[argon2]==5.2.*",
@@ -93,7 +93,7 @@ dependencies = [
"redis==7.4.*", "redis==7.4.*",
"reportlab==4.5.*", "reportlab==4.5.*",
"requests==2.32.*", "requests==2.32.*",
"sentry-sdk==2.61.*", "sentry-sdk==2.63.*",
"sepaxml==2.7.*", "sepaxml==2.7.*",
"stripe==7.9.*", "stripe==7.9.*",
"text-unidecode==1.*", "text-unidecode==1.*",
@@ -101,7 +101,7 @@ dependencies = [
"tqdm==4.*", "tqdm==4.*",
"ua-parser==1.0.*", "ua-parser==1.0.*",
"vobject==0.9.*", "vobject==0.9.*",
"webauthn==2.7.*", "webauthn==2.8.*",
"zeep==4.3.*" "zeep==4.3.*"
] ]
@@ -125,7 +125,7 @@ dev = [
"pytest-sugar", "pytest-sugar",
"pytest-xdist==3.8.*", "pytest-xdist==3.8.*",
"pytest-playwright", "pytest-playwright",
"pytest==9.0.*", "pytest==9.1.*",
"playwright", "playwright",
"responses", "responses",
] ]
@@ -139,8 +139,6 @@ build-backend = "backend"
backend-path = ["_build"] backend-path = ["_build"]
requires = [ requires = [
"setuptools", "setuptools",
"setuptools-rust",
"wheel",
"importlib_metadata", "importlib_metadata",
"tomli", "tomli",
] ]
+2
View File
@@ -110,6 +110,8 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
('POST', 'api-v1:checkinrpc.redeem'), ('POST', 'api-v1:checkinrpc.redeem'),
('GET', 'api-v1:checkinrpc.search'), ('GET', 'api-v1:checkinrpc.search'),
('GET', 'api-v1:reusablemedium-list'), ('GET', 'api-v1:reusablemedium-list'),
('POST', 'api-v1:reusablemedium-lookup'),
('PATCH', 'api-v1:reusablemedium-detail')
) )
+8
View File
@@ -88,11 +88,19 @@ class CheckinRPCRedeemInputSerializer(serializers.Serializer):
nonce = serializers.CharField(required=False, allow_null=True) nonce = serializers.CharField(required=False, allow_null=True)
datetime = serializers.DateTimeField(required=False, allow_null=True) datetime = serializers.DateTimeField(required=False, allow_null=True)
answers = serializers.JSONField(required=False, allow_null=True) answers = serializers.JSONField(required=False, allow_null=True)
exchange_medium_type = serializers.ChoiceField(required=False, choices=MEDIA_TYPES)
exchange_medium_identifier = serializers.CharField(required=False)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['lists'].child_relation.queryset = CheckinList.objects.filter(event__in=self.context['events']).select_related('event') self.fields['lists'].child_relation.queryset = CheckinList.objects.filter(event__in=self.context['events']).select_related('event')
def validate(self, attrs):
exchange_fields = ["exchange_medium_type", "exchange_medium_identifier"]
if any(attrs.get(k) is None for k in exchange_fields) and not all(attrs.get(k) is None for k in exchange_fields):
raise ValidationError("If you set any of exchange_medium_type or exchange_medium_identifier, you need to set both of them.")
return attrs
class MiniCheckinListSerializer(I18nAwareModelSerializer): class MiniCheckinListSerializer(I18nAwareModelSerializer):
event = serializers.SlugRelatedField(slug_field='slug', read_only=True) event = serializers.SlugRelatedField(slug_field='slug', read_only=True)
+5 -2
View File
@@ -73,7 +73,7 @@ from pretix.base.settings import (
LazyI18nStringList, validate_event_settings, LazyI18nStringList, validate_event_settings,
) )
from pretix.base.signals import api_event_settings_fields from pretix.base.signals import api_event_settings_fields
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -173,7 +173,7 @@ class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
) )
def get_event_url(self, event): def get_event_url(self, event):
return build_absolute_uri(event, 'presale:event.index') return eventreverse_absolute(event, 'presale:event.index')
class Meta: class Meta:
model = Event model = Event
@@ -871,6 +871,7 @@ class EventSettingsSerializer(SettingsSerializer):
'og_image', 'og_image',
'name_scheme', 'name_scheme',
'reusable_media_active', 'reusable_media_active',
'reusable_media_usage_enforced',
'reusable_media_type_barcode', 'reusable_media_type_barcode',
'reusable_media_type_barcode_identifier_length', 'reusable_media_type_barcode_identifier_length',
'reusable_media_type_nfc_uid', 'reusable_media_type_nfc_uid',
@@ -885,6 +886,7 @@ class EventSettingsSerializer(SettingsSerializer):
readonly_fields = [ readonly_fields = [
# These are read-only since they are currently only settable on organizers, not events # These are read-only since they are currently only settable on organizers, not events
'reusable_media_active', 'reusable_media_active',
'reusable_media_usage_enforced',
'reusable_media_type_barcode', 'reusable_media_type_barcode',
'reusable_media_type_barcode_identifier_length', 'reusable_media_type_barcode_identifier_length',
'reusable_media_type_nfc_uid', 'reusable_media_type_nfc_uid',
@@ -970,6 +972,7 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
'reusable_media_type_nfc_uid', 'reusable_media_type_nfc_uid',
'reusable_media_type_nfc_mf0aes', 'reusable_media_type_nfc_mf0aes',
'reusable_media_type_nfc_mf0aes_random_uid', 'reusable_media_type_nfc_mf0aes_random_uid',
'reusable_media_usage_enforced',
'system_question_order', 'system_question_order',
'tax_rule_payment', 'tax_rule_payment',
'tax_rule_cancellation', 'tax_rule_cancellation',
+17 -36
View File
@@ -76,7 +76,7 @@ from pretix.base.settings import (
) )
from pretix.base.signals import register_ticket_outputs from pretix.base.signals import register_ticket_outputs
from pretix.helpers.countries import CachedCountries from pretix.helpers.countries import CachedCountries
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -757,7 +757,7 @@ class PaymentURLField(serializers.URLField):
def to_representation(self, instance: OrderPayment): def to_representation(self, instance: OrderPayment):
if instance.state != OrderPayment.PAYMENT_STATE_CREATED: if instance.state != OrderPayment.PAYMENT_STATE_CREATED:
return None return None
return build_absolute_uri(instance.order.event, 'presale:event.order.pay', kwargs={ return eventreverse_absolute(instance.order.event, 'presale:event.order.pay', kwargs={
'order': instance.order.code, 'order': instance.order.code,
'secret': instance.order.secret, 'secret': instance.order.secret,
'payment': instance.pk, 'payment': instance.pk,
@@ -806,7 +806,7 @@ class OrderRefundSerializer(I18nAwareModelSerializer):
class OrderURLField(serializers.URLField): class OrderURLField(serializers.URLField):
def to_representation(self, instance: Order): def to_representation(self, instance: Order):
return build_absolute_uri(instance.event, 'presale:event.order', kwargs={ return eventreverse_absolute(instance.event, 'presale:event.order', kwargs={
'order': instance.code, 'order': instance.code,
'secret': instance.secret, 'secret': instance.secret,
}) })
@@ -1043,15 +1043,13 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
requested_valid_from = serializers.DateTimeField(required=False, allow_null=True) requested_valid_from = serializers.DateTimeField(required=False, allow_null=True)
use_reusable_medium = serializers.PrimaryKeyRelatedField(queryset=ReusableMedium.objects.none(), use_reusable_medium = serializers.PrimaryKeyRelatedField(queryset=ReusableMedium.objects.none(),
required=False, allow_null=True) required=False, allow_null=True)
add_to_reusable_medium = serializers.PrimaryKeyRelatedField(queryset=ReusableMedium.objects.none(),
required=False, allow_null=True)
class Meta: class Meta:
model = OrderPosition model = OrderPosition
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email', fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
'company', 'street', 'zipcode', 'city', 'country', 'state', 'is_bundled', 'company', 'street', 'zipcode', 'city', 'country', 'state', 'is_bundled',
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher', 'valid_from', 'valid_until', 'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher', 'valid_from', 'valid_until',
'requested_valid_from', 'use_reusable_medium', 'add_to_reusable_medium', 'discount') 'requested_valid_from', 'use_reusable_medium', 'discount')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -1063,8 +1061,6 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
with scopes_disabled(): with scopes_disabled():
if 'use_reusable_medium' in self.fields: if 'use_reusable_medium' in self.fields:
self.fields['use_reusable_medium'].queryset = ReusableMedium.objects.all() self.fields['use_reusable_medium'].queryset = ReusableMedium.objects.all()
if 'add_to_reusable_medium' in self.fields:
self.fields['add_to_reusable_medium'].queryset = ReusableMedium.objects.all()
def validate_secret(self, secret): def validate_secret(self, secret):
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists(): if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
@@ -1080,9 +1076,6 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
) )
return m return m
def validate_add_to_reusable_medium(self, m):
return self.validate_use_reusable_medium(m)
def validate_item(self, item): def validate_item(self, item):
if item.event != self.context['event']: if item.event != self.context['event']:
raise ValidationError( raise ValidationError(
@@ -1157,12 +1150,6 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
{'discount': ['You can only specify a discount if you do the price computation, but price is not set.']} {'discount': ['You can only specify a discount if you do the price computation, but price is not set.']}
) )
if 'use_reusable_medium' in data and 'add_to_reusable_medium' in data:
raise ValidationError({
'use_reusable_medium': ['You can only specify either use_reusable_medium or add_to_reusable_medium.'],
'add_to_reusable_medium': ['You can only specify either use_reusable_medium or add_to_reusable_medium.'],
})
return data return data
@@ -1602,7 +1589,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
pos_data['attendee_name_parts'] = { pos_data['attendee_name_parts'] = {
'_legacy': attendee_name '_legacy': attendee_name
} }
pos = OrderPosition(**{k: v for k, v in pos_data.items() if k not in ('answers', '_quotas', 'use_reusable_medium', 'add_to_reusable_medium')}) pos = OrderPosition(**{k: v for k, v in pos_data.items() if k not in ('answers', '_quotas', 'use_reusable_medium')})
if simulate: if simulate:
pos.order = order._wrapped pos.order = order._wrapped
else: else:
@@ -1676,7 +1663,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
for pos_data in positions_data: for pos_data in positions_data:
answers_data = pos_data.pop('answers', []) answers_data = pos_data.pop('answers', [])
use_reusable_medium = pos_data.pop('use_reusable_medium', None) use_reusable_medium = pos_data.pop('use_reusable_medium', None)
add_to_reusable_medium = pos_data.pop('add_to_reusable_medium', None)
pos = pos_data['__instance'] pos = pos_data['__instance']
pos._calculate_tax(invoice_address=ia) pos._calculate_tax(invoice_address=ia)
@@ -1718,14 +1704,17 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
answ.options.add(*options) answ.options.add(*options)
if use_reusable_medium: if use_reusable_medium:
for op_pk in use_reusable_medium.linked_orderpositions.values_list('pk', flat=True): if pos.item.media_policy not in (Item.MEDIA_POLICY_APPEND, Item.MEDIA_POLICY_APPEND_OR_NEW):
use_reusable_medium.log_action( for op_pk in use_reusable_medium.linked_orderpositions.values_list('pk', flat=True):
'pretix.reusable_medium.linked_orderposition.removed', use_reusable_medium.log_action(
data={ 'pretix.reusable_medium.linked_orderposition.removed',
'linked_orderposition': op_pk, data={
} 'linked_orderposition': op_pk,
) }
use_reusable_medium.linked_orderpositions.set([pos]) )
use_reusable_medium.linked_orderpositions.set([pos])
else:
use_reusable_medium.linked_orderpositions.add(pos)
use_reusable_medium.log_action( use_reusable_medium.log_action(
'pretix.reusable_medium.linked_orderposition.added', 'pretix.reusable_medium.linked_orderposition.added',
data={ data={
@@ -1733,15 +1722,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
'linked_orderposition': pos.pk, 'linked_orderposition': pos.pk,
} }
) )
elif add_to_reusable_medium: use_reusable_medium.touch()
add_to_reusable_medium.linked_orderpositions.add(pos)
add_to_reusable_medium.log_action(
'pretix.reusable_medium.linked_orderposition.added',
data={
'by_order': order.code,
'linked_orderposition': pos.pk,
}
)
if not simulate: if not simulate:
for cp in delete_cps: for cp in delete_cps:
+5 -4
View File
@@ -58,8 +58,8 @@ from pretix.helpers.permission_migration import (
OLD_TO_NEW_EVENT_COMPAT, OLD_TO_NEW_EVENT_MIGRATION, OLD_TO_NEW_EVENT_COMPAT, OLD_TO_NEW_EVENT_MIGRATION,
OLD_TO_NEW_ORGANIZER_COMPAT, OLD_TO_NEW_ORGANIZER_MIGRATION, OLD_TO_NEW_ORGANIZER_COMPAT, OLD_TO_NEW_ORGANIZER_MIGRATION,
) )
from pretix.helpers.urls import build_absolute_uri as build_global_uri from pretix.helpers.urls import mainreverse_absolute
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -71,7 +71,7 @@ class OrganizerSerializer(I18nAwareModelSerializer):
slug = serializers.CharField(read_only=True) slug = serializers.CharField(read_only=True)
def get_organizer_url(self, organizer): def get_organizer_url(self, organizer):
return build_absolute_uri(organizer, 'presale:organizer.index') return eventreverse_absolute(organizer, 'presale:organizer.index')
class Meta: class Meta:
model = Organizer model = Organizer
@@ -499,7 +499,7 @@ class TeamInviteSerializer(serializers.ModelSerializer):
'user': self, 'user': self,
'organizer': self.context['organizer'].name, 'organizer': self.context['organizer'].name,
'team': instance.team.name, 'team': instance.team.name,
'url': build_global_uri('control:auth.invite', kwargs={ 'url': mainreverse_absolute('control:auth.invite', kwargs={
'token': instance.token 'token': instance.token
}) })
}, },
@@ -605,6 +605,7 @@ class OrganizerSettingsSerializer(SettingsSerializer):
'cookie_consent_dialog_button_yes', 'cookie_consent_dialog_button_yes',
'cookie_consent_dialog_button_no', 'cookie_consent_dialog_button_no',
'reusable_media_active', 'reusable_media_active',
'reusable_media_usage_enforced',
'reusable_media_type_barcode', 'reusable_media_type_barcode',
'reusable_media_type_barcode_identifier_length', 'reusable_media_type_barcode_identifier_length',
'reusable_media_type_nfc_uid', 'reusable_media_type_nfc_uid',
+49 -6
View File
@@ -69,8 +69,10 @@ from pretix.base.models import (
from pretix.base.models.orders import PrintLog from pretix.base.models.orders import PrintLog
from pretix.base.permissions import AnyPermissionOf from pretix.base.permissions import AnyPermissionOf
from pretix.base.services.checkin import ( from pretix.base.services.checkin import (
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin, CheckInError, RequiredMediaExchangeError, RequiredQuestionsError, SQLLogic,
perform_checkin,
) )
from pretix.base.services.media import perform_media_exchange
from pretix.base.signals import checkin_annulled from pretix.base.signals import checkin_annulled
from pretix.helpers import OF_SELF from pretix.helpers import OF_SELF
@@ -454,7 +456,8 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force, checkin_type, ignore_unpaid, nonce, def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force, checkin_type, ignore_unpaid, nonce,
untrusted_input, user, auth, expand, pdf_data, request, questions_supported, canceled_supported, untrusted_input, user, auth, expand, pdf_data, request, questions_supported, canceled_supported,
source_type='barcode', legacy_url_support=False, simulate=False, gate=None, use_order_locale=False): source_type='barcode', legacy_url_support=False, simulate=False, gate=None, use_order_locale=False,
exchange_medium_type=None, exchange_medium_identifier=None):
if not checkinlists: if not checkinlists:
raise ValidationError('No check-in list passed.') raise ValidationError('No check-in list passed.')
@@ -463,6 +466,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
device = auth if isinstance(auth, Device) else None device = auth if isinstance(auth, Device) else None
gate = gate or (auth.gate if isinstance(auth, Device) else None) gate = gate or (auth.gate if isinstance(auth, Device) else None)
medium = None
context = { context = {
'request': request, 'request': request,
@@ -522,7 +526,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
# with respecting the force option), or it's a reusable medium (-> proceed with that) # with respecting the force option), or it's a reusable medium (-> proceed with that)
if not op_candidates: if not op_candidates:
try: try:
media = ReusableMedium.objects.active().filter( medium = ReusableMedium.objects.active().filter(
Exists(ReusableMedium.linked_orderpositions.through.objects.filter(reusablemedium_id=OuterRef('pk'))) Exists(ReusableMedium.linked_orderpositions.through.objects.filter(reusablemedium_id=OuterRef('pk')))
).get( ).get(
organizer_id=checkinlists[0].event.organizer_id, organizer_id=checkinlists[0].event.organizer_id,
@@ -630,7 +634,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data, 'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
}, status=400) }, status=400)
else: else:
linked_ops = media.linked_orderpositions.all().select_related("order").prefetch_related("addons") linked_ops = medium.linked_orderpositions.all().select_related("order").prefetch_related("addons")
linked_event_ids = {op.order.event_id for op in linked_ops} linked_event_ids = {op.order.event_id for op in linked_ops}
if not any(event_id in list_by_event for event_id in linked_event_ids): if not any(event_id in list_by_event for event_id in linked_event_ids):
# Medium exists but connected ticket is for the wrong event # Medium exists but connected ticket is for the wrong event
@@ -661,7 +665,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
op_candidates = [] op_candidates = []
for op in linked_ops: for op in linked_ops:
if op.order.event_id in list_by_event: if op.order.event_id in list_by_event:
reusable_medium_used = media reusable_medium_used = medium
op_candidates.append(op) op_candidates.append(op)
if list_by_event[op.order.event_id].addon_match: if list_by_event[op.order.event_id].addon_match:
op_candidates += list(op.addons.all()) op_candidates += list(op.addons.all())
@@ -804,7 +808,14 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
locale = op.order.event.settings.locale locale = op.order.event.settings.locale
with language(locale): with language(locale):
try: try:
perform_checkin( if exchange_medium_identifier and medium:
# Cannot scan a medium and then request to exchange it
raise CheckInError(
gettext('You cannot exchange a medium for a medium.'),
'error'
)
checkin_args = dict(
op=op, op=op,
clist=list_by_event[op.order.event_id], clist=list_by_event[op.order.event_id],
given_answers=given_answers, given_answers=given_answers,
@@ -822,7 +833,25 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
from_revoked_secret=from_revoked_secret, from_revoked_secret=from_revoked_secret,
simulate=simulate, simulate=simulate,
gate=gate, gate=gate,
reusable_medium=medium,
) )
if exchange_medium_identifier: # other fields are filled, see CheckinRPCRedeemInputSerializer.validate
with transaction.atomic():
# Do exchange and check-in atomically, i.e. both succeed or both fail
medium = perform_media_exchange(
organizer=request.organizer,
media_type=exchange_medium_type,
identifier=exchange_medium_identifier,
link_orderposition=op,
user=user,
auth=auth,
)
source_type = medium.media_type.identifier
checkin_args['reusable_medium'] = medium
perform_checkin(**checkin_args)
else:
perform_checkin(**checkin_args)
except RequiredQuestionsError as e: except RequiredQuestionsError as e:
return Response({ return Response({
'status': 'incomplete', 'status': 'incomplete',
@@ -834,6 +863,18 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
], ],
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data, 'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
}, status=400) }, status=400)
except RequiredMediaExchangeError as e:
return Response({
'status': 'exchange',
'require_attention': op.require_checkin_attention,
'checkin_texts': op.checkin_texts,
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
'media_policy': e.media_policy,
'media_type': e.media_type,
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
'reason': e.code,
'reason_explanation': e.msg,
}, status=400)
except CheckInError as e: except CheckInError as e:
if not simulate: if not simulate:
op.order.log_action('pretix.event.checkin.denied', data={ op.order.log_action('pretix.event.checkin.denied', data={
@@ -1021,6 +1062,8 @@ class CheckinRPCRedeemView(views.APIView):
canceled_supported=True, canceled_supported=True,
request=self.request, # this is not clean, but we need it in the serializers for URL generation request=self.request, # this is not clean, but we need it in the serializers for URL generation
legacy_url_support=False, legacy_url_support=False,
exchange_medium_type=s.validated_data.get('exchange_medium_type'),
exchange_medium_identifier=s.validated_data.get('exchange_medium_identifier'),
) )
+1 -1
View File
@@ -196,7 +196,7 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
return Response({"result": None}) return Response({"result": None})
@scopes_disabled() # we are sure enough that get_queryset() is correct, so we save some perforamnce @scopes_disabled() # we are sure enough that get_queryset() is correct, so we save some performance
def list(self, request, **kwargs): def list(self, request, **kwargs):
date = serializers.DateTimeField().to_representation(now()) date = serializers.DateTimeField().to_representation(now())
queryset = self.filter_queryset(self.get_queryset()) queryset = self.filter_queryset(self.get_queryset())
+2 -2
View File
@@ -36,7 +36,7 @@ from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from requests import RequestException from requests import RequestException
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -313,7 +313,7 @@ def _get_or_create_server_keypair(organizer):
def generate_id_token(customer, client, auth_time, nonce, scope, expires: datetime, scope_claims=False, with_code=None, with_access_token=None): def generate_id_token(customer, client, auth_time, nonce, scope, expires: datetime, scope_claims=False, with_code=None, with_access_token=None):
payload = { payload = {
'iss': build_absolute_uri(client.organizer, 'presale:organizer.index').rstrip('/'), 'iss': eventreverse_absolute(client.organizer, 'presale:organizer.index').rstrip('/'),
'aud': client.client_id, 'aud': client.client_id,
'exp': int(expires.timestamp()), 'exp': int(expires.timestamp()),
'iat': int(time.time()), 'iat': int(time.time()),
+3 -3
View File
@@ -28,7 +28,7 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.models import Checkin, InvoiceAddress, Order, Question from pretix.base.models import Checkin, InvoiceAddress, Order, Question
from pretix.base.settings import PERSON_NAME_SCHEMES from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
def get_answer(op, question_identifier=None): def get_answer(op, question_identifier=None):
@@ -545,7 +545,7 @@ def get_data_fields(event, for_model=None):
_("Order link"), _("Order link"),
Question.TYPE_STRING, Question.TYPE_STRING,
None, None,
lambda order: build_absolute_uri( lambda order: eventreverse_absolute(
event, event,
'presale:event.order', kwargs={ 'presale:event.order', kwargs={
'order': order.code, 'order': order.code,
@@ -560,7 +560,7 @@ def get_data_fields(event, for_model=None):
_("Ticket link"), _("Ticket link"),
Question.TYPE_STRING, Question.TYPE_STRING,
None, None,
lambda op: build_absolute_uri( lambda op: eventreverse_absolute(
event, event,
'presale:event.order.position', kwargs={ 'presale:event.order.position', kwargs={
'order': op.order.code, 'order': op.order.code,
+3 -3
View File
@@ -68,7 +68,7 @@ from ...control.forms.filter import get_all_payment_providers
from ...helpers import GroupConcat from ...helpers import GroupConcat
from ...helpers.iter import chunked_iterable from ...helpers.iter import chunked_iterable
from ...helpers.safe_openpyxl import remove_invalid_excel_chars from ...helpers.safe_openpyxl import remove_invalid_excel_chars
from ...multidomain.urlreverse import build_absolute_uri from ...multidomain.urlreverse import eventreverse_absolute
from ..exporter import ( from ..exporter import (
ListExporter, MultiSheetListExporter, OrganizerLevelExportMixin, ListExporter, MultiSheetListExporter, OrganizerLevelExportMixin,
) )
@@ -429,7 +429,7 @@ class OrderListExporter(MultiSheetListExporter):
])) ]))
row.append( row.append(
build_absolute_uri(order.event, 'presale:event.order', kwargs={ eventreverse_absolute(order.event, 'presale:event.order', kwargs={
'order': order.code, 'order': order.code,
'secret': order.secret, 'secret': order.secret,
}) })
@@ -855,7 +855,7 @@ class OrderListExporter(MultiSheetListExporter):
])) ]))
row.append( row.append(
build_absolute_uri(order.event, 'presale:event.order.position', kwargs={ eventreverse_absolute(order.event, 'presale:event.order.position', kwargs={
'order': order.code, 'order': order.code,
'secret': op.web_secret, 'secret': op.web_secret,
'position': op.positionid 'position': op.positionid
+76 -90
View File
@@ -22,9 +22,7 @@
import datetime import datetime
import logging import logging
import math import math
import re
import textwrap import textwrap
import unicodedata
from collections import defaultdict from collections import defaultdict
from decimal import Decimal from decimal import Decimal
from io import BytesIO from io import BytesIO
@@ -58,8 +56,8 @@ from pretix.base.services.currencies import SOURCE_NAMES
from pretix.base.signals import register_invoice_renderers from pretix.base.signals import register_invoice_renderers
from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.money import money_filter
from pretix.helpers.reportlab import ( from pretix.helpers.reportlab import (
FontFallbackParagraph, ThumbnailingImageReader, register_ttf_font_if_new, FontFallbackParagraph, PlainTextParagraph, ThumbnailingImageReader,
reshaper, normalize_text, register_ttf_font_if_new, reshaper,
) )
from pretix.presale.style import get_fonts from pretix.presale.style import get_fonts
@@ -259,18 +257,8 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
register_ttf_font_if_new(family + ' B I', finders.find(styles['bolditalic']['truetype'])) register_ttf_font_if_new(family + ' B I', finders.find(styles['bolditalic']['truetype']))
def _normalize(self, text): def _normalize(self, text):
# reportlab does not support unicode combination characters # alias kept for plugin compatibility
# It's important we do this before we use ArabicReshaper return normalize_text(text)
text = unicodedata.normalize("NFKC", text)
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
# to resolve all ligatures and python-bidi to switch RTL texts.
try:
text = "<br />".join(get_display(reshaper.reshape(l)) for l in re.split("<br ?/>", text))
except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
return text
def _upper(self, val): def _upper(self, val):
# We uppercase labels, but not in every language # We uppercase labels, but not in every language
@@ -351,10 +339,15 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
return 'invoice.pdf', 'application/pdf', buffer.read() return 'invoice.pdf', 'application/pdf', buffer.read()
def _clean_text(self, text, tags=None): def _clean_text(self, text, tags=None):
return self._normalize(bleach.clean( # For backwards compatibility with customer content, we need to support tags like <br> and <b> in a few text
text, # fields. Therefore, we can't use PlainTextParagraph for these, but run bleach instead to limit the allowed
tags=set(tags) if tags else set() # tags.
).strip().replace('<br>', '<br />').replace('\n', '<br />\n')) return self._normalize(
bleach.clean(
text,
tags=set(tags) if tags else set()
).strip().replace('<br>', '<br />').replace('\n', '<br />\n')
)
class PaidMarker(Flowable): class PaidMarker(Flowable):
@@ -405,8 +398,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
invoice_to_top = 52 * mm invoice_to_top = 52 * mm
def _draw_invoice_to(self, canvas): def _draw_invoice_to(self, canvas):
p = FontFallbackParagraph(self._clean_text(self.invoice.address_invoice_to), p = PlainTextParagraph(self.invoice.address_invoice_to, style=self.stylesheet['Normal'])
style=self.stylesheet['Normal'])
p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height) p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height)
p_size = p.wrap(self.invoice_to_width, self.invoice_to_height) p_size = p.wrap(self.invoice_to_width, self.invoice_to_height)
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - p_size[1] - self.invoice_to_top) p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - p_size[1] - self.invoice_to_top)
@@ -417,8 +409,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
invoice_from_top = 17 * mm invoice_from_top = 17 * mm
def _draw_invoice_from(self, canvas): def _draw_invoice_from(self, canvas):
p = FontFallbackParagraph( p = PlainTextParagraph(
self._clean_text(self.invoice.full_invoice_from), self.invoice.full_invoice_from,
style=self.stylesheet['InvoiceFrom'] style=self.stylesheet['InvoiceFrom']
) )
p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height) p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height)
@@ -548,13 +540,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_event(self, canvas): def _draw_event(self, canvas):
def shorten(txt): def shorten(txt):
txt = str(txt) txt = str(txt)
txt = bleach.clean(txt, tags=set()).strip() p = PlainTextParagraph(txt, style=self.stylesheet['Normal'])
p = FontFallbackParagraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
p_size = p.wrap(self.event_width, self.event_height) p_size = p.wrap(self.event_width, self.event_height)
while p_size[1] > 2 * self.stylesheet['Normal'].leading: while p_size[1] > 2 * self.stylesheet['Normal'].leading:
txt = ' '.join(txt.replace('', '').split()[:-1]) + '' txt = ' '.join(txt.replace('', '').split()[:-1]) + ''
p = FontFallbackParagraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal']) p = PlainTextParagraph(txt, style=self.stylesheet['Normal'])
p_size = p.wrap(self.event_width, self.event_height) p_size = p.wrap(self.event_width, self.event_height)
return txt return txt
@@ -572,7 +563,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
else: else:
p_str = shorten(self.invoice.event.name) p_str = shorten(self.invoice.event.name)
p = FontFallbackParagraph(self._normalize(p_str.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal']) p = PlainTextParagraph(p_str, style=self.stylesheet['Normal'])
p.wrapOn(canvas, self.event_width, self.event_height) p.wrapOn(canvas, self.event_width, self.event_height)
p_size = p.wrap(self.event_width, self.event_height) p_size = p.wrap(self.event_width, self.event_height)
p.drawOn(canvas, self.event_left, self.pagesize[1] - self.event_top - p_size[1]) p.drawOn(canvas, self.event_left, self.pagesize[1] - self.event_top - p_size[1])
@@ -645,39 +636,37 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
type_info_text = self.invoice.transmission_type_instance.pdf_info_text() type_info_text = self.invoice.transmission_type_instance.pdf_info_text()
if type_info_text: if type_info_text:
story.append(FontFallbackParagraph( story.append(PlainTextParagraph(
type_info_text, type_info_text,
self.stylesheet['WarningBlock'] self.stylesheet['WarningBlock']
)) ))
if self.invoice.custom_field: if self.invoice.custom_field:
story.append(FontFallbackParagraph( story.append(PlainTextParagraph(
'{}: {}'.format( '{}: {}'.format(
self._clean_text(str(self.invoice.event.settings.invoice_address_custom_field)), str(self.invoice.event.settings.invoice_address_custom_field),
self._clean_text(self.invoice.custom_field), self.invoice.custom_field,
), ),
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
if self.invoice.internal_reference: if self.invoice.internal_reference:
story.append(FontFallbackParagraph( story.append(PlainTextParagraph(
self._normalize(pgettext('invoice', 'Customer reference: {reference}').format( pgettext('invoice', 'Customer reference: {reference}').format(
reference=self._clean_text(self.invoice.internal_reference), reference=self.invoice.internal_reference,
)), ),
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
if self.invoice.invoice_to_vat_id: if self.invoice.invoice_to_vat_id:
story.append(FontFallbackParagraph( story.append(PlainTextParagraph(
self._normalize(pgettext('invoice', 'Customer VAT ID')) + ': ' + pgettext('invoice', 'Customer VAT ID') + ': ' + self.invoice.invoice_to_vat_id,
self._clean_text(self.invoice.invoice_to_vat_id),
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
if self.invoice.invoice_to_beneficiary: if self.invoice.invoice_to_beneficiary:
story.append(FontFallbackParagraph( story.append(PlainTextParagraph(
self._normalize(pgettext('invoice', 'Beneficiary')) + ':<br />' + pgettext('invoice', 'Beneficiary') + ':\n' + self.invoice.invoice_to_beneficiary,
self._clean_text(self.invoice.invoice_to_beneficiary),
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
@@ -707,11 +696,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story = [ story = [
NextPageTemplate('FirstPage'), NextPageTemplate('FirstPage'),
FontFallbackParagraph( PlainTextParagraph(
self._normalize( (
pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU' pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU'
else pgettext('invoice', 'Invoice') else pgettext('invoice', 'Invoice')
) if not self.invoice.is_cancellation else self._normalize(pgettext('invoice', 'Cancellation')), ) if not self.invoice.is_cancellation else pgettext('invoice', 'Cancellation'),
self.stylesheet['Heading1'] self.stylesheet['Heading1']
), ),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
@@ -733,17 +722,17 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
] ]
if has_taxes: if has_taxes:
tdata = [( tdata = [(
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']), PlainTextParagraph(pgettext('invoice', 'Description'), self.stylesheet['Bold']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']), PlainTextParagraph(pgettext('invoice', 'Qty'), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['BoldRightNoSplit']), PlainTextParagraph(pgettext('invoice', 'Tax rate'), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Net')), self.stylesheet['BoldRightNoSplit']), PlainTextParagraph(pgettext('invoice', 'Net'), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Gross')), self.stylesheet['BoldRightNoSplit']), PlainTextParagraph(pgettext('invoice', 'Gross'), self.stylesheet['BoldRightNoSplit']),
)] )]
else: else:
tdata = [( tdata = [(
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']), PlainTextParagraph(pgettext('invoice', 'Description'), self.stylesheet['Bold']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']), PlainTextParagraph(pgettext('invoice', 'Qty'), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Amount')), self.stylesheet['BoldRightNoSplit']), PlainTextParagraph(pgettext('invoice', 'Amount'), self.stylesheet['BoldRightNoSplit']),
)] )]
def _group_key(line): def _group_key(line):
@@ -780,8 +769,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
max_height = self.stylesheet['Normal'].leading * 5 max_height = self.stylesheet['Normal'].leading * 5
p_style = self.stylesheet['Normal'] p_style = self.stylesheet['Normal']
for __ in range(1000): for __ in range(1000):
p = FontFallbackParagraph( p = PlainTextParagraph(
self._clean_text(curr_description, tags=['br']), curr_description,
p_style p_style
) )
h = p.wrap(max_width, doc.height)[1] h = p.wrap(max_width, doc.height)[1]
@@ -862,7 +851,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
# Group together at the end of the invoice # Group together at the end of the invoice
request_show_service_date = period_line request_show_service_date = period_line
elif period_line: elif period_line:
description_p_list.append(FontFallbackParagraph( description_p_list.append(PlainTextParagraph(
period_line, period_line,
self.stylesheet['Fineprint'] self.stylesheet['Fineprint']
)) ))
@@ -874,7 +863,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
net_price=money_filter(net_value, self.invoice.event.currency), net_price=money_filter(net_value, self.invoice.event.currency),
gross_price=money_filter(gross_value, self.invoice.event.currency), gross_price=money_filter(gross_value, self.invoice.event.currency),
) )
description_p_list.append(FontFallbackParagraph( description_p_list.append(PlainTextParagraph(
single_price_line, single_price_line,
self.stylesheet['Fineprint'] self.stylesheet['Fineprint']
)) ))
@@ -883,11 +872,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
description_p_list.pop(0), description_p_list.pop(0),
str(len(lines)), str(len(lines)),
localize(tax_rate) + " %", localize(tax_rate) + " %",
FontFallbackParagraph( PlainTextParagraph(
money_filter(net_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '), money_filter(net_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
self.stylesheet['NormalRight'] self.stylesheet['NormalRight']
), ),
FontFallbackParagraph( PlainTextParagraph(
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '), money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
self.stylesheet['NormalRight'] self.stylesheet['NormalRight']
), ),
@@ -904,14 +893,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
single_price_line = pgettext('invoice', 'Single price: {price}').format( single_price_line = pgettext('invoice', 'Single price: {price}').format(
price=money_filter(gross_value, self.invoice.event.currency), price=money_filter(gross_value, self.invoice.event.currency),
) )
description_p_list.append(FontFallbackParagraph( description_p_list.append(PlainTextParagraph(
single_price_line, single_price_line,
self.stylesheet['Fineprint'] self.stylesheet['Fineprint']
)) ))
tdata.append(( tdata.append((
description_p_list.pop(0), description_p_list.pop(0),
str(len(lines)), str(len(lines)),
FontFallbackParagraph( PlainTextParagraph(
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '), money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
self.stylesheet['NormalRight'] self.stylesheet['NormalRight']
), ),
@@ -944,12 +933,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if has_taxes: if has_taxes:
tdata.append([ tdata.append([
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', '', '', PlainTextParagraph(pgettext('invoice', 'Invoice total'), self.stylesheet['Bold']), '', '', '',
money_filter(total, self.invoice.event.currency) money_filter(total, self.invoice.event.currency)
]) ])
else: else:
tdata.append([ tdata.append([
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', PlainTextParagraph(pgettext('invoice', 'Invoice total'), self.stylesheet['Bold']), '',
money_filter(total, self.invoice.event.currency) money_filter(total, self.invoice.event.currency)
]) ])
@@ -958,12 +947,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
pending_sum = self.invoice.order.pending_sum pending_sum = self.invoice.order.pending_sum
if pending_sum != total: if pending_sum != total:
tdata.append( tdata.append(
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Received payments')), self.stylesheet['Normal'])] + [PlainTextParagraph(pgettext('invoice', 'Received payments'), self.stylesheet['Normal'])] +
(['', '', ''] if has_taxes else ['']) + (['', '', ''] if has_taxes else ['']) +
[money_filter(pending_sum - total, self.invoice.event.currency)] [money_filter(pending_sum - total, self.invoice.event.currency)]
) )
tdata.append( tdata.append(
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Outstanding payments')), self.stylesheet['Bold'])] + [PlainTextParagraph(pgettext('invoice', 'Outstanding payments'), self.stylesheet['Bold'])] +
(['', '', ''] if has_taxes else ['']) + (['', '', ''] if has_taxes else ['']) +
[money_filter(pending_sum, self.invoice.event.currency)] [money_filter(pending_sum, self.invoice.event.currency)]
) )
@@ -980,12 +969,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
s=Sum('amount') s=Sum('amount')
)['s'] or Decimal('0.00') )['s'] or Decimal('0.00')
tdata.append( tdata.append(
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Paid by gift card')), self.stylesheet['Normal'])] + [PlainTextParagraph(pgettext('invoice', 'Paid by gift card'), self.stylesheet['Normal'])] +
(['', '', ''] if has_taxes else ['']) + (['', '', ''] if has_taxes else ['']) +
[money_filter(giftcard_sum, self.invoice.event.currency)] [money_filter(giftcard_sum, self.invoice.event.currency)]
) )
tdata.append( tdata.append(
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Remaining amount')), self.stylesheet['Bold'])] + [PlainTextParagraph(pgettext('invoice', 'Remaining amount'), self.stylesheet['Bold'])] +
(['', '', ''] if has_taxes else ['']) + (['', '', ''] if has_taxes else ['']) +
[money_filter(total - giftcard_sum, self.invoice.event.currency)] [money_filter(total - giftcard_sum, self.invoice.event.currency)]
) )
@@ -1008,14 +997,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story.append(Spacer(1, 10 * mm)) story.append(Spacer(1, 10 * mm))
if request_show_service_date: if request_show_service_date:
story.append(FontFallbackParagraph( story.append(PlainTextParagraph(
self._normalize(pgettext('invoice', 'Invoice period: {daterange}').format(daterange=request_show_service_date)), pgettext('invoice', 'Invoice period: {daterange}').format(daterange=request_show_service_date),
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
if self.invoice.payment_provider_text: if self.invoice.payment_provider_text:
story.append(FontFallbackParagraph( story.append(FontFallbackParagraph(
self._normalize(self.invoice.payment_provider_text), self._clean_text(self.invoice.payment_provider_text, tags=['br', 'b']),
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
@@ -1039,10 +1028,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
('FONTNAME', (0, 0), (-1, -1), self.font_regular), ('FONTNAME', (0, 0), (-1, -1), self.font_regular),
] ]
thead = [ thead = [
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['Fineprint']), PlainTextParagraph(pgettext('invoice', 'Tax rate'), self.stylesheet['Fineprint']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Net value')), self.stylesheet['FineprintRight']), PlainTextParagraph(pgettext('invoice', 'Net value'), self.stylesheet['FineprintRight']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Gross value')), self.stylesheet['FineprintRight']), PlainTextParagraph(pgettext('invoice', 'Gross value'), self.stylesheet['FineprintRight']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax')), self.stylesheet['FineprintRight']), PlainTextParagraph(pgettext('invoice', 'Tax'), self.stylesheet['FineprintRight']),
'' ''
] ]
tdata = [thead] tdata = [thead]
@@ -1053,7 +1042,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
continue continue
tax = taxvalue_map[idx] tax = taxvalue_map[idx]
tdata.append([ tdata.append([
FontFallbackParagraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']), PlainTextParagraph(localize(rate) + " % " + name, self.stylesheet['Fineprint']),
money_filter(gross - tax, self.invoice.event.currency), money_filter(gross - tax, self.invoice.event.currency),
money_filter(gross, self.invoice.event.currency), money_filter(gross, self.invoice.event.currency),
money_filter(tax, self.invoice.event.currency), money_filter(tax, self.invoice.event.currency),
@@ -1072,7 +1061,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
table.setStyle(TableStyle(tstyledata)) table.setStyle(TableStyle(tstyledata))
story.append(Spacer(5 * mm, 5 * mm)) story.append(Spacer(5 * mm, 5 * mm))
story.append(KeepTogether([ story.append(KeepTogether([
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Included taxes')), self.stylesheet['FineprintHeading']), PlainTextParagraph(pgettext('invoice', 'Included taxes'), self.stylesheet['FineprintHeading']),
table table
])) ]))
@@ -1089,7 +1078,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
net = gross - tax net = gross - tax
tdata.append([ tdata.append([
FontFallbackParagraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']), PlainTextParagraph(localize(rate) + " % " + name, self.stylesheet['Fineprint']),
fmt(net), fmt(gross), fmt(tax), '' fmt(net), fmt(gross), fmt(tax), ''
]) ])
@@ -1098,13 +1087,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story.append(KeepTogether([ story.append(KeepTogether([
Spacer(1, height=2 * mm), Spacer(1, height=2 * mm),
FontFallbackParagraph( PlainTextParagraph(
self._normalize(pgettext( pgettext(
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on ' 'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
'{date}, this corresponds to:' '{date}, this corresponds to:'
).format(rate=localize(self.invoice.foreign_currency_rate), ).format(rate=localize(self.invoice.foreign_currency_rate),
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"), authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"))), date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT")),
self.stylesheet['Fineprint'] self.stylesheet['Fineprint']
), ),
Spacer(1, height=3 * mm), Spacer(1, height=3 * mm),
@@ -1113,14 +1102,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate: elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate:
foreign_total = round_decimal(total * self.invoice.foreign_currency_rate) foreign_total = round_decimal(total * self.invoice.foreign_currency_rate)
story.append(Spacer(1, 5 * mm)) story.append(Spacer(1, 5 * mm))
story.append(FontFallbackParagraph(self._normalize( story.append(PlainTextParagraph(
pgettext( pgettext(
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on ' 'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
'{date}, the invoice total corresponds to {total}.' '{date}, the invoice total corresponds to {total}.'
).format(rate=localize(self.invoice.foreign_currency_rate), ).format(rate=localize(self.invoice.foreign_currency_rate),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"), date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"),
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"), authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
total=fmt(foreign_total))), total=fmt(foreign_total)),
self.stylesheet['Fineprint'] self.stylesheet['Fineprint']
)) ))
@@ -1162,11 +1151,8 @@ class Modern1Renderer(ClassicInvoiceRenderer):
def _draw_invoice_from(self, canvas): def _draw_invoice_from(self, canvas):
if not self.invoice.address_invoice_from: if not self.invoice.address_invoice_from:
return return
c = [ c = self.invoice.address_invoice_from.strip().split('\n')
self._clean_text(l) p = PlainTextParagraph(' · '.join(c), style=self.stylesheet['Sender'])
for l in self.invoice.address_invoice_from.strip().split('\n')
]
p = FontFallbackParagraph(self._normalize(' · '.join(c)), style=self.stylesheet['Sender'])
p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm) p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm)
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm) p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm)
super()._draw_invoice_from(canvas) super()._draw_invoice_from(canvas)
@@ -1225,8 +1211,8 @@ class Modern1Renderer(ClassicInvoiceRenderer):
_draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm, **kwargs) _draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm, **kwargs)
] ]
p = FontFallbackParagraph( p = PlainTextParagraph(
self._normalize(date_format(self.invoice.date, "DATE_FORMAT")), date_format(self.invoice.date, "DATE_FORMAT"),
style=ParagraphStyle(name=f'Normal{value_size}', fontName=self.font_regular, fontSize=value_size, leading=value_size * 1.2) style=ParagraphStyle(name=f'Normal{value_size}', fontName=self.font_regular, fontSize=value_size, leading=value_size * 1.2)
) )
w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize) w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize)
@@ -1283,7 +1269,7 @@ class Modern1SimplifiedRenderer(Modern1Renderer):
i = [] i = []
if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage: if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage:
i.append(FontFallbackParagraph( i.append(PlainTextParagraph(
pgettext('invoice', 'Event date: {date_range}').format( pgettext('invoice', 'Event date: {date_range}').format(
date_range=self.invoice.event.get_date_range_display(), date_range=self.invoice.event.get_date_range_display(),
), ),
@@ -44,7 +44,8 @@ class Command(Parent):
# Start the vite server in the background # Start the vite server in the background
vite_server = subprocess.Popen( vite_server = subprocess.Popen(
["npm", "run", "dev:control"], ["npm", "run", "dev:control"],
cwd=Path(__file__).parent.parent.parent.parent.parent cwd=Path(__file__).parent.parent.parent.parent.parent,
stdin=subprocess.DEVNULL
) )
def cleanup(): def cleanup():
+20 -14
View File
@@ -26,6 +26,7 @@ from django.utils.translation import gettext_lazy as _
class BaseMediaType: class BaseMediaType:
medium_created_by_server = False medium_created_by_server = False
medium_created_from_unknown_supported = False
supports_orderposition = False supports_orderposition = False
supports_giftcard = False supports_giftcard = False
@@ -56,7 +57,7 @@ class BaseMediaType:
def is_active(self, organizer): def is_active(self, organizer):
return organizer.settings.get(f'reusable_media_type_{self.identifier}', as_type=bool, default=False) return organizer.settings.get(f'reusable_media_type_{self.identifier}', as_type=bool, default=False)
def handle_unknown(self, organizer, identifier, user, auth): def handle_unknown(self, organizer, identifier, user, auth, force_create=False):
pass pass
def handle_new(self, organizer, medium, user, auth): def handle_new(self, organizer, medium, user, auth):
@@ -88,23 +89,32 @@ class NfcUidMediaType(BaseMediaType):
verbose_name = _('NFC UID-based') verbose_name = _('NFC UID-based')
icon = 'pretixbase/img/media/nfc_uid.svg' icon = 'pretixbase/img/media/nfc_uid.svg'
medium_created_by_server = False medium_created_by_server = False
medium_created_from_unknown_supported = True
supports_giftcard = True supports_giftcard = True
supports_orderposition = False supports_orderposition = True
def handle_unknown(self, organizer, identifier, user, auth): def handle_unknown(self, organizer, identifier, user, auth, force_create=False):
from pretix.base.models import GiftCard, ReusableMedium from pretix.base.models import GiftCard, ReusableMedium
if organizer.settings.get(f'reusable_media_type_{self.identifier}_autocreate_giftcard', as_type=bool): create_giftcard = organizer.settings.get(f'reusable_media_type_{self.identifier}_autocreate_giftcard', as_type=bool)
if create_giftcard or force_create:
if identifier.startswith("08"): if identifier.startswith("08"):
# Don't create gift cards for NFC UIDs that start with 08, which represents NFC cards that issue random # Don't create gift cards for NFC UIDs that start with 08, which represents NFC cards that issue random
# UIDs on every read, so they won't be useful. # UIDs on every read, so they won't be useful.
return return
with transaction.atomic(): with transaction.atomic():
gc = GiftCard.objects.create( if create_giftcard:
issuer=organizer, gc = GiftCard.objects.create(
expires=organizer.default_gift_card_expiry, issuer=organizer,
currency=organizer.settings.get(f'reusable_media_type_{self.identifier}_autocreate_giftcard_currency'), expires=organizer.default_gift_card_expiry,
) currency=organizer.settings.get(f'reusable_media_type_{self.identifier}_autocreate_giftcard_currency'),
)
gc.log_action(
'pretix.giftcards.created',
user=user, auth=auth,
)
else:
gc = None
m = ReusableMedium.objects.create( m = ReusableMedium.objects.create(
type=self.identifier, type=self.identifier,
identifier=identifier, identifier=identifier,
@@ -116,10 +126,6 @@ class NfcUidMediaType(BaseMediaType):
'pretix.reusable_medium.created.auto', 'pretix.reusable_medium.created.auto',
user=user, auth=auth, user=user, auth=auth,
) )
gc.log_action(
'pretix.giftcards.created',
user=user, auth=auth,
)
return m return m
@@ -129,7 +135,7 @@ class NfcMf0aesMediaType(BaseMediaType):
icon = 'pretixbase/img/media/nfc_secure.svg' icon = 'pretixbase/img/media/nfc_secure.svg'
medium_created_by_server = False medium_created_by_server = False
supports_giftcard = True supports_giftcard = True
supports_orderposition = False supports_orderposition = True
def handle_new(self, organizer, medium, user, auth): def handle_new(self, organizer, medium, user, auth):
from pretix.base.models import GiftCard from pretix.base.models import GiftCard
+4 -2
View File
@@ -282,10 +282,12 @@ def metric_values():
# Throwaway metrics # Throwaway metrics
exact_tables = [ exact_tables = [
Order, OrderPosition, Invoice, Event, Organizer Order, Invoice, Event, Organizer
] ]
for m in apps.get_models(): # Count all models for m in apps.get_models(): # Count all models
if any(issubclass(m, p) for p in exact_tables): if issubclass(m, OrderPosition):
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = m.all.count()
elif any(issubclass(m, p) for p in exact_tables):
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = m.objects.count() metrics['pretix_model_instances']['{model="%s"}' % m._meta] = m.objects.count()
else: else:
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = estimate_count_fast(m) metrics['pretix_model_instances']['{model="%s"}' % m._meta] = estimate_count_fast(m)
+14 -4
View File
@@ -74,6 +74,7 @@ class LocaleMiddleware(MiddlewareMixin):
def process_request(self, request: HttpRequest): def process_request(self, request: HttpRequest):
language = get_language_from_request(request) language = get_language_from_request(request)
region = None
# Normally, this middleware runs *before* the event is set. However, on event frontend pages it # Normally, this middleware runs *before* the event is set. However, on event frontend pages it
# might be run a second time by pretix.presale.EventMiddleware and in this case the event is already # might be run a second time by pretix.presale.EventMiddleware and in this case the event is already
# set and can be taken into account for the decision. # set and can be taken into account for the decision.
@@ -94,15 +95,16 @@ class LocaleMiddleware(MiddlewareMixin):
if '-' not in language and settings_holder.settings.region: if '-' not in language and settings_holder.settings.region:
language += '-' + settings_holder.settings.region language += '-' + settings_holder.settings.region
if settings_holder.settings.region: if settings_holder.settings.region:
set_region(settings_holder.settings.region) region = settings_holder.settings.region
else: else:
gs = global_settings_object(request) gs = global_settings_object(request)
if '-' not in language and gs.settings.region: if '-' not in language and gs.settings.region:
language += '-' + gs.settings.region language += '-' + gs.settings.region
if gs.settings.region: if gs.settings.region:
set_region(gs.settings.region) region = gs.settings.region
translation.activate(language) translation.activate(language)
set_region(region)
request.LANGUAGE_CODE = get_language_without_region() request.LANGUAGE_CODE = get_language_without_region()
tzname = None tzname = None
@@ -368,8 +370,16 @@ class RejectInvalidInputMiddleware(MiddlewareMixin):
if "\x00" in request.META['QUERY_STRING'] or "%00" in request.META['QUERY_STRING']: if "\x00" in request.META['QUERY_STRING'] or "%00" in request.META['QUERY_STRING']:
raise BadRequest("Invalid characters in input.") raise BadRequest("Invalid characters in input.")
if request.method in ('POST', 'PUT', 'PATCH') and request.content_type == "application/x-www-form-urlencoded": if request.method in ('POST', 'PUT', 'PATCH') and request.content_type == "application/x-www-form-urlencoded":
if any("\x00" in value for key, value_list in request.POST.lists() for value in value_list): try:
raise BadRequest("Invalid characters in input.") post_data = request.POST.lists()
except BadRequest:
# Reading request.POST wasn't possible, probably an invalid charset. Django will crash once we actually
# use request.POST, but if we don't, let's not crash it (required for some weird payment provider
# webhooks, e.g. computop).
pass
else:
if any("\x00" in value for key, value_list in post_data for value in value_list):
raise BadRequest("Invalid characters in input.")
class CustomCommonMiddleware(CommonMiddleware): class CustomCommonMiddleware(CommonMiddleware):
+3 -3
View File
@@ -57,7 +57,7 @@ from django_otp.models import Device
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.helpers.urls import build_absolute_uri from pretix.helpers.urls import mainreverse_absolute
from ...helpers.countries import FastCountryField from ...helpers.countries import FastCountryField
from ...helpers.u2f import pub_key_from_der, websafe_decode from ...helpers.u2f import pub_key_from_der, websafe_decode
@@ -378,7 +378,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
{ {
'user': self, 'user': self,
'messages': msg, 'messages': msg,
'url': build_absolute_uri('control:user.settings'), 'url': mainreverse_absolute('control:user.settings'),
'instance': settings.PRETIX_INSTANCE_NAME, 'instance': settings.PRETIX_INSTANCE_NAME,
}, },
event=None, event=None,
@@ -466,7 +466,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
{ {
'instance': settings.PRETIX_INSTANCE_NAME, 'instance': settings.PRETIX_INSTANCE_NAME,
'user': self, 'user': self,
'url': (build_absolute_uri('control:auth.forgot.recover') 'url': (mainreverse_absolute('control:auth.forgot.recover')
+ '?id=%d&token=%s' % (self.id, default_token_generator.make_token(self))) + '?id=%d&token=%s' % (self.id, default_token_generator.make_token(self)))
}, },
None, locale=self.locale, user=self None, locale=self.locale, user=self
+6
View File
@@ -346,11 +346,14 @@ class Checkin(models.Model):
REASON_INCOMPLETE = 'incomplete' REASON_INCOMPLETE = 'incomplete'
REASON_ALREADY_REDEEMED = 'already_redeemed' REASON_ALREADY_REDEEMED = 'already_redeemed'
REASON_AMBIGUOUS = 'ambiguous' REASON_AMBIGUOUS = 'ambiguous'
REASON_MEDIUM_INVALID = 'medium_invalid'
REASON_MEDIUM_EXISTS = 'medium_exists'
REASON_ERROR = 'error' REASON_ERROR = 'error'
REASON_BLOCKED = 'blocked' REASON_BLOCKED = 'blocked'
REASON_UNAPPROVED = 'unapproved' REASON_UNAPPROVED = 'unapproved'
REASON_INVALID_TIME = 'invalid_time' REASON_INVALID_TIME = 'invalid_time'
REASON_ANNULLED = 'annulled' REASON_ANNULLED = 'annulled'
REASON_ALREADY_EXCHANGED = 'already_exchanged'
REASONS = ( REASONS = (
(REASON_CANCELED, _('Order canceled')), (REASON_CANCELED, _('Order canceled')),
(REASON_INVALID, _('Unknown ticket')), (REASON_INVALID, _('Unknown ticket')),
@@ -366,6 +369,9 @@ class Checkin(models.Model):
(REASON_UNAPPROVED, _('Order not approved')), (REASON_UNAPPROVED, _('Order not approved')),
(REASON_INVALID_TIME, _('Ticket not valid at this time')), (REASON_INVALID_TIME, _('Ticket not valid at this time')),
(REASON_ANNULLED, _('Check-in annulled')), (REASON_ANNULLED, _('Check-in annulled')),
(REASON_ALREADY_EXCHANGED, _('Ticket already exchanged')),
(REASON_MEDIUM_INVALID, _('Reusable medium invalid')),
(REASON_MEDIUM_EXISTS, _('Reusable medium already exists')),
) )
successful = models.BooleanField( successful = models.BooleanField(
+4 -4
View File
@@ -167,7 +167,7 @@ class Customer(LoggedModel):
def send_security_notice(self, message, email=None): def send_security_notice(self, message, email=None):
from pretix.base.services.mail import SendMailException, mail from pretix.base.services.mail import SendMailException, mail
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
try: try:
with language(self.locale): with language(self.locale):
@@ -178,7 +178,7 @@ class Customer(LoggedModel):
{ {
**self.get_email_context(), **self.get_email_context(),
'message': str(message), 'message': str(message),
'url': build_absolute_uri(self.organizer, 'presale:organizer.customer.index') 'url': eventreverse_absolute(self.organizer, 'presale:organizer.customer.index')
}, },
customer=self, customer=self,
organizer=self.organizer, organizer=self.organizer,
@@ -299,12 +299,12 @@ class Customer(LoggedModel):
def send_activation_mail(self): def send_activation_mail(self):
from pretix.base.services.mail import mail from pretix.base.services.mail import mail
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.presale.forms.customer import TokenGenerator from pretix.presale.forms.customer import TokenGenerator
ctx = self.get_email_context() ctx = self.get_email_context()
token = TokenGenerator().make_token(self) token = TokenGenerator().make_token(self)
ctx['url'] = build_absolute_uri( ctx['url'] = eventreverse_absolute(
self.organizer, self.organizer,
'presale:organizer.customer.activate' 'presale:organizer.customer.activate'
) + '?id=' + self.identifier + '&token=' + token ) + '?id=' + self.identifier + '&token=' + token
+18 -5
View File
@@ -724,7 +724,7 @@ class Event(EventMixin, LoggedModel):
@property @property
def social_image(self): def social_image(self):
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
img = None img = None
logo_file = self.settings.get('logo_image', as_type=str, default='')[7:] logo_file = self.settings.get('logo_image', as_type=str, default='')[7:]
@@ -742,7 +742,7 @@ class Event(EventMixin, LoggedModel):
logger.exception(f'Failed to create thumbnail of {logo_file}') logger.exception(f'Failed to create thumbnail of {logo_file}')
img = default_storage.url(logo_file) img = default_storage.url(logo_file)
if img: if img:
return urljoin(build_absolute_uri(self, 'presale:event.index'), img) return urljoin(eventreverse_absolute(self, 'presale:event.index'), img)
def _seats(self, ignore_voucher=None): def _seats(self, ignore_voucher=None):
from .seating import Seat from .seating import Seat
@@ -883,6 +883,8 @@ class Event(EventMixin, LoggedModel):
ItemProgramTime, ItemVariationMetaValue, Question, Quota, ItemProgramTime, ItemVariationMetaValue, Question, Quota,
) )
is_cross_organizer = other.organizer_id != self.organizer_id
# Note: avoid self.set_active_plugins(), it causes trouble e.g. for the badges plugin. # Note: avoid self.set_active_plugins(), it causes trouble e.g. for the badges plugin.
# Plugins can create data in installed() hook based on existing data of the event. # Plugins can create data in installed() hook based on existing data of the event.
# Calling set_active_plugins() results in defaults being created while actually data # Calling set_active_plugins() results in defaults being created while actually data
@@ -911,6 +913,15 @@ class Event(EventMixin, LoggedModel):
for emv in EventMetaValue.objects.filter(event=other): for emv in EventMetaValue.objects.filter(event=other):
emv.pk = None emv.pk = None
emv.event = self emv.event = self
if is_cross_organizer:
try:
emv.property = self.organizer.meta_properties.get(name=emv.property.name)
except EventMetaProperty.DoesNotExist:
meta_prop = emv.property
meta_prop.pk = None
meta_prop.organizer = self.organizer
meta_prop.save(force_insert=True)
emv.property = meta_prop
emv.save(force_insert=True) emv.save(force_insert=True)
for fl in EventFooterLink.objects.filter(event=other): for fl in EventFooterLink.objects.filter(event=other):
@@ -964,13 +975,13 @@ class Event(EventMixin, LoggedModel):
if i.tax_rule_id: if i.tax_rule_id:
i.tax_rule = tax_map[i.tax_rule_id] i.tax_rule = tax_map[i.tax_rule_id]
if i.grant_membership_type and other.organizer_id != self.organizer_id: if i.grant_membership_type and is_cross_organizer:
i.grant_membership_type = None i.grant_membership_type = None
i.save() # no force_insert since i.picture.save could have already inserted i.save() # no force_insert since i.picture.save could have already inserted
i.log_action('pretix.object.cloned') i.log_action('pretix.object.cloned')
if require_membership_types and other.organizer_id == self.organizer_id: if require_membership_types and not is_cross_organizer:
i.require_membership_types.set(require_membership_types) i.require_membership_types.set(require_membership_types)
if not i.all_sales_channels: if not i.all_sales_channels:
@@ -985,7 +996,7 @@ class Event(EventMixin, LoggedModel):
v._prefetched_objects_cache = {} v._prefetched_objects_cache = {}
v.save(force_insert=True) v.save(force_insert=True)
if require_membership_types and other.organizer_id == self.organizer_id: if require_membership_types and not is_cross_organizer:
v.require_membership_types.set(require_membership_types) v.require_membership_types.set(require_membership_types)
if not v.all_sales_channels: if not v.all_sales_channels:
v.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels])) v.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels]))
@@ -1869,6 +1880,8 @@ class EventMetaValue(LoggedModel):
self.event.cache.clear() self.event.cache.clear()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.event and self.event.organizer != self.property.organizer:
raise ValidationError(_("Property and event must belong to the same organizer."))
super().save(*args, **kwargs) super().save(*args, **kwargs)
if self.event: if self.event:
self.event.cache.clear() self.event.cache.clear()
+16 -6
View File
@@ -452,11 +452,16 @@ class Item(LoggedModel):
MEDIA_POLICY_REUSE = 'reuse' MEDIA_POLICY_REUSE = 'reuse'
MEDIA_POLICY_NEW = 'new' MEDIA_POLICY_NEW = 'new'
MEDIA_POLICY_REUSE_OR_NEW = 'reuse_or_new' MEDIA_POLICY_REUSE_OR_NEW = 'reuse_or_new'
MEDIA_POLICY_APPEND = 'append'
MEDIA_POLICY_APPEND_OR_NEW = 'append_or_new'
MEDIA_POLICIES = ( MEDIA_POLICIES = (
(None, _("Don't use re-usable media, use regular one-off tickets")), (None, _("Don't use reusable media, use regular one-off tickets")),
(MEDIA_POLICY_REUSE, _('Require an existing medium to be re-used')),
(MEDIA_POLICY_NEW, _('Require a previously unknown medium to be newly added')), (MEDIA_POLICY_NEW, _('Require a previously unknown medium to be newly added')),
(MEDIA_POLICY_REUSE_OR_NEW, _('Require either an existing or a new medium to be used')), (MEDIA_POLICY_REUSE, _('Require an existing medium to be reused, replacing any previous tickets')),
(MEDIA_POLICY_REUSE_OR_NEW, _('Require either an existing or a new medium to be used, replacing any previous tickets')),
(MEDIA_POLICY_APPEND, _('Require an existing medium to be reused, adding to any previous tickets')),
(MEDIA_POLICY_APPEND_OR_NEW,
_('Require either an existing or a new medium to be used, adding to any previous tickets')),
) )
objects = ItemQuerySetManager() objects = ItemQuerySetManager()
@@ -769,7 +774,7 @@ class Item(LoggedModel):
null=True, blank=True, max_length=16, null=True, blank=True, max_length=16,
verbose_name=_('Reusable media policy'), verbose_name=_('Reusable media policy'),
help_text=_( help_text=_(
'If this product should be stored on a re-usable physical medium, you can attach a physical media policy. ' 'If this product should be stored on a reusable physical medium, you can attach a physical media policy. '
'This is not required for regular tickets, which just use a one-time barcode, but only for products like ' 'This is not required for regular tickets, which just use a one-time barcode, but only for products like '
'renewable season tickets or re-chargeable gift card wristbands. ' 'renewable season tickets or re-chargeable gift card wristbands. '
'This is an advanced feature that also requires specific configuration of ticketing and printing settings.' 'This is an advanced feature that also requires specific configuration of ticketing and printing settings.'
@@ -778,7 +783,7 @@ class Item(LoggedModel):
media_type = models.CharField( media_type = models.CharField(
max_length=100, max_length=100,
null=True, blank=True, null=True, blank=True,
choices=[(None, _("Don't use re-usable media, use regular one-off tickets"))] + [(k, v) for k, v in MEDIA_TYPES.items()], choices=[(None, _("Don't use reusable media, use regular one-off tickets"))] + [(k, v) for k, v in MEDIA_TYPES.items()],
verbose_name=_('Reusable media type'), verbose_name=_('Reusable media type'),
help_text=_( help_text=_(
'Select the type of physical medium that should be used for this product. Note that not all media types ' 'Select the type of physical medium that should be used for this product. Note that not all media types '
@@ -995,6 +1000,11 @@ class Item(LoggedModel):
raise ValidationError(_('The selected media type does not support usage for tickets currently.')) raise ValidationError(_('The selected media type does not support usage for tickets currently.'))
if not mt.supports_giftcard and issue_giftcard: if not mt.supports_giftcard and issue_giftcard:
raise ValidationError(_('The selected media type does not support usage for gift cards currently.')) raise ValidationError(_('The selected media type does not support usage for gift cards currently.'))
if media_policy in (Item.MEDIA_POLICY_NEW, Item.MEDIA_POLICY_APPEND_OR_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW):
if not mt.medium_created_by_server and not mt.medium_created_from_unknown_supported:
raise ValidationError(_('The selected media type requires all media to be registered in the system '
'prior to their usage. Therefore, the selected media policy does not make '
'sense for this media type.'))
if issue_giftcard: if issue_giftcard:
raise ValidationError(_('You currently cannot create gift cards with a reusable media policy. Instead, ' raise ValidationError(_('You currently cannot create gift cards with a reusable media policy. Instead, '
'gift cards for some reusable media types can be created or re-charged directly ' 'gift cards for some reusable media types can be created or re-charged directly '
@@ -2220,7 +2230,7 @@ class Quota(LoggedModel):
class ItemMetaProperty(LoggedModel): class ItemMetaProperty(LoggedModel):
""" """
An event can have ItemMetaProperty objects attached to define meta information fields An event can have ItemMetaProperty objects attached to define meta information fields
for its items. This information can be re-used for example in ticket layouts. for its items. This information can be reused for example in ticket layouts.
:param event: The event this property is defined for. :param event: The event this property is defined for.
:type event: Event :type event: Event
+4 -1
View File
@@ -129,7 +129,10 @@ class ReusableMedium(LoggedModel):
@property @property
def is_expired(self): def is_expired(self):
return self.expires and self.expires > now() return self.expires and self.expires < now()
def touch(self):
self.save(update_fields=['updated'])
class Meta: class Meta:
unique_together = (("identifier", "type", "organizer"),) unique_together = (("identifier", "type", "organizer"),)
+50 -28
View File
@@ -354,38 +354,60 @@ class Order(LockModel, LoggedModel):
def _transaction_key_reset(self): def _transaction_key_reset(self):
self.__initial_status_paid_or_pending = self.status in (Order.STATUS_PENDING, Order.STATUS_PAID) and not self.require_approval self.__initial_status_paid_or_pending = self.status in (Order.STATUS_PENDING, Order.STATUS_PAID) and not self.require_approval
def gracefully_delete(self, user=None, auth=None): @classmethod
from . import GiftCard, GiftCardTransaction, Membership, Voucher def gracefully_delete_bulk(cls, event, orders, user=None, auth=None):
# Expects to be called in a transaction
if not self.testmode: from . import (
raise TypeError("Only test mode orders can be deleted.") GiftCard, GiftCardTransaction, LogEntry, Membership, Voucher,
self.log_action(
'pretix.event.order.deleted', user=user, auth=auth,
data={
'code': self.code,
}
) )
order_gracefully_delete.send(self.event, order=self) if not transaction.get_connection().in_atomic_block:
raise Exception('gracefully_delete_bulk should only be called in atomic transaction!')
if self.status != Order.STATUS_CANCELED: logs_create = []
for position in self.positions.all(): for o in orders:
if position.voucher: if not o.testmode:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1)) raise TypeError("Only test mode orders can be deleted.")
order_gracefully_delete.send(event, order=o)
logs_create.append(o.log_action(
'pretix.event.order.deleted', user=user, auth=auth,
data={
'code': o.code,
},
save=False,
))
LogEntry.bulk_create_and_postprocess(logs_create)
GiftCardTransaction.objects.filter(payment__in=self.payments.all()).update(payment=None) voucher_ids = OrderPosition.objects.filter(
GiftCardTransaction.objects.filter(refund__in=self.refunds.all()).update(refund=None) order__in=orders,
GiftCardTransaction.objects.filter(order=self).update(order=None) voucher__isnull=False
GiftCard.objects.filter(issued_in__in=self.positions.all()).update(issued_in=None) ).exclude(order__status=Order.STATUS_CANCELED).values_list("voucher_id", flat=True)
Membership.objects.filter(granted_in__order=self, testmode=True).update(granted_in=None) voucher_usages = Counter(voucher_ids)
OrderPosition.all.filter(order=self, addon_to__isnull=False).delete() for v_id, usage_count in voucher_usages.items():
OrderPosition.all.filter(order=self).delete() Voucher.objects.filter(pk=v_id).update(redeemed=Greatest(0, F('redeemed') - usage_count))
OrderFee.all.filter(order=self).delete()
Transaction.objects.filter(order=self).delete() GiftCardTransaction.objects.filter(payment__order__in=orders).update(payment=None)
self.refunds.all().delete() GiftCardTransaction.objects.filter(refund__order__in=orders).update(refund=None)
self.payments.all().delete() GiftCardTransaction.objects.filter(order__in=orders).update(order=None)
self.event.cache.delete('complain_testmode_orders') GiftCard.objects.filter(issued_in__order__in=orders).update(issued_in=None)
self.delete() Membership.objects.filter(granted_in__order__in=orders, testmode=True).update(granted_in=None)
OrderPosition.all.filter(order__in=orders, addon_to__isnull=False).delete()
OrderPosition.all.filter(order__in=orders).delete()
OrderFee.all.filter(order__in=orders).delete()
Transaction.objects.filter(order__in=orders).delete()
OrderRefund.objects.filter(order__in=orders).delete()
OrderPayment.objects.filter(order__in=orders).delete()
if isinstance(orders, models.QuerySet):
orders.delete()
else:
Order.objects.filter(pk__in=[o.pk for o in orders]).delete()
event.cache.delete('complain_testmode_orders')
def gracefully_delete(self, user=None, auth=None):
if not self.testmode:
raise TypeError("Only test mode orders can be deleted.")
Order.gracefully_delete_bulk(self.event, Order.objects.filter(pk=self.pk), user, auth)
def email_confirm_secret(self): def email_confirm_secret(self):
return self.tagged_secret("email_confirm", 9) return self.tagged_secret("email_confirm", 9)
+6 -3
View File
@@ -118,7 +118,10 @@ class SeatingPlan(LoggedModel):
for zi, z in enumerate(self.layout_data['zones']): for zi, z in enumerate(self.layout_data['zones']):
zpos = (z['position']['x'], z['position']['y']) zpos = (z['position']['x'], z['position']['y'])
for ri, r in enumerate(z['rows']): for ri, r in enumerate(z['rows']):
rpos = (zpos[0] + r['position']['x'], zpos[1] + r['position']['y']) rpos = (
zpos[0] + r.get('position', {}).get('x', 0),
zpos[1] + r.get('position', {}).get('y', 0),
)
row_label = None row_label = None
if r.get('row_label'): if r.get('row_label'):
row_label = r['row_label'].replace("%s", r.get('row_number', str(ri))) row_label = r['row_label'].replace("%s", r.get('row_number', str(ri)))
@@ -147,8 +150,8 @@ class SeatingPlan(LoggedModel):
zone=z['name'], zone=z['name'],
category=s['category'], category=s['category'],
sorting_rank=rank, sorting_rank=rank,
x=rpos[0] + s['position']['x'], x=rpos[0] + s.get('position', {}).get('x', 0),
y=rpos[1] + s['position']['y'], y=rpos[1] + s.get('position', {}).get('y', 0),
) )
+2 -2
View File
@@ -43,7 +43,7 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.models import Event, LogEntry from pretix.base.models import Event, LogEntry
from pretix.base.signals import register_notification_types from pretix.base.signals import register_notification_types
from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.money import money_filter
from pretix.helpers.urls import build_absolute_uri from pretix.helpers.urls import mainreverse_absolute
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_ALL_TYPES = None _ALL_TYPES = None
@@ -170,7 +170,7 @@ class ParametrizedOrderNotificationType(NotificationType):
def build_notification(self, logentry: LogEntry): def build_notification(self, logentry: LogEntry):
order = logentry.content_object order = logentry.content_object
order_url = build_absolute_uri( order_url = mainreverse_absolute(
'control:event.order', 'control:event.order',
kwargs={ kwargs={
'organizer': logentry.event.organizer.slug, 'organizer': logentry.event.organizer.slug,
+2 -2
View File
@@ -71,7 +71,7 @@ from pretix.helpers import OF_SELF
from pretix.helpers.countries import CachedCountries from pretix.helpers.countries import CachedCountries
from pretix.helpers.format import format_map from pretix.helpers.format import format_map
from pretix.helpers.money import DecimalTextInput from pretix.helpers.money import DecimalTextInput
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.presale.views import get_cart from pretix.presale.views import get_cart
from pretix.presale.views.cart import cart_session, get_or_create_cart_id from pretix.presale.views.cart import cart_session, get_or_create_cart_id
@@ -379,7 +379,7 @@ class BasePaymentProvider:
if not self.settings.get('_hidden_seed'): if not self.settings.get('_hidden_seed'):
self.settings.set('_hidden_seed', get_random_string(64)) self.settings.set('_hidden_seed', get_random_string(64))
hidden_url = build_absolute_uri(self.event, 'presale:event.payment.unlock', kwargs={ hidden_url = eventreverse_absolute(self.event, 'presale:event.payment.unlock', kwargs={
'hash': hashlib.sha256((self.settings._hidden_seed + self.event.slug).encode()).hexdigest(), 'hash': hashlib.sha256((self.settings._hidden_seed + self.event.slug).encode()).hexdigest(),
}) })
+6 -1
View File
@@ -372,6 +372,11 @@ DEFAULT_VARIABLES = OrderedDict((
"editor_sample": _("Atlantis"), "editor_sample": _("Atlantis"),
"evaluate": lambda op, order, ev: str(getattr(order.invoice_address.country, 'name', '')) if getattr(order, 'invoice_address', None) else '' "evaluate": lambda op, order, ev: str(getattr(order.invoice_address.country, 'name', '')) if getattr(order, 'invoice_address', None) else ''
}), }),
("invoice_custom_field", {
"label": _("Invoice custom recipient field"),
"editor_sample": _("Custom recipient field"),
"evaluate": lambda op, order, ev: order.invoice_address.custom_field if getattr(order, 'invoice_address', None) else ''
}),
("addons", { ("addons", {
"label": _("List of Add-Ons"), "label": _("List of Add-Ons"),
"editor_sample": _("Add-on 1\n2x Add-on 2"), "editor_sample": _("Add-on 1\n2x Add-on 2"),
@@ -1062,7 +1067,7 @@ class Renderer:
except: except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text))) logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
p = Paragraph(text, style=style) p = Paragraph(text, style=style) # not using AutoEscapeParagraph is safe as we escape above
return p, ad, lineheight return p, ad, lineheight
def _draw_textcontainer(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict): def _draw_textcontainer(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
+2 -2
View File
@@ -287,11 +287,11 @@ def _check_position_constraints(
raise CartPositionError(error_messages['unavailable']) raise CartPositionError(error_messages['unavailable'])
# Invalid media policy for online sale # Invalid media policy for online sale
if item.media_policy in (Item.MEDIA_POLICY_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW): if item.media_policy in (Item.MEDIA_POLICY_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW, Item.MEDIA_POLICY_APPEND_OR_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW):
mt = MEDIA_TYPES[item.media_type] mt = MEDIA_TYPES[item.media_type]
if not mt.medium_created_by_server: if not mt.medium_created_by_server:
raise CartPositionError(error_messages['media_usage_not_implemented']) raise CartPositionError(error_messages['media_usage_not_implemented'])
elif item.media_policy == Item.MEDIA_POLICY_REUSE: elif item.media_policy in (Item.MEDIA_POLICY_REUSE, Item.MEDIA_POLICY_APPEND):
raise CartPositionError(error_messages['media_usage_not_implemented']) raise CartPositionError(error_messages['media_usage_not_implemented'])
# Item removed from sales channel # Item removed from sales channel
+30 -2
View File
@@ -867,6 +867,15 @@ class RequiredQuestionsError(Exception):
super().__init__(msg) super().__init__(msg)
class RequiredMediaExchangeError(Exception):
def __init__(self, msg, code, media_policy, media_type):
self.msg = msg
self.code = code
self.media_policy = media_policy
self.media_type = media_type
super().__init__(msg)
def _save_answers(op, answers, given_answers): def _save_answers(op, answers, given_answers):
def _create_answer(question, answer): def _create_answer(question, answer):
try: try:
@@ -939,7 +948,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True, ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True,
user=None, auth=None, canceled_supported=False, type=Checkin.TYPE_ENTRY, user=None, auth=None, canceled_supported=False, type=Checkin.TYPE_ENTRY,
raw_barcode=None, raw_source_type=None, from_revoked_secret=False, simulate=False, raw_barcode=None, raw_source_type=None, from_revoked_secret=False, simulate=False,
gate=None): gate=None, reusable_medium=None):
""" """
Create a checkin for this particular order position and check-in list. Fails with CheckInError if the check in is Create a checkin for this particular order position and check-in list. Fails with CheckInError if the check in is
not valid at this time. not valid at this time.
@@ -955,6 +964,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
:param datetime: The datetime of the checkin, defaults to now. :param datetime: The datetime of the checkin, defaults to now.
:param simulate: If true, the check-in is not saved. :param simulate: If true, the check-in is not saved.
:param gate: The gate the check-in was performed at. :param gate: The gate the check-in was performed at.
:param reusable_medium: The medium that is available for an exchange
""" """
# !!!!!!!!! # !!!!!!!!!
@@ -1035,7 +1045,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
with transaction.atomic(): with transaction.atomic():
# Lock order positions, if it is an entry. We don't need it for exits, as a race condition wouldn't be problematic # Lock order positions, if it is an entry. We don't need it for exits, as a race condition wouldn't be problematic
opqs = OrderPosition.all opqs = OrderPosition.all.select_related("order", "item")
if type != Checkin.TYPE_EXIT: if type != Checkin.TYPE_EXIT:
opqs = opqs.select_for_update(of=OF_SELF) opqs = opqs.select_for_update(of=OF_SELF)
op = opqs.get(pk=op.pk) op = opqs.get(pk=op.pk)
@@ -1101,6 +1111,24 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
require_answers require_answers
) )
required_media_policy = op.item.media_policy
required_media_type = op.item.media_type
require_a_medium = required_media_policy and required_media_type
linked_media = op.linked_media
if require_a_medium and not reusable_medium and not force:
if not linked_media.exists():
raise RequiredMediaExchangeError(
_('Ticket needs to be exchanged to a suitable medium.'),
'exchange',
required_media_policy,
required_media_type
)
elif op.organizer.settings.reusable_media_usage_enforced:
raise CheckInError(
_('This ticket has already been exchanged for a reusable medium that now needs to be used instead.'),
'already_exchanged',
)
device = None device = None
if isinstance(auth, Device): if isinstance(auth, Device):
device = auth device = auth
+3 -3
View File
@@ -51,7 +51,7 @@ from pretix.base.signals import (
) )
from pretix.celery_app import app from pretix.celery_app import app
from pretix.helpers import OF_SELF, repeatable_reads_transaction from pretix.helpers import OF_SELF, repeatable_reads_transaction
from pretix.helpers.urls import build_absolute_uri from pretix.helpers.urls import mainreverse_absolute
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -455,7 +455,7 @@ def scheduled_organizer_export(self, organizer: Organizer, schedule: int) -> Non
schedule, schedule,
organizer, organizer,
exporter, exporter,
build_absolute_uri( mainreverse_absolute(
'control:organizer.export', 'control:organizer.export',
kwargs={ kwargs={
'organizer': organizer.slug, 'organizer': organizer.slug,
@@ -481,7 +481,7 @@ def scheduled_event_export(self, event: Event, schedule: int) -> None:
schedule, schedule,
event, event,
exporter, exporter,
build_absolute_uri( mainreverse_absolute(
'control:event.orders.export', 'control:event.orders.export',
kwargs={ kwargs={
'event': event.slug, 'event': event.slug,
+3 -3
View File
@@ -85,7 +85,7 @@ from pretix.helpers.format import (
FormattedString, PlainHtmlAlternativeString, SafeFormatter, format_map, FormattedString, PlainHtmlAlternativeString, SafeFormatter, format_map,
) )
from pretix.helpers.hierarkey import clean_filename from pretix.helpers.hierarkey import clean_filename
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.presale.ical import get_private_icals from pretix.presale.ical import get_private_icals
logger = logging.getLogger('pretix.base.mail') logger = logging.getLogger('pretix.base.mail')
@@ -997,7 +997,7 @@ def _wrap_plain_body(content_plain, signature, event, order, position, no_order_
body_plain += _( body_plain += _(
"You can view your order details at the following URL:\n{orderurl}." "You can view your order details at the following URL:\n{orderurl}."
).replace("\n", "\r\n").format( ).replace("\n", "\r\n").format(
orderurl=build_absolute_uri( orderurl=eventreverse_absolute(
order.event, 'presale:event.order.position', kwargs={ order.event, 'presale:event.order.position', kwargs={
'order': order.code, 'order': order.code,
'secret': position.web_secret, 'secret': position.web_secret,
@@ -1013,7 +1013,7 @@ def _wrap_plain_body(content_plain, signature, event, order, position, no_order_
body_plain += _( body_plain += _(
"You can view your order details at the following URL:\n{orderurl}." "You can view your order details at the following URL:\n{orderurl}."
).replace("\n", "\r\n").format( ).replace("\n", "\r\n").format(
event=event.name, orderurl=build_absolute_uri( event=event.name, orderurl=eventreverse_absolute(
order.event, 'presale:event.order.open', kwargs={ order.event, 'presale:event.order.open', kwargs={
'order': order.code, 'order': order.code,
'secret': order.secret, 'secret': order.secret,
+176 -2
View File
@@ -23,10 +23,13 @@ import secrets
from django.db import IntegrityError from django.db import IntegrityError
from django.db.models import Q from django.db.models import Q
from django.utils.translation import gettext as _
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from pretix.base.models import GiftCardAcceptance from pretix.base.media import MEDIA_TYPES
from pretix.base.models.media import MediumKeySet from pretix.base.models import Checkin, GiftCardAcceptance, Item
from pretix.base.models.media import MediumKeySet, ReusableMedium
from pretix.base.services.checkin import CheckInError
def create_nfc_mf0aes_keyset(organizer): def create_nfc_mf0aes_keyset(organizer):
@@ -70,3 +73,174 @@ def get_keysets_for_organizer(organizer):
if new_set: if new_set:
sets.append(new_set) sets.append(new_set)
return sets return sets
def perform_media_exchange(organizer, media_type, identifier, link_orderposition, user, auth):
"""
Create or retrieve a medium, then link the order position to it. Expected to be called in a transaction.
:param organizer: Organizer to operate in
:param media_type: Type of medium to operate with
:param identifier: Identifier of the medium
:param link_orderposition: Position to link to the medium
:return: ReusableMedium
"""
medium = None
media_policy = link_orderposition.item.media_policy
if media_type not in MEDIA_TYPES: # should be caught by serializer already
raise CheckInError(
_('Invalid medium type.'),
Checkin.REASON_ERROR,
reason=_('Invalid medium type.'),
)
if not MEDIA_TYPES[media_type].is_active(organizer):
raise CheckInError(
_('Medium type is not enabled for organizer.'),
Checkin.REASON_ERROR,
reason=_('Medium type is not enabled for organizer.'),
)
if link_orderposition.item.media_type != media_type:
raise CheckInError(
_('Incorrect medium type for product.'),
Checkin.REASON_PRODUCT,
reason=_('Incorrect medium type for product.'),
)
if link_orderposition.linked_media.exists():
raise CheckInError(
_('Ticket is already exchanged for reusable medium.'),
Checkin.REASON_ALREADY_EXCHANGED,
reason=_('Ticket is already exchanged for reusable medium.'),
)
if media_policy in (Item.MEDIA_POLICY_APPEND, Item.MEDIA_POLICY_APPEND_OR_NEW, Item.MEDIA_POLICY_NEW):
link_action = "append"
else:
link_action = "replace"
if media_policy in (Item.MEDIA_POLICY_REUSE, Item.MEDIA_POLICY_APPEND):
try:
medium = ReusableMedium.objects.get(
type=media_type,
identifier=identifier,
organizer=organizer,
)
except ReusableMedium.DoesNotExist:
raise CheckInError(
_('Reusable medium not found.'),
Checkin.REASON_MEDIUM_INVALID,
reason=_('Reusable medium not found.'),
)
else:
if medium.is_expired or not medium.active:
raise CheckInError(
_('Reusable medium is inactive or expired.'),
Checkin.REASON_MEDIUM_INVALID,
reason=_('Reusable medium is inactive or expired.'),
)
elif media_policy in (Item.MEDIA_POLICY_REUSE_OR_NEW, Item.MEDIA_POLICY_APPEND_OR_NEW):
try:
medium = ReusableMedium.objects.get(
type=media_type,
identifier=identifier,
organizer=organizer,
)
except ReusableMedium.DoesNotExist:
if not MEDIA_TYPES[media_type].medium_created_from_unknown_supported:
raise CheckInError(
_('Reusable medium not found and could not be created.'),
Checkin.REASON_MEDIUM_INVALID,
)
medium = MEDIA_TYPES[media_type].handle_unknown(organizer, identifier, user, auth, force_create=True)
if not medium:
raise CheckInError(
_('Reusable medium not found and could not be created.'),
Checkin.REASON_MEDIUM_INVALID,
)
if medium.is_expired or not medium.active:
raise CheckInError(
_('Reusable medium is inactive or expired.'),
Checkin.REASON_MEDIUM_INVALID,
reason=_('Reusable medium is inactive or expired.'),
)
elif media_policy == Item.MEDIA_POLICY_NEW:
if not MEDIA_TYPES[media_type].medium_created_from_unknown_supported:
raise CheckInError(
_('Reusable medium not found and could not be created.'),
Checkin.REASON_MEDIUM_INVALID,
)
try:
medium = MEDIA_TYPES[media_type].handle_unknown(organizer, identifier, user, auth, force_create=True)
except IntegrityError:
raise CheckInError(
_('Reusable medium already exists.'),
Checkin.REASON_MEDIUM_EXISTS,
)
else:
if not medium:
raise CheckInError(
_('Reusable medium could not be created.'),
Checkin.REASON_MEDIUM_INVALID,
)
else:
raise CheckInError(
_('Product does not support medium exchange.'),
Checkin.REASON_PRODUCT,
reason=_('Product does not support medium exchange.'),
)
if link_action == 'append':
medium.linked_orderpositions.add(link_orderposition)
medium.log_action(
'pretix.reusable_medium.linked_orderposition.added',
user=user,
auth=auth,
data={
'linked_orderposition': link_orderposition,
}
)
elif link_action == 'replace':
already_found = False
for op_pk in medium.linked_orderpositions.values_list('pk', flat=True):
if op_pk == link_orderposition.pk:
already_found = True
continue
else:
medium.log_action(
'pretix.reusable_medium.linked_orderposition.removed',
data={
'linked_orderposition': op_pk,
}
)
if not already_found:
medium.linked_orderpositions.set([link_orderposition])
medium.log_action(
'pretix.reusable_medium.linked_orderposition.added',
user=user,
auth=auth,
data={
'linked_orderposition': link_orderposition,
}
)
link_orderposition.order.log_action(
'pretix.reusable_medium.exchanged',
data={
'position': link_orderposition.pk,
'positionid': link_orderposition.positionid,
'medium': medium.pk,
'medium_identifier': medium.identifier,
'medium_type': medium.media_type.identifier,
}
)
medium.touch()
return medium
+3 -3
View File
@@ -37,7 +37,7 @@ from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask
from pretix.base.signals import notification from pretix.base.signals import notification
from pretix.celery_app import app from pretix.celery_app import app
from pretix.helpers.celery import get_task_priority from pretix.helpers.celery import get_task_priority
from pretix.helpers.urls import build_absolute_uri from pretix.helpers.urls import mainreverse_absolute
@app.task(base=TransactionAwareTask, acks_late=True, max_retries=9, default_retry_delay=900) @app.task(base=TransactionAwareTask, acks_late=True, max_retries=9, default_retry_delay=900)
@@ -136,10 +136,10 @@ def send_notification_mail(notification: Notification, user: User):
'site_url': settings.SITE_URL, 'site_url': settings.SITE_URL,
'color': settings.PRETIX_PRIMARY_COLOR, 'color': settings.PRETIX_PRIMARY_COLOR,
'notification': notification, 'notification': notification,
'settings_url': build_absolute_uri( 'settings_url': mainreverse_absolute(
'control:user.settings.notifications', 'control:user.settings.notifications',
), ),
'disable_url': build_absolute_uri( 'disable_url': mainreverse_absolute(
'control:user.settings.notifications.off', 'control:user.settings.notifications.off',
kwargs={ kwargs={
'token': user.notifications_token, 'token': user.notifications_token,
+1 -1
View File
@@ -3506,7 +3506,7 @@ def signal_listener_issue_media(sender: Event, order: Order, **kwargs):
from pretix.base.models import ReusableMedium from pretix.base.models import ReusableMedium
for p in order.positions.all(): for p in order.positions.all():
if p.item.media_policy in (Item.MEDIA_POLICY_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW): if p.item.media_policy in (Item.MEDIA_POLICY_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW, Item.MEDIA_POLICY_APPEND_OR_NEW):
mt = MEDIA_TYPES[p.item.media_type] mt = MEDIA_TYPES[p.item.media_type]
if mt.medium_created_by_server and not p.linked_media.exists(): if mt.medium_created_by_server and not p.linked_media.exists():
rm = ReusableMedium.objects.create( rm = ReusableMedium.objects.create(
+29 -29
View File
@@ -327,7 +327,7 @@ def get_best_name(position_or_address, parts=False):
@receiver(register_text_placeholders, dispatch_uid="pretixbase_register_text_placeholders") @receiver(register_text_placeholders, dispatch_uid="pretixbase_register_text_placeholders")
def base_placeholders(sender, **kwargs): def base_placeholders(sender, **kwargs):
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
def _event_sample(event): def _event_sample(event):
if event.has_subevents: if event.has_subevents:
@@ -388,14 +388,14 @@ def base_placeholders(sender, **kwargs):
lambda event: LazyDate(now() + timedelta(days=15)) lambda event: LazyDate(now() + timedelta(days=15))
), ),
SimpleFunctionalTextPlaceholder( SimpleFunctionalTextPlaceholder(
'url', ['order', 'event'], lambda order, event: build_absolute_uri( 'url', ['order', 'event'], lambda order, event: eventreverse_absolute(
event, event,
'presale:event.order.open', kwargs={ 'presale:event.order.open', kwargs={
'order': order.code, 'order': order.code,
'secret': order.secret, 'secret': order.secret,
'hash': order.email_confirm_secret() 'hash': order.email_confirm_secret()
} }
), lambda event: build_absolute_uri( ), lambda event: eventreverse_absolute(
event, event,
'presale:event.order.open', kwargs={ 'presale:event.order.open', kwargs={
'order': 'F8VVL', 'order': 'F8VVL',
@@ -406,7 +406,7 @@ def base_placeholders(sender, **kwargs):
), ),
SimpleButtonPlaceholder( SimpleButtonPlaceholder(
'url_button', ['order', 'event'], 'url_button', ['order', 'event'],
url_func=lambda order, event: build_absolute_uri( url_func=lambda order, event: eventreverse_absolute(
event, event,
'presale:event.order.open', kwargs={ 'presale:event.order.open', kwargs={
'order': order.code, 'order': order.code,
@@ -415,7 +415,7 @@ def base_placeholders(sender, **kwargs):
} }
), ),
text_func=lambda order, event: _("View order details"), text_func=lambda order, event: _("View order details"),
sample_url_func=lambda event: build_absolute_uri( sample_url_func=lambda event: eventreverse_absolute(
event, event,
'presale:event.order.open', kwargs={ 'presale:event.order.open', kwargs={
'order': 'F8VVL', 'order': 'F8VVL',
@@ -426,13 +426,13 @@ def base_placeholders(sender, **kwargs):
sample_text_func=lambda event: _("View order details"), sample_text_func=lambda event: _("View order details"),
), ),
SimpleFunctionalTextPlaceholder( SimpleFunctionalTextPlaceholder(
'url_info_change', ['order', 'event'], lambda order, event: build_absolute_uri( 'url_info_change', ['order', 'event'], lambda order, event: eventreverse_absolute(
event, event,
'presale:event.order.modify', kwargs={ 'presale:event.order.modify', kwargs={
'order': order.code, 'order': order.code,
'secret': order.secret, 'secret': order.secret,
} }
), lambda event: build_absolute_uri( ), lambda event: eventreverse_absolute(
event, event,
'presale:event.order.modify', kwargs={ 'presale:event.order.modify', kwargs={
'order': 'F8VVL', 'order': 'F8VVL',
@@ -441,13 +441,13 @@ def base_placeholders(sender, **kwargs):
), ),
), ),
SimpleFunctionalTextPlaceholder( SimpleFunctionalTextPlaceholder(
'url_products_change', ['order', 'event'], lambda order, event: build_absolute_uri( 'url_products_change', ['order', 'event'], lambda order, event: eventreverse_absolute(
event, event,
'presale:event.order.change', kwargs={ 'presale:event.order.change', kwargs={
'order': order.code, 'order': order.code,
'secret': order.secret, 'secret': order.secret,
} }
), lambda event: build_absolute_uri( ), lambda event: eventreverse_absolute(
event, event,
'presale:event.order.change', kwargs={ 'presale:event.order.change', kwargs={
'order': 'F8VVL', 'order': 'F8VVL',
@@ -456,13 +456,13 @@ def base_placeholders(sender, **kwargs):
), ),
), ),
SimpleFunctionalTextPlaceholder( SimpleFunctionalTextPlaceholder(
'url_cancel', ['order', 'event'], lambda order, event: build_absolute_uri( 'url_cancel', ['order', 'event'], lambda order, event: eventreverse_absolute(
event, event,
'presale:event.order.cancel', kwargs={ 'presale:event.order.cancel', kwargs={
'order': order.code, 'order': order.code,
'secret': order.secret, 'secret': order.secret,
} }
), lambda event: build_absolute_uri( ), lambda event: eventreverse_absolute(
event, event,
'presale:event.order.cancel', kwargs={ 'presale:event.order.cancel', kwargs={
'order': 'F8VVL', 'order': 'F8VVL',
@@ -471,7 +471,7 @@ def base_placeholders(sender, **kwargs):
), ),
), ),
SimpleFunctionalTextPlaceholder( SimpleFunctionalTextPlaceholder(
'url', ['event', 'position'], lambda event, position: build_absolute_uri( 'url', ['event', 'position'], lambda event, position: eventreverse_absolute(
event, event,
'presale:event.order.position', 'presale:event.order.position',
kwargs={ kwargs={
@@ -480,7 +480,7 @@ def base_placeholders(sender, **kwargs):
'position': position.positionid 'position': position.positionid
} }
), ),
lambda event: build_absolute_uri( lambda event: eventreverse_absolute(
event, event,
'presale:event.order.position', kwargs={ 'presale:event.order.position', kwargs={
'order': 'F8VVL', 'order': 'F8VVL',
@@ -491,7 +491,7 @@ def base_placeholders(sender, **kwargs):
), ),
SimpleButtonPlaceholder( SimpleButtonPlaceholder(
'url_button', ['event', 'position'], 'url_button', ['event', 'position'],
url_func=lambda event, position: build_absolute_uri( url_func=lambda event, position: eventreverse_absolute(
event, event,
'presale:event.order.position', kwargs={ 'presale:event.order.position', kwargs={
'order': position.order.code, 'order': position.order.code,
@@ -500,7 +500,7 @@ def base_placeholders(sender, **kwargs):
} }
), ),
text_func=lambda event, position: _("View registration details"), text_func=lambda event, position: _("View registration details"),
sample_url_func=lambda event: build_absolute_uri( sample_url_func=lambda event: eventreverse_absolute(
event, event,
'presale:event.order.position', kwargs={ 'presale:event.order.position', kwargs={
'order': 'F8VVL', 'order': 'F8VVL',
@@ -511,14 +511,14 @@ def base_placeholders(sender, **kwargs):
sample_text_func=lambda event: _("View registration details"), sample_text_func=lambda event: _("View registration details"),
), ),
SimpleFunctionalTextPlaceholder( SimpleFunctionalTextPlaceholder(
'url_info_change', ['position', 'event'], lambda position, event: build_absolute_uri( 'url_info_change', ['position', 'event'], lambda position, event: eventreverse_absolute(
event, event,
'presale:event.order.position.modify', kwargs={ 'presale:event.order.position.modify', kwargs={
'order': position.order.code, 'order': position.order.code,
'secret': position.web_secret, 'secret': position.web_secret,
'position': position.positionid 'position': position.positionid
} }
), lambda event: build_absolute_uri( ), lambda event: eventreverse_absolute(
event, event,
'presale:event.order.position.modify', kwargs={ 'presale:event.order.position.modify', kwargs={
'order': 'F8VVL', 'order': 'F8VVL',
@@ -528,14 +528,14 @@ def base_placeholders(sender, **kwargs):
), ),
), ),
SimpleFunctionalTextPlaceholder( SimpleFunctionalTextPlaceholder(
'url_products_change', ['position', 'event'], lambda position, event: build_absolute_uri( 'url_products_change', ['position', 'event'], lambda position, event: eventreverse_absolute(
event, event,
'presale:event.order.position.change', kwargs={ 'presale:event.order.position.change', kwargs={
'order': position.order.code, 'order': position.order.code,
'secret': position.web_secret, 'secret': position.web_secret,
'position': position.positionid 'position': position.positionid
} }
), lambda event: build_absolute_uri( ), lambda event: eventreverse_absolute(
event, event,
'presale:event.order.position.change', kwargs={ 'presale:event.order.position.change', kwargs={
'order': 'F8VVL', 'order': 'F8VVL',
@@ -581,20 +581,20 @@ def base_placeholders(sender, **kwargs):
), ),
SimpleFunctionalTextPlaceholder( SimpleFunctionalTextPlaceholder(
'url_remove', ['waiting_list_voucher', 'event'], 'url_remove', ['waiting_list_voucher', 'event'],
lambda waiting_list_voucher, event: build_absolute_uri( lambda waiting_list_voucher, event: eventreverse_absolute(
event, 'presale:event.waitinglist.remove' event, 'presale:event.waitinglist.remove'
) + '?voucher=' + waiting_list_voucher.code, ) + '?voucher=' + waiting_list_voucher.code,
lambda event: build_absolute_uri( lambda event: eventreverse_absolute(
event, event,
'presale:event.waitinglist.remove', 'presale:event.waitinglist.remove',
) + '?voucher=68CYU2H6ZTP3WLK5', ) + '?voucher=68CYU2H6ZTP3WLK5',
), ),
SimpleFunctionalTextPlaceholder( SimpleFunctionalTextPlaceholder(
'url', ['waiting_list_voucher', 'event'], 'url', ['waiting_list_voucher', 'event'],
lambda waiting_list_voucher, event: build_absolute_uri( lambda waiting_list_voucher, event: eventreverse_absolute(
event, 'presale:event.redeem' event, 'presale:event.redeem'
) + '?voucher=' + waiting_list_voucher.code, ) + '?voucher=' + waiting_list_voucher.code,
lambda event: build_absolute_uri( lambda event: eventreverse_absolute(
event, event,
'presale:event.redeem', 'presale:event.redeem',
) + '?voucher=68CYU2H6ZTP3WLK5', ) + '?voucher=68CYU2H6ZTP3WLK5',
@@ -611,7 +611,7 @@ def base_placeholders(sender, **kwargs):
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join( 'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
'* {} - {}'.format( '* {} - {}'.format(
order.full_code, order.full_code,
build_absolute_uri(event, 'presale:event.order.open', kwargs={ eventreverse_absolute(event, 'presale:event.order.open', kwargs={
'event': event.slug, 'event': event.slug,
'organizer': event.organizer.slug, 'organizer': event.organizer.slug,
'order': order.code, 'order': order.code,
@@ -623,7 +623,7 @@ def base_placeholders(sender, **kwargs):
), lambda event: '\n' + '\n\n'.join( ), lambda event: '\n' + '\n\n'.join(
'* {} - {}'.format( '* {} - {}'.format(
'{}-{}'.format(event.slug.upper(), order['code']), '{}-{}'.format(event.slug.upper(), order['code']),
build_absolute_uri(event, 'presale:event.order.open', kwargs={ eventreverse_absolute(event, 'presale:event.order.open', kwargs={
'event': event.slug, 'event': event.slug,
'organizer': event.organizer.slug, 'organizer': event.organizer.slug,
'order': order['code'], 'order': order['code'],
@@ -662,13 +662,13 @@ def base_placeholders(sender, **kwargs):
# join vouchers with two spaces at end of line so markdown-parser inserts a <br> # join vouchers with two spaces at end of line so markdown-parser inserts a <br>
'voucher_url_list', ['event', 'voucher_list'], 'voucher_url_list', ['event', 'voucher_list'],
lambda event, voucher_list: ' \n'.join([ lambda event, voucher_list: ' \n'.join([
build_absolute_uri( eventreverse_absolute(
event, 'presale:event.redeem' event, 'presale:event.redeem'
) + '?voucher=' + c ) + '?voucher=' + c
for c in voucher_list for c in voucher_list
]), ]),
lambda event: ' \n'.join([ lambda event: ' \n'.join([
build_absolute_uri( eventreverse_absolute(
event, 'presale:event.redeem' event, 'presale:event.redeem'
) + '?voucher=' + c ) + '?voucher=' + c
for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2'] for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2']
@@ -676,10 +676,10 @@ def base_placeholders(sender, **kwargs):
inline=False, inline=False,
), ),
SimpleFunctionalTextPlaceholder( SimpleFunctionalTextPlaceholder(
'url', ['event', 'voucher_list'], lambda event, voucher_list: build_absolute_uri(event, 'presale:event.index', kwargs={ 'url', ['event', 'voucher_list'], lambda event, voucher_list: eventreverse_absolute(event, 'presale:event.index', kwargs={
'event': event.slug, 'event': event.slug,
'organizer': event.organizer.slug, 'organizer': event.organizer.slug,
}), lambda event: build_absolute_uri(event, 'presale:event.index', kwargs={ }), lambda event: eventreverse_absolute(event, 'presale:event.index', kwargs={
'event': event.slug, 'event': event.slug,
'organizer': event.organizer.slug, 'organizer': event.organizer.slug,
}) })
+2 -2
View File
@@ -37,7 +37,7 @@ from pretix.base.services.mail import mail
from pretix.base.settings import GlobalSettingsObject from pretix.base.settings import GlobalSettingsObject
from pretix.base.signals import periodic_task from pretix.base.signals import periodic_task
from pretix.celery_app import app from pretix.celery_app import app
from pretix.helpers.urls import build_absolute_uri from pretix.helpers.urls import mainreverse_absolute
@receiver(signal=periodic_task) @receiver(signal=periodic_task)
@@ -121,7 +121,7 @@ def send_update_notification_email():
) )
), ),
{ {
'url': build_absolute_uri('control:global.update') 'url': mainreverse_absolute('control:global.update')
}, },
) )
+16 -3
View File
@@ -211,12 +211,25 @@ DEFAULTS = {
'form_class': forms.BooleanField, 'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Activate re-usable media"), label=_("Activate reusable media"),
help_text=_("The re-usable media feature allows you to connect tickets and gift cards with physical media " help_text=_("The reusable media feature allows you to connect tickets and gift cards with physical media "
"such as wristbands or chip cards that may be re-used for different tickets or gift cards " "such as wristbands or chip cards that may be reused for different tickets or gift cards "
"later.") "later.")
) )
}, },
'reusable_media_usage_enforced': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Enforce the usage of issued reusable media for check-in"),
help_text=_("If enabled, a ticket barcode will not be accepted anymore, if a reusable medium has been "
"created and linked to a ticket. Keeping this option turned off will treat the reusable "
"medium and ticket as equals."),
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_settings-reusable_media_active'}),
)
},
'reusable_media_type_barcode': { 'reusable_media_type_barcode': {
'default': 'False', 'default': 'False',
'type': bool, 'type': bool,
+1 -1
View File
@@ -11,6 +11,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}"> <link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}">
<script type="text/javascript" src="{% static "pretixbase/js/errors.js" %}"></script>
{% block custom_header %}{% endblock %} {% block custom_header %}{% endblock %}
{% if css_theme %} {% if css_theme %}
<link rel="stylesheet" type="text/css" href="{{ css_theme }}" /> <link rel="stylesheet" type="text/css" href="{{ css_theme }}" />
@@ -21,5 +22,4 @@
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>
</body> </body>
<script src="{% static "pretixbase/js/errors.js" %}"></script>
</html> </html>
@@ -2,13 +2,14 @@
{% load i18n %} {% load i18n %}
{% load rich_text %} {% load rich_text %}
{% load static %} {% load static %}
{% load wrap_in %}
{% block title %}{% trans "Redirect" %}{% endblock %} {% block title %}{% trans "Redirect" %}{% endblock %}
{% block content %} {% block content %}
<i class="fa fa-link fa-fw big-icon"></i> <i class="fa fa-link fa-fw big-icon"></i>
<div class="error-details"> <div class="error-details">
<h1>{% trans "Redirect" %}</h1> <h1>{% trans "Redirect" %}</h1>
<h3> <h3>
{% blocktrans trimmed with host="<strong>"|add:hostname|add:"</strong>"|safe %} {% blocktrans trimmed with host=hostname|wrap_in:'strong' %}
The link you clicked on wants to redirect you to a destination on the website {{ host }}. The link you clicked on wants to redirect you to a destination on the website {{ host }}.
{% endblocktrans %} {% endblocktrans %}
{% blocktrans trimmed %} {% blocktrans trimmed %}
+2 -2
View File
@@ -80,7 +80,7 @@ from pretix.control.forms.widgets import Select2
from pretix.helpers.countries import CachedCountries from pretix.helpers.countries import CachedCountries
from pretix.multidomain.models import AlternativeDomainAssignment, KnownDomain from pretix.multidomain.models import AlternativeDomainAssignment, KnownDomain
from pretix.multidomain.urlreverse import ( from pretix.multidomain.urlreverse import (
build_absolute_uri, get_organizer_domain, eventreverse_absolute, get_organizer_domain,
) )
from pretix.plugins.banktransfer.payment import BankTransfer from pretix.plugins.banktransfer.payment import BankTransfer
from pretix.presale.style import get_fonts from pretix.presale.style import get_fonts
@@ -219,7 +219,7 @@ class EventWizardBasicsForm(I18nModelForm):
self.fields['location'].widget.attrs['placeholder'] = _( self.fields['location'].widget.attrs['placeholder'] = _(
'Sample Conference Center\nHeidelberg, Germany' 'Sample Conference Center\nHeidelberg, Germany'
) )
self.fields['slug'].widget.prefix = build_absolute_uri(self.organizer, 'presale:organizer.index') self.fields['slug'].widget.prefix = eventreverse_absolute(self.organizer, 'presale:organizer.index')
self.fields['tax_rate']._required = True # Do not render as optional because it is conditionally required self.fields['tax_rate']._required = True # Do not render as optional because it is conditionally required
if self.has_subevents: if self.has_subevents:
del self.fields['presale_start'] del self.fields['presale_start']
+7 -1
View File
@@ -1342,7 +1342,13 @@ class QuestionAnswerFilterForm(forms.Form):
opqs = opqs.filter(canceled=False) opqs = opqs.filter(canceled=False)
if fdata.get("item", "") != "": if fdata.get("item", "") != "":
i = fdata.get("item", "") i = fdata.get("item", "")
opqs = opqs.filter(item_id__in=(i,)) if '-' in i:
opqs = opqs.filter(
item_id=i.split('-')[0],
variation_id=i.split('-')[1],
)
else:
opqs = opqs.filter(item_id=i)
return opqs return opqs
+3 -2
View File
@@ -88,7 +88,7 @@ from pretix.control.forms.event import (
) )
from pretix.control.forms.widgets import Select2, Select2Multiple from pretix.control.forms.widgets import Select2, Select2Multiple
from pretix.multidomain.models import KnownDomain from pretix.multidomain.models import KnownDomain
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
class OrganizerForm(I18nModelForm): class OrganizerForm(I18nModelForm):
@@ -636,6 +636,7 @@ class OrganizerSettingsForm(SettingsForm):
'cookie_consent_dialog_button_yes', 'cookie_consent_dialog_button_yes',
'cookie_consent_dialog_button_no', 'cookie_consent_dialog_button_no',
'reusable_media_active', 'reusable_media_active',
'reusable_media_usage_enforced',
'reusable_media_type_barcode', 'reusable_media_type_barcode',
'reusable_media_type_barcode_identifier_length', 'reusable_media_type_barcode_identifier_length',
'reusable_media_type_nfc_uid', 'reusable_media_type_nfc_uid',
@@ -790,7 +791,7 @@ class MailSettingsForm(SettingsForm):
} }
if 'url' in base_parameters: if 'url' in base_parameters:
placeholders['url'] = build_absolute_uri( placeholders['url'] = eventreverse_absolute(
self.organizer, self.organizer,
'presale:organizer.customer.activate' 'presale:organizer.customer.activate'
) + '?token=' + get_random_string(30) ) + '?token=' + get_random_string(30)
+1
View File
@@ -746,6 +746,7 @@ class CoreUserImpersonatedLogEntryType(UserImpersonatedLogEntryType):
'pretix.reusable_medium.linked_orderposition.added': _('A new ticket has been added to the medium.'), 'pretix.reusable_medium.linked_orderposition.added': _('A new ticket has been added to the medium.'),
'pretix.reusable_medium.linked_orderposition.removed': _('A ticket has been removed from the medium.'), 'pretix.reusable_medium.linked_orderposition.removed': _('A ticket has been removed from the medium.'),
'pretix.reusable_medium.linked_orderposition.changed': _('The medium has been connected to a new ticket.'), 'pretix.reusable_medium.linked_orderposition.changed': _('The medium has been connected to a new ticket.'),
'pretix.reusable_medium.exchanged': _('The ticket #{positionid} was exchanged for reusable medium {medium_identifier}.'),
'pretix.reusable_medium.linked_giftcard.changed': _('The medium has been connected to a new gift card.'), 'pretix.reusable_medium.linked_giftcard.changed': _('The medium has been connected to a new gift card.'),
'pretix.email.error': _('Sending of an email has failed.'), 'pretix.email.error': _('Sending of an email has failed.'),
'pretix.event.comment': _('The event\'s internal comment has been updated.'), 'pretix.event.comment': _('The event\'s internal comment has been updated.'),
+23 -22
View File
@@ -38,6 +38,7 @@ from pretix import __version__
from pretix.base.models import Order, OrderPayment, Transaction from pretix.base.models import Order, OrderPayment, Transaction
from pretix.base.plugins import get_all_plugins from pretix.base.plugins import get_all_plugins
from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.money import money_filter
from pretix.helpers.reportlab import PlainTextParagraph
from pretix.plugins.reports.exporters import ReportlabExportMixin from pretix.plugins.reports.exporters import ReportlabExportMixin
from pretix.settings import DATA_DIR from pretix.settings import DATA_DIR
@@ -79,23 +80,23 @@ class SysReport(ReportlabExportMixin):
style_small.fontSize = 6 style_small.fontSize = 6
story = [ story = [
Paragraph("System report", headlinestyle), PlainTextParagraph("System report", headlinestyle),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
Paragraph("Usage", subheadlinestyle), PlainTextParagraph("Usage", subheadlinestyle),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
self._usage_table(), self._usage_table(),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
Paragraph("Installed versions", subheadlinestyle), PlainTextParagraph("Installed versions", subheadlinestyle),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
self._tech_table(), self._tech_table(),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
Paragraph("Plugins", subheadlinestyle), PlainTextParagraph("Plugins", subheadlinestyle),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
Paragraph(self._get_plugin_versions(), style_small), PlainTextParagraph(self._get_plugin_versions(), style_small),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
Paragraph("Custom templates", subheadlinestyle), PlainTextParagraph("Custom templates", subheadlinestyle),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
Paragraph(self._get_custom_templates(), style_small), PlainTextParagraph(self._get_custom_templates(), style_small),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
] ]
@@ -121,13 +122,13 @@ class SysReport(ReportlabExportMixin):
("RIGHTPADDING", (-1, 0), (-1, -1), 0), ("RIGHTPADDING", (-1, 0), (-1, -1), 0),
] ]
tdata = [ tdata = [
[Paragraph("Site URL:", style), Paragraph(settings.SITE_URL, style)], [PlainTextParagraph("Site URL:", style), Paragraph(settings.SITE_URL, style)],
[Paragraph("pretix version:", style), Paragraph(__version__, style)], [PlainTextParagraph("pretix version:", style), Paragraph(__version__, style)],
[Paragraph("Python version:", style), Paragraph(sys.version, style)], [PlainTextParagraph("Python version:", style), Paragraph(sys.version, style)],
[Paragraph("Platform:", style), Paragraph(platform.platform(), style)], [PlainTextParagraph("Platform:", style), Paragraph(platform.platform(), style)],
[ [
Paragraph("Database engine:", style), PlainTextParagraph("Database engine:", style),
Paragraph(settings.DATABASES["default"]["ENGINE"], style), PlainTextParagraph(settings.DATABASES["default"]["ENGINE"], style),
], ],
] ]
table = Table(tdata, colWidths=colwidths, repeatRows=0) table = Table(tdata, colWidths=colwidths, repeatRows=0)
@@ -206,7 +207,7 @@ class SysReport(ReportlabExportMixin):
year_last = now().year year_last = now().year
tdata = [ tdata = [
[ [
Paragraph(l, style_small_head) PlainTextParagraph(l, style_small_head)
for l in ( for l in (
"Time frame", "Time frame",
"Currency", "Currency",
@@ -257,19 +258,19 @@ class SysReport(ReportlabExportMixin):
tdata.append( tdata.append(
( (
Paragraph( PlainTextParagraph(
date_format(first_day, "M Y") date_format(first_day, "M Y")
+ " " + " "
+ date_format(after_day - timedelta(days=1), "M Y"), + date_format(after_day - timedelta(days=1), "M Y"),
style_small, style_small,
), ),
Paragraph(c, style_small), PlainTextParagraph(c, style_small),
Paragraph(str(orders_count), style_small) if i == 0 else "", PlainTextParagraph(str(orders_count), style_small) if i == 0 else "",
Paragraph(money_filter(revenue_data.get("s_net") or 0, c), style_small), PlainTextParagraph(money_filter(revenue_data.get("s_net") or 0, c), style_small),
Paragraph(str(testmode_count), style_small) if i == 0 else "", PlainTextParagraph(str(testmode_count), style_small) if i == 0 else "",
Paragraph(str(unconfirmed_count), style_small) if i == 0 else "", PlainTextParagraph(str(unconfirmed_count), style_small) if i == 0 else "",
Paragraph(str(revenue_data.get("c") or 0), style_small), PlainTextParagraph(str(revenue_data.get("c") or 0), style_small),
Paragraph(money_filter(revenue_data.get("s_gross") or 0, c), style_small), PlainTextParagraph(money_filter(revenue_data.get("s_gross") or 0, c), style_small),
) )
) )
@@ -66,6 +66,7 @@
<script type="text/javascript" src="{% static "lightbox/js/lightbox.js" %}"></script> <script type="text/javascript" src="{% static "lightbox/js/lightbox.js" %}"></script>
<script type="text/javascript" src="{% static "are-you-sure/jquery.are-you-sure.js" %}"></script> <script type="text/javascript" src="{% static "are-you-sure/jquery.are-you-sure.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/addressform.js" %}"></script> <script type="text/javascript" src="{% static "pretixbase/js/addressform.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/errors.js" %}"></script>
{% endcompress %} {% endcompress %}
{{ html_head|safe }} {{ html_head|safe }}
@@ -54,6 +54,8 @@
<span class="fa fa-check-circle"></span> <span class="fa fa-check-circle"></span>
{% elif result.status == "incomplete" %} {% elif result.status == "incomplete" %}
<span class="fa fa-question-circle"></span> <span class="fa fa-question-circle"></span>
{% elif result.status == "exchange" %}
<span class="fa fa-recycle"></span>
{% elif result.status == "error" %} {% elif result.status == "error" %}
{% if result.reason == "already_redeemed" %} {% if result.reason == "already_redeemed" %}
<span class="fa fa-warning"></span> <span class="fa fa-warning"></span>
@@ -79,6 +81,14 @@
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
{% elif result.status == "exchange" %}
<h3 class="nomargin-top">{% trans "Media exchange required" %}</h3>
<p>
{% blocktrans trimmed with media_policy=media_policies|getitem:result.media_policy media_type=media_types|getitem:result.media_type %}
This ticket needs to be exchanged into a <strong>{{ media_type }}</strong> reusable medium.
<strong>{{ media_policy }}</strong>.
{% endblocktrans %}
</p>
{% elif result.status == "error" %} {% elif result.status == "error" %}
<h3 class="nomargin-top">{{ reason_labels|getitem:result.reason }}</h3> <h3 class="nomargin-top">{{ reason_labels|getitem:result.reason }}</h3>
{% if result.reason_explanation %} {% if result.reason_explanation %}
@@ -222,6 +222,7 @@
<fieldset> <fieldset>
<legend>{% trans "Reusable media" %}</legend> <legend>{% trans "Reusable media" %}</legend>
{% bootstrap_field sform.reusable_media_active layout="control" %} {% bootstrap_field sform.reusable_media_active layout="control" %}
{% bootstrap_field sform.reusable_media_usage_enforced layout="control" %}
<div data-display-dependency="#{{ sform.reusable_media_active.id_for_label }}"> <div data-display-dependency="#{{ sform.reusable_media_active.id_for_label }}">
<div class="panel panel-default"> <div class="panel panel-default">
@@ -2,6 +2,7 @@
{% load i18n %} {% load i18n %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load getitem %} {% load getitem %}
{% load icon %}
{% block inner %} {% block inner %}
{% if team %} {% if team %}
<h1>{% trans "Team:" %} {{ team.name }}</h1> <h1>{% trans "Team:" %} {{ team.name }}</h1>
@@ -25,6 +26,18 @@
<legend>{% trans "Organizer permissions" %}</legend> <legend>{% trans "Organizer permissions" %}</legend>
{% bootstrap_field form.all_organizer_permissions layout="control" %} {% bootstrap_field form.all_organizer_permissions layout="control" %}
<div class="team-permission-groups col-md-9 col-md-offset-3" data-display-dependency="#id_all_organizer_permissions" data-inverse> <div class="team-permission-groups col-md-9 col-md-offset-3" data-display-dependency="#id_all_organizer_permissions" data-inverse>
<p class="text-muted">
{% icon "info-circle" %}
{% blocktrans trimmed %}
Even if a team has no access to a certain category of data, they might still be able to see
parts of this data when it is linked to data they can see.
{% endblocktrans %}
{% blocktrans trimmed %}
For example, someone with access to customer accounts will be able to see some information
about gift cards linked to a customer account, even if they generally can't see gift cards
directly.
{% endblocktrans %}
</p>
{% for f in form.organizer_field_names %} {% for f in form.organizer_field_names %}
{% bootstrap_field form|getitem:f layout="control" %} {% bootstrap_field form|getitem:f layout="control" %}
{% endfor %} {% endfor %}
@@ -37,6 +50,17 @@
{% bootstrap_field form.limit_events layout="control" %} {% bootstrap_field form.limit_events layout="control" %}
{% bootstrap_field form.all_event_permissions layout="control" %} {% bootstrap_field form.all_event_permissions layout="control" %}
<div class="team-permission-groups col-md-9 col-md-offset-3" data-display-dependency="#id_all_event_permissions" data-inverse> <div class="team-permission-groups col-md-9 col-md-offset-3" data-display-dependency="#id_all_event_permissions" data-inverse>
<p class="text-muted">
{% icon "info-circle" %}
{% blocktrans trimmed %}
Even if a team has no access to a certain category of data, they might still be able to see
parts of this data when it is linked to data they can see.
{% endblocktrans %}
{% blocktrans trimmed %}
For example, someone with access to orders will be able to see some information about
vouchers used to create an order, even if they generally can't see vouchers directly.
{% endblocktrans %}
</p>
{% for f in form.event_field_names %} {% for f in form.event_field_names %}
{% bootstrap_field form|getitem:f layout="control" %} {% bootstrap_field form|getitem:f layout="control" %}
{% endfor %} {% endfor %}
@@ -19,9 +19,7 @@
{% endif %} {% endif %}
</h1> </h1>
<script type="application/json" id="editor-data"> {{ layout|json_script:"editor-data" }}
{{ layout|safe }}
</script>
<div class="row"> <div class="row">
<div class="col-md-9"> <div class="col-md-9">
<div class="panel panel-default panel-pdf-editor"> <div class="panel panel-default panel-pdf-editor">
+3 -1
View File
@@ -50,7 +50,7 @@ from i18nfield.strings import LazyI18nString
from pretix.api.views.checkin import _redeem_process from pretix.api.views.checkin import _redeem_process
from pretix.base.media import MEDIA_TYPES from pretix.base.media import MEDIA_TYPES
from pretix.base.models import Checkin, LogEntry, Order, OrderPosition from pretix.base.models import Checkin, Item, LogEntry, Order, OrderPosition
from pretix.base.models.checkin import CheckinList from pretix.base.models.checkin import CheckinList
from pretix.base.models.orders import PrintLog from pretix.base.models.orders import PrintLog
from pretix.base.permissions import AnyPermissionOf from pretix.base.permissions import AnyPermissionOf
@@ -533,6 +533,8 @@ class CheckInListSimulator(EventPermissionRequiredMixin, FormView):
checkinlist=self.list, checkinlist=self.list,
result=self.result, result=self.result,
reason_labels=dict(Checkin.REASONS), reason_labels=dict(Checkin.REASONS),
media_policies=dict(Item.MEDIA_POLICIES),
media_types=dict(MEDIA_TYPES),
) )
def form_valid(self, form): def form_valid(self, form):
+11 -7
View File
@@ -41,7 +41,7 @@ from collections import OrderedDict, defaultdict
from decimal import Decimal from decimal import Decimal
from io import BytesIO from io import BytesIO
from itertools import groupby from itertools import groupby
from urllib.parse import urlparse, urlsplit from urllib.parse import urlsplit
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
import bleach import bleach
@@ -64,7 +64,6 @@ from django.shortcuts import get_object_or_404, redirect
from django.urls import NoReverseMatch, reverse from django.urls import NoReverseMatch, reverse
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.html import conditional_escape, format_html from django.utils.html import conditional_escape, format_html
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext, gettext_lazy as _, gettext_noop from django.utils.translation import gettext, gettext_lazy as _, gettext_noop
@@ -97,7 +96,9 @@ from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.views.mailsetup import MailSettingsSetupView from pretix.control.views.mailsetup import MailSettingsSetupView
from pretix.control.views.user import RecentAuthenticationRequiredMixin from pretix.control.views.user import RecentAuthenticationRequiredMixin
from pretix.helpers.database import rolledback_transaction from pretix.helpers.database import rolledback_transaction
from pretix.multidomain.urlreverse import build_absolute_uri, get_event_domain from pretix.multidomain.urlreverse import (
eventreverse_absolute, get_event_domain,
)
from pretix.presale.views.widget import ( from pretix.presale.views.widget import (
version_default as widget_version_default, version_default as widget_version_default,
) )
@@ -1148,8 +1149,11 @@ class EventLive(EventPermissionRequiredMixin, TemplateView):
if request.POST.get("delete") == "yes": if request.POST.get("delete") == "yes":
try: try:
with transaction.atomic(): with transaction.atomic():
for order in request.event.orders.filter(testmode=True): Order.gracefully_delete_bulk(
order.gracefully_delete(user=self.request.user) request.event,
request.event.orders.filter(testmode=True),
user=self.request.user
)
except ProtectedError: except ProtectedError:
messages.error(self.request, _('An order could not be deleted as some constraints (e.g. data ' messages.error(self.request, _('An order could not be deleted as some constraints (e.g. data '
'created by plug-ins) do not allow it.')) 'created by plug-ins) do not allow it.'))
@@ -1734,10 +1738,10 @@ class EventQRCode(EventPermissionRequiredMixin, View):
permission = None permission = None
def get(self, request, *args, filetype, **kwargs): def get(self, request, *args, filetype, **kwargs):
url = build_absolute_uri(request.event, 'presale:event.index') url = eventreverse_absolute(request.event, 'presale:event.index')
if "url" in request.GET: if "url" in request.GET:
if url_has_allowed_host_and_scheme(request.GET["url"], allowed_hosts=[urlparse(url).netloc]): if request.GET["url"].startswith(url):
url = request.GET["url"] url = request.GET["url"]
else: else:
raise PermissionDenied("Untrusted URL") raise PermissionDenied("Untrusted URL")
+17 -3
View File
@@ -139,6 +139,7 @@ from pretix.helpers import OF_SELF
from pretix.helpers.compat import CompatDeleteView from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.format import SafeFormatter, format_map from pretix.helpers.format import SafeFormatter, format_map
from pretix.helpers.hierarkey import clean_filename from pretix.helpers.hierarkey import clean_filename
from pretix.helpers.iter import chunked_iterable
from pretix.helpers.json import CustomJSONEncoder from pretix.helpers.json import CustomJSONEncoder
from pretix.helpers.safedownload import check_token from pretix.helpers.safedownload import check_token
from pretix.presale.signals import question_form_fields from pretix.presale.signals import question_form_fields
@@ -240,7 +241,7 @@ class BaseOrderBulkActionView(OrderSearchMixin, EventPermissionRequiredMixin, As
raise NotImplementedError() raise NotImplementedError()
def execute_bulk(self, queryset: QuerySet, form: forms.Form): def execute_bulk(self, queryset: QuerySet, form: forms.Form):
qs = self.allowed_for(self.allowed_for(self.get_queryset())) qs = self.allowed_for(self.get_queryset())
total = qs.count() total = qs.count()
orders_with_successful_action = 0 orders_with_successful_action = 0
for i, o in enumerate(qs): for i, o in enumerate(qs):
@@ -394,8 +395,21 @@ class OrderDeleteBulkActionView(BaseOrderBulkActionView):
testmode=True, testmode=True,
) )
def execute_single(self, instance, form: forms.Form): def execute_bulk(self, queryset: QuerySet, form: forms.Form):
instance.gracefully_delete(user=self.request.user) qs = self.allowed_for(self.get_queryset())
total = qs.count()
all_ids = list(qs.values_list("id", flat=True))
orders_with_successful_action = 0
for chunk in chunked_iterable(all_ids, 1000):
Order.gracefully_delete_bulk(
self.request.event,
qs.filter(id__in=chunk),
user=self.request.user,
)
orders_with_successful_action += len(chunk)
self.async_set_progress(orders_with_successful_action / total * 100)
return orders_with_successful_action, total
class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin, ListView): class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin, ListView):
+10 -8
View File
@@ -139,8 +139,8 @@ from pretix.helpers import OF_SELF, GroupConcat
from pretix.helpers.compat import CompatDeleteView from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.dicts import merge_dicts from pretix.helpers.dicts import merge_dicts
from pretix.helpers.format import SafeFormatter, format_map from pretix.helpers.format import SafeFormatter, format_map
from pretix.helpers.urls import build_absolute_uri as build_global_uri from pretix.helpers.urls import mainreverse_absolute
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.presale.forms.customer import TokenGenerator from pretix.presale.forms.customer import TokenGenerator
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -1039,7 +1039,7 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
'user': self, 'user': self,
'organizer': self.request.organizer.name, 'organizer': self.request.organizer.name,
'team': instance.team.name, 'team': instance.team.name,
'url': build_global_uri('control:auth.invite', kwargs={ 'url': mainreverse_absolute('control:auth.invite', kwargs={
'token': instance.token 'token': instance.token
}) })
}, },
@@ -2851,10 +2851,12 @@ class SSOProviderUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequire
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
ctx['redirect_uri'] = build_absolute_uri(self.request.organizer, 'presale:organizer.customer.login.return', ctx['redirect_uri'] = eventreverse_absolute(
kwargs={ self.request.organizer, 'presale:organizer.customer.login.return',
'provider': self.object.pk kwargs={
}) 'provider': self.object.pk
}
)
return ctx return ctx
def get_form_kwargs(self): def get_form_kwargs(self):
@@ -3085,7 +3087,7 @@ class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
self.customer.log_action('pretix.customer.password.resetrequested', {}, user=self.request.user) self.customer.log_action('pretix.customer.password.resetrequested', {}, user=self.request.user)
ctx = self.customer.get_email_context() ctx = self.customer.get_email_context()
token = TokenGenerator().make_token(self.customer) token = TokenGenerator().make_token(self.customer)
ctx['url'] = build_absolute_uri( ctx['url'] = eventreverse_absolute(
self.request.organizer, self.request.organizer,
'presale:organizer.customer.recoverpw' 'presale:organizer.customer.recoverpw'
) + '?id=' + self.customer.identifier + '&token=' + token ) + '?id=' + self.customer.identifier + '&token=' + token
+1 -1
View File
@@ -284,7 +284,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
ctx['pdf'] = self.get_current_background() ctx['pdf'] = self.get_current_background()
ctx['variables'] = self.get_variables() ctx['variables'] = self.get_variables()
ctx['images'] = self.get_images() ctx['images'] = self.get_images()
ctx['layout'] = json.dumps(self.get_current_layout()) ctx['layout'] = self.get_current_layout()
ctx['title'] = self.title ctx['title'] = self.title
ctx['locales'] = [p for p in settings.LANGUAGES if p[0] in self.request.event.settings.locales] ctx['locales'] = [p for p in settings.LANGUAGES if p[0] in self.request.event.settings.locales]
ctx['maxfilesize'] = self.maxfilesize ctx['maxfilesize'] = self.maxfilesize
+1 -1
View File
@@ -200,7 +200,7 @@ def giftcard_select2(request, **kwargs):
except ValueError: except ValueError:
page = 1 page = 1
if request.user.has_organizer_permission(request.organizer, 'organizer.giftcards:write', request): if request.user.has_organizer_permission(request.organizer, 'organizer.giftcards:read', request):
qs = request.organizer.issued_gift_cards.filter( qs = request.organizer.issued_gift_cards.filter(
Q(secret__icontains=query) Q(secret__icontains=query)
).order_by('secret') ).order_by('secret')
+2 -2
View File
@@ -77,7 +77,7 @@ from pretix.control.views import PaginationMixin
from pretix.helpers.compat import CompatDeleteView from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.format import SafeFormatter, format_map from pretix.helpers.format import SafeFormatter, format_map
from pretix.helpers.models import modelcopy from pretix.helpers.models import modelcopy
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView): class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
@@ -338,7 +338,7 @@ class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
} }
if self.object.subevent_id: if self.object.subevent_id:
url_params['subevent'] = self.object.subevent_id url_params['subevent'] = self.object.subevent_id
ctx['url'] = build_absolute_uri(self.request.event, "presale:event.redeem") + "?" + urlencode(url_params) ctx['url'] = eventreverse_absolute(self.request.event, "presale:event.redeem") + "?" + urlencode(url_params)
return ctx return ctx
+5
View File
@@ -29,3 +29,8 @@ class PretixHelpersConfig(AppConfig):
def ready(self): def ready(self):
from .monkeypatching import monkeypatch_all_at_ready from .monkeypatching import monkeypatch_all_at_ready
monkeypatch_all_at_ready() monkeypatch_all_at_ready()
# Ensure reportlab does not make any calls to the internet or the local disk
from reportlab import rl_config
rl_config.trustedHosts = []
rl_config.trustedSchemes = ['data']
+21
View File
@@ -27,6 +27,7 @@ from datetime import datetime
from http import cookies from http import cookies
from django.conf import settings from django.conf import settings
from django.core.exceptions import SuspiciousFileOperation
from PIL import Image from PIL import Image
from requests.adapters import HTTPAdapter from requests.adapters import HTTPAdapter
from urllib3.connection import HTTPConnection, HTTPSConnection from urllib3.connection import HTTPConnection, HTTPSConnection
@@ -40,6 +41,8 @@ from urllib3.util.connection import (
) )
from urllib3.util.timeout import _DEFAULT_TIMEOUT from urllib3.util.timeout import _DEFAULT_TIMEOUT
from pretix.helpers.reportlab import ThumbnailingImageReader
_cgnat_net = ipaddress.ip_network('100.64.0.0/10') _cgnat_net = ipaddress.ip_network('100.64.0.0/10')
@@ -230,9 +233,27 @@ def monkeypatch_cookie_morsel():
cookies.Morsel._reserved.setdefault("partitioned", "Partitioned") cookies.Morsel._reserved.setdefault("partitioned", "Partitioned")
def monkeypatch_reportlab_imagereader():
from reportlab.lib import utils
old_init = utils.ImageReader.__init__
def new_init(self, fileName, ident=None): # noqa
if not isinstance(fileName, Image.Image) and not hasattr(fileName, 'read') and not hasattr(fileName, 'str'):
if not isinstance(self, ThumbnailingImageReader):
# ThumbnailingImageReader is only used by us explicitly and not by using <img> in html, so it is safe
raise SuspiciousFileOperation("reportlab should not be reading images from disk")
return types.MethodType(old_init, self)(
fileName, ident
)
utils.ImageReader.__init__ = new_init
def monkeypatch_all_at_ready(): def monkeypatch_all_at_ready():
monkeypatch_vobject_performance() monkeypatch_vobject_performance()
monkeypatch_pillow_safer() monkeypatch_pillow_safer()
monkeypatch_requests_timeout() monkeypatch_requests_timeout()
monkeypatch_urllib3_ssrf_protection() monkeypatch_urllib3_ssrf_protection()
monkeypatch_cookie_morsel() monkeypatch_cookie_morsel()
monkeypatch_reportlab_imagereader()
+39
View File
@@ -20,14 +20,19 @@
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
import logging import logging
import re
import unicodedata
from arabic_reshaper import ArabicReshaper from arabic_reshaper import ArabicReshaper
from bidi import get_display
from django.conf import settings from django.conf import settings
from django.utils.functional import SimpleLazyObject from django.utils.functional import SimpleLazyObject
from django.utils.html import escape
from PIL import Image from PIL import Image
from reportlab.lib.styles import ParagraphStyle from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.utils import ImageReader from reportlab.lib.utils import ImageReader
from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import Paragraph from reportlab.platypus import Paragraph
from pretix.presale.style import get_fonts from pretix.presale.style import get_fonts
@@ -70,6 +75,20 @@ reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={
})) }))
def normalize_text(text: str) -> str:
# reportlab does not support unicode combination characters
# It's important we do this before we use ArabicReshaper
text = unicodedata.normalize("NFKC", text)
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
# to resolve all ligatures and python-bidi to switch RTL texts.
try:
text = "\n".join(get_display(reshaper.reshape(l)) for l in re.split("\n", text))
except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
return text
class FontFallbackParagraph(Paragraph): class FontFallbackParagraph(Paragraph):
def __init__(self, text, style=None, *args, **kwargs): def __init__(self, text, style=None, *args, **kwargs):
if style is None: if style is None:
@@ -87,6 +106,8 @@ class FontFallbackParagraph(Paragraph):
if not text: if not text:
return True return True
font = pdfmetrics.getFont(font_name) font = pdfmetrics.getFont(font_name)
if not isinstance(font, TTFont):
return True
return all( return all(
ord(c) in font.face.charToGlyph or not c.isprintable() ord(c) in font.face.charToGlyph or not c.isprintable()
for c in text for c in text
@@ -102,6 +123,24 @@ class FontFallbackParagraph(Paragraph):
return family return family
class PlainTextParagraph(FontFallbackParagraph):
def __init__(self, text, style=None, linebreaks=True, *args, **kwargs):
if not isinstance(text, str):
if hasattr(text, '__html__'):
raise ValueError("It is contradictory to pass escaped content to PlainTextParagraph")
text = str(text)
# Normalize unicode and apply reshaping
text = normalize_text(text)
# Escape any HTML in the text
text = escape(text)
if linebreaks:
text = text.strip().replace("\n", "<br />\n")
super().__init__(text, style, *args, **kwargs)
def register_ttf_font_if_new(name, path): def register_ttf_font_if_new(name, path):
from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont from reportlab.pdfbase.ttfonts import TTFont
+2 -2
View File
@@ -35,7 +35,7 @@ from geoip2.errors import AddressNotFoundError
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.services.mail import mail from pretix.base.services.mail import mail
from pretix.helpers.http import get_client_ip from pretix.helpers.http import get_client_ip
from pretix.helpers.urls import build_absolute_uri from pretix.helpers.urls import mainreverse_absolute
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -175,7 +175,7 @@ def handle_login_source(user, request):
'source': src, 'source': src,
'country': Country(str(country)).name if country else _('Unknown country'), 'country': Country(str(country)).name if country else _('Unknown country'),
'instance': settings.PRETIX_INSTANCE_NAME, 'instance': settings.PRETIX_INSTANCE_NAME,
'url': build_absolute_uri('control:user.settings') 'url': mainreverse_absolute('control:user.settings')
}, },
event=None, event=None,
user=user, user=user,
@@ -0,0 +1,33 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix 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 logging
from django import template
from django.utils.html import format_html
register = template.Library()
logger = logging.getLogger(__name__)
@register.filter
def wrap_in(content, tag_name):
return format_html(f'<{tag_name}>{{}}</{tag_name}>', content)
+10
View File
@@ -19,6 +19,7 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
import warnings
from urllib.parse import urljoin from urllib.parse import urljoin
from django.conf import settings from django.conf import settings
@@ -26,6 +27,15 @@ from django.urls import reverse
def build_absolute_uri(urlname, args=None, kwargs=None): def build_absolute_uri(urlname, args=None, kwargs=None):
warnings.warn(
'Usage of build_absolute_uri is confusing since there are many functions with that name. '
'Replace this usage with ',
DeprecationWarning
)
return mainreverse_absolute(urlname, args, kwargs)
def mainreverse_absolute(urlname, args=None, kwargs=None):
from pretix.multidomain import maindomain_urlconf from pretix.multidomain import maindomain_urlconf
return urljoin(settings.SITE_URL, reverse(urlname, args=args, kwargs=kwargs, urlconf=maindomain_urlconf)) return urljoin(settings.SITE_URL, reverse(urlname, args=args, kwargs=kwargs, urlconf=maindomain_urlconf))
+158 -270
View File
@@ -4,16 +4,16 @@ msgstr ""
"Project-Id-Version: 1\n" "Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-27 15:47+0000\n" "POT-Creation-Date: 2026-05-27 15:47+0000\n"
"PO-Revision-Date: 2026-05-22 07:03+0000\n" "PO-Revision-Date: 2026-06-20 17:00+0000\n"
"Last-Translator: Nikolai <nikolai@lengefeldt.de>\n" "Last-Translator: Nikolai <nikolai@lengefeldt.de>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix/da/" "Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix/"
">\n" "da/>\n"
"Language: da\n" "Language: da\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 2026.5\n" "X-Generator: Weblate 2026.6.1\n"
#: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html:670 #: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html:670
#: pretix/control/templates/pretixcontrol/events/index.html:166 #: pretix/control/templates/pretixcontrol/events/index.html:166
@@ -612,16 +612,17 @@ msgstr ""
"underordnede objekter som f.eks. varianter eller pakker." "underordnede objekter som f.eks. varianter eller pakker."
#: pretix/api/webhooks.py:413 #: pretix/api/webhooks.py:413
#, fuzzy
#| msgid "Quota handling"
msgid "Quota changed" msgid "Quota changed"
msgstr "Håndtering af kvoter" msgstr "Kvoten er ændret"
#: pretix/api/webhooks.py:414 #: pretix/api/webhooks.py:414
msgid "" msgid ""
"This includes related events like creation, deletion, opening or closing of " "This includes related events like creation, deletion, opening or closing of "
"quotas. No webhook is sent for changes to the resulting availability." "quotas. No webhook is sent for changes to the resulting availability."
msgstr "" msgstr ""
"Dette omfatter relaterede hændelser såsom oprettelse, sletning, åbning eller "
"lukning af kvoter. Der sendes ingen webhook ved ændringer i den deraf "
"følgende tilgængelighed."
#: pretix/api/webhooks.py:419 #: pretix/api/webhooks.py:419
msgid "Shop taken live" msgid "Shop taken live"
@@ -3394,12 +3395,12 @@ msgstr "Indtast venligst et kortere navn."
msgid "" msgid ""
"The field \"%(label)s\" may not contain special characters such as " "The field \"%(label)s\" may not contain special characters such as "
"\"%(chars)s\"." "\"%(chars)s\"."
msgstr "" msgstr "Feltet \"%(label)s\" må ikke indeholde specialtegn såsom \"%(chars)s\"."
#: pretix/base/forms/questions.py:305 #: pretix/base/forms/questions.py:305
#, python-format #, python-format
msgid "The field \"%(label)s\" may not contain an URL (%(url)s)." msgid "The field \"%(label)s\" may not contain an URL (%(url)s)."
msgstr "" msgstr "Feltet \"%(label)s\" må ikke indeholde en URL (%(url)s)."
#: pretix/base/forms/questions.py:338 #: pretix/base/forms/questions.py:338
msgctxt "phonenumber" msgctxt "phonenumber"
@@ -8290,19 +8291,14 @@ msgid "Program times"
msgstr "Programtider" msgstr "Programtider"
#: pretix/base/pdf.py:503 #: pretix/base/pdf.py:503
#, fuzzy
#| msgid ""
#| "2017-05-31 10:00 12:00\n"
#| "2017-05-31 14:00 16:00\n"
#| "2017-05-31 14:00 2017-06-01 14:00"
msgid "" msgid ""
"2017-05-31 10:00 12:00, Room 1\n" "2017-05-31 10:00 12:00, Room 1\n"
"2017-05-31 14:00 16:00, Room 2\n" "2017-05-31 14:00 16:00, Room 2\n"
"2017-05-31 14:00 2017-06-01 14:00, Building A" "2017-05-31 14:00 2017-06-01 14:00, Building A"
msgstr "" msgstr ""
"31. maj 2017 kl. 10.00 12.00\n" "31. maj 2017 kl. 10.00 12.00, lokale 1\n"
"31. maj 2017 kl. 14.00 16.00\n" "31. maj 2017 kl. 14.00 16.00, lokale 2\n"
"31. maj 2017 kl. 14.00 1. juni 2017 kl. 14.00" "31. maj 2017 kl. 14.00 1. juni 2017 kl. 14.00, bygning A"
#: pretix/base/pdf.py:507 #: pretix/base/pdf.py:507
msgid "Reusable Medium ID" msgid "Reusable Medium ID"
@@ -8827,13 +8823,7 @@ msgid "This voucher code is not known in our database."
msgstr "Denne voucherkode genkendes ikke." msgstr "Denne voucherkode genkendes ikke."
#: pretix/base/services/cart.py:165 #: pretix/base/services/cart.py:165
#, fuzzy, python-format #, python-format
#| msgid ""
#| "The voucher code \"%(voucher)s\" can only be used if you select at least "
#| "%(number)s matching products."
#| msgid_plural ""
#| "The voucher code \"%(voucher)s\" can only be used if you select at least "
#| "%(number)s matching products."
msgid "" msgid ""
"The voucher code \"%(voucher)s\" can only be used if you select at least " "The voucher code \"%(voucher)s\" can only be used if you select at least "
"%(number)s matching product." "%(number)s matching product."
@@ -8841,22 +8831,14 @@ msgid_plural ""
"The voucher code \"%(voucher)s\" can only be used if you select at least " "The voucher code \"%(voucher)s\" can only be used if you select at least "
"%(number)s matching products." "%(number)s matching products."
msgstr[0] "" msgstr[0] ""
"Voucherkoden \"%(voucher)s\" kan kun blive brugt, hvis du vælger mindst " "Voucherkoden \"%(voucher)s\" kan kun blive brugt, hvis du vælger mindst %"
"%(number)s passende produkter." "(number)s passende produkter."
msgstr[1] "" msgstr[1] ""
"Voucherkoden \"%(voucher)s\" kan kun blive brugt, hvis du vælger mindst " "Voucherkoderne \"%(voucher)s\" kan kun blive brugt, hvis du vælger mindst %"
"%(number)s passende produkter." "(number)s passende produkter."
#: pretix/base/services/cart.py:170 #: pretix/base/services/cart.py:170
#, fuzzy, python-format #, python-format
#| msgid ""
#| "The voucher code \"%(voucher)s\" can only be used if you select at least "
#| "%(number)s matching products. We have therefore removed some positions "
#| "from your cart that can no longer be purchased like this."
#| msgid_plural ""
#| "The voucher code \"%(voucher)s\" can only be used if you select at least "
#| "%(number)s matching products. We have therefore removed some positions "
#| "from your cart that can no longer be purchased like this."
msgid "" msgid ""
"The voucher code \"%(voucher)s\" can only be used if you select at least " "The voucher code \"%(voucher)s\" can only be used if you select at least "
"%(number)s matching product. We have therefore removed some positions from " "%(number)s matching product. We have therefore removed some positions from "
@@ -8866,12 +8848,12 @@ msgid_plural ""
"%(number)s matching products. We have therefore removed some positions from " "%(number)s matching products. We have therefore removed some positions from "
"your cart that can no longer be purchased like this." "your cart that can no longer be purchased like this."
msgstr[0] "" msgstr[0] ""
"Voucherkoden \"%(voucher)s\" kan kun blive brugt, hvis du vælger mindst " "Voucherkoden \"%(voucher)s\" kan kun blive brugt, hvis du vælger mindst %"
"%(number)s passende produkter. Vi har derfor fjernet nogle poster fra din " "(number)s passende produkter. Vi har derfor fjernet nogle poster fra din "
"indkøbskurv, som ikke længere kan blive købt på denne måde." "indkøbskurv, som ikke længere kan blive købt på denne måde."
msgstr[1] "" msgstr[1] ""
"Voucherkoden \"%(voucher)s\" kan kun blive brugt, hvis du vælger mindst " "Voucherkoderne \"%(voucher)s\" kan kun blive brugt, hvis du vælger mindst %"
"%(number)s passende produkter. Vi har derfor fjernet nogle poster fra din " "(number)s passende produkter. Vi har derfor fjernet nogle poster fra din "
"indkøbskurv, som ikke længere kan blive købt på denne måde." "indkøbskurv, som ikke længere kan blive købt på denne måde."
#: pretix/base/services/cart.py:176 #: pretix/base/services/cart.py:176
@@ -14105,6 +14087,8 @@ msgid ""
"You entered an URL, which is not allowed. Please remove %(match)s from your " "You entered an URL, which is not allowed. Please remove %(match)s from your "
"input." "input."
msgstr "" msgstr ""
"Du har indtastet en URL, som ikke er tilladt. Fjern venligst %(match)s fra "
"din indtastning."
#: pretix/base/views/errors.py:48 #: pretix/base/views/errors.py:48
msgid "" msgid ""
@@ -16027,14 +16011,8 @@ msgid "inactive"
msgstr "inaktiv" msgstr "inaktiv"
#: pretix/control/forms/item.py:1414 #: pretix/control/forms/item.py:1414
#, fuzzy
#| msgid ""
#| "Sample Conference Center\n"
#| "Heidelberg, Germany"
msgid "Sample Conference Center, Heidelberg, Germany" msgid "Sample Conference Center, Heidelberg, Germany"
msgstr "" msgstr "Eksempelkonferencecenter, Århus, Danmark"
"Eksempelkonferencecenter\n"
"Andeby, Danmark"
#: pretix/control/forms/mailsetup.py:42 #: pretix/control/forms/mailsetup.py:42
msgid "Hostname" msgid "Hostname"
@@ -17819,12 +17797,11 @@ msgstr ""
"afvist." "afvist."
#: pretix/control/logdisplay.py:571 #: pretix/control/logdisplay.py:571
#, fuzzy
msgid "" msgid ""
"An email has been sent to notify the user that the order has been approved." "An email has been sent to notify the user that the order has been approved."
msgstr "" msgstr ""
"En e-mail er blevet sendt for at fortælle brugeren at bestillingen er " "Der er sendt en e-mail for at informere brugeren om, at ordren er blevet "
"modtaget." "godkendt."
#: pretix/control/logdisplay.py:574 #: pretix/control/logdisplay.py:574
msgid "" msgid ""
@@ -17835,13 +17812,12 @@ msgstr ""
"modtaget og kræver betaling." "modtaget og kræver betaling."
#: pretix/control/logdisplay.py:575 #: pretix/control/logdisplay.py:575
#, fuzzy
msgid "" msgid ""
"An email has been sent to notify the user that the order has been received " "An email has been sent to notify the user that the order has been received "
"and requires approval." "and requires approval."
msgstr "" msgstr ""
"En e-mail er blevet sendt for at fortælle brugeren at bestillingen er " "Der er sendt en e-mail for at informere brugeren om, at ordren er modtaget "
"modtaget og kræver betaling." "og skal godkendes."
#: pretix/control/logdisplay.py:578 #: pretix/control/logdisplay.py:578
msgid "" msgid ""
@@ -17851,21 +17827,22 @@ msgstr ""
"bestillingsdetaljer." "bestillingsdetaljer."
#: pretix/control/logdisplay.py:579 #: pretix/control/logdisplay.py:579
#, fuzzy
msgid "An email has been sent to notify the user that the payment failed." msgid "An email has been sent to notify the user that the payment failed."
msgstr "" msgstr ""
"En e-mail er blevet sendt for at fortælle brugeren at betaling er modtaget." "Der er sendt en e-mail for at informere brugeren om, at betalingen "
"mislykkedes."
#: pretix/control/logdisplay.py:586 #: pretix/control/logdisplay.py:586
msgid "The voucher has been created." msgid "The voucher has been created."
msgstr "Voucheren er blevet oprettet." msgstr "Voucheren er blevet oprettet."
#: pretix/control/logdisplay.py:589 #: pretix/control/logdisplay.py:589
#, fuzzy
msgid "" msgid ""
"The voucher has been set to expire because the recipient removed themselves " "The voucher has been set to expire because the recipient removed themselves "
"from the waiting list." "from the waiting list."
msgstr "En rabatkode er blevet sendt til en person på ventelisten." msgstr ""
"Kuponen er sat til at udløbe, fordi modtageren har fjernet sig selv fra "
"ventelisten."
#: pretix/control/logdisplay.py:590 #: pretix/control/logdisplay.py:590
msgid "The voucher has been changed." msgid "The voucher has been changed."
@@ -17876,16 +17853,13 @@ msgid "The voucher has been deleted."
msgstr "Voucheren er blevet slettet." msgstr "Voucheren er blevet slettet."
#: pretix/control/logdisplay.py:592 #: pretix/control/logdisplay.py:592
#, fuzzy
#| msgid "The selected voucher has been deleted."
msgid "Cart positions including the voucher have been deleted." msgid "Cart positions including the voucher have been deleted."
msgstr "Den valgte rabatkode er blevet slettet." msgstr "Indkøbskurvens varer, herunder gavekortet, er blevet slettet."
#: pretix/control/logdisplay.py:593 #: pretix/control/logdisplay.py:593
#, fuzzy, python-brace-format #, python-brace-format
#| msgid "A voucher has been sent to a person on the waiting list."
msgid "The voucher has been assigned to {email} through the waiting list." msgid "The voucher has been assigned to {email} through the waiting list."
msgstr "En rabatkode er blevet sendt til en person på ventelisten." msgstr "Kuponen er blevet tildelt {email} via ventelisten."
#: pretix/control/logdisplay.py:602 #: pretix/control/logdisplay.py:602
#, python-brace-format #, python-brace-format
@@ -17905,9 +17879,8 @@ msgid "The category has been changed."
msgstr "Kategorien er blevet ændret." msgstr "Kategorien er blevet ændret."
#: pretix/control/logdisplay.py:620 #: pretix/control/logdisplay.py:620
#, fuzzy
msgid "The category has been reordered." msgid "The category has been reordered."
msgstr "Kategorien er blevet slettet." msgstr "Kategorien er blevet omorganiseret."
#: pretix/control/logdisplay.py:627 #: pretix/control/logdisplay.py:627
msgid "The tax rule has been added." msgid "The tax rule has been added."
@@ -17937,14 +17910,14 @@ msgid "{user} has been invited to the team."
msgstr "{user} er blevet inviteret med i gruppen." msgstr "{user} er blevet inviteret med i gruppen."
#: pretix/control/logdisplay.py:644 #: pretix/control/logdisplay.py:644
#, fuzzy, python-brace-format #, python-brace-format
msgid "Invite for {user} has been deleted." msgid "Invite for {user} has been deleted."
msgstr "Invitationen til {user} er blevet trukket tilbage." msgstr "Invitationen til {user} er blevet slettet."
#: pretix/control/logdisplay.py:645 #: pretix/control/logdisplay.py:645
#, fuzzy, python-brace-format #, python-brace-format
msgid "Invite for {user} has been resent." msgid "Invite for {user} has been resent."
msgstr "Invitationen til {user} er blevet trukket tilbage." msgstr "Invitationen til {user} er blevet sendt igen."
#: pretix/control/logdisplay.py:656 #: pretix/control/logdisplay.py:656
#, python-brace-format #, python-brace-format
@@ -17962,294 +17935,245 @@ msgstr "Din e-mailadresse er blevet ændret til {email}."
#: pretix/control/logdisplay.py:673 #: pretix/control/logdisplay.py:673
msgid "Your account has been enabled." msgid "Your account has been enabled."
msgstr "" msgstr "Din konto er nu aktiveret."
#: pretix/control/logdisplay.py:675 #: pretix/control/logdisplay.py:675
msgid "Your account has been disabled." msgid "Your account has been disabled."
msgstr "" msgstr "Din konto er blevet deaktiveret."
#: pretix/control/logdisplay.py:680 pretix/presale/views/customer.py:642 #: pretix/control/logdisplay.py:680 pretix/presale/views/customer.py:642
#, fuzzy, python-brace-format #, python-brace-format
#| msgid ""
#| "The email address has been changed from \"{old_email}\" to \"{new_email}"
#| "\"."
msgid "Your email address has been changed from {old_email} to {email}." msgid "Your email address has been changed from {old_email} to {email}."
msgstr "" msgstr "Din e-mailadresse er blevet ændret fra {old_email} til {email}."
"E-mailadressen er blevet ændret fra \"{old_email}\" til \"{new_email}\"."
#: pretix/control/logdisplay.py:681 #: pretix/control/logdisplay.py:681
#, fuzzy, python-brace-format #, python-brace-format
msgid "Your email address {email} has been confirmed." msgid "Your email address {email} has been confirmed."
msgstr "Din kurv er blevet opdateret." msgstr "Din e-mailadresse {email} er blevet bekræftet."
#: pretix/control/logdisplay.py:693 #: pretix/control/logdisplay.py:693
#, python-brace-format #, python-brace-format
msgid "You impersonated {}." msgid "You impersonated {}."
msgstr "" msgstr "Du udgav dig for at være {}."
#: pretix/control/logdisplay.py:694 #: pretix/control/logdisplay.py:694
#, python-brace-format #, python-brace-format
msgid "You stopped impersonating {}." msgid "You stopped impersonating {}."
msgstr "" msgstr "Du er holdt op med at udgive dig for at være {}."
#: pretix/control/logdisplay.py:701 #: pretix/control/logdisplay.py:701
msgid "This object has been created by cloning." msgid "This object has been created by cloning."
msgstr "Dette objekt er blevet oprettet ved kloning" msgstr "Dette objekt er blevet oprettet ved kloning"
#: pretix/control/logdisplay.py:702 #: pretix/control/logdisplay.py:702
#, fuzzy
msgid "The organizer has been changed." msgid "The organizer has been changed."
msgstr "Bestillingen er blevet ændret." msgstr "Arrangøren er blevet ændret."
#: pretix/control/logdisplay.py:703 #: pretix/control/logdisplay.py:703
#, fuzzy
msgid "The organizer settings have been changed." msgid "The organizer settings have been changed."
msgstr "Gruppeindstillingerne er blevet ændret." msgstr "Indstillingerne for arrangøren er blevet ændret."
#: pretix/control/logdisplay.py:704 pretix/control/logdisplay.py:849 #: pretix/control/logdisplay.py:704 pretix/control/logdisplay.py:849
#, fuzzy
msgid "The footer links have been changed." msgid "The footer links have been changed."
msgstr "Bestillingsdetaljerne er blevet ændret." msgstr "Linkene i sidefoden er blevet ændret."
#: pretix/control/logdisplay.py:705 pretix/control/logdisplay.py:754 #: pretix/control/logdisplay.py:705 pretix/control/logdisplay.py:754
#, fuzzy
msgid "A scheduled export has been added." msgid "A scheduled export has been added."
msgstr "Kvoten er blevet tilføjet." msgstr "Der er tilføjet en planlagt eksport."
#: pretix/control/logdisplay.py:706 pretix/control/logdisplay.py:755 #: pretix/control/logdisplay.py:706 pretix/control/logdisplay.py:755
#, fuzzy
msgid "A scheduled export has been changed." msgid "A scheduled export has been changed."
msgstr "Produktet er blevet ændret." msgstr "En planlagt eksport er blevet ændret."
#: pretix/control/logdisplay.py:707 pretix/control/logdisplay.py:756 #: pretix/control/logdisplay.py:707 pretix/control/logdisplay.py:756
#, fuzzy
msgid "A scheduled export has been deleted." msgid "A scheduled export has been deleted."
msgstr "Det valgte produkt er blevet slettet." msgstr "En planlagt eksport er blevet slettet."
#: pretix/control/logdisplay.py:708 pretix/control/logdisplay.py:757 #: pretix/control/logdisplay.py:708 pretix/control/logdisplay.py:757
#, fuzzy
msgid "A scheduled export has been executed." msgid "A scheduled export has been executed."
msgstr "Det valgte produkt er blevet slettet." msgstr "En planlagt eksport er blevet udført."
#: pretix/control/logdisplay.py:709 pretix/control/logdisplay.py:758 #: pretix/control/logdisplay.py:709 pretix/control/logdisplay.py:758
#, python-brace-format #, python-brace-format
msgid "A scheduled export has failed: {reason}." msgid "A scheduled export has failed: {reason}."
msgstr "" msgstr "En planlagt eksport er mislykket: {reason}."
#: pretix/control/logdisplay.py:710 #: pretix/control/logdisplay.py:710
#, fuzzy
#| msgid "The email has been queued to be sent."
msgid "Failed emails have been scheduled to be retried." msgid "Failed emails have been scheduled to be retried."
msgstr "Denne e-mail er sat i udsendelseskøen." msgstr ""
"Der er planlagt et nyt forsøg på at sende de e-mails, der ikke kunne leveres."
#: pretix/control/logdisplay.py:711 #: pretix/control/logdisplay.py:711
#, fuzzy
#| msgid "The order details have been changed."
msgid "Queued emails have been aborted." msgid "Queued emails have been aborted."
msgstr "Bestillingsdetaljerne er blevet ændret." msgstr "E-mails i køen er blevet afbrudt."
#: pretix/control/logdisplay.py:712 #: pretix/control/logdisplay.py:712
#, fuzzy
msgid "Gift card acceptance for another organizer has been added." msgid "Gift card acceptance for another organizer has been added."
msgstr "Den nye arrangør er blevet oprettet." msgstr ""
"Der er nu tilføjet mulighed for at acceptere gavekort fra en anden arrangør."
#: pretix/control/logdisplay.py:713 #: pretix/control/logdisplay.py:713
#, fuzzy
msgid "Gift card acceptance for another organizer has been removed." msgid "Gift card acceptance for another organizer has been removed."
msgstr "Den nye arrangør er blevet oprettet." msgstr ""
"Muligheden for at modtage gavekort fra en anden arrangør er blevet fjernet."
#: pretix/control/logdisplay.py:714 #: pretix/control/logdisplay.py:714
#, fuzzy
msgid "A new gift card acceptor has been invited." msgid "A new gift card acceptor has been invited."
msgstr "Gruppen er blevet oprettet." msgstr "Der er blevet inviteret en ny modtager af gavekort."
#: pretix/control/logdisplay.py:715 #: pretix/control/logdisplay.py:715
#, fuzzy
msgid "A gift card acceptor has been removed." msgid "A gift card acceptor has been removed."
msgstr "Gruppen er blevet oprettet." msgstr "En gavekort-modtager er blevet fjernet."
#: pretix/control/logdisplay.py:716 #: pretix/control/logdisplay.py:716
#, fuzzy
msgid "A gift card issuer has been removed or declined." msgid "A gift card issuer has been removed or declined."
msgstr "Den valgte kategorien er blevet slettet." msgstr "En udsteder af gavekort er blevet fjernet eller afvist."
#: pretix/control/logdisplay.py:717 #: pretix/control/logdisplay.py:717
#, fuzzy
msgid "A new gift card issuer has been accepted." msgid "A new gift card issuer has been accepted."
msgstr "Den valgte kategorien er blevet slettet." msgstr "En ny udsteder af gavekort er blevet godkendt."
#: pretix/control/logdisplay.py:718 #: pretix/control/logdisplay.py:718
#, fuzzy
msgid "The webhook has been created." msgid "The webhook has been created."
msgstr "Gruppen er blevet oprettet." msgstr "Webhooken er blevet oprettet."
#: pretix/control/logdisplay.py:719 #: pretix/control/logdisplay.py:719
#, fuzzy
msgid "The webhook has been changed." msgid "The webhook has been changed."
msgstr "Kategorien er blevet ændret." msgstr "Webhooken er blevet ændret."
#: pretix/control/logdisplay.py:720 #: pretix/control/logdisplay.py:720
msgid "The webhook call retry jobs have been manually expedited." msgid "The webhook call retry jobs have been manually expedited."
msgstr "" msgstr ""
"Opgaverne vedrørende gentagelse af webhook-kald er blevet fremskyndet "
"manuelt."
#: pretix/control/logdisplay.py:721 #: pretix/control/logdisplay.py:721
#, fuzzy
msgid "The webhook call retry jobs have been dropped." msgid "The webhook call retry jobs have been dropped."
msgstr "Gruppen er blevet oprettet." msgstr "Opgaverne til gentagelse af webhook-kald er blevet annulleret."
#: pretix/control/logdisplay.py:722 #: pretix/control/logdisplay.py:722
#, fuzzy
msgid "The SSO provider has been created." msgid "The SSO provider has been created."
msgstr "Bestilling oprettet." msgstr "SSO-udbyderen er blevet oprettet."
#: pretix/control/logdisplay.py:723 #: pretix/control/logdisplay.py:723
#, fuzzy
msgid "The SSO provider has been changed." msgid "The SSO provider has been changed."
msgstr "Bestillingen er blevet ændret." msgstr "SSO-udbyderen er blevet skiftet."
#: pretix/control/logdisplay.py:724 #: pretix/control/logdisplay.py:724
#, fuzzy
msgid "The SSO provider has been deleted." msgid "The SSO provider has been deleted."
msgstr "Bestillingen er blevet tilbagebetalt." msgstr "SSO-udbyderen er blevet slettet."
#: pretix/control/logdisplay.py:725 #: pretix/control/logdisplay.py:725
#, fuzzy
msgid "The SSO client has been created." msgid "The SSO client has been created."
msgstr "Arrangementsdatoen er blevet oprettet." msgstr "SSO-klienten er oprettet."
#: pretix/control/logdisplay.py:726 #: pretix/control/logdisplay.py:726
#, fuzzy
msgid "The SSO client has been changed." msgid "The SSO client has been changed."
msgstr "Kategorien er blevet ændret." msgstr "SSO-klienten er blevet ændret."
#: pretix/control/logdisplay.py:727 #: pretix/control/logdisplay.py:727
#, fuzzy
msgid "The SSO client has been deleted." msgid "The SSO client has been deleted."
msgstr "Produktet er blevet slettet." msgstr "SSO-klienten er blevet slettet."
#: pretix/control/logdisplay.py:728 pretix/control/views/organizer.py:2715 #: pretix/control/logdisplay.py:728 pretix/control/views/organizer.py:2715
#, fuzzy
msgid "The membership type has been created." msgid "The membership type has been created."
msgstr "Arrangementsdatoen er blevet oprettet." msgstr "Medlemskabstypen er oprettet."
#: pretix/control/logdisplay.py:729 #: pretix/control/logdisplay.py:729
#, fuzzy
msgid "The membership type has been changed." msgid "The membership type has been changed."
msgstr "Arrangementsdatoen er blevet ændret." msgstr "Medlemskabstypen er blevet ændret."
#: pretix/control/logdisplay.py:730 #: pretix/control/logdisplay.py:730
#, fuzzy
msgid "The membership type has been deleted." msgid "The membership type has been deleted."
msgstr "Arrangementsdatoen er blevet slettet." msgstr "Medlemskabstypen er blevet slettet."
#: pretix/control/logdisplay.py:731 pretix/control/views/organizer.py:3556 #: pretix/control/logdisplay.py:731 pretix/control/views/organizer.py:3556
#, fuzzy
#| msgctxt "subevent"
#| msgid "The new date has been created."
msgid "The sales channel has been created." msgid "The sales channel has been created."
msgstr "Den nye dato er blevet oprettet." msgstr "Salgskanalen er oprettet."
#: pretix/control/logdisplay.py:732 #: pretix/control/logdisplay.py:732
#, fuzzy
msgid "The sales channel has been changed." msgid "The sales channel has been changed."
msgstr "Voucheren er blevet ændret." msgstr "Salgskanalen er blevet ændret."
#: pretix/control/logdisplay.py:733 #: pretix/control/logdisplay.py:733
#, fuzzy
#| msgid "The selected list has been deleted."
msgid "The sales channel has been deleted." msgid "The sales channel has been deleted."
msgstr "Den valgte lister er blevet slettet." msgstr "Salgskanalen er blevet slettet."
#: pretix/control/logdisplay.py:734 #: pretix/control/logdisplay.py:734
#, fuzzy
msgid "The account has been created." msgid "The account has been created."
msgstr "Gruppen er blevet oprettet." msgstr "Kontoen er oprettet."
#: pretix/control/logdisplay.py:735 #: pretix/control/logdisplay.py:735
#, fuzzy
msgid "The account has been changed." msgid "The account has been changed."
msgstr "Kategorien er blevet ændret." msgstr "Kontoen er blevet ændret."
#: pretix/control/logdisplay.py:736 #: pretix/control/logdisplay.py:736
#, fuzzy
msgid "A membership for this account has been added." msgid "A membership for this account has been added."
msgstr "Den valgte konfiguration er blevet slettet." msgstr "Der er tilføjet et medlemskab til denne konto."
#: pretix/control/logdisplay.py:737 #: pretix/control/logdisplay.py:737
#, fuzzy
msgid "A membership of this account has been changed." msgid "A membership of this account has been changed."
msgstr "En svarmulighed er blevet ændret." msgstr "Et medlemskab på denne konto er blevet ændret."
#: pretix/control/logdisplay.py:738 #: pretix/control/logdisplay.py:738
#, fuzzy
msgid "A membership of this account has been deleted." msgid "A membership of this account has been deleted."
msgstr "Den valgte konfiguration er blevet slettet." msgstr "Et medlemskab tilknyttet denne konto er blevet slettet."
#: pretix/control/logdisplay.py:739 #: pretix/control/logdisplay.py:739
#, fuzzy
msgid "The account has been disabled and anonymized." msgid "The account has been disabled and anonymized."
msgstr "Bestillingen er blevet tilbagebetalt." msgstr "Kontoen er blevet deaktiveret og anonymiseret."
#: pretix/control/logdisplay.py:740 #: pretix/control/logdisplay.py:740
#, fuzzy
msgid "A new password has been requested." msgid "A new password has been requested."
msgstr "Adgangskoden er blevet nulstillet." msgstr "Der er anmodet om en ny adgangskode."
#: pretix/control/logdisplay.py:741 #: pretix/control/logdisplay.py:741
#, fuzzy
msgid "A new password has been set." msgid "A new password has been set."
msgstr "Adgangskoden er blevet nulstillet." msgstr "Der er blevet oprettet en ny adgangskode."
#: pretix/control/logdisplay.py:743 #: pretix/control/logdisplay.py:743
#, fuzzy
msgid "The reusable medium has been created." msgid "The reusable medium has been created."
msgstr "Gruppen er blevet oprettet." msgstr "Det genanvendelige medie er blevet oprettet."
#: pretix/control/logdisplay.py:744 #: pretix/control/logdisplay.py:744
#, fuzzy
msgid "The reusable medium has been created automatically." msgid "The reusable medium has been created automatically."
msgstr "Arrangementsdatoen er blevet oprettet." msgstr "Det genanvendelige medie er blevet oprettet automatisk."
#: pretix/control/logdisplay.py:745 #: pretix/control/logdisplay.py:745
#, fuzzy
msgid "The reusable medium has been changed." msgid "The reusable medium has been changed."
msgstr "Momsreglen er blevet ændret." msgstr "Det genanvendelige medie er blevet udskiftet."
#: pretix/control/logdisplay.py:746 #: pretix/control/logdisplay.py:746
#, fuzzy
msgid "The medium has been connected to a new ticket." msgid "The medium has been connected to a new ticket."
msgstr "Denne e-mail er sat i udsendelseskøen." msgstr "Mediet er blevet knyttet til en ny sag."
#: pretix/control/logdisplay.py:747 #: pretix/control/logdisplay.py:747
#, fuzzy
msgid "The medium has been connected to a new gift card." msgid "The medium has been connected to a new gift card."
msgstr "Denne e-mail er sat i udsendelseskøen." msgstr "Mediet er blevet knyttet til et nyt gavekort."
#: pretix/control/logdisplay.py:749 #: pretix/control/logdisplay.py:749
msgid "The event's internal comment has been updated." msgid "The event's internal comment has been updated."
msgstr "Intern kommentar opdateret." msgstr "Intern kommentar opdateret."
#: pretix/control/logdisplay.py:750 #: pretix/control/logdisplay.py:750
#, fuzzy
msgid "The event has been canceled." msgid "The event has been canceled."
msgstr "Bestillingen er blevet annulleret." msgstr "Arrangementet er blevet aflyst."
#: pretix/control/logdisplay.py:751 #: pretix/control/logdisplay.py:751
#, fuzzy
msgid "An event has been deleted." msgid "An event has been deleted."
msgstr "Arrangementsdatoen er blevet slettet." msgstr "En begivenhed er blevet slettet."
#: pretix/control/logdisplay.py:752 #: pretix/control/logdisplay.py:752
msgid "A removal process for personal data has been started." msgid "A removal process for personal data has been started."
msgstr "" msgstr "Der er iværksat en sletningsproces for personoplysninger."
#: pretix/control/logdisplay.py:753 #: pretix/control/logdisplay.py:753
msgid "A removal process for personal data has been completed." msgid "A removal process for personal data has been completed."
msgstr "" msgstr "En sletning af personoplysninger er blevet gennemført."
#: pretix/control/logdisplay.py:759 #: pretix/control/logdisplay.py:759
msgid "The user has been created." msgid "The user has been created."
msgstr "" msgstr "Brugeren er blevet oprettet."
#: pretix/control/logdisplay.py:760 #: pretix/control/logdisplay.py:760
#, python-brace-format #, python-brace-format
@@ -18257,6 +18181,8 @@ msgid ""
"A first login using {agent_type} on {os_type} from {country} has been " "A first login using {agent_type} on {os_type} from {country} has been "
"detected." "detected."
msgstr "" msgstr ""
"Der er registreret en første login med {agent_type} på {os_type} fra "
"{country}."
#: pretix/control/logdisplay.py:762 pretix/control/views/user.py:474 #: pretix/control/logdisplay.py:762 pretix/control/views/user.py:474
#: pretix/control/views/user.py:534 pretix/control/views/user.py:593 #: pretix/control/views/user.py:534 pretix/control/views/user.py:593
@@ -18273,9 +18199,8 @@ msgid "Your two-factor emergency codes have been regenerated."
msgstr "Dine sikkerhedskoder til tofaktorgodkendelse er blevet genoprettet." msgstr "Dine sikkerhedskoder til tofaktorgodkendelse er blevet genoprettet."
#: pretix/control/logdisplay.py:765 #: pretix/control/logdisplay.py:765
#, fuzzy
msgid "A two-factor emergency code has been generated." msgid "A two-factor emergency code has been generated."
msgstr "Dine sikkerhedskoder til tofaktorgodkendelse er blevet genoprettet." msgstr "Der er genereret en tofaktorkode til nødsituationer."
#: pretix/control/logdisplay.py:766 #: pretix/control/logdisplay.py:766
#, python-brace-format #, python-brace-format
@@ -18295,20 +18220,19 @@ msgstr ""
#: pretix/control/logdisplay.py:770 #: pretix/control/logdisplay.py:770
msgid "Notifications have been enabled." msgid "Notifications have been enabled."
msgstr "" msgstr "Notifikationer er blevet aktiveret."
#: pretix/control/logdisplay.py:771 #: pretix/control/logdisplay.py:771
msgid "Notifications have been disabled." msgid "Notifications have been disabled."
msgstr "" msgstr "Notifikationer er blevet deaktiveret."
#: pretix/control/logdisplay.py:772 #: pretix/control/logdisplay.py:772
msgid "Your notification settings have been changed." msgid "Your notification settings have been changed."
msgstr "" msgstr "Dine notifikationsindstillinger er blevet ændret."
#: pretix/control/logdisplay.py:773 #: pretix/control/logdisplay.py:773
#, fuzzy
msgid "This user has been anonymized." msgid "This user has been anonymized."
msgstr "Bestillingen er blevet tilbagebetalt." msgstr "Denne bruger er blevet anonymiseret."
#: pretix/control/logdisplay.py:777 #: pretix/control/logdisplay.py:777
msgid "Password reset mail sent." msgid "Password reset mail sent."
@@ -18323,20 +18247,21 @@ msgid ""
"A repeated password reset has been denied, as the last request was less than " "A repeated password reset has been denied, as the last request was less than "
"24 hours ago." "24 hours ago."
msgstr "" msgstr ""
"En gentagen anmodning om nulstilling af adgangskode er blevet afvist, da den "
"seneste anmodning blev fremsat for mindre end 24 timer siden."
#: pretix/control/logdisplay.py:781 #: pretix/control/logdisplay.py:781
#, fuzzy, python-brace-format #, python-brace-format
msgid "The organizer \"{name}\" has been deleted." msgid "The organizer \"{name}\" has been deleted."
msgstr "Varianten \"{value}\" er blevet slettet." msgstr "Arrangøren \"{name}\" er blevet slettet."
#: pretix/control/logdisplay.py:782 pretix/control/logdisplay.py:1013 #: pretix/control/logdisplay.py:782 pretix/control/logdisplay.py:1013
msgid "A voucher has been sent to a person on the waiting list." msgid "A voucher has been sent to a person on the waiting list."
msgstr "En rabatkode er blevet sendt til en person på ventelisten." msgstr "En rabatkode er blevet sendt til en person på ventelisten."
#: pretix/control/logdisplay.py:783 #: pretix/control/logdisplay.py:783
#, fuzzy
msgid "An entry has been transferred to another waiting list." msgid "An entry has been transferred to another waiting list."
msgstr "En bruger er føjet til gruppen." msgstr "En ansøgning er blevet flyttet til en anden venteliste."
#: pretix/control/logdisplay.py:784 #: pretix/control/logdisplay.py:784
msgid "The team has been created." msgid "The team has been created."
@@ -18351,19 +18276,16 @@ msgid "The team has been deleted."
msgstr "Gruppen er blevet slettet." msgstr "Gruppen er blevet slettet."
#: pretix/control/logdisplay.py:787 pretix/control/views/organizer.py:2381 #: pretix/control/logdisplay.py:787 pretix/control/views/organizer.py:2381
#, fuzzy
msgid "The gate has been created." msgid "The gate has been created."
msgstr "Gruppen er blevet oprettet." msgstr "Porten er blevet oprettet."
#: pretix/control/logdisplay.py:788 #: pretix/control/logdisplay.py:788
#, fuzzy
msgid "The gate has been changed." msgid "The gate has been changed."
msgstr "Kategorien er blevet ændret." msgstr "Porten er blevet ændret."
#: pretix/control/logdisplay.py:789 #: pretix/control/logdisplay.py:789
#, fuzzy
msgid "The gate has been deleted." msgid "The gate has been deleted."
msgstr "Gruppen er blevet slettet." msgstr "Porten er blevet slettet."
#: pretix/control/logdisplay.py:790 #: pretix/control/logdisplay.py:790
msgctxt "subevent" msgctxt "subevent"
@@ -18371,10 +18293,9 @@ msgid "The event date has been deleted."
msgstr "Arrangementsdatoen er blevet slettet." msgstr "Arrangementsdatoen er blevet slettet."
#: pretix/control/logdisplay.py:791 #: pretix/control/logdisplay.py:791
#, fuzzy
msgctxt "subevent" msgctxt "subevent"
msgid "The event date has been canceled." msgid "The event date has been canceled."
msgstr "Arrangementsdatoen er blevet ændret." msgstr "Begivenhedsdatoen er blevet aflyst."
#: pretix/control/logdisplay.py:792 #: pretix/control/logdisplay.py:792
msgctxt "subevent" msgctxt "subevent"
@@ -18402,58 +18323,50 @@ msgid "A quota has been removed from the event date."
msgstr "En kvote er blevet fjernet fra arrangementsdatoen." msgstr "En kvote er blevet fjernet fra arrangementsdatoen."
#: pretix/control/logdisplay.py:797 #: pretix/control/logdisplay.py:797
#, fuzzy
msgid "The device has been created." msgid "The device has been created."
msgstr "Arrangementsdatoen er blevet oprettet." msgstr "Enheden er blevet oprettet."
#: pretix/control/logdisplay.py:798 #: pretix/control/logdisplay.py:798
#, fuzzy
msgid "The device has been changed." msgid "The device has been changed."
msgstr "Voucheren er blevet ændret." msgstr "Enheden er blevet udskiftet."
#: pretix/control/logdisplay.py:799 #: pretix/control/logdisplay.py:799
#, fuzzy
msgid "Access of the device has been revoked." msgid "Access of the device has been revoked."
msgstr "Enheden er blevet fjernet." msgstr "Adgangen til enheden er blevet tilbagekaldt."
#: pretix/control/logdisplay.py:800 #: pretix/control/logdisplay.py:800
#, fuzzy
msgid "The device has been initialized." msgid "The device has been initialized."
msgstr "Enheden er blevet fjernet." msgstr "Enheden er blevet initialiseret."
#: pretix/control/logdisplay.py:801 #: pretix/control/logdisplay.py:801
#, fuzzy
msgid "The access token of the device has been regenerated." msgid "The access token of the device has been regenerated."
msgstr "Fakturaen er blevet genereret igen." msgstr "Enhedens adgangstoken er blevet genereret på ny."
#: pretix/control/logdisplay.py:802 #: pretix/control/logdisplay.py:802
msgid "The device has notified the server of an hardware or software update." msgid "The device has notified the server of an hardware or software update."
msgstr "" msgstr ""
"Enheden har meddelt serveren, at der er en hardware- eller "
"softwareopdatering."
#: pretix/control/logdisplay.py:803 #: pretix/control/logdisplay.py:803
#, fuzzy
msgid "The gift card has been created." msgid "The gift card has been created."
msgstr "Gruppen er blevet oprettet." msgstr "Gavekortet er oprettet."
#: pretix/control/logdisplay.py:804 pretix/control/views/organizer.py:1943 #: pretix/control/logdisplay.py:804 pretix/control/views/organizer.py:1943
#, fuzzy
msgid "The gift card has been changed." msgid "The gift card has been changed."
msgstr "Gruppen er blevet oprettet." msgstr "Gavekortet er blevet ændret."
#: pretix/control/logdisplay.py:805 #: pretix/control/logdisplay.py:805
#, fuzzy
msgid "A manual transaction has been performed." msgid "A manual transaction has been performed."
msgstr "Spørgsmålet er blevet tilføjet." msgstr "Der er udført en manuel overførsel."
#: pretix/control/logdisplay.py:806 #: pretix/control/logdisplay.py:806
#, fuzzy
msgid "A payment has been performed." msgid "A payment has been performed."
msgstr "Betalingsbetingelser er blevet ændret." msgstr "Der er blevet foretaget en betaling."
#: pretix/control/logdisplay.py:807 #: pretix/control/logdisplay.py:807
#, fuzzy
msgid "A refund has been performed. " msgid "A refund has been performed. "
msgstr "Bestilling oprettet." msgstr "Der er blevet foretaget en tilbagebetaling. "
#: pretix/control/logdisplay.py:808 #: pretix/control/logdisplay.py:808
#, python-brace-format #, python-brace-format
@@ -18466,44 +18379,34 @@ msgid "The token \"{name}\" has been revoked."
msgstr "Token \"{name}\" slettet." msgstr "Token \"{name}\" slettet."
#: pretix/control/logdisplay.py:810 #: pretix/control/logdisplay.py:810
#, fuzzy
#| msgid "The check-in list has been deleted."
msgid "The check-in and print log state has been reset." msgid "The check-in and print log state has been reset."
msgstr "Tjek-ind-listen er blevet slettet." msgstr "Status for indtjeknings- og udskriftsloggen er blevet nulstillet."
#: pretix/control/logdisplay.py:817 pretix/control/logdisplay.py:876 #: pretix/control/logdisplay.py:817 pretix/control/logdisplay.py:876
#, fuzzy
#| msgid "A plugin has been enabled."
msgid "The plugin has been enabled." msgid "The plugin has been enabled."
msgstr "En plugin er blevet aktiveret." msgstr "Plugin'et er blevet aktiveret."
#: pretix/control/logdisplay.py:818 pretix/control/logdisplay.py:877 #: pretix/control/logdisplay.py:818 pretix/control/logdisplay.py:877
#: pretix/control/views/event.py:497 pretix/control/views/organizer.py:757 #: pretix/control/views/event.py:497 pretix/control/views/organizer.py:757
#, fuzzy
#| msgid "A plugin has been disabled."
msgid "The plugin has been disabled." msgid "The plugin has been disabled."
msgstr "En plugin er blevet deaktiveret." msgstr "Plugin'et er blevet deaktiveret."
#: pretix/control/logdisplay.py:821 pretix/control/logdisplay.py:880 #: pretix/control/logdisplay.py:821 pretix/control/logdisplay.py:880
#, fuzzy, python-brace-format #, python-brace-format
#| msgid "Question {val}"
msgid "Plugin {val}" msgid "Plugin {val}"
msgstr "Spørgsmål {val}" msgstr "Plugin {val}"
#: pretix/control/logdisplay.py:836 #: pretix/control/logdisplay.py:836
#, fuzzy
msgid "A meta property has been added to this event." msgid "A meta property has been added to this event."
msgstr "En bruger er føjet til gruppen." msgstr "Der er tilføjet en metaegenskab til denne begivenhed."
#: pretix/control/logdisplay.py:837 #: pretix/control/logdisplay.py:837
#, fuzzy
msgid "A meta property has been removed from this event." msgid "A meta property has been removed from this event."
msgstr "En bruger er blevet fjernet fra gruppen." msgstr "En metaegenskab er blevet fjernet fra denne begivenhed."
#: pretix/control/logdisplay.py:838 #: pretix/control/logdisplay.py:838
#, fuzzy
msgid "A meta property has been changed on this event." msgid "A meta property has been changed on this event."
msgstr "En kvote er blevet ændret på arrangementsdatoen." msgstr "En metaegenskab er blevet ændret i forbindelse med denne begivenhed."
#: pretix/control/logdisplay.py:839 #: pretix/control/logdisplay.py:839
msgid "The event settings have been changed." msgid "The event settings have been changed."
@@ -18515,31 +18418,27 @@ msgstr "Indstillingerne for download af billet er blevet ændret."
#: pretix/control/logdisplay.py:843 #: pretix/control/logdisplay.py:843
msgid "The shop has been taken live." msgid "The shop has been taken live."
msgstr "" msgstr "Butikken er nu gået live."
#: pretix/control/logdisplay.py:844 #: pretix/control/logdisplay.py:844
msgid "The shop has been taken offline." msgid "The shop has been taken offline."
msgstr "" msgstr "Butikken er blevet taget offline."
#: pretix/control/logdisplay.py:845 #: pretix/control/logdisplay.py:845
#, fuzzy
msgid "The shop has been taken into test mode." msgid "The shop has been taken into test mode."
msgstr "Bestillingen er blevet tilbagebetalt." msgstr "Butikken er nu sat i testtilstand."
#: pretix/control/logdisplay.py:846 #: pretix/control/logdisplay.py:846
#, fuzzy
msgid "The test mode has been disabled." msgid "The test mode has been disabled."
msgstr "Den valgte dato er blevet slettet." msgstr "Testtilstanden er blevet deaktiveret."
#: pretix/control/logdisplay.py:847 #: pretix/control/logdisplay.py:847
#, fuzzy
msgid "The event has been created." msgid "The event has been created."
msgstr "Arrangementsdatoen er blevet oprettet." msgstr "Begivenheden er blevet oprettet."
#: pretix/control/logdisplay.py:848 #: pretix/control/logdisplay.py:848
#, fuzzy
msgid "The event details have been changed." msgid "The event details have been changed."
msgstr "Indstillingerne for arrangementet er blevet ændret." msgstr "Begivenhedens oplysninger er blevet ændret."
#: pretix/control/logdisplay.py:850 #: pretix/control/logdisplay.py:850
msgid "An answer option has been added to the question." msgid "An answer option has been added to the question."
@@ -18582,10 +18481,9 @@ msgid "The check-in list has been changed."
msgstr "Tjek-ind-listen er blevet ændret." msgstr "Tjek-ind-listen er blevet ændret."
#: pretix/control/logdisplay.py:869 #: pretix/control/logdisplay.py:869
#, fuzzy, python-brace-format #, python-brace-format
#| msgid "Check-in list"
msgid "Check-in list {val}" msgid "Check-in list {val}"
msgstr "Tjek-ind-liste" msgstr "Tjek-ind-liste {val}"
#: pretix/control/logdisplay.py:896 #: pretix/control/logdisplay.py:896
msgid "The product has been created." msgid "The product has been created."
@@ -18596,9 +18494,8 @@ msgid "The product has been changed."
msgstr "Produktet er blevet ændret." msgstr "Produktet er blevet ændret."
#: pretix/control/logdisplay.py:898 #: pretix/control/logdisplay.py:898
#, fuzzy
msgid "The product has been reordered." msgid "The product has been reordered."
msgstr "Produktet er blevet oprettet." msgstr "Produktet er blevet genbestilt."
#: pretix/control/logdisplay.py:899 #: pretix/control/logdisplay.py:899
msgid "The product has been deleted." msgid "The product has been deleted."
@@ -18617,37 +18514,28 @@ msgid "An add-on has been changed on this product."
msgstr "En tilføjelse er blevet ændret på dette produkt." msgstr "En tilføjelse er blevet ændret på dette produkt."
#: pretix/control/logdisplay.py:903 #: pretix/control/logdisplay.py:903
#, fuzzy
msgid "A bundled item has been added to this product." msgid "A bundled item has been added to this product."
msgstr "En tilføjelse er blevet føjet til dette produkt." msgstr "Der er blevet tilføjet en medfølgende vare til dette produkt."
#: pretix/control/logdisplay.py:904 #: pretix/control/logdisplay.py:904
#, fuzzy
msgid "A bundled item has been removed from this product." msgid "A bundled item has been removed from this product."
msgstr "En tilføjelse er blevet fjernet fra dette produkt." msgstr "En medfølgende vare er blevet fjernet fra dette produkt."
#: pretix/control/logdisplay.py:905 #: pretix/control/logdisplay.py:905
#, fuzzy
msgid "A bundled item has been changed on this product." msgid "A bundled item has been changed on this product."
msgstr "En tilføjelse er blevet ændret dette produkt." msgstr "En medfølgende vare er blevet ændret for dette produkt."
#: pretix/control/logdisplay.py:906 #: pretix/control/logdisplay.py:906
#, fuzzy
#| msgid "An add-on has been added to this product."
msgid "A program time has been added to this product." msgid "A program time has been added to this product."
msgstr "En tilføjelse er blevet føjet til dette produkt." msgstr "Der er blevet tilføjet en programtid til dette produkt."
#: pretix/control/logdisplay.py:907 #: pretix/control/logdisplay.py:907
#, fuzzy
#| msgid "An add-on has been changed on this product."
msgid "A program time has been changed on this product." msgid "A program time has been changed on this product."
msgstr "En tilføjelse er blevet ændret på dette produkt." msgstr "En programtid for dette produkt er blevet ændret."
#: pretix/control/logdisplay.py:908 #: pretix/control/logdisplay.py:908
#, fuzzy
#| msgid "An add-on has been removed from this product."
msgid "A program time has been removed from this product." msgid "A program time has been removed from this product."
msgstr "En tilføjelse er blevet fjernet fra dette produkt." msgstr "En programtid er blevet fjernet fra dette produkt."
#: pretix/control/logdisplay.py:915 #: pretix/control/logdisplay.py:915
#, python-brace-format #, python-brace-format
@@ -18665,9 +18553,9 @@ msgid "The variation \"{value}\" has been changed."
msgstr "Varianten \"{value}\" er blevet ændret." msgstr "Varianten \"{value}\" er blevet ændret."
#: pretix/control/logdisplay.py:934 #: pretix/control/logdisplay.py:934
#, fuzzy, python-brace-format #, python-brace-format
msgid "Payment {local_id} has been confirmed." msgid "Payment {local_id} has been confirmed."
msgstr "Betalingsmetoden er blevet ændret." msgstr "Betalingen {local_id} er blevet bekræftet."
#: pretix/control/logdisplay.py:935 #: pretix/control/logdisplay.py:935
#, fuzzy, python-brace-format #, fuzzy, python-brace-format
+6 -6
View File
@@ -5,16 +5,16 @@ msgstr ""
"Project-Id-Version: 1\n" "Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-27 15:47+0000\n" "POT-Creation-Date: 2026-05-27 15:47+0000\n"
"PO-Revision-Date: 2026-05-27 15:20+0000\n" "PO-Revision-Date: 2026-06-09 20:00+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n" "Last-Translator: Mira <weller@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/de/" "Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/"
">\n" "de/>\n"
"Language: de\n" "Language: de\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 2026.5\n" "X-Generator: Weblate 2026.6.1\n"
"X-Poedit-Bookmarks: -1,-1,904,-1,-1,-1,-1,-1,-1,-1\n" "X-Poedit-Bookmarks: -1,-1,904,-1,-1,-1,-1,-1,-1,-1\n"
#: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html:670 #: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html:670
@@ -11353,7 +11353,7 @@ msgid ""
msgstr "" msgstr ""
"Wenn diese Option deaktiviert ist, werden Tickets nur für Produkte " "Wenn diese Option deaktiviert ist, werden Tickets nur für Produkte "
"aktiviert, bei denen die Option \"Berechtigt zum Eintritt\" gesetzt ist. Sie " "aktiviert, bei denen die Option \"Berechtigt zum Eintritt\" gesetzt ist. Sie "
"können die Ticketgenerierung auch in den Einstellungen von jedes Produktes " "können die Ticketgenerierung auch in den Einstellungen jedes Produktes "
"einzeln abschalten." "einzeln abschalten."
#: pretix/base/settings.py:1813 #: pretix/base/settings.py:1813
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: 1\n" "Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-27 15:47+0000\n" "POT-Creation-Date: 2026-05-27 15:47+0000\n"
"PO-Revision-Date: 2026-05-27 15:20+0000\n" "PO-Revision-Date: 2026-06-09 20:00+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n" "Last-Translator: Mira <weller@rami.io>\n"
"Language-Team: German (informal) <https://translate.pretix.eu/projects/" "Language-Team: German (informal) <https://translate.pretix.eu/projects/"
"pretix/pretix/de_Informal/>\n" "pretix/pretix/de_Informal/>\n"
"Language: de_Informal\n" "Language: de_Informal\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 2026.5\n" "X-Generator: Weblate 2026.6.1\n"
#: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html:670 #: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html:670
#: pretix/control/templates/pretixcontrol/events/index.html:166 #: pretix/control/templates/pretixcontrol/events/index.html:166
@@ -11339,8 +11339,8 @@ msgid ""
"issuing in every product separately." "issuing in every product separately."
msgstr "" msgstr ""
"Wenn diese Option deaktiviert ist, werden Tickets nur für Produkte " "Wenn diese Option deaktiviert ist, werden Tickets nur für Produkte "
"aktiviert, bei denen die Option \"Berechtigt zum Eintritt\" gesetzt ist. Sie " "aktiviert, bei denen die Option \"Berechtigt zum Eintritt\" gesetzt ist. Du "
"können die Ticketgenerierung auch in den Einstellungen von jedes Produktes " "kannst die Ticketgenerierung auch in den Einstellungen jedes Produktes "
"einzeln abschalten." "einzeln abschalten."
#: pretix/base/settings.py:1813 #: pretix/base/settings.py:1813
+4 -4
View File
@@ -4,8 +4,8 @@ msgstr ""
"Project-Id-Version: 1\n" "Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-27 15:47+0000\n" "POT-Creation-Date: 2026-05-27 15:47+0000\n"
"PO-Revision-Date: 2026-05-29 17:00+0000\n" "PO-Revision-Date: 2026-06-08 17:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n" "Last-Translator: Sébastien BRUNEAU <s.bruneau@beauvaisis.fr>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/" "Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/"
"fr/>\n" "fr/>\n"
"Language: fr\n" "Language: fr\n"
@@ -13,7 +13,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n" "Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 2026.5\n" "X-Generator: Weblate 2026.6.1\n"
#: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html:670 #: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html:670
#: pretix/control/templates/pretixcontrol/events/index.html:166 #: pretix/control/templates/pretixcontrol/events/index.html:166
@@ -35123,7 +35123,7 @@ msgstr "Vous devez cocher toutes les cases en bas de la page."
#: pretix/presale/forms/checkout.py:67 #: pretix/presale/forms/checkout.py:67
msgid "Email address (repeated)" msgid "Email address (repeated)"
msgstr "Adresse de courriel (répété)" msgstr "Adresse de courriel (répétée)"
#: pretix/presale/forms/checkout.py:68 #: pretix/presale/forms/checkout.py:68
msgid "" msgid ""
+29 -41
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-27 14:47+0000\n" "POT-Creation-Date: 2026-05-27 14:47+0000\n"
"PO-Revision-Date: 2026-01-23 15:00+0000\n" "PO-Revision-Date: 2026-06-18 21:00+0000\n"
"Last-Translator: Vajda Tamás <vajda.tamas@szwg.hu>\n" "Last-Translator: Szurofka Márton <szurofka.marton@sze.hu>\n"
"Language-Team: Hungarian <https://translate.pretix.eu/projects/pretix/pretix-" "Language-Team: Hungarian <https://translate.pretix.eu/projects/pretix/pretix-"
"js/hu/>\n" "js/hu/>\n"
"Language: hu\n" "Language: hu\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.15.2\n" "X-Generator: Weblate 2026.6.1\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -31,16 +31,16 @@ msgstr "Megjegyzés:"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:34 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:34
msgid "PayPal" msgid "PayPal"
msgstr "" msgstr "PayPal"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:35 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:35
msgid "Venmo" msgid "Venmo"
msgstr "" msgstr "Venmo"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:36 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:36
#: pretix/static/pretixpresale/js/walletdetection.js:38 #: pretix/static/pretixpresale/js/walletdetection.js:38
msgid "Apple Pay" msgid "Apple Pay"
msgstr "" msgstr "Apple Pay"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:37 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:37
msgid "Itaú" msgid "Itaú"
@@ -56,7 +56,7 @@ msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:40 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:40
msgid "PayPal Pay Later" msgid "PayPal Pay Later"
msgstr "" msgstr "PayPal Későbbi Fizetés"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:41 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:41
msgid "iDEAL | Wero" msgid "iDEAL | Wero"
@@ -148,7 +148,7 @@ msgstr "A fizetés megerősítése…"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:254 #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:254
msgid "Payment method unavailable" msgid "Payment method unavailable"
msgstr "" msgstr "Fizetésimód nem elérhető"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:63 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:63
@@ -163,12 +163,12 @@ msgstr "Kifizetett megrendelések"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Attendees (ordered)" msgid "Attendees (ordered)"
msgstr "" msgstr "Résztvevők (megrendelve)"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Attendees (paid)" msgid "Attendees (paid)"
msgstr "" msgstr "Résztvevők (fizetve)"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:51 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:51
msgid "Total revenue" msgid "Total revenue"
@@ -187,18 +187,13 @@ msgid "Contacting your bank …"
msgstr "Kapcsolatfelvétel a bankjával…" msgstr "Kapcsolatfelvétel a bankjával…"
#: pretix/static/lightbox/js/lightbox.js:96 #: pretix/static/lightbox/js/lightbox.js:96
#, fuzzy
#| msgctxt "widget"
#| msgid "Close"
msgid "close" msgid "close"
msgstr "Bezárás" msgstr "bezárás"
#: pretix/static/pretixbase/js/addressform.js:108 #: pretix/static/pretixbase/js/addressform.js:108
#: pretix/static/pretixpresale/js/ui/main.js:529 #: pretix/static/pretixpresale/js/ui/main.js:529
#, fuzzy
#| msgid "Cart expired"
msgid "required" msgid "required"
msgstr "A kosár lejárt" msgstr "a kosár lejárt"
#: pretix/static/pretixbase/js/asynctask.js:13 #: pretix/static/pretixbase/js/asynctask.js:13
#, fuzzy #, fuzzy
@@ -276,6 +271,8 @@ msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:286 #: pretix/static/pretixbase/js/asynctask.js:286
msgid "If this takes longer than a few minutes, please contact us." msgid "If this takes longer than a few minutes, please contact us."
msgstr "" msgstr ""
"Amennyiben ez több időt vesz igénybe, mint néhány perc, kérjük jelezze "
"felénk."
#: pretix/static/pretixbase/js/asynctask.js:341 #: pretix/static/pretixbase/js/asynctask.js:341
msgid "Close message" msgid "Close message"
@@ -302,26 +299,20 @@ msgid "Group of objects"
msgstr "tárgy csoport" msgstr "tárgy csoport"
#: pretix/static/pretixcontrol/js/ui/editor.js:909 #: pretix/static/pretixcontrol/js/ui/editor.js:909
#, fuzzy
#| msgid "Text object"
msgid "Text object (deprecated)" msgid "Text object (deprecated)"
msgstr "Szöveg" msgstr "Szöveg elem (elavult)"
#: pretix/static/pretixcontrol/js/ui/editor.js:911 #: pretix/static/pretixcontrol/js/ui/editor.js:911
#, fuzzy
#| msgid "Text object"
msgid "Text box" msgid "Text box"
msgstr "Szöveg" msgstr "Szövegdoboz"
#: pretix/static/pretixcontrol/js/ui/editor.js:913 #: pretix/static/pretixcontrol/js/ui/editor.js:913
msgid "Barcode area" msgid "Barcode area"
msgstr "Vonalkód terület" msgstr "Vonalkód terület"
#: pretix/static/pretixcontrol/js/ui/editor.js:915 #: pretix/static/pretixcontrol/js/ui/editor.js:915
#, fuzzy
#| msgid "Barcode area"
msgid "Image area" msgid "Image area"
msgstr "Vonalkód terület" msgstr "Kép helye"
#: pretix/static/pretixcontrol/js/ui/editor.js:917 #: pretix/static/pretixcontrol/js/ui/editor.js:917
msgid "Powered by pretix" msgid "Powered by pretix"
@@ -361,19 +352,16 @@ msgid "Unknown error."
msgstr "Ismeretlen hiba." msgstr "Ismeretlen hiba."
#: pretix/static/pretixcontrol/js/ui/main.js:310 #: pretix/static/pretixcontrol/js/ui/main.js:310
#, fuzzy
#| msgid "Your color has great contrast and is very easy to read!"
msgid "Your color has great contrast and will provide excellent accessibility." msgid "Your color has great contrast and will provide excellent accessibility."
msgstr "A választott színek remek kontrasztot adnak, és nagyon könnyű olvasni!" msgstr "A választott színek remek kontrasztot adnak, és nagyon könnyű olvasni."
#: pretix/static/pretixcontrol/js/ui/main.js:314 #: pretix/static/pretixcontrol/js/ui/main.js:314
#, fuzzy
#| msgid "Your color has decent contrast and is probably good-enough to read!"
msgid "" msgid ""
"Your color has decent contrast and is sufficient for minimum accessibility " "Your color has decent contrast and is sufficient for minimum accessibility "
"requirements." "requirements."
msgstr "" msgstr ""
"A választott színek kontrasztja elégséges, és valószínűleg jól olvasható!" "A választott színek kontrasztja elégséges, és elegendő a minimum "
"követelménynek."
#: pretix/static/pretixcontrol/js/ui/main.js:318 #: pretix/static/pretixcontrol/js/ui/main.js:318
msgid "" msgid ""
@@ -384,7 +372,7 @@ msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:446 #: pretix/static/pretixcontrol/js/ui/main.js:446
#: pretix/static/pretixcontrol/js/ui/main.js:466 #: pretix/static/pretixcontrol/js/ui/main.js:466
msgid "Search query" msgid "Search query"
msgstr "" msgstr "Keresési lekérdezés"
#: pretix/static/pretixcontrol/js/ui/main.js:464 #: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "All" msgid "All"
@@ -404,7 +392,7 @@ msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:845 #: pretix/static/pretixcontrol/js/ui/main.js:845
msgid "Invalid page number." msgid "Invalid page number."
msgstr "" msgstr "Helytelen oldalszám."
#: pretix/static/pretixcontrol/js/ui/main.js:1003 #: pretix/static/pretixcontrol/js/ui/main.js:1003
msgid "Use a different name internally" msgid "Use a different name internally"
@@ -465,7 +453,7 @@ msgstr "A kosár lejárt"
#: pretix/static/pretixpresale/js/ui/cart.js:58 #: pretix/static/pretixpresale/js/ui/cart.js:58
#: pretix/static/pretixpresale/js/ui/cart.js:84 #: pretix/static/pretixpresale/js/ui/cart.js:84
msgid "Your cart is about to expire." msgid "Your cart is about to expire."
msgstr "" msgstr "Kosara tartalma hamarosan lejár."
#: pretix/static/pretixpresale/js/ui/cart.js:62 #: pretix/static/pretixpresale/js/ui/cart.js:62
#, fuzzy #, fuzzy
@@ -532,32 +520,32 @@ msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:16 #: pretix/static/pretixpresale/js/widget/widget.js:16
msgctxt "widget" msgctxt "widget"
msgid "Quantity" msgid "Quantity"
msgstr "" msgstr "Mennyiség"
#: pretix/static/pretixpresale/js/widget/widget.js:17 #: pretix/static/pretixpresale/js/widget/widget.js:17
msgctxt "widget" msgctxt "widget"
msgid "Decrease quantity" msgid "Decrease quantity"
msgstr "" msgstr "Mennyiség csökkentése"
#: pretix/static/pretixpresale/js/widget/widget.js:18 #: pretix/static/pretixpresale/js/widget/widget.js:18
msgctxt "widget" msgctxt "widget"
msgid "Increase quantity" msgid "Increase quantity"
msgstr "" msgstr "Mennyiség növelése"
#: pretix/static/pretixpresale/js/widget/widget.js:19 #: pretix/static/pretixpresale/js/widget/widget.js:19
msgctxt "widget" msgctxt "widget"
msgid "Filter events by" msgid "Filter events by"
msgstr "" msgstr "Események szűrése a következő szerint"
#: pretix/static/pretixpresale/js/widget/widget.js:20 #: pretix/static/pretixpresale/js/widget/widget.js:20
msgctxt "widget" msgctxt "widget"
msgid "Filter" msgid "Filter"
msgstr "" msgstr "Szűrés"
#: pretix/static/pretixpresale/js/widget/widget.js:21 #: pretix/static/pretixpresale/js/widget/widget.js:21
msgctxt "widget" msgctxt "widget"
msgid "Price" msgid "Price"
msgstr "" msgstr "Ár"
#: pretix/static/pretixpresale/js/widget/widget.js:22 #: pretix/static/pretixpresale/js/widget/widget.js:22
#, javascript-format #, javascript-format
+200 -174
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-27 15:47+0000\n" "POT-Creation-Date: 2026-05-27 15:47+0000\n"
"PO-Revision-Date: 2025-10-17 16:55+0000\n" "PO-Revision-Date: 2026-06-20 17:00+0000\n"
"Last-Translator: fd <fd@denkena-consulting.com>\n" "Last-Translator: Nikita Mitasov <me@ch4og.com>\n"
"Language-Team: Russian <https://translate.pretix.eu/projects/pretix/pretix/" "Language-Team: Russian <https://translate.pretix.eu/projects/pretix/pretix/"
"ru/>\n" "ru/>\n"
"Language: ru\n" "Language: ru\n"
@@ -18,7 +18,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && " "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
"X-Generator: Weblate 5.13.3\n" "X-Generator: Weblate 2026.6.1\n"
#: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html:670 #: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html:670
#: pretix/control/templates/pretixcontrol/events/index.html:166 #: pretix/control/templates/pretixcontrol/events/index.html:166
@@ -53,7 +53,7 @@ msgstr "В продаже"
#: pretix/_base_settings.py:89 #: pretix/_base_settings.py:89
msgid "English" msgid "English"
msgstr "английский язык" msgstr "Английский язык"
#: pretix/_base_settings.py:90 #: pretix/_base_settings.py:90
msgid "German" msgid "German"
@@ -466,7 +466,7 @@ msgstr "Этот пользователь уже имеет разрешения
#: pretix/api/views/cart.py:209 #: pretix/api/views/cart.py:209
msgid "" msgid ""
"The specified voucher has already been used the maximum number of times." "The specified voucher has already been used the maximum number of times."
msgstr "Указанный ваучер уже использован максимальное количество раз." msgstr "Указанный промокод уже использован максимальное количество раз."
#: pretix/api/views/checkin.py:645 pretix/api/views/checkin.py:652 #: pretix/api/views/checkin.py:645 pretix/api/views/checkin.py:652
msgid "Medium connected to other event" msgid "Medium connected to other event"
@@ -565,65 +565,50 @@ msgid "Refund of payment failed"
msgstr "Возврат платежа не удался" msgstr "Возврат платежа не удался"
#: pretix/api/webhooks.py:359 #: pretix/api/webhooks.py:359
#, fuzzy
#| msgctxt "checkoutflow"
#| msgid "Order confirmed"
msgid "Payment confirmed" msgid "Payment confirmed"
msgstr "Заказ подтверждён" msgstr "Платёж подтверждён"
#: pretix/api/webhooks.py:363 #: pretix/api/webhooks.py:363
msgid "Order approved" msgid "Order approved"
msgstr "" msgstr "Заказ подтверждён"
#: pretix/api/webhooks.py:367 #: pretix/api/webhooks.py:367
msgid "Order denied" msgid "Order denied"
msgstr "" msgstr "Заказ отклонён"
#: pretix/api/webhooks.py:371 #: pretix/api/webhooks.py:371
#, fuzzy
#| msgid "Order date"
msgid "Order deleted" msgid "Order deleted"
msgstr "Дата заказа" msgstr "Заказ удалён"
#: pretix/api/webhooks.py:375 #: pretix/api/webhooks.py:375
msgid "Ticket checked in" msgid "Ticket checked in"
msgstr "" msgstr "Билет использован"
#: pretix/api/webhooks.py:379 #: pretix/api/webhooks.py:379
msgid "Ticket check-in reverted" msgid "Ticket check-in reverted"
msgstr "" msgstr "Использование билета отменено"
#: pretix/api/webhooks.py:383 #: pretix/api/webhooks.py:383
#, fuzzy
#| msgid "Event date"
msgid "Event created" msgid "Event created"
msgstr "Дата мероприятия" msgstr "Мероприятие создано"
#: pretix/api/webhooks.py:387 #: pretix/api/webhooks.py:387
#, fuzzy
#| msgid "Event date range"
msgid "Event details changed" msgid "Event details changed"
msgstr иапазон дат мероприятия" msgstr анные о мероприятии изменились"
#: pretix/api/webhooks.py:391 #: pretix/api/webhooks.py:391
#, fuzzy
#| msgid "Event date"
msgid "Event deleted" msgid "Event deleted"
msgstr "Дата мероприятия" msgstr "Мероприятие удалено"
#: pretix/api/webhooks.py:395 #: pretix/api/webhooks.py:395
#, fuzzy
#| msgid "Event begin date and time"
msgctxt "subevent" msgctxt "subevent"
msgid "Event series date added" msgid "Event series date added"
msgstr ата и время начала мероприятия" msgstr обавлена дата серии мероприятий"
#: pretix/api/webhooks.py:399 #: pretix/api/webhooks.py:399
#, fuzzy
#| msgid "Event date range"
msgctxt "subevent" msgctxt "subevent"
msgid "Event series date changed" msgid "Event series date changed"
msgstr "Диапазон дат мероприятия" msgstr "Изменена дата серии мероприятий"
#: pretix/api/webhooks.py:403 #: pretix/api/webhooks.py:403
#, fuzzy #, fuzzy
@@ -866,14 +851,13 @@ msgid "<a {a_attr}>powered by {name} based on pretix</a>"
msgstr "на базе <a %(a_attr)s>pretix</a>" msgstr "на базе <a %(a_attr)s>pretix</a>"
#: pretix/base/context.py:55 #: pretix/base/context.py:55
#, fuzzy, python-format #, python-format
#| msgid "<a %(a_attr)s>event ticketing powered by pretix</a>"
msgid "<a %(a_attr)s>ticketing powered by pretix</a>" msgid "<a %(a_attr)s>ticketing powered by pretix</a>"
msgstr "<a %(a_attr)s>event ticketing powered by pretix</a>" msgstr "<a %(a_attr)s>продажа билетов на базе pretix</a>"
#: pretix/base/context.py:64 #: pretix/base/context.py:64
msgid "source code" msgid "source code"
msgstr "Промокод" msgstr "исходный код"
#: pretix/base/customersso/oidc.py:61 #: pretix/base/customersso/oidc.py:61
#, python-brace-format #, python-brace-format
@@ -2777,10 +2761,8 @@ msgid "Voucher"
msgstr "" msgstr ""
#: pretix/base/exporters/orderlist.py:656 #: pretix/base/exporters/orderlist.py:656
#, fuzzy
#| msgid "Voucher code used:"
msgid "Voucher budget usage" msgid "Voucher budget usage"
msgstr "Использованный код ваучера:" msgstr "Использовано по промокоду"
#: pretix/base/exporters/orderlist.py:657 #: pretix/base/exporters/orderlist.py:657
#, fuzzy #, fuzzy
@@ -3176,7 +3158,7 @@ msgstr "Заказы в ожидании"
#: pretix/base/exporters/orderlist.py:1209 #: pretix/base/exporters/orderlist.py:1209
msgid "Blocking vouchers" msgid "Blocking vouchers"
msgstr "Блокирующие ваучеры" msgstr "Блокирующие промокоды"
#: pretix/base/exporters/orderlist.py:1210 pretix/control/views/item.py:1149 #: pretix/base/exporters/orderlist.py:1210 pretix/control/views/item.py:1149
msgid "Current user's carts" msgid "Current user's carts"
@@ -4606,7 +4588,7 @@ msgstr "Архивные события не найдены."
#: pretix/base/modelimport_vouchers.py:50 pretix/base/models/vouchers.py:491 #: pretix/base/modelimport_vouchers.py:50 pretix/base/models/vouchers.py:491
msgid "A voucher with this code already exists." msgid "A voucher with this code already exists."
msgstr "Ваучер с этим кодом уже существует." msgstr "Промокод с этим кодом уже существует."
#: pretix/base/modelimport_vouchers.py:68 pretix/base/models/memberships.py:57 #: pretix/base/modelimport_vouchers.py:68 pretix/base/models/memberships.py:57
#: pretix/base/models/vouchers.py:199 pretix/control/views/vouchers.py:121 #: pretix/base/models/vouchers.py:199 pretix/control/views/vouchers.py:121
@@ -4708,7 +4690,7 @@ msgstr ""
#: pretix/base/modelimport_vouchers.py:334 pretix/base/models/vouchers.py:300 #: pretix/base/modelimport_vouchers.py:334 pretix/base/models/vouchers.py:300
msgid "Shows hidden products that match this voucher" msgid "Shows hidden products that match this voucher"
msgstr "Показывает скрытые продукты, которые соответствуют этому ваучеру" msgstr "Показывает скрытые продукты, которые соответствуют этому промокоду"
#: pretix/base/modelimport_vouchers.py:343 pretix/base/models/vouchers.py:304 #: pretix/base/modelimport_vouchers.py:343 pretix/base/models/vouchers.py:304
msgid "Offer all add-on products for free when redeeming this voucher" msgid "Offer all add-on products for free when redeeming this voucher"
@@ -4960,11 +4942,8 @@ msgid "Order not approved"
msgstr "Срок действия заказа истёк" msgstr "Срок действия заказа истёк"
#: pretix/base/models/checkin.py:367 #: pretix/base/models/checkin.py:367
#, fuzzy
#| msgctxt "subevent"
#| msgid "This voucher is not valid for this event date."
msgid "Ticket not valid at this time" msgid "Ticket not valid at this time"
msgstr "Этот ваучер не действителен на эту дату мероприятия." msgstr "Билет не действителен в это время"
#: pretix/base/models/checkin.py:368 #: pretix/base/models/checkin.py:368
#, fuzzy #, fuzzy
@@ -7761,9 +7740,9 @@ msgid ""
"same value for multiple vouchers, you can get statistics on how many of them " "same value for multiple vouchers, you can get statistics on how many of them "
"have been redeemed etc." "have been redeemed etc."
msgstr "" msgstr ""
"Вы можете использовать это поле для группировки нескольких ваучеров. Если вы " "Вы можете использовать это поле для группировки нескольких промокодов. Если "
"введёте одно и то же значение для нескольких ваучеров, вы сможете получить " ы введёте одно и то же значение для нескольких промокодов, вы сможете "
"статистику о том, сколько из них было использовано и т. д." "получить статистику о том, сколько из них было использовано и т. д."
#: pretix/base/models/vouchers.py:316 pretix/base/permissions.py:242 #: pretix/base/models/vouchers.py:316 pretix/base/permissions.py:242
#: pretix/control/navigation.py:289 #: pretix/control/navigation.py:289
@@ -7808,7 +7787,7 @@ msgstr "Этот вариант не относится к этому проду
#: pretix/base/models/vouchers.py:358 #: pretix/base/models/vouchers.py:358
msgid "It is currently not possible to create vouchers for add-on products." msgid "It is currently not possible to create vouchers for add-on products."
msgstr "" msgstr ""
"В настоящее время невозможно создать ваучеры для дополнительных продуктов." "В настоящее время невозможно создать промокоды для дополнительных продуктов."
#: pretix/base/models/vouchers.py:360 pretix/base/models/vouchers.py:472 #: pretix/base/models/vouchers.py:360 pretix/base/models/vouchers.py:472
#, fuzzy #, fuzzy
@@ -7824,7 +7803,7 @@ msgid ""
"This voucher has already been redeemed %(redeemed)s times. You cannot reduce " "This voucher has already been redeemed %(redeemed)s times. You cannot reduce "
"the maximum number of usages below this number." "the maximum number of usages below this number."
msgstr "" msgstr ""
"Этот ваучер уже был использован %(redeemed)s раз. Вы не можете уменьшить " "Этот промокод уже был использован %(redeemed)s раз. Вы не можете уменьшить "
"максимальное количество использований ниже этого числа." "максимальное количество использований ниже этого числа."
#: pretix/base/models/vouchers.py:379 #: pretix/base/models/vouchers.py:379
@@ -7843,7 +7822,7 @@ msgstr ""
msgid "" msgid ""
"If you want this voucher to block quota, you need to select a specific date." "If you want this voucher to block quota, you need to select a specific date."
msgstr "" msgstr ""
"Если вы хотите, чтобы этот ваучер блокировал квоту, вы должны выбрать " "Если вы хотите, чтобы этот промокод блокировал квоту, вы должны выбрать "
"конкретную дату." "конкретную дату."
#: pretix/base/models/vouchers.py:387 #: pretix/base/models/vouchers.py:387
@@ -7857,7 +7836,7 @@ msgid ""
"You cannot create a voucher that blocks quota as the selected product or " "You cannot create a voucher that blocks quota as the selected product or "
"quota is currently sold out or completely reserved." "quota is currently sold out or completely reserved."
msgstr "" msgstr ""
"Вы не можете создать ваучер, который блокирует квоту, поскольку выбранный " "Вы не можете создать промокод, который блокирует квоту, поскольку выбранный "
"продукт или квота в настоящее время распроданы или полностью зарезервированы." "продукт или квота в настоящее время распроданы или полностью зарезервированы."
#: pretix/base/models/vouchers.py:507 #: pretix/base/models/vouchers.py:507
@@ -7879,7 +7858,7 @@ msgstr "В листе ожидания с"
#: pretix/base/models/waitinglist.py:84 #: pretix/base/models/waitinglist.py:84
msgid "Assigned voucher" msgid "Assigned voucher"
msgstr "Присвоенный ваучер" msgstr "Присвоенный промокод"
#: pretix/base/models/waitinglist.py:93 #: pretix/base/models/waitinglist.py:93
msgid "The product the user waits for." msgid "The product the user waits for."
@@ -7913,7 +7892,7 @@ msgstr "Эта запись анонимизирована и больше не
#: pretix/base/models/waitinglist.py:197 #: pretix/base/models/waitinglist.py:197
msgid "A voucher has already been sent to this person." msgid "A voucher has already been sent to this person."
msgstr "Ваучер уже отправлен данному лицу." msgstr "Промокод уже отправлен данному лицу."
#: pretix/base/models/waitinglist.py:208 #: pretix/base/models/waitinglist.py:208
#, python-brace-format #, python-brace-format
@@ -8083,8 +8062,8 @@ msgid ""
"You entered a voucher instead of a gift card. Vouchers can only be entered " "You entered a voucher instead of a gift card. Vouchers can only be entered "
"on the first page of the shop below the product selection." "on the first page of the shop below the product selection."
msgstr "" msgstr ""
"Вы ввели ваучер вместо подарочного сертификата. Ваучеры можно вводить только " "Вы ввели промокод вместо подарочного сертификата. Промокод можно вводить "
"на первой странице магазина под ассортиментом продуктов." "только на первой странице магазина под ассортиментом продуктов."
#: pretix/base/payment.py:151 #: pretix/base/payment.py:151
msgid "This gift card is not known." msgid "This gift card is not known."
@@ -9373,7 +9352,7 @@ msgstr "Введённая цена слишком высока."
#: pretix/base/services/cart.py:163 #: pretix/base/services/cart.py:163
msgid "This voucher code is not known in our database." msgid "This voucher code is not known in our database."
msgstr "Этот код ваучера не известен в нашей базе данных." msgstr "Этот промокод не известен в нашей базе данных."
#: pretix/base/services/cart.py:165 #: pretix/base/services/cart.py:165
#, fuzzy, python-format #, fuzzy, python-format
@@ -9407,7 +9386,7 @@ msgstr[2] ""
msgid "" msgid ""
"This voucher code has already been used the maximum number of times allowed." "This voucher code has already been used the maximum number of times allowed."
msgstr "" msgstr ""
"Этот код ваучера уже был использован максимально допустимое количество раз." "Этот промокод уже был использован максимально допустимое количество раз."
#: pretix/base/services/cart.py:178 #: pretix/base/services/cart.py:178
#, python-format #, python-format
@@ -9417,15 +9396,16 @@ msgid ""
"or that you tried to redeem it before but did not complete the checkout " "or that you tried to redeem it before but did not complete the checkout "
"process. You can try to use it again in %d minutes." "process. You can try to use it again in %d minutes."
msgstr "" msgstr ""
"Этот код ваучера в настоящее время заблокирован, так как он уже содержится в " "Этот промокод в настоящее время заблокирован, так как он уже содержится в "
"корзине. Это может означать, что кто-то другой сейчас использует этот " "корзине. Это может означать, что кто-то другой сейчас использует этот "
"ваучер, или что вы пытались его использовать ранее, но не завершили процесс " "промокод, или что вы пытались его использовать ранее, но не завершили "
"оформления. Вы можете попробовать использовать его снова через %d минут." "процесс оформления. Вы можете попробовать использовать его снова через %d "
"минут."
#: pretix/base/services/cart.py:183 #: pretix/base/services/cart.py:183
#, python-format #, python-format
msgid "This voucher code can only be redeemed %d more times." msgid "This voucher code can only be redeemed %d more times."
msgstr "Этот код ваучера можно использовать только ещё %d раз." msgstr "Этот промокод можно использовать ещё %d раз."
#: pretix/base/services/cart.py:184 #: pretix/base/services/cart.py:184
msgid "" msgid ""
@@ -9438,23 +9418,20 @@ msgid ""
"You already used this voucher code. Remove the associated line from your " "You already used this voucher code. Remove the associated line from your "
"cart if you want to use it for a different product." "cart if you want to use it for a different product."
msgstr "" msgstr ""
"Вы уже использовали этот код ваучера. Удалите связанную строку из вашей " "Вы уже использовали этот промокод. Удалите связанную строку из вашей "
"корзины, если вы хотите использовать его для другого продукта." "корзины, если вы хотите использовать его для другого продукта."
#: pretix/base/services/cart.py:189 #: pretix/base/services/cart.py:189
msgid "This voucher is expired." msgid "This voucher is expired."
msgstr "Срок действия этого ваучера истёк." msgstr "Срок действия этого промокода истёк."
#: pretix/base/services/cart.py:190 #: pretix/base/services/cart.py:190
msgid "This voucher is not valid for this product." msgid "This voucher is not valid for this product."
msgstr "Этот ваучер недействителен для данного продукта." msgstr "Этот промокод недействителен для данного продукта."
#: pretix/base/services/cart.py:191 #: pretix/base/services/cart.py:191
#, fuzzy
#| msgctxt "subevent"
#| msgid "This voucher is not valid for this event date."
msgid "This voucher is not valid for this seat." msgid "This voucher is not valid for this seat."
msgstr "Этот ваучер не действителен на эту дату мероприятия." msgstr "Этот промокод не действителен для данного места."
#: pretix/base/services/cart.py:193 #: pretix/base/services/cart.py:193
msgid "" msgid ""
@@ -9466,16 +9443,17 @@ msgstr ""
#: pretix/base/services/cart.py:198 #: pretix/base/services/cart.py:198
msgid "Your voucher is valid for a product that is currently not for sale." msgid "Your voucher is valid for a product that is currently not for sale."
msgstr "" msgstr ""
"Ваш ваучер действителен для продукта, который в настоящее время не продаётся." "Ваш промокод действителен для продукта, который в настоящее время не "
"продаётся."
#: pretix/base/services/cart.py:199 #: pretix/base/services/cart.py:199
msgctxt "subevent" msgctxt "subevent"
msgid "This voucher is not valid for this event date." msgid "This voucher is not valid for this event date."
msgstr "Этот ваучер не действителен на эту дату мероприятия." msgstr "Этот промокод не действителен на эту дату мероприятия."
#: pretix/base/services/cart.py:200 #: pretix/base/services/cart.py:200
msgid "You need a valid voucher code to order this product." msgid "You need a valid voucher code to order this product."
msgstr "Необходим действительный код ваучера для заказа этого продукта." msgstr "Необходим действительный промокод для заказа этого продукта."
#: pretix/base/services/cart.py:201 #: pretix/base/services/cart.py:201
msgctxt "subevent" msgctxt "subevent"
@@ -9578,8 +9556,8 @@ msgid ""
"You entered a gift card instead of a voucher. Gift cards can be entered " "You entered a gift card instead of a voucher. Gift cards can be entered "
"later on when you're asked for your payment details." "later on when you're asked for your payment details."
msgstr "" msgstr ""
"Вы ввели подарочный сертификат вместо ваучера. Подарочные сертификаты можно " "Вы ввели подарочный сертификат вместо промокода. Подарочные сертификаты "
"ввести позже, когда вас попросят указать реквизиты для оплаты." "можно ввести позже, когда вас попросят указать реквизиты для оплаты."
#: pretix/base/services/cart.py:224 #: pretix/base/services/cart.py:224
msgid "" msgid ""
@@ -9746,11 +9724,9 @@ msgid "This order is not yet approved."
msgstr "Этот заказ не ожидает утверждения." msgstr "Этот заказ не ожидает утверждения."
#: pretix/base/services/checkin.py:999 pretix/base/services/checkin.py:1003 #: pretix/base/services/checkin.py:999 pretix/base/services/checkin.py:1003
#, fuzzy, python-brace-format #, python-brace-format
#| msgctxt "subevent"
#| msgid "This voucher is not valid for this event date."
msgid "This ticket is only valid after {datetime}." msgid "This ticket is only valid after {datetime}."
msgstr "Этот ваучер не действителен на эту дату мероприятия." msgstr "Этот билет действителен только после {datetime}."
#: pretix/base/services/checkin.py:1013 pretix/base/services/checkin.py:1017 #: pretix/base/services/checkin.py:1013 pretix/base/services/checkin.py:1017
#, fuzzy, python-brace-format #, fuzzy, python-brace-format
@@ -9908,6 +9884,12 @@ msgid ""
"We are sending this email because you configured us to do so in your event " "We are sending this email because you configured us to do so in your event "
"settings." "settings."
msgstr "" msgstr ""
"Здравствуйте,\n"
"\n"
"новый счет для заказа {order} на {event} создан, смотрите вложение.\n"
"\n"
"Вы получили это письмо, поскольку в настройках мероприятия была включена "
"отправка таких уведомлений."
#: pretix/base/services/mail.py:144 #: pretix/base/services/mail.py:144
#, fuzzy #, fuzzy
@@ -10163,8 +10145,8 @@ msgid ""
"The voucher code used for one of the items in your cart is not known in our " "The voucher code used for one of the items in your cart is not known in our "
"database." "database."
msgstr "" msgstr ""
"Код ваучера, использованный для одной из позиций в вашей корзине, неизвестен " "Промокод, использованный для одной из позиций в вашей корзине, неизвестен в "
"в нашей базе данных." "нашей базе данных."
#: pretix/base/services/orders.py:173 #: pretix/base/services/orders.py:173
msgid "" msgid ""
@@ -10172,30 +10154,24 @@ msgid ""
"used the maximum number of times allowed. We removed this item from your " "used the maximum number of times allowed. We removed this item from your "
"cart." "cart."
msgstr "" msgstr ""
"Код ваучера, использованный для одной из позиций в вашей корзине, уже был " "Промокод, использованный для одной из позиций в вашей корзине, уже был "
"использован максимально допустимое количество раз. Мы удалили эту позицию из " "использован максимально допустимое количество раз. Мы удалили эту позицию из "
"вашей корзины." "вашей корзины."
#: pretix/base/services/orders.py:177 #: pretix/base/services/orders.py:177
#, fuzzy
#| msgid ""
#| "The voucher code used for one of the items in your cart has already been "
#| "used the maximum number of times allowed. We removed this item from your "
#| "cart."
msgid "" msgid ""
"The voucher code used for one of the items in your cart has already been too " "The voucher code used for one of the items in your cart has already been too "
"often. We adjusted the price of the item in your cart." "often. We adjusted the price of the item in your cart."
msgstr "" msgstr ""
"Код ваучера, использованный для одной из позиций в вашей корзине, уже был " "Промокод, применённый к одному из товаров в вашей корзине, был использован "
"использован максимально допустимое количество раз. Мы удалили эту позицию из " "слишком много раз. Мы скорректировали цену товара в вашей корзине."
"вашей корзины."
#: pretix/base/services/orders.py:181 #: pretix/base/services/orders.py:181
msgid "" msgid ""
"The voucher code used for one of the items in your cart is expired. We " "The voucher code used for one of the items in your cart is expired. We "
"removed this item from your cart." "removed this item from your cart."
msgstr "" msgstr ""
"Срок действия кода ваучера, использованного для одной из позиций в вашей " "Срок действия промокода, использованного для одной из позиций в вашей "
"корзине, истёк. Мы удалили эту позицию из вашей корзины." "корзине, истёк. Мы удалили эту позицию из вашей корзины."
#: pretix/base/services/orders.py:184 #: pretix/base/services/orders.py:184
@@ -10203,14 +10179,12 @@ msgid ""
"The voucher code used for one of the items in your cart is not valid for " "The voucher code used for one of the items in your cart is not valid for "
"this item. We removed this item from your cart." "this item. We removed this item from your cart."
msgstr "" msgstr ""
"Код ваучера, использованный для одной из позиций в вашей корзине, " "Промокод, использованный для одной из позиций в вашей корзине, "
"недействителен для этой позиции. Мы удалили эту позицию из вашей корзины." "недействителен для этой позиции. Мы удалили эту позицию из вашей корзины."
#: pretix/base/services/orders.py:186 #: pretix/base/services/orders.py:186
#, fuzzy
#| msgid "You need a valid voucher code to order this product."
msgid "You need a valid voucher code to order one of the products." msgid "You need a valid voucher code to order one of the products."
msgstr "Необходим действительный код ваучера для заказа этого продукта." msgstr "Необходим действительный промокод для одного из продуктов."
#: pretix/base/services/orders.py:187 #: pretix/base/services/orders.py:187
msgid "" msgid ""
@@ -12221,6 +12195,16 @@ msgid ""
"Best regards, \n" "Best regards, \n"
"Your {event} team" "Your {event} team"
msgstr "" msgstr ""
"Здравствуйте,\n"
"\n"
"вы получили это письмо, потому что запросили ссылку\n"
"на ваш заказ для мероприятия {event}.\n"
"\n"
"Вы можете изменить данные заказа и просмотреть его статус по ссылке:\n"
"{url}\n"
"\n"
"С уважением, \n"
"Команда мероприятия {event}"
#: pretix/base/settings.py:2461 #: pretix/base/settings.py:2461
#, python-brace-format #, python-brace-format
@@ -12240,6 +12224,15 @@ msgid ""
"Best regards, \n" "Best regards, \n"
"Your {event} team" "Your {event} team"
msgstr "" msgstr ""
"Здравствуйте,\n"
"\n"
"кто-то запросил список ваших заказов для мероприятия {event}.\n"
"Список заказов::\n"
"\n"
"{orders}\n"
"\n"
"С уважением, \n"
"Команда мероприятия {event}"
#: pretix/base/settings.py:2481 #: pretix/base/settings.py:2481
#, python-brace-format #, python-brace-format
@@ -12269,6 +12262,17 @@ msgid ""
"Best regards, \n" "Best regards, \n"
"Your {event} team" "Your {event} team"
msgstr "" msgstr ""
"Здравствуйте,\n"
"\n"
"ваш заказ для мероприятия {event} прошел успешно. Поскольку ваш заказ "
"состоит только из бесплатных позиций,\n"
"оплата не требуется.\n"
"\n"
"Вы можете изменить детали заказа и посмотреть его статус по ссылке:\n"
"{url}\n"
"\n"
"С уважением, \n"
"Команда мероприятия {event}"
#: pretix/base/settings.py:2518 #: pretix/base/settings.py:2518
#, python-brace-format #, python-brace-format
@@ -12285,6 +12289,18 @@ msgid ""
"Best regards, \n" "Best regards, \n"
"Your {event} team" "Your {event} team"
msgstr "" msgstr ""
"Здравствуйте,\n"
"\n"
"мы успешно получили ваш заказ для мероприятия {event}. Поскольку вы "
"заказали\n"
"товар, требующий подтверждения со стороны организатора, просим вас\n"
"подождать нашего следующего письма.\n"
"\n"
"Вы можете изменить данные заказа и просмотреть его статус по ссылке:\n"
"{url}\n"
"\n"
"С уважением, \n"
"Команда мероприятия {event}"
#: pretix/base/settings.py:2536 #: pretix/base/settings.py:2536
#, python-brace-format #, python-brace-format
@@ -12303,6 +12319,19 @@ msgid ""
"Best regards, \n" "Best regards, \n"
"Your {event} team" "Your {event} team"
msgstr "" msgstr ""
"Здравствуйте,\n"
"\n"
"мы успешно получили ваш заказ на сумму {total_with_currency} для мероприятия "
"{event}.\n"
"Пожалуйста, завершите оплату до {expire_date}.\n"
"\n"
"{payment_info}\n"
"\n"
"Вы можете изменить данные заказа и просмотреть его статус по ссылке:\n"
"{url}\n"
"\n"
"С уважением, \n"
"Команда мероприятия {event}"
#: pretix/base/settings.py:2554 #: pretix/base/settings.py:2554
#, fuzzy #, fuzzy
@@ -12354,6 +12383,15 @@ msgid ""
"Best regards, \n" "Best regards, \n"
"Your {event} team" "Your {event} team"
msgstr "" msgstr ""
"Здравствуйте,\n"
"\n"
"ваш заказ для мероприятия {event} был изменен.\n"
"\n"
"Вы можете посмотреть статус своего заказа по ссылке:\n"
"{url}\n"
"\n"
"С уважением, \n"
"Команда мероприятия {event}"
#: pretix/base/settings.py:2614 #: pretix/base/settings.py:2614
#, python-brace-format #, python-brace-format
@@ -12375,6 +12413,17 @@ msgid ""
"Best regards, \n" "Best regards, \n"
"Your {event} team" "Your {event} team"
msgstr "" msgstr ""
"Здравствуйте,\n"
"\n"
"мы успешно получили вашу оплату за мероприятие {event}. Спасибо!\n"
"\n"
"{payment_info}\n"
"\n"
"Вы можете изменить данные заказа и просмотреть его статус по ссылке:\n"
"{url}\n"
"\n"
"С уважением, \n"
"Команда мероприятия {event}"
#: pretix/base/settings.py:2636 #: pretix/base/settings.py:2636
#, python-brace-format #, python-brace-format
@@ -12426,6 +12475,17 @@ msgid ""
"Best regards, \n" "Best regards, \n"
"Your {event} team" "Your {event} team"
msgstr "" msgstr ""
"Здравствуйте,\n"
"\n"
"мы не получили полной оплаты вашего заказа для мероприятия {event}.\n"
"Обратите внимание: мы можем гарантировать ваш заказ только в том случае,\n"
"если оплата поступит до {expire_date}.\n"
"\n"
"Информацию об оплате и статус заказа можно посмотреть по ссылке:\n"
"{url}\n"
"\n"
"С уважением, \n"
"Команда мероприятия {event}"
#: pretix/base/settings.py:2685 #: pretix/base/settings.py:2685
#, python-brace-format #, python-brace-format
@@ -13648,10 +13708,8 @@ msgid "created by"
msgstr "создано" msgstr "создано"
#: pretix/base/templates/pretixbase/email/order_details.html:34 #: pretix/base/templates/pretixbase/email/order_details.html:34
#, fuzzy
#| msgid "Bancontact"
msgid "Contact:" msgid "Contact:"
msgstr "Bancontact" msgstr "Связаться:"
#: pretix/base/templates/pretixbase/email/order_details.html:54 #: pretix/base/templates/pretixbase/email/order_details.html:54
#, fuzzy, python-format #, fuzzy, python-format
@@ -13684,10 +13742,8 @@ msgstr ""
#: pretix/base/templates/pretixbase/email/order_details.html:136 #: pretix/base/templates/pretixbase/email/order_details.html:136
#: pretix/presale/templates/pretixpresale/event/base.html:223 #: pretix/presale/templates/pretixpresale/event/base.html:223
#: pretix/presale/templates/pretixpresale/organizers/base.html:101 #: pretix/presale/templates/pretixpresale/organizers/base.html:101
#, fuzzy
#| msgid "Bancontact"
msgid "Contact" msgid "Contact"
msgstr "Bancontact" msgstr "Связаться"
#: pretix/base/templates/pretixbase/email/shred_completed.txt:2 #: pretix/base/templates/pretixbase/email/shred_completed.txt:2
#, python-format #, python-format
@@ -13758,16 +13814,17 @@ msgid "Proceed to %(host)s"
msgstr "Приступить к оформлению заказа" msgstr "Приступить к оформлению заказа"
#: pretix/base/templates/source.html:5 pretix/base/templates/source.html:9 #: pretix/base/templates/source.html:5 pretix/base/templates/source.html:9
#, fuzzy
#| msgid "Voucher code"
msgid "Source code" msgid "Source code"
msgstr "Код ваучера" msgstr "Исходный код"
#: pretix/base/templates/source.html:10 #: pretix/base/templates/source.html:10
msgid "" msgid ""
"This site is powered by free software. If you want to read the license terms " "This site is powered by free software. If you want to read the license terms "
"or obtain the source code, follow these links or instructions:" "or obtain the source code, follow these links or instructions:"
msgstr "" msgstr ""
"Этот сайт работает на свободном программном обеспечении. Если вы хотите "
"ознакомиться с условиями лицензии или получить исходный код, перейдите по "
"следующим ссылкам или следуйте инструкциям:"
#: pretix/base/ticketoutput.py:182 #: pretix/base/ticketoutput.py:182
#, fuzzy #, fuzzy
@@ -15702,6 +15759,7 @@ msgid ""
"This installation of pretix includes changes or extensions made to the " "This installation of pretix includes changes or extensions made to the "
"source code." "source code."
msgstr "" msgstr ""
"Данная установка pretix содержит изменения или расширения исходного кода."
#: pretix/control/forms/global_settings.py:162 #: pretix/control/forms/global_settings.py:162
msgid "Usage of pretix" msgid "Usage of pretix"
@@ -15792,10 +15850,8 @@ msgid ""
msgstr "" msgstr ""
#: pretix/control/forms/global_settings.py:217 #: pretix/control/forms/global_settings.py:217
#, fuzzy
#| msgid "Your registration"
msgid "Source code instructions" msgid "Source code instructions"
msgstr "Ваша регистрация" msgstr "Инструкции для получения исходного кода"
#: pretix/control/forms/global_settings.py:219 #: pretix/control/forms/global_settings.py:219
msgid "" msgid ""
@@ -16761,10 +16817,8 @@ msgid "An medium with this type and identifier is already registered."
msgstr "Ваучер с этим кодом уже существует." msgstr "Ваучер с этим кодом уже существует."
#: pretix/control/forms/organizer.py:1059 #: pretix/control/forms/organizer.py:1059
#, fuzzy
#| msgid "A voucher with this code already exists."
msgid "An account with this customer ID is already registered." msgid "An account with this customer ID is already registered."
msgstr "Ваучер с этим кодом уже существует." msgstr "Аккаунт с таким ID клиента уже существует."
#: pretix/control/forms/organizer.py:1076 #: pretix/control/forms/organizer.py:1076
#: pretix/control/templates/pretixcontrol/organizers/customer.html:62 #: pretix/control/templates/pretixcontrol/organizers/customer.html:62
@@ -16887,10 +16941,8 @@ msgid "The selected organizer has already been invited."
msgstr "Выбранный организатор не найден." msgstr "Выбранный организатор не найден."
#: pretix/control/forms/organizer.py:1379 #: pretix/control/forms/organizer.py:1379
#, fuzzy
#| msgid "A voucher with this code already exists."
msgid "A sales channel with the same identifier already exists." msgid "A sales channel with the same identifier already exists."
msgstr "Ваучер с этим кодом уже существует." msgstr "Канал продаж с таким идентификатором уже существует."
#: pretix/control/forms/organizer.py:1391 #: pretix/control/forms/organizer.py:1391
msgid "Events with active plugin" msgid "Events with active plugin"
@@ -17173,10 +17225,8 @@ msgid "Only includes active products."
msgstr "" msgstr ""
#: pretix/control/forms/waitinglist.py:115 #: pretix/control/forms/waitinglist.py:115
#, fuzzy
#| msgid "A voucher with this code already exists."
msgid "A voucher for this waiting list entry was already sent out." msgid "A voucher for this waiting list entry was already sent out."
msgstr "Ваучер с этим кодом уже существует." msgstr "Промокод для данного места в листе ожидания уже отправлен."
#: pretix/control/forms/waitinglist.py:125 #: pretix/control/forms/waitinglist.py:125
#, fuzzy #, fuzzy
@@ -18812,7 +18862,7 @@ msgstr "Оплата"
#: pretix/presale/templates/pretixpresale/organizers/index.html:89 #: pretix/presale/templates/pretixpresale/organizers/index.html:89
#: pretix/presale/templates/pretixpresale/organizers/index.html:91 #: pretix/presale/templates/pretixpresale/organizers/index.html:91
msgid "Tickets" msgid "Tickets"
msgstr "" msgstr "Билеты"
#: pretix/control/navigation.py:97 #: pretix/control/navigation.py:97
#: pretix/control/templates/pretixcontrol/event/tax.html:4 #: pretix/control/templates/pretixcontrol/event/tax.html:4
@@ -22104,7 +22154,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/fragment_quota_box_paid.html:3 #: pretix/control/templates/pretixcontrol/fragment_quota_box_paid.html:3
#, python-format #, python-format
msgid "Currently available: %(num)s" msgid "Currently available: %(num)s"
msgstr "" msgstr "Сейчас доступно: %(num)s"
#: pretix/control/templates/pretixcontrol/global_license.html:8 #: pretix/control/templates/pretixcontrol/global_license.html:8
msgid "" msgid ""
@@ -24027,7 +24077,7 @@ msgstr "Начальная дата мероприятия"
#: pretix/control/templates/pretixcontrol/order/index.html:498 #: pretix/control/templates/pretixcontrol/order/index.html:498
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:55 #: pretix/presale/templates/pretixpresale/event/fragment_cart.html:55
msgid "Voucher code used:" msgid "Voucher code used:"
msgstr "Использованный код ваучера:" msgstr "Использованный промокод:"
#: pretix/control/templates/pretixcontrol/order/index.html:500 #: pretix/control/templates/pretixcontrol/order/index.html:500
#, fuzzy, python-format #, fuzzy, python-format
@@ -27824,10 +27874,8 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/vouchers/import_start.html:6 #: pretix/control/templates/pretixcontrol/vouchers/import_start.html:6
#: pretix/control/templates/pretixcontrol/vouchers/index.html:81 #: pretix/control/templates/pretixcontrol/vouchers/index.html:81
#: pretix/control/templates/pretixcontrol/vouchers/index.html:94 #: pretix/control/templates/pretixcontrol/vouchers/index.html:94
#, fuzzy
#| msgid "Blocking vouchers"
msgid "Import vouchers" msgid "Import vouchers"
msgstr "Блокирующие ваучеры" msgstr "Импорт промокодов"
#: pretix/control/templates/pretixcontrol/vouchers/index.html:10 #: pretix/control/templates/pretixcontrol/vouchers/index.html:10
msgid "" msgid ""
@@ -30761,6 +30809,8 @@ msgid ""
"After completing your purchase, we will ask you to transfer the money to the " "After completing your purchase, we will ask you to transfer the money to the "
"following bank account, using a personal reference code:" "following bank account, using a personal reference code:"
msgstr "" msgstr ""
"После завершения покупки, мы попросим вас перевести оплату на следующий "
"банковский счет, указав персональный код назначения платежа:"
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html:11 #: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html:11
msgid "" msgid ""
@@ -30771,23 +30821,27 @@ msgstr ""
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html:14 #: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html:14
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/pending.html:21 #: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/pending.html:21
msgid "Reference code (important):" msgid "Reference code (important):"
msgstr "" msgstr "Код назначения платежа (важно):"
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html:30 #: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html:30
msgid "We will assign you a personal reference code in the next step." msgid "We will assign you a personal reference code in the next step."
msgstr "" msgstr "Мы присвоим вам персональный код назначения платежа на следующем шаге."
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html:36 #: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html:36
msgid "" msgid ""
"We will assign you a personal reference code to use after you completed the " "We will assign you a personal reference code to use after you completed the "
"order." "order."
msgstr "" msgstr ""
"Мы присвоим вам персональный код назначения платежа после завершения "
"оформления заказа."
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html:41 #: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html:41
msgid "" msgid ""
"After completing your purchase, we will ask you to transfer the money to our " "After completing your purchase, we will ask you to transfer the money to our "
"bank account, using a personal reference code." "bank account, using a personal reference code."
msgstr "" msgstr ""
"После завершения покупки мы попросим вас перевести оплату на следующий "
"банковский счёт, указав персональный код назначения платежа."
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/control.html:6 #: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/control.html:6
#, fuzzy #, fuzzy
@@ -30810,7 +30864,7 @@ msgstr "Общая сумма"
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/control.html:38 #: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/control.html:38
msgid "Reference code" msgid "Reference code"
msgstr "" msgstr "Код назначения платежа"
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_assign.html:4 #: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_assign.html:4
msgid "" msgid ""
@@ -30949,7 +31003,7 @@ msgstr "Сумма:"
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/pending.html:34 #: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/pending.html:34
#: pretix/plugins/stripe/templates/pretixplugins/stripe/pending.html:30 #: pretix/plugins/stripe/templates/pretixplugins/stripe/pending.html:30
msgid "There is no further action required on this website." msgid "There is no further action required on this website."
msgstr "" msgstr "Дальнейших действий на этом сайте не требуется."
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/pending.html:35 #: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/pending.html:35
#: pretix/plugins/stripe/templates/pretixplugins/stripe/pending.html:31 #: pretix/plugins/stripe/templates/pretixplugins/stripe/pending.html:31
@@ -34363,11 +34417,9 @@ msgstr ""
#: pretix/presale/forms/renderers.py:66 #: pretix/presale/forms/renderers.py:66
#: pretix/presale/templates/pretixpresale/event/fragment_voucher_form.html:14 #: pretix/presale/templates/pretixpresale/event/fragment_voucher_form.html:14
#, fuzzy
#| msgid "expired"
msgctxt "form" msgctxt "form"
msgid "required" msgid "required"
msgstr "истекло" msgstr "обязательно"
#: pretix/presale/ical.py:87 pretix/presale/ical.py:146 #: pretix/presale/ical.py:87 pretix/presale/ical.py:146
#: pretix/presale/ical.py:182 #: pretix/presale/ical.py:182
@@ -34559,10 +34611,8 @@ msgid "We're now trying to book these add-ons for you!"
msgstr "Сейчас мы пытаемся забронировать эти дополнительные продукты для вас!" msgstr "Сейчас мы пытаемся забронировать эти дополнительные продукты для вас!"
#: pretix/presale/templates/pretixpresale/event/checkout_addons.html:28 #: pretix/presale/templates/pretixpresale/event/checkout_addons.html:28
#, fuzzy
#| msgid "Additional information"
msgid "Additional options for" msgid "Additional options for"
msgstr "Дополнительная информация" msgstr "Дополнительные опции"
#: pretix/presale/templates/pretixpresale/event/checkout_addons.html:60 #: pretix/presale/templates/pretixpresale/event/checkout_addons.html:60
#, fuzzy #, fuzzy
@@ -34614,10 +34664,8 @@ msgid "Cart expired"
msgstr "Срок действия корзины истёк" msgstr "Срок действия корзины истёк"
#: pretix/presale/templates/pretixpresale/event/checkout_base.html:36 #: pretix/presale/templates/pretixpresale/event/checkout_base.html:36
#, fuzzy
#| msgid "Show variants"
msgid "Show full cart" msgid "Show full cart"
msgstr "Показать варианты" msgstr "Показать полную корзину"
#: pretix/presale/templates/pretixpresale/event/checkout_base.html:52 #: pretix/presale/templates/pretixpresale/event/checkout_base.html:52
#: pretix/presale/templates/pretixpresale/event/index.html:86 #: pretix/presale/templates/pretixpresale/event/index.html:86
@@ -34949,21 +34997,12 @@ msgstr[1] ""
msgstr[2] "" msgstr[2] ""
#: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:26 #: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:26
#, fuzzy, python-format #, python-format
#| msgid ""
#| "You can choose between %(min_count)s and %(max_count)s options from this "
#| "category."
msgid "You can choose one option from this category." msgid "You can choose one option from this category."
msgid_plural "You can choose up to %(max_count)s options from this category." msgid_plural "You can choose up to %(max_count)s options from this category."
msgstr[0] "" msgstr[0] "Вы можете выбрать не более %(max_count)s варианта из этой категории."
"Вы можете выбрать между вариантами %(min_count)s и %(max_count)s из этой " msgstr[1] "Вы можете выбрать не более %(max_count)s вариантов из этой категории."
"категории." msgstr[2] "Вы можете выбрать не более %(max_count)s вариантов из этой категории."
msgstr[1] ""
"Вы можете выбрать между вариантами %(min_count)s и %(max_count)s из этой "
"категории."
msgstr[2] ""
"Вы можете выбрать между вариантами %(min_count)s и %(max_count)s из этой "
"категории."
#: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:34 #: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:34
#, python-format #, python-format
@@ -35363,8 +35402,7 @@ msgid ""
msgstr "" msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:516 #: pretix/presale/templates/pretixpresale/event/fragment_cart.html:516
#, fuzzy, python-format #, python-format
#| msgid "The items in your cart are reserved for you for %(minutes)s minutes."
msgid "The items in your cart are reserved for you for %(minutes)s minutes." msgid "The items in your cart are reserved for you for %(minutes)s minutes."
msgstr "Позиции в вашей корзине зарезервированы для вас на %(minutes)s минут." msgstr "Позиции в вашей корзине зарезервированы для вас на %(minutes)s минут."
@@ -35413,7 +35451,7 @@ msgstr "Очистить корзину"
#: pretix/presale/templates/pretixpresale/event/index.html:248 #: pretix/presale/templates/pretixpresale/event/index.html:248
#: pretix/presale/templates/pretixpresale/event/voucher_form.html:16 #: pretix/presale/templates/pretixpresale/event/voucher_form.html:16
msgid "Redeem a voucher" msgid "Redeem a voucher"
msgstr "Использовать ваучер/промокод" msgstr "Использовать промокод"
#: pretix/presale/templates/pretixpresale/event/fragment_cart_box.html:71 #: pretix/presale/templates/pretixpresale/event/fragment_cart_box.html:71
msgid "We're applying this voucher to your cart..." msgid "We're applying this voucher to your cart..."
@@ -35422,7 +35460,7 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_cart_box.html:79 #: pretix/presale/templates/pretixpresale/event/fragment_cart_box.html:79
#: pretix/presale/templates/pretixpresale/event/fragment_voucher_form.html:27 #: pretix/presale/templates/pretixpresale/event/fragment_voucher_form.html:27
msgid "Redeem voucher" msgid "Redeem voucher"
msgstr "Использовать ваучер" msgstr "Активировать промокод"
#: pretix/presale/templates/pretixpresale/event/fragment_change_confirm.html:10 #: pretix/presale/templates/pretixpresale/event/fragment_change_confirm.html:10
#, fuzzy #, fuzzy
@@ -35746,7 +35784,7 @@ msgstr "Изменить цену для %(item)s"
#: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html:4 #: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html:4
#, python-format #, python-format
msgid "%(num)s currently available" msgid "%(num)s currently available"
msgstr "" msgstr "%(num)s доступно сейчас"
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar.html:5 #: pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar.html:5
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html:5 #: pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html:5
@@ -35999,32 +36037,23 @@ msgstr ""
"завершения процесса." "завершения процесса."
#: pretix/presale/templates/pretixpresale/event/order.html:56 #: pretix/presale/templates/pretixpresale/event/order.html:56
#, fuzzy
#| msgid ""
#| "Please bookmark or save the link to this exact page if you want to access "
#| "your order later. We also sent you an email containing the link to the "
#| "address you specified."
msgid "" msgid ""
"Please bookmark or save the link to this exact page if you want to access " "Please bookmark or save the link to this exact page if you want to access "
"your order later. We also sent you an email to the address you specified " "your order later. We also sent you an email to the address you specified "
"containing the link to this page." "containing the link to this page."
msgstr "" msgstr ""
"Пожалуйста, добавьте в закладки или сохраните ссылку именно на эту страницу, " "Пожалуйста, сохраните закладку или ссылку на эту страницу, если вам "
"если вы хотите получить доступ к вашему заказу позже. Мы также отправили вам " "потребуется доступ к заказу позже. Мы также отправили письмо на указанный "
"электронное письмо со ссылкой на указанный вами адрес." "вами адрес электронной почты со ссылкой на эту страницу."
#: pretix/presale/templates/pretixpresale/event/order.html:60 #: pretix/presale/templates/pretixpresale/event/order.html:60
#, fuzzy
#| msgid ""
#| "Please save the following link if you want to access your order later. We "
#| "also sent you an email containing the link to the address you specified."
msgid "" msgid ""
"Please save the following link if you want to access your order later. We " "Please save the following link if you want to access your order later. We "
"also sent you an email to the address you specified containing the link." "also sent you an email to the address you specified containing the link."
msgstr "" msgstr ""
"Пожалуйста, сохраните следующую ссылку, если вы хотите получить доступ к " "Пожалуйста, сохраните ссылку на эту страницу, если вам потребуется доступ к "
"вашему заказу позже. Мы также отправили вам электронное письмо со ссылкой на " "заказу позже. Мы также отправили письмо на указанный вами адрес электронной "
"указанный вами адрес." "почты со ссылкой на эту страницу."
#: pretix/presale/templates/pretixpresale/event/order.html:75 #: pretix/presale/templates/pretixpresale/event/order.html:75
#: pretix/presale/templates/pretixpresale/event/position.html:18 #: pretix/presale/templates/pretixpresale/event/position.html:18
@@ -36551,14 +36580,11 @@ msgstr "Включить вебхук"
#: pretix/presale/templates/pretixpresale/event/voucher.html:36 #: pretix/presale/templates/pretixpresale/event/voucher.html:36
#: pretix/presale/templates/pretixpresale/event/voucher_form.html:9 #: pretix/presale/templates/pretixpresale/event/voucher_form.html:9
msgid "Voucher redemption" msgid "Voucher redemption"
msgstr "Использование ваучера" msgstr "Активация промокода"
#: pretix/presale/templates/pretixpresale/event/voucher.html:20 #: pretix/presale/templates/pretixpresale/event/voucher.html:20
#, fuzzy
#| msgctxt "subevent"
#| msgid "This voucher is not valid for this event date."
msgid "This voucher is valid only for the following specific date and time." msgid "This voucher is valid only for the following specific date and time."
msgstr "Этот ваучер не действителен на эту дату мероприятия." msgstr "Этот промокод действителен только на указанную дату и время."
#: pretix/presale/templates/pretixpresale/event/voucher.html:43 #: pretix/presale/templates/pretixpresale/event/voucher.html:43
msgid "" msgid ""
@@ -36577,8 +36603,8 @@ msgid ""
"You entered a voucher code that allows you to buy one of the following " "You entered a voucher code that allows you to buy one of the following "
"products at the specified price:" "products at the specified price:"
msgstr "" msgstr ""
"Вы ввели код ваучера, который позволяет вам купить один из следующих " "Вы ввели промокод, который позволяет вам купить один из следующих продуктов "
"продуктов по указанной цене:" "по указанной цене:"
#: pretix/presale/templates/pretixpresale/event/voucher.html:112 #: pretix/presale/templates/pretixpresale/event/voucher.html:112
#, python-format #, python-format
@@ -27,7 +27,7 @@ from django.urls import NoReverseMatch
from django.utils.encoding import smart_str from django.utils.encoding import smart_str
from django.utils.html import conditional_escape from django.utils.html import conditional_escape
from pretix.multidomain.urlreverse import build_absolute_uri, mainreverse from pretix.multidomain.urlreverse import eventreverse_absolute, mainreverse
register = template.Library() register = template.Library()
@@ -49,7 +49,7 @@ class EventURLNode(URLNode):
url = '' url = ''
try: try:
if self.absolute: if self.absolute:
url = build_absolute_uri(event, view_name, kwargs=kwargs) url = eventreverse_absolute(event, view_name, kwargs=kwargs)
elif self.event is False: elif self.event is False:
url = mainreverse(view_name, kwargs) url = mainreverse(view_name, kwargs)
else: else:
+10
View File
@@ -32,6 +32,7 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License. # License for the specific language governing permissions and limitations under the License.
import warnings
from urllib.parse import urljoin, urlsplit from urllib.parse import urljoin, urlsplit
from django.conf import settings from django.conf import settings
@@ -190,6 +191,15 @@ def eventreverse(obj, name, kwargs=None):
def build_absolute_uri(obj, urlname, kwargs=None): def build_absolute_uri(obj, urlname, kwargs=None):
warnings.warn(
'Usage of build_absolute_uri is confusing since there are many functions with that name. '
'Replace this usage with eventreverse_absolute',
DeprecationWarning
)
return eventreverse_absolute(obj, urlname, kwargs)
def eventreverse_absolute(obj, urlname, kwargs=None):
""" """
Works similar to ``eventreverse`` but always returns an absolute URL. Works similar to ``eventreverse`` but always returns an absolute URL.
+9 -9
View File
@@ -64,7 +64,7 @@ from pretix.base.timeframes import (
from pretix.control.forms.widgets import Select2 from pretix.control.forms.widgets import Select2
from pretix.helpers.filenames import safe_for_filename from pretix.helpers.filenames import safe_for_filename
from pretix.helpers.iter import chunked_iterable from pretix.helpers.iter import chunked_iterable
from pretix.helpers.reportlab import FontFallbackParagraph from pretix.helpers.reportlab import PlainTextParagraph
from pretix.helpers.templatetags.jsonfield import JSONExtract from pretix.helpers.templatetags.jsonfield import JSONExtract
from pretix.plugins.reports.exporters import ReportlabExportMixin from pretix.plugins.reports.exporters import ReportlabExportMixin
@@ -343,7 +343,7 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
] ]
story = [ story = [
FontFallbackParagraph( PlainTextParagraph(
cl.name, cl.name,
headlinestyle headlinestyle
), ),
@@ -351,7 +351,7 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
if cl.subevent: if cl.subevent:
story += [ story += [
Spacer(1, 3 * mm), Spacer(1, 3 * mm),
FontFallbackParagraph( PlainTextParagraph(
'{} ({} {})'.format( '{} ({} {})'.format(
cl.subevent.name, cl.subevent.name,
cl.subevent.get_date_range_display(), cl.subevent.get_date_range_display(),
@@ -381,10 +381,10 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
headrowstyle.fontName = 'OpenSansBd' headrowstyle.fontName = 'OpenSansBd'
for q in questions: for q in questions:
txt = str(q.question) txt = str(q.question)
p = FontFallbackParagraph(txt, headrowstyle) p = PlainTextParagraph(txt, headrowstyle)
while p.wrap(colwidths[len(tdata[0])], 5000)[1] > 30 * mm: while p.wrap(colwidths[len(tdata[0])], 5000)[1] > 30 * mm:
txt = txt[:len(txt) - 50] + "..." txt = txt[:len(txt) - 50] + "..."
p = FontFallbackParagraph(txt, headrowstyle) p = PlainTextParagraph(txt, headrowstyle)
tdata[0].append(p) tdata[0].append(p)
qs = self._get_queryset(cl, form_data) qs = self._get_queryset(cl, form_data)
@@ -431,8 +431,8 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
CBFlowable(bool(op.last_checked_in)) if not op.blocked else '', CBFlowable(bool(op.last_checked_in)) if not op.blocked else '',
'' if op.order.status != Order.STATUS_PAID else '', '' if op.order.status != Order.STATUS_PAID else '',
op.order.code, op.order.code,
FontFallbackParagraph(name, self.get_style()), PlainTextParagraph(name, self.get_style()),
FontFallbackParagraph(bleach.clean(str(item), tags={'br'}).strip().replace('<br>', '<br/>'), self.get_style()), PlainTextParagraph(bleach.clean(str(item), tags={'br'}).strip().replace('<br>', '<br/>'), self.get_style()),
] ]
acache = {} acache = {}
if op.addon_to: if op.addon_to:
@@ -443,10 +443,10 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
for q in questions: for q in questions:
txt = acache.get(q.pk, '') txt = acache.get(q.pk, '')
txt = bleach.clean(txt, tags={'br'}).strip().replace('<br>', '<br/>') txt = bleach.clean(txt, tags={'br'}).strip().replace('<br>', '<br/>')
p = FontFallbackParagraph(txt, self.get_style()) p = PlainTextParagraph(txt, self.get_style())
while p.wrap(colwidths[len(row)], 5000)[1] > 50 * mm: while p.wrap(colwidths[len(row)], 5000)[1] > 50 * mm:
txt = txt[:len(txt) - 50] + "..." txt = txt[:len(txt) - 50] + "..."
p = FontFallbackParagraph(txt, self.get_style()) p = PlainTextParagraph(txt, self.get_style())
row.append(p) row.append(p)
if op.order.status != Order.STATUS_PAID: if op.order.status != Order.STATUS_PAID:
tstyledata += [ tstyledata += [
+6 -6
View File
@@ -58,7 +58,7 @@ from pretix.base.forms import SecretKeySettingsField
from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota
from pretix.base.payment import BasePaymentProvider, PaymentException from pretix.base.payment import BasePaymentProvider, PaymentException
from pretix.base.settings import SettingsSandbox from pretix.base.settings import SettingsSandbox
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.plugins.paypal.api import Api from pretix.plugins.paypal.api import Api
from pretix.plugins.paypal.models import ReferencedPayPalObject from pretix.plugins.paypal.models import ReferencedPayPalObject
@@ -268,8 +268,8 @@ class Paypal(BasePaymentProvider):
"payment_method": "paypal", "payment_method": "paypal",
}, },
"redirect_urls": { "redirect_urls": {
"return_url": build_absolute_uri(request.event, 'plugins:paypal:return', kwargs=kwargs), "return_url": eventreverse_absolute(request.event, 'plugins:paypal:return', kwargs=kwargs),
"cancel_url": build_absolute_uri(request.event, 'plugins:paypal:abort', kwargs=kwargs), "cancel_url": eventreverse_absolute(request.event, 'plugins:paypal:abort', kwargs=kwargs),
}, },
"transactions": [ "transactions": [
{ {
@@ -351,7 +351,7 @@ class Paypal(BasePaymentProvider):
if request.session.get('iframe_session', False): if request.session.get('iframe_session', False):
signer = signing.Signer(salt='safe-redirect') signer = signing.Signer(salt='safe-redirect')
return ( return (
build_absolute_uri(request.event, 'plugins:paypal:redirect') + '?url=' + eventreverse_absolute(request.event, 'plugins:paypal:redirect') + '?url=' +
urllib.parse.quote(signer.sign(link.href)) urllib.parse.quote(signer.sign(link.href))
) )
else: else:
@@ -613,8 +613,8 @@ class Paypal(BasePaymentProvider):
"payment_method": "paypal", "payment_method": "paypal",
}, },
"redirect_urls": { "redirect_urls": {
"return_url": build_absolute_uri(request.event, 'plugins:paypal:return'), "return_url": eventreverse_absolute(request.event, 'plugins:paypal:return'),
"cancel_url": build_absolute_uri(request.event, 'plugins:paypal:abort'), "cancel_url": eventreverse_absolute(request.event, 'plugins:paypal:abort'),
}, },
"transactions": [ "transactions": [
{ {
+6 -6
View File
@@ -56,8 +56,8 @@ from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota
from pretix.base.payment import BasePaymentProvider, PaymentException from pretix.base.payment import BasePaymentProvider, PaymentException
from pretix.base.settings import SettingsSandbox from pretix.base.settings import SettingsSandbox
from pretix.helpers import OF_SELF from pretix.helpers import OF_SELF
from pretix.helpers.urls import build_absolute_uri as build_global_uri from pretix.helpers.urls import mainreverse_absolute
from pretix.multidomain.urlreverse import build_absolute_uri, eventreverse from pretix.multidomain.urlreverse import eventreverse, eventreverse_absolute
from pretix.plugins.paypal2.client.core.environment import ( from pretix.plugins.paypal2.client.core.environment import (
LiveEnvironment, SandboxEnvironment, LiveEnvironment, SandboxEnvironment,
) )
@@ -264,7 +264,7 @@ class PaypalSettingsHolder(BasePaymentProvider):
settings_content = "<div class='alert alert-info'>%s<br /><code>%s</code></div>" % ( settings_content = "<div class='alert alert-info'>%s<br /><code>%s</code></div>" % (
_('Please configure a PayPal Webhook to the following endpoint in order to automatically cancel orders ' _('Please configure a PayPal Webhook to the following endpoint in order to automatically cancel orders '
'when payments are refunded externally.'), 'when payments are refunded externally.'),
build_global_uri('plugins:paypal2:webhook') mainreverse_absolute('plugins:paypal2:webhook')
) )
if self.event.currency not in SUPPORTED_CURRENCIES: if self.event.currency not in SUPPORTED_CURRENCIES:
@@ -321,7 +321,7 @@ class PaypalSettingsHolder(BasePaymentProvider):
], ],
"partner_config_override": { "partner_config_override": {
"partner_logo_url": urllib.parse.urljoin(settings.SITE_URL, static('pretixbase/img/pretix-logo.svg')), "partner_logo_url": urllib.parse.urljoin(settings.SITE_URL, static('pretixbase/img/pretix-logo.svg')),
"return_url": build_global_uri('plugins:paypal2:isu.return', kwargs={ "return_url": mainreverse_absolute('plugins:paypal2:isu.return', kwargs={
'organizer': self.event.organizer.slug, 'organizer': self.event.organizer.slug,
'event': self.event.slug, 'event': self.event.slug,
}) })
@@ -585,8 +585,8 @@ class PaypalMethod(BasePaymentProvider):
'locale': request.LANGUAGE_CODE.split('-')[0], 'locale': request.LANGUAGE_CODE.split('-')[0],
'shipping_preference': 'NO_SHIPPING', # 'SET_PROVIDED_ADDRESS', # Do not set on non-ship order? 'shipping_preference': 'NO_SHIPPING', # 'SET_PROVIDED_ADDRESS', # Do not set on non-ship order?
'user_action': 'CONTINUE', 'user_action': 'CONTINUE',
'return_url': build_absolute_uri(request.event, 'plugins:paypal2:return', kwargs=kwargs), 'return_url': eventreverse_absolute(request.event, 'plugins:paypal2:return', kwargs=kwargs),
'cancel_url': build_absolute_uri(request.event, 'plugins:paypal2:abort', kwargs=kwargs), 'cancel_url': eventreverse_absolute(request.event, 'plugins:paypal2:abort', kwargs=kwargs),
}, },
}) })
response = self.client.execute(paymentreq) response = self.client.execute(paymentreq)
+87 -87
View File
@@ -36,7 +36,7 @@ from reportlab.lib import colors, pagesizes
from reportlab.lib.enums import TA_CENTER, TA_RIGHT from reportlab.lib.enums import TA_CENTER, TA_RIGHT
from reportlab.lib.units import mm from reportlab.lib.units import mm
from reportlab.platypus import ( from reportlab.platypus import (
KeepTogether, PageTemplate, Paragraph, Spacer, Table, TableStyle, KeepTogether, PageTemplate, Spacer, Table, TableStyle,
) )
from pretix.base.exporter import BaseExporter from pretix.base.exporter import BaseExporter
@@ -49,7 +49,7 @@ from pretix.base.timeframes import (
resolve_timeframe_to_datetime_start_inclusive_end_exclusive, resolve_timeframe_to_datetime_start_inclusive_end_exclusive,
) )
from pretix.control.forms.filter import get_all_payment_providers from pretix.control.forms.filter import get_all_payment_providers
from pretix.helpers.reportlab import FontFallbackParagraph from pretix.helpers.reportlab import PlainTextParagraph
from pretix.plugins.reports.exporters import ReportlabExportMixin from pretix.plugins.reports.exporters import ReportlabExportMixin
@@ -270,18 +270,18 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
def _transaction_group_header_label(self): def _transaction_group_header_label(self):
return _("Event") + " / " + _("Product") return _("Event") + " / " + _("Product")
def _transaction_group_label(self, form_data, r): def _transaction_group(self, form_data, r):
if not self.is_multievent and not form_data.get("split_subevents"): if not self.is_multievent and not form_data.get("split_subevents"):
return None return None, None
if r.get("subevent_id"): if r.get("subevent_id"):
return "{} - {} ({}) [{}]".format( return "{} - {} ({}) [{}]".format(
r["order__event__name"], r["order__event__name"],
r["subevent__name"], r["subevent__name"],
date_format(r["subevent__date_from"]), date_format(r["subevent__date_from"]),
r["order__event__slug"] r["order__event__slug"]
) ), f"subevent-{r['subevent_id']}"
else: else:
return "{} [{}]".format(r["order__event__name"], r["order__event__slug"]) return "{} [{}]".format(r["order__event__name"], r["order__event__slug"]), f"event-{r['order__event__slug']}"
def _transaction_row_label(self, r): def _transaction_row_label(self, r):
if r["item_id"]: if r["item_id"]:
@@ -311,13 +311,13 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
tdata = [ tdata = [
[ [
FontFallbackParagraph(self._transaction_group_header_label(), tstyle_bold), PlainTextParagraph(self._transaction_group_header_label(), tstyle_bold),
FontFallbackParagraph(_("Price"), tstyle_bold_right), PlainTextParagraph(_("Price"), tstyle_bold_right),
FontFallbackParagraph(_("Tax rate"), tstyle_bold_right), PlainTextParagraph(_("Tax rate"), tstyle_bold_right),
FontFallbackParagraph("#", tstyle_bold_right), PlainTextParagraph("#", tstyle_bold_right),
FontFallbackParagraph(_("Net total"), tstyle_bold_right), PlainTextParagraph(_("Net total"), tstyle_bold_right),
FontFallbackParagraph(_("Tax total"), tstyle_bold_right), PlainTextParagraph(_("Tax total"), tstyle_bold_right),
FontFallbackParagraph(_("Gross total"), tstyle_bold_right), PlainTextParagraph(_("Gross total"), tstyle_bold_right),
] ]
] ]
@@ -343,17 +343,17 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
last_group = None last_group = None
last_group_head_idx = 0 last_group_head_idx = 0
for r in qs: for r in qs:
e = self._transaction_group_label(form_data, r) group_label, group_id = self._transaction_group(form_data, r)
if e != last_group: if group_id != last_group:
if last_group_head_idx > 0 and e is not None: if last_group_head_idx > 0 and group_id is not None:
tdata[last_group_head_idx][4] = Paragraph(money_filter(sum_price_by_group - sum_tax_by_group, currency), tstyle_bold_right), tdata[last_group_head_idx][4] = PlainTextParagraph(money_filter(sum_price_by_group - sum_tax_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][5] = Paragraph(money_filter(sum_tax_by_group, currency), tstyle_bold_right), tdata[last_group_head_idx][5] = PlainTextParagraph(money_filter(sum_tax_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][6] = Paragraph(money_filter(sum_price_by_group, currency), tstyle_bold_right), tdata[last_group_head_idx][6] = PlainTextParagraph(money_filter(sum_price_by_group, currency), tstyle_bold_right),
tdata.append( tdata.append(
[ [
FontFallbackParagraph( PlainTextParagraph(
e, group_label,
tstyle_bold, tstyle_bold,
), ),
"", "",
@@ -367,7 +367,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
tstyledata.append( tstyledata.append(
("SPAN", (0, len(tdata) - 1), (3, len(tdata) - 1)), ("SPAN", (0, len(tdata) - 1), (3, len(tdata) - 1)),
) )
last_group = e last_group = group_id
last_group_head_idx = len(tdata) - 1 last_group_head_idx = len(tdata) - 1
sum_price_by_group = Decimal("0.00") sum_price_by_group = Decimal("0.00")
sum_tax_by_group = Decimal("0.00") sum_tax_by_group = Decimal("0.00")
@@ -375,20 +375,20 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
text = self._transaction_row_label(r) text = self._transaction_row_label(r)
tdata.append( tdata.append(
[ [
FontFallbackParagraph(text, tstyle), PlainTextParagraph(text, tstyle),
Paragraph( PlainTextParagraph(
money_filter(r["price"], currency) money_filter(r["price"], currency)
if "price" in r and r["price"] is not None if "price" in r and r["price"] is not None
else "", else "",
tstyle_right, tstyle_right,
), ),
Paragraph(localize(r["tax_rate"].normalize()) + " %", tstyle_right), PlainTextParagraph(localize(r["tax_rate"].normalize()) + " %", tstyle_right),
Paragraph(str(r["sum_cont"]), tstyle_right), PlainTextParagraph(str(r["sum_cont"]), tstyle_right),
Paragraph( PlainTextParagraph(
money_filter(r["sum_price"] - r["sum_tax"], currency), tstyle_right money_filter(r["sum_price"] - r["sum_tax"], currency), tstyle_right
), ),
Paragraph(money_filter(r["sum_tax"], currency), tstyle_right), PlainTextParagraph(money_filter(r["sum_tax"], currency), tstyle_right),
Paragraph(money_filter(r["sum_price"], currency), tstyle_right), PlainTextParagraph(money_filter(r["sum_price"], currency), tstyle_right),
] ]
) )
sum_cnt_by_tax_rate[r["tax_rate"]] += r["sum_cont"] sum_cnt_by_tax_rate[r["tax_rate"]] += r["sum_cont"]
@@ -398,19 +398,19 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
sum_tax_by_group += r["sum_tax"] sum_tax_by_group += r["sum_tax"]
if last_group_head_idx > 0 and last_group is not None: if last_group_head_idx > 0 and last_group is not None:
tdata[last_group_head_idx][4] = Paragraph(money_filter(sum_price_by_group - sum_tax_by_group, currency), tstyle_bold_right), tdata[last_group_head_idx][4] = PlainTextParagraph(money_filter(sum_price_by_group - sum_tax_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][5] = Paragraph(money_filter(sum_tax_by_group, currency), tstyle_bold_right), tdata[last_group_head_idx][5] = PlainTextParagraph(money_filter(sum_tax_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][6] = Paragraph(money_filter(sum_price_by_group, currency), tstyle_bold_right), tdata[last_group_head_idx][6] = PlainTextParagraph(money_filter(sum_price_by_group, currency), tstyle_bold_right),
if len(sum_tax_by_tax_rate) > 1: if len(sum_tax_by_tax_rate) > 1:
for tax_rate in sorted(sum_tax_by_tax_rate.keys(), reverse=True): for tax_rate in sorted(sum_tax_by_tax_rate.keys(), reverse=True):
tdata.append( tdata.append(
[ [
FontFallbackParagraph(_("Sum"), tstyle), PlainTextParagraph(_("Sum"), tstyle),
Paragraph("", tstyle_right), PlainTextParagraph("", tstyle_right),
Paragraph(localize(tax_rate.normalize()) + " %", tstyle_right), PlainTextParagraph(localize(tax_rate.normalize()) + " %", tstyle_right),
Paragraph("", tstyle_right), PlainTextParagraph("", tstyle_right),
Paragraph( PlainTextParagraph(
money_filter( money_filter(
sum_price_by_tax_rate[tax_rate] sum_price_by_tax_rate[tax_rate]
- sum_tax_by_tax_rate[tax_rate], - sum_tax_by_tax_rate[tax_rate],
@@ -418,10 +418,10 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
), ),
tstyle_right, tstyle_right,
), ),
Paragraph( PlainTextParagraph(
money_filter(sum_tax_by_tax_rate[tax_rate], currency), tstyle_right money_filter(sum_tax_by_tax_rate[tax_rate], currency), tstyle_right
), ),
Paragraph( PlainTextParagraph(
money_filter(sum_price_by_tax_rate[tax_rate], currency), money_filter(sum_price_by_tax_rate[tax_rate], currency),
tstyle_right, tstyle_right,
), ),
@@ -439,11 +439,11 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
tdata.append( tdata.append(
[ [
FontFallbackParagraph(_("Sum"), tstyle_bold), PlainTextParagraph(_("Sum"), tstyle_bold),
Paragraph("", tstyle_right), PlainTextParagraph("", tstyle_right),
Paragraph("", tstyle_right), PlainTextParagraph("", tstyle_right),
Paragraph("", tstyle_bold_right), PlainTextParagraph("", tstyle_bold_right),
Paragraph( PlainTextParagraph(
money_filter( money_filter(
sum(sum_price_by_tax_rate.values()) sum(sum_price_by_tax_rate.values())
- sum(sum_tax_by_tax_rate.values()), - sum(sum_tax_by_tax_rate.values()),
@@ -451,11 +451,11 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
), ),
tstyle_bold_right, tstyle_bold_right,
), ),
Paragraph( PlainTextParagraph(
money_filter(sum(sum_tax_by_tax_rate.values()), currency), money_filter(sum(sum_tax_by_tax_rate.values()), currency),
tstyle_bold_right, tstyle_bold_right,
), ),
Paragraph( PlainTextParagraph(
money_filter(sum(sum_price_by_tax_rate.values()), currency), money_filter(sum(sum_price_by_tax_rate.values()), currency),
tstyle_bold_right, tstyle_bold_right,
), ),
@@ -493,10 +493,10 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
tdata = [ tdata = [
[ [
FontFallbackParagraph(_("Payment method"), tstyle_bold), PlainTextParagraph(_("Payment method"), tstyle_bold),
FontFallbackParagraph(_("Payments"), tstyle_bold_right), PlainTextParagraph(_("Payments"), tstyle_bold_right),
FontFallbackParagraph(_("Refunds"), tstyle_bold_right), PlainTextParagraph(_("Refunds"), tstyle_bold_right),
FontFallbackParagraph(_("Total"), tstyle_bold_right), PlainTextParagraph(_("Total"), tstyle_bold_right),
] ]
] ]
@@ -537,20 +537,20 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
for p in providers: for p in providers:
tdata.append( tdata.append(
[ [
Paragraph(provider_names.get(p, p), tstyle), PlainTextParagraph(provider_names.get(p, p), tstyle),
FontFallbackParagraph( PlainTextParagraph(
money_filter(payments_by_provider[p], currency) money_filter(payments_by_provider[p], currency)
if p in payments_by_provider if p in payments_by_provider
else "", else "",
tstyle_right, tstyle_right,
), ),
Paragraph( PlainTextParagraph(
money_filter(refunds_by_provider[p], currency) money_filter(refunds_by_provider[p], currency)
if p in refunds_by_provider if p in refunds_by_provider
else "", else "",
tstyle_right, tstyle_right,
), ),
Paragraph( PlainTextParagraph(
money_filter( money_filter(
payments_by_provider.get(p, Decimal("0.00")) payments_by_provider.get(p, Decimal("0.00"))
- refunds_by_provider.get(p, Decimal("0.00")), - refunds_by_provider.get(p, Decimal("0.00")),
@@ -563,20 +563,20 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
tdata.append( tdata.append(
[ [
FontFallbackParagraph(_("Sum"), tstyle_bold), PlainTextParagraph(_("Sum"), tstyle_bold),
Paragraph( PlainTextParagraph(
money_filter( money_filter(
sum(payments_by_provider.values(), Decimal("0.00")), currency sum(payments_by_provider.values(), Decimal("0.00")), currency
), ),
tstyle_bold_right, tstyle_bold_right,
), ),
Paragraph( PlainTextParagraph(
money_filter( money_filter(
sum(refunds_by_provider.values(), Decimal("0.00")), currency sum(refunds_by_provider.values(), Decimal("0.00")), currency
), ),
tstyle_bold_right, tstyle_bold_right,
), ),
Paragraph( PlainTextParagraph(
money_filter( money_filter(
sum(payments_by_provider.values(), Decimal("0.00")) sum(payments_by_provider.values(), Decimal("0.00"))
- sum(refunds_by_provider.values(), Decimal("0.00")), - sum(refunds_by_provider.values(), Decimal("0.00")),
@@ -641,7 +641,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
open_before = tx_before - p_before + r_before open_before = tx_before - p_before + r_before
tdata.append( tdata.append(
[ [
FontFallbackParagraph( PlainTextParagraph(
_("Pending payments at {datetime}").format( _("Pending payments at {datetime}").format(
datetime=date_format( datetime=date_format(
(df_start - datetime.timedelta.resolution).astimezone( (df_start - datetime.timedelta.resolution).astimezone(
@@ -653,7 +653,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
tstyle, tstyle,
), ),
"", "",
Paragraph(money_filter(open_before, currency), tstyle_right), PlainTextParagraph(money_filter(open_before, currency), tstyle_right),
] ]
) )
else: else:
@@ -670,30 +670,30 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
] or Decimal("0.00") ] or Decimal("0.00")
tdata.append( tdata.append(
[ [
FontFallbackParagraph(_("Orders"), tstyle), PlainTextParagraph(_("Orders"), tstyle),
Paragraph("+", tstyle_center), PlainTextParagraph("+", tstyle_center),
Paragraph(money_filter(tx_during, currency), tstyle_right), PlainTextParagraph(money_filter(tx_during, currency), tstyle_right),
] ]
) )
tdata.append( tdata.append(
[ [
FontFallbackParagraph(_("Payments"), tstyle), PlainTextParagraph(_("Payments"), tstyle),
Paragraph("-", tstyle_center), PlainTextParagraph("-", tstyle_center),
Paragraph(money_filter(p_during, currency), tstyle_right), PlainTextParagraph(money_filter(p_during, currency), tstyle_right),
] ]
) )
tdata.append( tdata.append(
[ [
FontFallbackParagraph(_("Refunds"), tstyle), PlainTextParagraph(_("Refunds"), tstyle),
Paragraph("+", tstyle_center), PlainTextParagraph("+", tstyle_center),
Paragraph(money_filter(r_during, currency), tstyle_right), PlainTextParagraph(money_filter(r_during, currency), tstyle_right),
] ]
) )
open_after = open_before + tx_during - p_during + r_during open_after = open_before + tx_during - p_during + r_during
tdata.append( tdata.append(
[ [
Paragraph( PlainTextParagraph(
_("Pending payments at {datetime}").format( _("Pending payments at {datetime}").format(
datetime=date_format( datetime=date_format(
((df_end or now()) - datetime.timedelta.resolution).astimezone( ((df_end or now()) - datetime.timedelta.resolution).astimezone(
@@ -704,8 +704,8 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
), ),
tstyle_bold, tstyle_bold,
), ),
Paragraph("=", tstyle_center), PlainTextParagraph("=", tstyle_center),
Paragraph(money_filter(open_after, currency), tstyle_bold_right), PlainTextParagraph(money_filter(open_after, currency), tstyle_bold_right),
] ]
) )
tstyledata += [ tstyledata += [
@@ -752,7 +752,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
) )
tdata.append( tdata.append(
[ [
Paragraph( PlainTextParagraph(
_("Total gift card value at {datetime}").format( _("Total gift card value at {datetime}").format(
datetime=date_format( datetime=date_format(
(df_start - datetime.timedelta.resolution).astimezone( (df_start - datetime.timedelta.resolution).astimezone(
@@ -763,7 +763,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
), ),
tstyle, tstyle,
), ),
Paragraph(money_filter(tx_before, currency), tstyle_right), PlainTextParagraph(money_filter(tx_before, currency), tstyle_right),
] ]
) )
else: else:
@@ -774,8 +774,8 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
] or Decimal("0.00") ] or Decimal("0.00")
tdata.append( tdata.append(
[ [
FontFallbackParagraph(_("Gift card transactions (credit)"), tstyle), PlainTextParagraph(_("Gift card transactions (credit)"), tstyle),
Paragraph(money_filter(tx_during_pos, currency), tstyle_right), PlainTextParagraph(money_filter(tx_during_pos, currency), tstyle_right),
] ]
) )
@@ -784,15 +784,15 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
] or Decimal("0.00") ] or Decimal("0.00")
tdata.append( tdata.append(
[ [
FontFallbackParagraph(_("Gift card transactions (debit)"), tstyle), PlainTextParagraph(_("Gift card transactions (debit)"), tstyle),
Paragraph(money_filter(tx_during_neg, currency), tstyle_right), PlainTextParagraph(money_filter(tx_during_neg, currency), tstyle_right),
] ]
) )
open_after = tx_before + tx_during_pos + tx_during_neg open_after = tx_before + tx_during_pos + tx_during_neg
tdata.append( tdata.append(
[ [
Paragraph( PlainTextParagraph(
_("Total gift card value at {datetime}").format( _("Total gift card value at {datetime}").format(
datetime=date_format( datetime=date_format(
((df_end or now()) - datetime.timedelta.resolution).astimezone( ((df_end or now()) - datetime.timedelta.resolution).astimezone(
@@ -803,7 +803,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
), ),
tstyle_bold, tstyle_bold,
), ),
Paragraph(money_filter(open_after, currency), tstyle_bold_right), PlainTextParagraph(money_filter(open_after, currency), tstyle_bold_right),
] ]
) )
tstyledata += [ tstyledata += [
@@ -854,10 +854,10 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
style_small.leading = 10 style_small.leading = 10
story = [ story = [
FontFallbackParagraph(self.verbose_name, style_h1), PlainTextParagraph(self.verbose_name, style_h1),
Spacer(0, 3 * mm), Spacer(0, 3 * mm),
FontFallbackParagraph( PlainTextParagraph(
"<br />".join(escape(f) for f in self.describe_filters(form_data)), "\n".join(escape(f) for f in self.describe_filters(form_data)),
style_small, style_small,
), ),
] ]
@@ -870,7 +870,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
if s: if s:
story += [ story += [
Spacer(0, 3 * mm), Spacer(0, 3 * mm),
FontFallbackParagraph(_("Orders") + c_head, style_h2), PlainTextParagraph(_("Orders") + c_head, style_h2),
Spacer(0, 3 * mm), Spacer(0, 3 * mm),
*s *s
] ]
@@ -881,7 +881,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
if s: if s:
story += [ story += [
Spacer(0, 8 * mm), Spacer(0, 8 * mm),
FontFallbackParagraph(_("Payments") + c_head, style_h2), PlainTextParagraph(_("Payments") + c_head, style_h2),
Spacer(0, 3 * mm), Spacer(0, 3 * mm),
*s *s
] ]
@@ -894,7 +894,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
Spacer(0, 8 * mm), Spacer(0, 8 * mm),
KeepTogether( KeepTogether(
[ [
FontFallbackParagraph(_("Open items") + c_head, style_h2), PlainTextParagraph(_("Open items") + c_head, style_h2),
Spacer(0, 3 * mm), Spacer(0, 3 * mm),
*s *s
] ]
@@ -912,7 +912,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
Spacer(0, 8 * mm), Spacer(0, 8 * mm),
KeepTogether( KeepTogether(
[ [
FontFallbackParagraph(_("Gift cards") + c_head, style_h2), PlainTextParagraph(_("Gift cards") + c_head, style_h2),
Spacer(0, 3 * mm), Spacer(0, 3 * mm),
*s, *s,
] ]
+13 -13
View File
@@ -70,7 +70,7 @@ from pretix.base.timeframes import (
) )
from pretix.control.forms.filter import OverviewFilterForm from pretix.control.forms.filter import OverviewFilterForm
from pretix.helpers.reportlab import ( from pretix.helpers.reportlab import (
FontFallbackParagraph, register_ttf_font_if_new, PlainTextParagraph, register_ttf_font_if_new,
) )
from pretix.presale.style import get_fonts from pretix.presale.style import get_fonts
@@ -282,7 +282,7 @@ class OverviewReport(Report):
headlinestyle.fontSize = 15 headlinestyle.fontSize = 15
headlinestyle.fontName = 'OpenSansBd' headlinestyle.fontName = 'OpenSansBd'
story = [ story = [
FontFallbackParagraph(_('Orders by product') + ' ' + (_('(excl. taxes)') if net else _('(incl. taxes)')), headlinestyle), PlainTextParagraph(_('Orders by product') + ' ' + (_('(excl. taxes)') if net else _('(incl. taxes)')), headlinestyle),
Spacer(1, 5 * mm) Spacer(1, 5 * mm)
] ]
return story return story
@@ -292,7 +292,7 @@ class OverviewReport(Report):
if form_data.get('date_axis') and form_data.get('date_range'): if form_data.get('date_axis') and form_data.get('date_range'):
d_start, d_end = resolve_timeframe_to_dates_inclusive(now(), form_data['date_range'], self.timezone) d_start, d_end = resolve_timeframe_to_dates_inclusive(now(), form_data['date_range'], self.timezone)
story += [ story += [
FontFallbackParagraph(_('{axis} between {start} and {end}').format( PlainTextParagraph(_('{axis} between {start} and {end}').format(
axis=dict(OverviewFilterForm(event=self.event).fields['date_axis'].choices)[form_data.get('date_axis')], axis=dict(OverviewFilterForm(event=self.event).fields['date_axis'].choices)[form_data.get('date_axis')],
start=date_format(d_start, 'SHORT_DATE_FORMAT') if d_start else '', start=date_format(d_start, 'SHORT_DATE_FORMAT') if d_start else '',
end=date_format(d_end, 'SHORT_DATE_FORMAT') if d_end else '', end=date_format(d_end, 'SHORT_DATE_FORMAT') if d_end else '',
@@ -305,13 +305,13 @@ class OverviewReport(Report):
subevent = self.event.subevents.get(pk=self.form_data.get('subevent')) subevent = self.event.subevents.get(pk=self.form_data.get('subevent'))
except SubEvent.DoesNotExist: except SubEvent.DoesNotExist:
subevent = self.form_data.get('subevent') subevent = self.form_data.get('subevent')
story.append(FontFallbackParagraph(pgettext('subevent', 'Date: {}').format(subevent), self.get_style())) story.append(PlainTextParagraph(pgettext('subevent', 'Date: {}').format(subevent), self.get_style()))
story.append(Spacer(1, 5 * mm)) story.append(Spacer(1, 5 * mm))
if form_data.get('subevent_date_range'): if form_data.get('subevent_date_range'):
d_start, d_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['subevent_date_range'], self.timezone) d_start, d_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['subevent_date_range'], self.timezone)
story += [ story += [
FontFallbackParagraph(_('{axis} between {start} and {end}').format( PlainTextParagraph(_('{axis} between {start} and {end}').format(
axis=_('Event date'), axis=_('Event date'),
start=date_format(d_start, 'SHORT_DATE_FORMAT') if d_start else '', start=date_format(d_start, 'SHORT_DATE_FORMAT') if d_start else '',
end=date_format(d_end - timedelta(hours=1), 'SHORT_DATE_FORMAT') if d_end else '', end=date_format(d_end - timedelta(hours=1), 'SHORT_DATE_FORMAT') if d_end else '',
@@ -384,13 +384,13 @@ class OverviewReport(Report):
tdata = [ tdata = [
[ [
_('Product'), _('Product'),
FontFallbackParagraph(_('Canceled'), tstyle_th), PlainTextParagraph(_('Canceled'), tstyle_th),
'', '',
FontFallbackParagraph(_('Expired'), tstyle_th), PlainTextParagraph(_('Expired'), tstyle_th),
'', '',
FontFallbackParagraph(_('Approval pending'), tstyle_th), PlainTextParagraph(_('Approval pending'), tstyle_th),
'', '',
FontFallbackParagraph(_('Purchased'), tstyle_th), PlainTextParagraph(_('Purchased'), tstyle_th),
'', '', '', '', '' '', '', '', '', ''
], ],
[ [
@@ -421,14 +421,14 @@ class OverviewReport(Report):
for tup in items_by_category: for tup in items_by_category:
if tup[0]: if tup[0]:
tdata.append([ tdata.append([
FontFallbackParagraph(str(tup[0]), tstyle_bold) PlainTextParagraph(str(tup[0]), tstyle_bold)
]) ])
for l, s in states: for l, s in states:
tdata[-1].append(str(tup[0].num[l][0])) tdata[-1].append(str(tup[0].num[l][0]))
tdata[-1].append(floatformat(tup[0].num[l][2 if net else 1], places)) tdata[-1].append(floatformat(tup[0].num[l][2 if net else 1], places))
for item in tup[1]: for item in tup[1]:
tdata.append([ tdata.append([
FontFallbackParagraph(str(item), tstyle) PlainTextParagraph(str(item), tstyle)
]) ])
for l, s in states: for l, s in states:
tdata[-1].append(str(item.num[l][0])) tdata[-1].append(str(item.num[l][0]))
@@ -436,7 +436,7 @@ class OverviewReport(Report):
if item.has_variations: if item.has_variations:
for var in item.all_variations: for var in item.all_variations:
tdata.append([ tdata.append([
FontFallbackParagraph(" " + str(var), tstyle) PlainTextParagraph(" " + str(var), tstyle)
]) ])
for l, s in states: for l, s in states:
tdata[-1].append(str(var.num[l][0])) tdata[-1].append(str(var.num[l][0]))
@@ -568,7 +568,7 @@ class OrderTaxListReportPDF(Report):
tstyledata.append(('SPAN', (5 + 2 * i, 0), (6 + 2 * i, 0))) tstyledata.append(('SPAN', (5 + 2 * i, 0), (6 + 2 * i, 0)))
story = [ story = [
FontFallbackParagraph(_('Orders by tax rate ({currency})').format(currency=self.event.currency), headlinestyle), PlainTextParagraph(_('Orders by tax rate ({currency})').format(currency=self.event.currency), headlinestyle),
Spacer(1, 5 * mm) Spacer(1, 5 * mm)
] ]
tdata = [ tdata = [
+9 -9
View File
@@ -75,8 +75,8 @@ from pretix.base.settings import SettingsSandbox
from pretix.helpers import OF_SELF from pretix.helpers import OF_SELF
from pretix.helpers.countries import CachedCountries from pretix.helpers.countries import CachedCountries
from pretix.helpers.http import get_client_ip from pretix.helpers.http import get_client_ip
from pretix.helpers.urls import build_absolute_uri as build_global_uri from pretix.helpers.urls import mainreverse_absolute
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.plugins.stripe.forms import StripeKeyValidator from pretix.plugins.stripe.forms import StripeKeyValidator
from pretix.plugins.stripe.models import ( from pretix.plugins.stripe.models import (
ReferencedStripeObject, RegisteredApplePayDomain, ReferencedStripeObject, RegisteredApplePayDomain,
@@ -197,7 +197,7 @@ class StripeSettingsHolder(BasePaymentProvider):
).format( ).format(
self.settings.connect_client_id, self.settings.connect_client_id,
request.session['payment_stripe_oauth_token'], request.session['payment_stripe_oauth_token'],
urllib.parse.quote(build_global_uri('plugins:stripe:oauth.return')), urllib.parse.quote(mainreverse_absolute('plugins:stripe:oauth.return')),
) )
def settings_content_render(self, request): def settings_content_render(self, request):
@@ -229,7 +229,7 @@ class StripeSettingsHolder(BasePaymentProvider):
_('Please configure a <a href="https://dashboard.stripe.com/account/webhooks">Stripe Webhook</a> to ' _('Please configure a <a href="https://dashboard.stripe.com/account/webhooks">Stripe Webhook</a> to '
'the following endpoint in order to automatically cancel orders when charges are refunded externally ' 'the following endpoint in order to automatically cancel orders when charges are refunded externally '
'and to process asynchronous payment methods like SOFORT.'), 'and to process asynchronous payment methods like SOFORT.'),
build_global_uri('plugins:stripe:webhook') mainreverse_absolute('plugins:stripe:webhook')
) )
@property @property
@@ -746,7 +746,7 @@ class StripeMethod(BasePaymentProvider):
def redirect(self, request, url): def redirect(self, request, url):
if request.session.get('iframe_session', False): if request.session.get('iframe_session', False):
return ( return (
build_absolute_uri(request.event, 'plugins:stripe:redirect') + eventreverse_absolute(request.event, 'plugins:stripe:redirect') +
'?data=' + signing.dumps({ '?data=' + signing.dumps({
'url': url, 'url': url,
'session': { 'session': {
@@ -941,7 +941,7 @@ class StripeMethod(BasePaymentProvider):
}, },
# TODO: Is this sufficient? # TODO: Is this sufficient?
idempotency_key=str(self.event.id) + payment.order.code + idempotency_key_seed, idempotency_key=str(self.event.id) + payment.order.code + idempotency_key_seed,
return_url=build_absolute_uri(self.event, 'plugins:stripe:sca.return', kwargs={ return_url=eventreverse_absolute(self.event, 'plugins:stripe:sca.return', kwargs={
'order': payment.order.code, 'order': payment.order.code,
'payment': payment.pk, 'payment': payment.pk,
'hash': payment.order.tagged_secret('plugins:stripe'), 'hash': payment.order.tagged_secret('plugins:stripe'),
@@ -1047,13 +1047,13 @@ class StripeMethod(BasePaymentProvider):
raise PaymentException(_('Stripe reported an error: %s') % intent.last_payment_error.message) raise PaymentException(_('Stripe reported an error: %s') % intent.last_payment_error.message)
def _redirect_to_sca(self, request, payment): def _redirect_to_sca(self, request, payment):
url = build_absolute_uri(self.event, 'plugins:stripe:sca', kwargs={ url = eventreverse_absolute(self.event, 'plugins:stripe:sca', kwargs={
'order': payment.order.code, 'order': payment.order.code,
'payment': payment.pk, 'payment': payment.pk,
'hash': payment.order.tagged_secret('plugins:stripe'), 'hash': payment.order.tagged_secret('plugins:stripe'),
}) })
if not self.redirect_in_widget_allowed and request.session.get('iframe_session', False): if not self.redirect_in_widget_allowed and request.session.get('iframe_session', False):
return build_absolute_uri(self.event, 'plugins:stripe:redirect') + '?data=' + signing.dumps({ return eventreverse_absolute(self.event, 'plugins:stripe:redirect') + '?data=' + signing.dumps({
'url': url, 'url': url,
'session': {}, 'session': {},
}, salt='safe-redirect') }, salt='safe-redirect')
@@ -1068,7 +1068,7 @@ class StripeMethod(BasePaymentProvider):
intent = stripe.PaymentIntent.confirm( intent = stripe.PaymentIntent.confirm(
payment_info['id'], payment_info['id'],
return_url=build_absolute_uri(self.event, 'plugins:stripe:sca.return', kwargs={ return_url=eventreverse_absolute(self.event, 'plugins:stripe:sca.return', kwargs={
'order': payment.order.code, 'order': payment.order.code,
'payment': payment.pk, 'payment': payment.pk,
'hash': payment.order.tagged_secret('plugins:stripe'), 'hash': payment.order.tagged_secret('plugins:stripe'),
+2 -2
View File
@@ -64,7 +64,7 @@ from pretix.control.views.event import DecoupleMixin
from pretix.control.views.organizer import OrganizerDetailViewMixin from pretix.control.views.organizer import OrganizerDetailViewMixin
from pretix.helpers import OF_SELF from pretix.helpers import OF_SELF
from pretix.helpers.http import redirect_to_url from pretix.helpers.http import redirect_to_url
from pretix.multidomain.urlreverse import build_absolute_uri, eventreverse from pretix.multidomain.urlreverse import eventreverse, eventreverse_absolute
from pretix.plugins.stripe.forms import OrganizerStripeSettingsForm from pretix.plugins.stripe.forms import OrganizerStripeSettingsForm
from pretix.plugins.stripe.models import ReferencedStripeObject from pretix.plugins.stripe.models import ReferencedStripeObject
from pretix.plugins.stripe.tasks import ( from pretix.plugins.stripe.tasks import (
@@ -90,7 +90,7 @@ def redirect_view(request, *args, **kwargs):
params = request.GET.copy() params = request.GET.copy()
params['go'] = '1' params['go'] = '1'
r = render(request, 'pretixplugins/stripe/redirect.html', { r = render(request, 'pretixplugins/stripe/redirect.html', {
'url': build_absolute_uri(request.event, 'plugins:stripe:redirect') + '?' + urllib.parse.urlencode(params), 'url': eventreverse_absolute(request.event, 'plugins:stripe:redirect') + '?' + urllib.parse.urlencode(params),
}) })
r._csp_ignore = True r._csp_ignore = True
return r return r
+2 -1
View File
@@ -51,6 +51,7 @@ from django.http import HttpResponseNotAllowed, JsonResponse
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils import translation from django.utils import translation
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.html import conditional_escape
from django.utils.translation import ( from django.utils.translation import (
get_language, gettext_lazy as _, pgettext_lazy, get_language, gettext_lazy as _, pgettext_lazy,
) )
@@ -1634,7 +1635,7 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
meta_info = { meta_info = {
'contact_form_data': self.cart_session.get('contact_form_data', {}), 'contact_form_data': self.cart_session.get('contact_form_data', {}),
'confirm_messages': [ 'confirm_messages': [
str(m) for m in self.confirm_messages.values() conditional_escape(str(m)) for m in self.confirm_messages.values()
] ]
} }
api_meta = {} api_meta = {}
+2 -2
View File
@@ -44,7 +44,7 @@ from pretix.base.forms.questions import (
from pretix.base.i18n import get_language_without_region from pretix.base.i18n import get_language_without_region
from pretix.base.models import Customer from pretix.base.models import Customer
from pretix.helpers.http import get_client_ip from pretix.helpers.http import get_client_ip
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
class TokenGenerator(PasswordResetTokenGenerator): class TokenGenerator(PasswordResetTokenGenerator):
@@ -84,7 +84,7 @@ class AuthenticationForm(forms.Form):
self.customer_cache = None self.customer_cache = None
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['password'].help_text = "<a target='_blank' href='{}'>{}</a>".format( self.fields['password'].help_text = "<a target='_blank' href='{}'>{}</a>".format(
build_absolute_uri(False, 'presale:organizer.customer.resetpw', kwargs={ eventreverse_absolute(False, 'presale:organizer.customer.resetpw', kwargs={
'organizer': request.organizer.slug, 'organizer': request.organizer.slug,
}), }),
_('Forgot your password?') _('Forgot your password?')
+6 -6
View File
@@ -32,7 +32,7 @@ from django.utils.translation import gettext as _
from pretix.base.email import get_email_context from pretix.base.email import get_email_context
from pretix.base.models import Event from pretix.base.models import Event
from pretix.helpers.format import format_map from pretix.helpers.format import format_map
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
def get_public_ical(events): def get_public_ical(events):
@@ -48,9 +48,9 @@ def get_public_ical(events):
event = ev if isinstance(ev, Event) else ev.event event = ev if isinstance(ev, Event) else ev.event
tz = event.timezone tz = event.timezone
if isinstance(ev, Event): if isinstance(ev, Event):
url = build_absolute_uri(event, 'presale:event.index') url = eventreverse_absolute(event, 'presale:event.index')
else: else:
url = build_absolute_uri(event, 'presale:event.index', { url = eventreverse_absolute(event, 'presale:event.index', {
'subevent': ev.pk 'subevent': ev.pk
}) })
@@ -134,7 +134,7 @@ def get_private_icals(event, positions):
program_times = p.item.program_times.all() program_times = p.item.program_times.all()
if program_times: if program_times:
# if program times have been configured, they are preferred for the position's calendar entries # if program times have been configured, they are preferred for the position's calendar entries
url = build_absolute_uri(event, 'presale:event.index') url = eventreverse_absolute(event, 'presale:event.index')
for index, pt in enumerate(program_times): for index, pt in enumerate(program_times):
summary = _('{event} - {item}').format(event=ev, item=p.item.name) summary = _('{event} - {item}').format(event=ev, item=p.item.name)
if event.settings.mail_attach_ical_description: if event.settings.mail_attach_ical_description:
@@ -167,11 +167,11 @@ def get_private_icals(event, positions):
else: else:
# without program times, the subevent or event times are used for calendar entries, preferring subevents # without program times, the subevent or event times are used for calendar entries, preferring subevents
if p.subevent: if p.subevent:
url = build_absolute_uri(event, 'presale:event.index', { url = eventreverse_absolute(event, 'presale:event.index', {
'subevent': p.subevent.pk 'subevent': p.subevent.pk
}) })
else: else:
url = build_absolute_uri(event, 'presale:event.index') url = eventreverse_absolute(event, 'presale:event.index')
if event.settings.mail_attach_ical_description: if event.settings.mail_attach_ical_description:
ctx = get_email_context(event=event, event_or_subevent=ev) ctx = get_email_context(event=event, event_or_subevent=ev)
+1 -1
View File
@@ -144,7 +144,7 @@ checkout_confirm_messages = EventPluginSignal()
This signal is sent out to retrieve short messages that need to be acknowledged by the user before the This signal is sent out to retrieve short messages that need to be acknowledged by the user before the
order can be completed. This is typically used for something like "accept the terms and conditions". order can be completed. This is typically used for something like "accept the terms and conditions".
Receivers are expected to return a dictionary where the keys are globally unique identifiers for the Receivers are expected to return a dictionary where the keys are globally unique identifiers for the
message and the values can be arbitrary HTML. message and the values can be a SafeString containing arbitrary HTML, or a string that will be HTML-escaped.
As with all event plugin signals, the ``sender`` keyword argument will contain the event. As with all event plugin signals, the ``sender`` keyword argument will contain the event.
""" """
@@ -176,7 +176,7 @@
<div class="checkbox"> <div class="checkbox">
<label for="input_confirm_{{ key }}"> <label for="input_confirm_{{ key }}">
<input type="checkbox" class="checkbox" value="yes" name="confirm_{{ key }}" id="input_confirm_{{ key }}" required> <input type="checkbox" class="checkbox" value="yes" name="confirm_{{ key }}" id="input_confirm_{{ key }}" required>
{{ desc|safe }} {{ desc }}
</label> </label>
</div> </div>
{% endfor %} {% endfor %}
@@ -4,6 +4,7 @@
{% load eventsignal %} {% load eventsignal %}
{% load money %} {% load money %}
{% load eventurl %} {% load eventurl %}
{% load wrap_in %}
{% block title %}{% trans "Registration details" %}{% endblock %} {% block title %}{% trans "Registration details" %}{% endblock %}
{% block content %} {% block content %}
<h2 class="h1"> <h2 class="h1">
@@ -48,7 +49,7 @@
</div> </div>
<div class="panel-body"> <div class="panel-body">
<p> <p>
{% blocktrans trimmed with email="<strong>"|add:order.email|add:"</strong>"|safe %} {% blocktrans trimmed with email=order.email|wrap_in:"strong" %}
This order is managed for you by {{ email }}. Please contact them for any questions regarding This order is managed for you by {{ email }}. Please contact them for any questions regarding
payment, cancellation or changes to this order. payment, cancellation or changes to this order.
{% endblocktrans %} {% endblocktrans %}
@@ -22,4 +22,5 @@
<script type="text/javascript" src="{% static "pretixpresale/js/ui/iframe.js" %}"></script> <script type="text/javascript" src="{% static "pretixpresale/js/ui/iframe.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/addressform.js" %}"></script> <script type="text/javascript" src="{% static "pretixbase/js/addressform.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/deanonymize_email.js" %}"></script> <script type="text/javascript" src="{% static "pretixbase/js/deanonymize_email.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/errors.js" %}"></script>
{% endcompress %} {% endcompress %}
+2 -2
View File
@@ -59,7 +59,7 @@ from pretix.base.timemachine import time_machine_now_assigned_from_request
from pretix.helpers.http import redirect_to_url from pretix.helpers.http import redirect_to_url
from pretix.multidomain.models import KnownDomain from pretix.multidomain.models import KnownDomain
from pretix.multidomain.urlreverse import ( from pretix.multidomain.urlreverse import (
build_absolute_uri, get_event_domain, get_organizer_domain, eventreverse_absolute, get_event_domain, get_organizer_domain,
) )
from pretix.presale.signals import process_request, process_response from pretix.presale.signals import process_request, process_response
@@ -283,7 +283,7 @@ def _detect_event(request, require_live=True, require_plugin=None):
domain, domainmode = get_event_domain(request.event, fallback=False, return_mode=True) domain, domainmode = get_event_domain(request.event, fallback=False, return_mode=True)
if not domain and request_domain_mode == KnownDomain.MODE_ORG_ALT_DOMAIN: if not domain and request_domain_mode == KnownDomain.MODE_ORG_ALT_DOMAIN:
path = request.get_full_path().split("/", 2)[-1] path = request.get_full_path().split("/", 2)[-1]
r = redirect_to_url(build_absolute_uri(request.event, "presale:event.index") + path) r = redirect_to_url(eventreverse_absolute(request.event, "presale:event.index") + path)
r['Access-Control-Allow-Origin'] = '*' r['Access-Control-Allow-Origin'] = '*'
return r return r
elif domain and domain != request.host: elif domain and domain != request.host:
+8 -6
View File
@@ -56,7 +56,7 @@ from pretix.base.signals import customer_created, customer_signed_in
from pretix.helpers.compat import CompatDeleteView from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.http import redirect_to_url from pretix.helpers.http import redirect_to_url
from pretix.multidomain.models import KnownDomain from pretix.multidomain.models import KnownDomain
from pretix.multidomain.urlreverse import build_absolute_uri, eventreverse from pretix.multidomain.urlreverse import eventreverse, eventreverse_absolute
from pretix.presale.forms.customer import ( from pretix.presale.forms.customer import (
AuthenticationForm, ChangeInfoForm, ChangePasswordForm, RegistrationForm, AuthenticationForm, ChangeInfoForm, ChangePasswordForm, RegistrationForm,
ResetPasswordForm, SetPasswordForm, TokenGenerator, ResetPasswordForm, SetPasswordForm, TokenGenerator,
@@ -316,8 +316,10 @@ class ResetPasswordView(FormView):
customer.log_action('pretix.customer.password.resetrequested', {}) customer.log_action('pretix.customer.password.resetrequested', {})
ctx = customer.get_email_context() ctx = customer.get_email_context()
token = TokenGenerator().make_token(customer) token = TokenGenerator().make_token(customer)
ctx['url'] = build_absolute_uri(self.request.organizer, ctx['url'] = eventreverse_absolute(
'presale:organizer.customer.recoverpw') + '?id=' + customer.identifier + '&token=' + token self.request.organizer,
'presale:organizer.customer.recoverpw'
) + '?id=' + customer.identifier + '&token=' + token
mail( mail(
customer.email, customer.email,
self.request.organizer.settings.mail_subject_customer_reset, self.request.organizer.settings.mail_subject_customer_reset,
@@ -574,7 +576,7 @@ class ChangeInformationView(CustomerAccountBaseMixin, FormView):
new_email = form.cleaned_data['email'] new_email = form.cleaned_data['email']
form.cleaned_data['email'] = form.instance.email = self.initial_email form.cleaned_data['email'] = form.instance.email = self.initial_email
ctx = form.instance.get_email_context() ctx = form.instance.get_email_context()
ctx['url'] = build_absolute_uri( ctx['url'] = eventreverse_absolute(
self.request.organizer, self.request.organizer,
'presale:organizer.customer.change.confirm' 'presale:organizer.customer.change.confirm'
) + '?token=' + dumps({ ) + '?token=' + dumps({
@@ -703,7 +705,7 @@ class SSOLoginView(RedirectBackMixin, View):
request.session[f'pretix_customerauth_{self.provider.pk}_nonce'] = nonce request.session[f'pretix_customerauth_{self.provider.pk}_nonce'] = nonce
request.session[f'pretix_customerauth_{self.provider.pk}_popup_origin'] = popup_origin request.session[f'pretix_customerauth_{self.provider.pk}_popup_origin'] = popup_origin
request.session[f'pretix_customerauth_{self.provider.pk}_cross_domain_requested'] = self.request.GET.get("request_cross_domain_customer_auth") == "true" request.session[f'pretix_customerauth_{self.provider.pk}_cross_domain_requested'] = self.request.GET.get("request_cross_domain_customer_auth") == "true"
redirect_uri = build_absolute_uri(self.request.organizer, 'presale:organizer.customer.login.return', kwargs={ redirect_uri = eventreverse_absolute(self.request.organizer, 'presale:organizer.customer.login.return', kwargs={
'provider': self.provider.pk 'provider': self.provider.pk
}) })
@@ -777,7 +779,7 @@ class SSOLoginReturnView(RedirectBackMixin, View):
), ),
popup_origin, popup_origin,
) )
redirect_uri = build_absolute_uri( redirect_uri = eventreverse_absolute(
self.request.organizer, 'presale:organizer.customer.login.return', self.request.organizer, 'presale:organizer.customer.login.return',
kwargs={ kwargs={
'provider': self.provider.pk 'provider': self.provider.pk
+6 -6
View File
@@ -46,7 +46,7 @@ from pretix.base.models.customers import (
) )
from pretix.helpers.http import redirect_to_url from pretix.helpers.http import redirect_to_url
from pretix.multidomain.middlewares import CsrfViewMiddleware from pretix.multidomain.middlewares import CsrfViewMiddleware
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.presale.forms.customer import AuthenticationForm from pretix.presale.forms.customer import AuthenticationForm
from pretix.presale.utils import customer_login, get_customer_auth_time from pretix.presale.utils import customer_login, get_customer_auth_time
@@ -519,17 +519,17 @@ class ConfigurationView(View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
return JsonResponse({ return JsonResponse({
'issuer': build_absolute_uri(request.organizer, 'presale:organizer.index').rstrip('/'), 'issuer': eventreverse_absolute(request.organizer, 'presale:organizer.index').rstrip('/'),
'authorization_endpoint': build_absolute_uri( 'authorization_endpoint': eventreverse_absolute(
request.organizer, 'presale:organizer.oauth2.v1.authorize' request.organizer, 'presale:organizer.oauth2.v1.authorize'
), ),
'token_endpoint': build_absolute_uri( 'token_endpoint': eventreverse_absolute(
request.organizer, 'presale:organizer.oauth2.v1.token' request.organizer, 'presale:organizer.oauth2.v1.token'
), ),
'userinfo_endpoint': build_absolute_uri( 'userinfo_endpoint': eventreverse_absolute(
request.organizer, 'presale:organizer.oauth2.v1.userinfo' request.organizer, 'presale:organizer.oauth2.v1.userinfo'
), ),
'jwks_uri': build_absolute_uri( 'jwks_uri': eventreverse_absolute(
request.organizer, 'presale:organizer.oauth2.v1.jwks' request.organizer, 'presale:organizer.oauth2.v1.jwks'
), ),
'scopes_supported': [k for k, v in CustomerSSOClient.SCOPE_CHOICES], 'scopes_supported': [k for k, v in CustomerSSOClient.SCOPE_CHOICES],
+2 -2
View File
@@ -89,7 +89,7 @@ from pretix.base.views.mixins import OrderQuestionsViewMixin
from pretix.base.views.tasks import AsyncAction from pretix.base.views.tasks import AsyncAction
from pretix.helpers.http import redirect_to_url from pretix.helpers.http import redirect_to_url
from pretix.helpers.safedownload import check_token from pretix.helpers.safedownload import check_token
from pretix.multidomain.urlreverse import build_absolute_uri, eventreverse from pretix.multidomain.urlreverse import eventreverse, eventreverse_absolute
from pretix.presale.forms.checkout import InvoiceAddressForm, QuestionsForm from pretix.presale.forms.checkout import InvoiceAddressForm, QuestionsForm
from pretix.presale.forms.order import OrderPositionChangeForm from pretix.presale.forms.order import OrderPositionChangeForm
from pretix.presale.signals import question_form_fields_overrides from pretix.presale.signals import question_form_fields_overrides
@@ -312,7 +312,7 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TicketPageMixin,
state__in=[OrderPayment.PAYMENT_STATE_CANCELED, OrderPayment.PAYMENT_STATE_FAILED] state__in=[OrderPayment.PAYMENT_STATE_CANCELED, OrderPayment.PAYMENT_STATE_FAILED]
).exists() and self.order.status == Order.STATUS_PENDING: ).exists() and self.order.status == Order.STATUS_PENDING:
ctx['generate_invoice_requires'] = 'payment' ctx['generate_invoice_requires'] = 'payment'
ctx['url'] = build_absolute_uri( ctx['url'] = eventreverse_absolute(
self.request.event, 'presale:event.order', kwargs={ self.request.event, 'presale:event.order', kwargs={
'order': self.order.code, 'order': self.order.code,
'secret': self.order.secret 'secret': self.order.secret
+2 -2
View File
@@ -74,7 +74,7 @@ from pretix.helpers.formats.en.formats import (
from pretix.helpers.http import redirect_to_url from pretix.helpers.http import redirect_to_url
from pretix.helpers.i18n import parse_date_localized from pretix.helpers.i18n import parse_date_localized
from pretix.helpers.thumb import get_thumbnail from pretix.helpers.thumb import get_thumbnail
from pretix.multidomain.urlreverse import build_absolute_uri, eventreverse from pretix.multidomain.urlreverse import eventreverse, eventreverse_absolute
from pretix.presale.forms.organizer import EventListFilterForm from pretix.presale.forms.organizer import EventListFilterForm
from pretix.presale.ical import get_public_ical from pretix.presale.ical import get_public_ical
from pretix.presale.signals import filter_subevents from pretix.presale.signals import filter_subevents
@@ -1380,7 +1380,7 @@ class OrganizerFavicon(View):
class RedirectToOrganizerIndex(View): class RedirectToOrganizerIndex(View):
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
return redirect_to_url(build_absolute_uri(self.request.organizer, "presale:organizer.index")) return redirect_to_url(eventreverse_absolute(self.request.organizer, "presale:organizer.index"))
class AccessibilityView(OrganizerViewMixin, EventListMixin, TemplateView): class AccessibilityView(OrganizerViewMixin, EventListMixin, TemplateView):
+6 -6
View File
@@ -65,7 +65,7 @@ from pretix.base.settings import GlobalSettingsObject
from pretix.base.templatetags.rich_text import rich_text from pretix.base.templatetags.rich_text import rich_text
from pretix.helpers.daterange import daterange from pretix.helpers.daterange import daterange
from pretix.helpers.thumb import get_thumbnail from pretix.helpers.thumb import get_thumbnail
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.presale.forms.organizer import meta_filtersets from pretix.presale.forms.organizer import meta_filtersets
from pretix.presale.style import get_theme_vars_css from pretix.presale.style import get_theme_vars_css
from pretix.presale.views.cart import get_or_create_cart_id from pretix.presale.views.cart import get_or_create_cart_id
@@ -303,7 +303,7 @@ def get_picture(event, picture, size=None):
logger.exception(f'Failed to create thumbnail of {picture.name}') logger.exception(f'Failed to create thumbnail of {picture.name}')
if not thumb: if not thumb:
thumb = default_storage.url(picture.name) thumb = default_storage.url(picture.name)
return urljoin(build_absolute_uri(event, 'presale:event.index'), thumb) return urljoin(eventreverse_absolute(event, 'presale:event.index'), thumb)
class WidgetAPIProductList(EventListMixin, View): class WidgetAPIProductList(EventListMixin, View):
@@ -547,7 +547,7 @@ class WidgetAPIProductList(EventListMixin, View):
'location': str(ev.location), 'location': str(ev.location),
'date_range': self._get_date_range(ev, event, tz=tz), 'date_range': self._get_date_range(ev, event, tz=tz),
'availability': self._get_availability(ev, event, tz=tz), 'availability': self._get_availability(ev, event, tz=tz),
'event_url': build_absolute_uri(event, 'presale:event.index'), 'event_url': eventreverse_absolute(event, 'presale:event.index'),
'subevent': ev.pk if isinstance(ev, SubEvent) else None, 'subevent': ev.pk if isinstance(ev, SubEvent) else None,
}) })
return events return events
@@ -748,7 +748,7 @@ class WidgetAPIProductList(EventListMixin, View):
'location': str(ev.location), 'location': str(ev.location),
'date_range': self._get_date_range(ev, ev.event, tz), 'date_range': self._get_date_range(ev, ev.event, tz),
'availability': self._get_availability(ev, ev.event, tz=tz), 'availability': self._get_availability(ev, ev.event, tz=tz),
'event_url': build_absolute_uri(ev.event, 'presale:event.index'), 'event_url': eventreverse_absolute(ev.event, 'presale:event.index'),
'subevent': ev.pk, 'subevent': ev.pk,
} for ev in evs } for ev in evs
] ]
@@ -771,7 +771,7 @@ class WidgetAPIProductList(EventListMixin, View):
'location': str(event.location), 'location': str(event.location),
'date_range': dr, 'date_range': dr,
'availability': avail, 'availability': avail,
'event_url': build_absolute_uri(event, 'presale:event.index'), 'event_url': eventreverse_absolute(event, 'presale:event.index'),
}) })
cache.set(cache_key, data, 30) cache.set(cache_key, data, 30)
@@ -796,7 +796,7 @@ class WidgetAPIProductList(EventListMixin, View):
return self.response(cached_data) return self.response(cached_data)
data = { data = {
'target_url': build_absolute_uri(request.event, 'presale:event.index'), 'target_url': eventreverse_absolute(request.event, 'presale:event.index'),
'subevent': self.subevent.pk if self.subevent else None, 'subevent': self.subevent.pk if self.subevent else None,
'currency': request.event.currency, 'currency': request.event.currency,
'display_net_prices': request.event.settings.display_net_prices, 'display_net_prices': request.event.settings.display_net_prices,
-1
View File
@@ -1,5 +1,4 @@
'use strict'; 'use strict';
{ {
const globals = this; const globals = this;
+3 -2
View File
@@ -174,15 +174,16 @@ function async_task_error(jqXHR, textStatus, errorThrown) {
var respdom = $(jqXHR.responseText); var respdom = $(jqXHR.responseText);
var c = respdom.filter('.container'); var c = respdom.filter('.container');
if (respdom.filter('form') && (respdom.filter('.has-error') || respdom.filter('.alert-danger'))) { if (respdom.filter('form') && (respdom.filter('.has-error') || respdom.filter('.alert-danger'))) {
// This is a failed form validation, let's just use it
if (respdom.filter('#page-wrapper') && $('#page-wrapper').length) { if (respdom.filter('#page-wrapper') && $('#page-wrapper').length) {
// This is a failed form validation, let's just use it
async_task_replace_page("#page-wrapper", respdom.find("#page-wrapper").html()); async_task_replace_page("#page-wrapper", respdom.find("#page-wrapper").html());
} else { } else {
async_task_replace_page("body", jqXHR.responseText.substring( async_task_replace_page("body", jqXHR.responseText.substring(
jqXHR.responseText.indexOf("<body"), jqXHR.responseText.indexOf("<body"),
jqXHR.responseText.indexOf("</body") jqXHR.responseText.indexOf("</body")
)); ));
document.dispatchEvent(new Event("pretix:async-task-error"))
} }
} else if (c.length > 0) { } else if (c.length > 0) {

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