Compare commits

...

65 Commits

Author SHA1 Message Date
Raphael Michel a3a07f5b76 Do not use redis cache at import time
During our [2026-06-27 incident](https://pretix.eu/about/en/blog/20260630-pretix-hosted-outage/),
we noticed that pretix is using redis at import time. This means that
gunicorn and celery process were unable to start on servers who could
currently not reach redis. This is kinda mitigated through auto-restart
on systemd or docker level, but that's not really how it is supposed to
work. Celery even has smart retry/reconnect logic that becomes pointless
this way.
2026-06-30 13:10:45 +02:00
Raphael Michel d40492748a Event product list: Hotfix for pathological performance in large event series (#6318)
* Event product list: Hotfix for pathological performance

* Stop outputting bullsht numbers to widget
2026-06-29 19:02:06 +02:00
Richard Schreiber f4ca230af7 Improve voucher import unique code checks (#6311)
* Check for duplicate codes in import

* Check for existing codes instead of failing on db-level

* as we do not lock, catch IntegrityErrors due to race-conditions on import

* fix flake8
2026-06-29 14:37:41 +02:00
Richard Schreiber 4fb1748bf6 Fix handling country=None in attendee profiles (#6309)
* Fix handling country=None in AttendeeProfile describe

* Update checkoutflow.py

* fix more occurences

* handle country=None in InvoiceAddress even if it is not allowed
2026-06-29 14:32:01 +02:00
Raphael Michel 9d668af102 Discover translatable strings in .ts files (Z#23238475) 2026-06-28 17:51:40 +02:00
Raphael Michel 80fd4a3b2a Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (6343 of 6343 strings)

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

powered by weblate
2026-06-28 17:48:28 +02:00
Raphael Michel 65bb2283d4 Translations: Update German
Currently translated at 100.0% (6343 of 6343 strings)

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

powered by weblate
2026-06-28 17:48:28 +02:00
Raphael Michel 00751e8911 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (6343 of 6343 strings)

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

powered by weblate
2026-06-28 17:48:28 +02:00
Raphael Michel 957a066475 Translations: Update German
Currently translated at 100.0% (6343 of 6343 strings)

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

powered by weblate
2026-06-28 17:48:28 +02:00
Raphael Michel 7e077bdd7e Subevent detail: Fix incorrect ticket count (Z#23238481) 2026-06-28 17:43:22 +02:00
Raphael Michel e80d84ec3c Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2026-06-28 16:44:09 +02:00
Kim Lozano dc3b742d8c Translations: Update Catalan
Currently translated at 29.7% (1875 of 6302 strings)

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

powered by weblate
2026-06-28 16:42:32 +02:00
Raphael Michel 1f5fe1b237 SSRF protection: Edge case handling for CGNAT and v4/v6 mapping (Z#23236468) (#6260)
* SSRF protection: Edge case handling for CGNAT and v4/v6 mapping (Z#23236468)

* SMTP SSRF protection: Edge case handling for CGNAT and v4/v6 mapping (#6264)

---------

Co-authored-by: pajowu <engelhardt@pretix.eu>
2026-06-26 16:45:20 +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 Michel eb068f524c [SECURITY] Prevent reading of any local files in reportlab (CVE-2026-57535) 2026-06-25 16:46:13 +02:00
Raphael Michel f615595547 [SECURITY] Disable outbound and file access for reportlab (CVE-2026-57535) 2026-06-25 16:46:13 +02:00
Mira Weller 8bd78eefcf [SECURITY] Fix reflected XSS in redirection page (CVE-2026-57533) 2026-06-25 16:46:13 +02:00
Mira Weller 848f7fa0e5 [SECURITY] Fix stored XSS in ticket confirmation page (CVE-2026-13225) 2026-06-25 16:46:13 +02:00
Mira Weller 3442a543c8 [SECURITY] Hardening: Don't use |safe on confirm_messages 2026-06-25 16:46:13 +02:00
Mira Weller f88c24863d [SECURITY] Fix XSS in ticket layout JSON (CVE-2026-57532) 2026-06-25 16:46:13 +02:00
Lukas Bockstaller 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 Schreiber e8492cad3c Seating: fix handling optional position attribute (#6303) 2026-06-24 09:47:08 +02:00
Richard Schreiber 7ea5a2b59e PDF: add placeholder invoice_custom_field (#6298) 2026-06-24 09:46:43 +02:00
luelista 4c373518d0 Fix event meta property handling when cloning across organizers (Z#23231419) (#6306) 2026-06-23 19:01:32 +02:00
luelista 1521c0cfcd Fix URL matching in EventQRCode (Z#23237781) (#6304) 2026-06-23 18:34:31 +02:00
dependabot[bot] 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] 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
Raphael Michel 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 Michel 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 Mitasov 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
Nikolai 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 Mitasov 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árton 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 Mitasov 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] 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] 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] 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] 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] 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
luelista 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
Lukas Bockstaller 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 Michel ea928ea7d4 Widget: Fix handling of HTTP-429 errors 2026-06-16 09:11:33 +02:00
Richard Schreiber 36d49fbd77 Question detail: Fix crash in filter 2026-06-15 08:00:55 +02:00
Raphael Michel f0cb451c34 LocaleMiddleware: Correctly reset region for backend views 2026-06-12 15:30:06 +02:00
Raphael Michel 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] 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
Mira 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
Mira 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 BRUNEAU 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] 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] 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 Weinelt 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
luelista c4b9cc4143 Allow search by partial giftcard secret with organizer.giftcards:read (#6263) 2026-06-11 16:26:05 +02:00
Raphael Michel 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
pajowu 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
luelista 63ebe16fd3 Fix count in order bulk delete success message (#6270) 2026-06-11 16:25:38 +02:00
Martin Gross 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
luelista 784577d86f Fix markup of error template (#6265) 2026-06-10 14:16:46 +02:00
Richard Schreiber 07d27e66d1 Use HTTP-REFERER as fallback for vite_origins (#6246) 2026-06-09 13:24:46 +02:00
Richard Schreiber b404316dfd [SECURITY] Reusable media export: Respect giftcard permissions (CVE-2026-11764) (#6261) 2026-06-09 13:20:48 +02:00
luelista edf97a13cd Don't show warning if inactive products are used in checkin-rules (Z#23236197) (#6242) 2026-06-09 12:48:03 +02:00
dependabot[bot] c384bc2e7a Update bleach requirement from ==6.3.* to ==6.4.* (#6249)
Updates the requirements on [bleach](https://github.com/mozilla/bleach) to permit the latest version.
- [Changelog](https://github.com/mozilla/bleach/blob/main/CHANGES)
- [Commits](https://github.com/mozilla/bleach/compare/v6.3.0...v6.4.0)

---
updated-dependencies:
- dependency-name: bleach
  dependency-version: 6.4.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-08 17:33:50 +02:00
Raphael Michel f16034d0cc Check-in: Fix handling of optional file questions (Z#23236493) (#6251) 2026-06-08 14:25:50 +02:00
229 changed files with 138318 additions and 107188 deletions
+1 -2
View File
@@ -10,8 +10,7 @@ tests:
- cd src
- python manage.py check
- make all compress
- playwright install
- PRETIX_CONFIG_FILE=tests/ci_sqlite.cfg py.test -n 3 tests --maxfail=100
- PRETIX_CONFIG_FILE=tests/ci_sqlite.cfg py.test -n 3 tests --ignore=tests/e2e --maxfail=100
except:
- '/^v.*$/'
pypi:
+1 -2
View File
@@ -57,8 +57,7 @@ COPY vite.config.ts /pretix/vite.config.ts
RUN pip3 install -U \
pip \
setuptools \
wheel && \
setuptools && \
cd /pretix && \
PRETIX_DOCKER_BUILD=TRUE pip3 install \
-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
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.
:<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
order) when building texts (currently only the ``reason_explanation`` response field).
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
``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_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
@@ -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),
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 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**:
@@ -224,6 +228,9 @@ Checking a ticket in
* ``ambiguous`` - Multiple tickets match scan, rejected.
* ``revoked`` - Ticket code has been revoked.
* ``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.
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
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
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.
* ``revoked`` - Ticket code has been revoked.
* ``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``
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.
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).
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.
show_quota_left boolean Publicly show how many tickets are still available.
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_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)
* ``use_reusable_medium`` (optional, causes the new ticket to take over 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)
* ``use_reusable_medium`` (optional, causes the new ticket to be connected 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)
* ``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:
.. 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
as its first argument and can be used like this::
+109 -91
View File
@@ -370,14 +370,14 @@
"license": "MIT"
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
"integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@tybys/wasm-util": "^0.10.1"
"@tybys/wasm-util": "^0.10.2"
},
"funding": {
"type": "github",
@@ -427,9 +427,9 @@
}
},
"node_modules/@oxc-project/types": {
"version": "0.129.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.129.0.tgz",
"integrity": "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==",
"version": "0.133.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -758,9 +758,9 @@
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0.tgz",
"integrity": "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
"integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
"cpu": [
"arm64"
],
@@ -775,9 +775,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0.tgz",
"integrity": "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
"integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
"cpu": [
"arm64"
],
@@ -792,9 +792,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0.tgz",
"integrity": "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
"integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
"cpu": [
"x64"
],
@@ -809,9 +809,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0.tgz",
"integrity": "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
"integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
"cpu": [
"x64"
],
@@ -826,9 +826,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0.tgz",
"integrity": "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
"integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
"cpu": [
"arm"
],
@@ -843,13 +843,16 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0.tgz",
"integrity": "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
"integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -860,13 +863,16 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0.tgz",
"integrity": "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
"integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -877,13 +883,16 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0.tgz",
"integrity": "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
"integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
"cpu": [
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -894,13 +903,16 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0.tgz",
"integrity": "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
"integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
"cpu": [
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -911,13 +923,16 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0.tgz",
"integrity": "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
"integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -928,13 +943,16 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0.tgz",
"integrity": "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
"integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -945,9 +963,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0.tgz",
"integrity": "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
"integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
"cpu": [
"arm64"
],
@@ -962,9 +980,9 @@
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0.tgz",
"integrity": "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
"integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
"cpu": [
"wasm32"
],
@@ -981,9 +999,9 @@
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0.tgz",
"integrity": "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
"integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
"cpu": [
"arm64"
],
@@ -998,9 +1016,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0.tgz",
"integrity": "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
"integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
"cpu": [
"x64"
],
@@ -3158,9 +3176,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"funding": [
{
"type": "github",
@@ -3334,9 +3352,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"funding": [
{
"type": "opencollective",
@@ -3353,7 +3371,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"nanoid": "^3.3.12",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -3610,14 +3628,14 @@
}
},
"node_modules/rolldown": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0.tgz",
"integrity": "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.129.0",
"@rolldown/pluginutils": "1.0.0"
"@oxc-project/types": "=0.133.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
"rolldown": "bin/cli.mjs"
@@ -3626,27 +3644,27 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.0",
"@rolldown/binding-darwin-arm64": "1.0.0",
"@rolldown/binding-darwin-x64": "1.0.0",
"@rolldown/binding-freebsd-x64": "1.0.0",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0",
"@rolldown/binding-linux-arm64-gnu": "1.0.0",
"@rolldown/binding-linux-arm64-musl": "1.0.0",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0",
"@rolldown/binding-linux-s390x-gnu": "1.0.0",
"@rolldown/binding-linux-x64-gnu": "1.0.0",
"@rolldown/binding-linux-x64-musl": "1.0.0",
"@rolldown/binding-openharmony-arm64": "1.0.0",
"@rolldown/binding-wasm32-wasi": "1.0.0",
"@rolldown/binding-win32-arm64-msvc": "1.0.0",
"@rolldown/binding-win32-x64-msvc": "1.0.0"
"@rolldown/binding-android-arm64": "1.0.3",
"@rolldown/binding-darwin-arm64": "1.0.3",
"@rolldown/binding-darwin-x64": "1.0.3",
"@rolldown/binding-freebsd-x64": "1.0.3",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
"@rolldown/binding-linux-arm64-gnu": "1.0.3",
"@rolldown/binding-linux-arm64-musl": "1.0.3",
"@rolldown/binding-linux-ppc64-gnu": "1.0.3",
"@rolldown/binding-linux-s390x-gnu": "1.0.3",
"@rolldown/binding-linux-x64-gnu": "1.0.3",
"@rolldown/binding-linux-x64-musl": "1.0.3",
"@rolldown/binding-openharmony-arm64": "1.0.3",
"@rolldown/binding-wasm32-wasi": "1.0.3",
"@rolldown/binding-win32-arm64-msvc": "1.0.3",
"@rolldown/binding-win32-x64-msvc": "1.0.3"
}
},
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0.tgz",
"integrity": "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"dev": true,
"license": "MIT"
},
@@ -4325,9 +4343,9 @@
}
},
"node_modules/tinyglobby": {
"version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4465,17 +4483,17 @@
"license": "MIT"
},
"node_modules/vite": {
"version": "8.0.12",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.12.tgz",
"integrity": "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg==",
"version": "8.0.16",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
"postcss": "^8.5.14",
"rolldown": "1.0.0",
"tinyglobby": "^0.2.16"
"postcss": "^8.5.15",
"rolldown": "1.0.3",
"tinyglobby": "^0.2.17"
},
"bin": {
"vite": "bin/vite.js"
+7 -9
View File
@@ -29,12 +29,12 @@ classifiers = [
dependencies = [
"arabic-reshaper==3.0.1", # Support for Arabic in reportlab
"babel",
"BeautifulSoup4==4.14.*",
"bleach==6.3.*",
"BeautifulSoup4==4.15.*",
"bleach==6.4.*",
"celery==5.6.*",
"chardet==5.2.*",
"cryptography>=48.0.0",
"css-inline==0.20.*",
"cryptography>=49.0.0",
"css-inline==0.21.*",
"defusedcsv>=3.0.0",
"dnspython==2.*",
"Django[argon2]==5.2.*",
@@ -93,7 +93,7 @@ dependencies = [
"redis==7.4.*",
"reportlab==4.5.*",
"requests==2.32.*",
"sentry-sdk==2.61.*",
"sentry-sdk==2.63.*",
"sepaxml==2.7.*",
"stripe==7.9.*",
"text-unidecode==1.*",
@@ -101,7 +101,7 @@ dependencies = [
"tqdm==4.*",
"ua-parser==1.0.*",
"vobject==0.9.*",
"webauthn==2.7.*",
"webauthn==2.8.*",
"zeep==4.3.*"
]
@@ -125,7 +125,7 @@ dev = [
"pytest-sugar",
"pytest-xdist==3.8.*",
"pytest-playwright",
"pytest==9.0.*",
"pytest==9.1.*",
"playwright",
"responses",
]
@@ -139,8 +139,6 @@ build-backend = "backend"
backend-path = ["_build"]
requires = [
"setuptools",
"setuptools-rust",
"wheel",
"importlib_metadata",
"tomli",
]
+1 -1
View File
@@ -7,7 +7,7 @@ localecompile:
localegen:
./manage.py makemessages --keep-pot --ignore "pretix/static/npm_dir/*" $(LNGS)
./manage.py makemessages --keep-pot -d djangojs --ignore "pretix/static/npm_dir/*" --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS)
./manage.py makemessages --keep-pot -e js,ts,vue -d djangojs --ignore "pretix/static/npm_dir/*" --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS)
staticfiles: npminstall npmbuild jsi18n
./manage.py collectstatic --noinput
+2
View File
@@ -110,6 +110,8 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
('POST', 'api-v1:checkinrpc.redeem'),
('GET', 'api-v1:checkinrpc.search'),
('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)
datetime = serializers.DateTimeField(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):
super().__init__(*args, **kwargs)
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):
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,
)
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__)
@@ -173,7 +173,7 @@ class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
)
def get_event_url(self, event):
return build_absolute_uri(event, 'presale:event.index')
return eventreverse_absolute(event, 'presale:event.index')
class Meta:
model = Event
@@ -871,6 +871,7 @@ class EventSettingsSerializer(SettingsSerializer):
'og_image',
'name_scheme',
'reusable_media_active',
'reusable_media_usage_enforced',
'reusable_media_type_barcode',
'reusable_media_type_barcode_identifier_length',
'reusable_media_type_nfc_uid',
@@ -885,6 +886,7 @@ class EventSettingsSerializer(SettingsSerializer):
readonly_fields = [
# These are read-only since they are currently only settable on organizers, not events
'reusable_media_active',
'reusable_media_usage_enforced',
'reusable_media_type_barcode',
'reusable_media_type_barcode_identifier_length',
'reusable_media_type_nfc_uid',
@@ -970,6 +972,7 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
'reusable_media_type_nfc_uid',
'reusable_media_type_nfc_mf0aes',
'reusable_media_type_nfc_mf0aes_random_uid',
'reusable_media_usage_enforced',
'system_question_order',
'tax_rule_payment',
'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.helpers.countries import CachedCountries
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.multidomain.urlreverse import eventreverse_absolute
logger = logging.getLogger(__name__)
@@ -757,7 +757,7 @@ class PaymentURLField(serializers.URLField):
def to_representation(self, instance: OrderPayment):
if instance.state != OrderPayment.PAYMENT_STATE_CREATED:
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,
'secret': instance.order.secret,
'payment': instance.pk,
@@ -806,7 +806,7 @@ class OrderRefundSerializer(I18nAwareModelSerializer):
class OrderURLField(serializers.URLField):
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,
'secret': instance.secret,
})
@@ -1043,15 +1043,13 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
requested_valid_from = serializers.DateTimeField(required=False, allow_null=True)
use_reusable_medium = serializers.PrimaryKeyRelatedField(queryset=ReusableMedium.objects.none(),
required=False, allow_null=True)
add_to_reusable_medium = serializers.PrimaryKeyRelatedField(queryset=ReusableMedium.objects.none(),
required=False, allow_null=True)
class Meta:
model = OrderPosition
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
'company', 'street', 'zipcode', 'city', 'country', 'state', 'is_bundled',
'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):
super().__init__(*args, **kwargs)
@@ -1063,8 +1061,6 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
with scopes_disabled():
if 'use_reusable_medium' in self.fields:
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):
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
@@ -1080,9 +1076,6 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
)
return m
def validate_add_to_reusable_medium(self, m):
return self.validate_use_reusable_medium(m)
def validate_item(self, item):
if item.event != self.context['event']:
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.']}
)
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
@@ -1602,7 +1589,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
pos_data['attendee_name_parts'] = {
'_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:
pos.order = order._wrapped
else:
@@ -1676,7 +1663,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
for pos_data in positions_data:
answers_data = pos_data.pop('answers', [])
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._calculate_tax(invoice_address=ia)
@@ -1718,14 +1704,17 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
answ.options.add(*options)
if use_reusable_medium:
for op_pk in use_reusable_medium.linked_orderpositions.values_list('pk', flat=True):
use_reusable_medium.log_action(
'pretix.reusable_medium.linked_orderposition.removed',
data={
'linked_orderposition': op_pk,
}
)
use_reusable_medium.linked_orderpositions.set([pos])
if pos.item.media_policy not in (Item.MEDIA_POLICY_APPEND, Item.MEDIA_POLICY_APPEND_OR_NEW):
for op_pk in use_reusable_medium.linked_orderpositions.values_list('pk', flat=True):
use_reusable_medium.log_action(
'pretix.reusable_medium.linked_orderposition.removed',
data={
'linked_orderposition': op_pk,
}
)
use_reusable_medium.linked_orderpositions.set([pos])
else:
use_reusable_medium.linked_orderpositions.add(pos)
use_reusable_medium.log_action(
'pretix.reusable_medium.linked_orderposition.added',
data={
@@ -1733,15 +1722,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
'linked_orderposition': pos.pk,
}
)
elif add_to_reusable_medium:
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,
}
)
use_reusable_medium.touch()
if not simulate:
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_ORGANIZER_COMPAT, OLD_TO_NEW_ORGANIZER_MIGRATION,
)
from pretix.helpers.urls import build_absolute_uri as build_global_uri
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.helpers.urls import mainreverse_absolute
from pretix.multidomain.urlreverse import eventreverse_absolute
logger = logging.getLogger(__name__)
@@ -71,7 +71,7 @@ class OrganizerSerializer(I18nAwareModelSerializer):
slug = serializers.CharField(read_only=True)
def get_organizer_url(self, organizer):
return build_absolute_uri(organizer, 'presale:organizer.index')
return eventreverse_absolute(organizer, 'presale:organizer.index')
class Meta:
model = Organizer
@@ -499,7 +499,7 @@ class TeamInviteSerializer(serializers.ModelSerializer):
'user': self,
'organizer': self.context['organizer'].name,
'team': instance.team.name,
'url': build_global_uri('control:auth.invite', kwargs={
'url': mainreverse_absolute('control:auth.invite', kwargs={
'token': instance.token
})
},
@@ -605,6 +605,7 @@ class OrganizerSettingsSerializer(SettingsSerializer):
'cookie_consent_dialog_button_yes',
'cookie_consent_dialog_button_no',
'reusable_media_active',
'reusable_media_usage_enforced',
'reusable_media_type_barcode',
'reusable_media_type_barcode_identifier_length',
'reusable_media_type_nfc_uid',
+53 -7
View File
@@ -69,8 +69,10 @@ from pretix.base.models import (
from pretix.base.models.orders import PrintLog
from pretix.base.permissions import AnyPermissionOf
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.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,
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:
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
gate = gate or (auth.gate if isinstance(auth, Device) else None)
medium = None
context = {
'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)
if not op_candidates:
try:
media = ReusableMedium.objects.active().filter(
medium = ReusableMedium.objects.active().filter(
Exists(ReusableMedium.linked_orderpositions.through.objects.filter(reusablemedium_id=OuterRef('pk')))
).get(
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,
}, status=400)
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}
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
@@ -661,7 +665,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
op_candidates = []
for op in linked_ops:
if op.order.event_id in list_by_event:
reusable_medium_used = media
reusable_medium_used = medium
op_candidates.append(op)
if list_by_event[op.order.event_id].addon_match:
op_candidates += list(op.addons.all())
@@ -788,7 +792,10 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
if str(q.pk) in answers_data:
try:
if q.type == Question.TYPE_FILE:
given_answers[q] = _handle_file_upload(answers_data[str(q.pk)], user, auth)
if answers_data[str(q.pk)]:
given_answers[q] = _handle_file_upload(answers_data[str(q.pk)], user, auth)
else:
given_answers[q] = None
else:
given_answers[q] = q.clean_answer(answers_data[str(q.pk)])
except (ValidationError, BaseValidationError):
@@ -801,7 +808,14 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
locale = op.order.event.settings.locale
with language(locale):
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,
clist=list_by_event[op.order.event_id],
given_answers=given_answers,
@@ -819,7 +833,25 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
from_revoked_secret=from_revoked_secret,
simulate=simulate,
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:
return Response({
'status': 'incomplete',
@@ -831,6 +863,18 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
],
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
}, 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:
if not simulate:
op.order.log_action('pretix.event.checkin.denied', data={
@@ -1018,6 +1062,8 @@ class CheckinRPCRedeemView(views.APIView):
canceled_supported=True,
request=self.request, # this is not clean, but we need it in the serializers for URL generation
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})
@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):
date = serializers.DateTimeField().to_representation(now())
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 requests import RequestException
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.multidomain.urlreverse import eventreverse_absolute
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):
payload = {
'iss': build_absolute_uri(client.organizer, 'presale:organizer.index').rstrip('/'),
'iss': eventreverse_absolute(client.organizer, 'presale:organizer.index').rstrip('/'),
'aud': client.client_id,
'exp': int(expires.timestamp()),
'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.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):
@@ -545,7 +545,7 @@ def get_data_fields(event, for_model=None):
_("Order link"),
Question.TYPE_STRING,
None,
lambda order: build_absolute_uri(
lambda order: eventreverse_absolute(
event,
'presale:event.order', kwargs={
'order': order.code,
@@ -560,7 +560,7 @@ def get_data_fields(event, for_model=None):
_("Ticket link"),
Question.TYPE_STRING,
None,
lambda op: build_absolute_uri(
lambda op: eventreverse_absolute(
event,
'presale:event.order.position', kwargs={
'order': op.order.code,
+5
View File
@@ -57,6 +57,8 @@ logger = logging.getLogger('pretix.base.email')
T = TypeVar("T", bound=EmailBackend)
_cgnat_net = ipaddress.ip_network('100.64.0.0/10')
def test_custom_smtp_backend(backend: T, from_addr: str) -> None:
try:
@@ -253,12 +255,15 @@ def create_connection(address, timeout=socket.getdefaulttimeout(),
if not getattr(settings, "MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS", False):
ip_addr = ipaddress.ip_address(sa[0])
check_ip4 = ip_addr.ipv4_mapped if getattr(ip_addr, "ipv4_mapped", None) else ip_addr
if ip_addr.is_multicast:
raise socket.error(f"Request to multicast address {sa[0]} blocked")
if ip_addr.is_loopback or ip_addr.is_link_local:
raise socket.error(f"Request to local address {sa[0]} blocked")
if ip_addr.is_private:
raise socket.error(f"Request to private address {sa[0]} blocked")
if check_ip4 in _cgnat_net:
raise socket.error(f"Request to RFC 6598 address {sa[0]} blocked")
sock = None
try:
+3 -3
View File
@@ -68,7 +68,7 @@ from ...control.forms.filter import get_all_payment_providers
from ...helpers import GroupConcat
from ...helpers.iter import chunked_iterable
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 (
ListExporter, MultiSheetListExporter, OrganizerLevelExportMixin,
)
@@ -429,7 +429,7 @@ class OrderListExporter(MultiSheetListExporter):
]))
row.append(
build_absolute_uri(order.event, 'presale:event.order', kwargs={
eventreverse_absolute(order.event, 'presale:event.order', kwargs={
'order': order.code,
'secret': order.secret,
})
@@ -855,7 +855,7 @@ class OrderListExporter(MultiSheetListExporter):
]))
row.append(
build_absolute_uri(order.event, 'presale:event.order.position', kwargs={
eventreverse_absolute(order.event, 'presale:event.order.position', kwargs={
'order': order.code,
'secret': op.web_secret,
'position': op.positionid
+7 -1
View File
@@ -64,7 +64,13 @@ class ReusableMediaExporter(OrganizerLevelExportMixin, ListExporter):
yield headers
yield self.ProgressSetTotal(total=media.count())
can_read_giftcards = self.permission_holder.has_organizer_permission(self.organizer, 'organizer.giftcards:read')
for medium in media.iterator(chunk_size=1000):
giftcard_secret = medium.linked_giftcard.secret if medium.linked_giftcard_id else ''
if giftcard_secret and not can_read_giftcards:
giftcard_secret = giftcard_secret[:3] + ""
yield [
medium.type,
medium.identifier,
@@ -72,7 +78,7 @@ class ReusableMediaExporter(OrganizerLevelExportMixin, ListExporter):
date_format(medium.expires, 'SHORT_DATETIME_FORMAT') if medium.expires else '',
medium.customer.identifier if medium.customer_id else '',
', '.join([f"{op.order.code}-{op.positionid}" for op in medium.linked_orderpositions.all()]),
medium.linked_giftcard.secret if medium.linked_giftcard_id else '',
giftcard_secret,
medium.notes,
]
+15 -9
View File
@@ -53,6 +53,7 @@ from django.db.models import QuerySet
from django.forms import Select, widgets
from django.forms.widgets import FILE_INPUT_CONTRADICTION
from django.utils.formats import date_format
from django.utils.functional import lazy
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.text import format_lazy
@@ -324,16 +325,21 @@ class WrappedPhonePrefixSelect(Select):
initial = None
def __init__(self, initial=None):
choices = [("", "---------")]
def _get_choices():
choices = [("", "---------")]
if initial:
for prefix, values in COUNTRY_CODE_TO_REGION_CODE.items():
if all(v == REGION_CODE_FOR_NON_GEO_ENTITY for v in values):
continue
if initial in values:
self.initial = "+%d" % prefix
break
choices += get_phone_prefixes_sorted_and_localized()
return choices
choices = lazy(_get_choices, list)()
if initial:
for prefix, values in COUNTRY_CODE_TO_REGION_CODE.items():
if all(v == REGION_CODE_FOR_NON_GEO_ENTITY for v in values):
continue
if initial in values:
self.initial = "+%d" % prefix
break
choices += get_phone_prefixes_sorted_and_localized()
super().__init__(choices=choices, attrs={
'aria-label': pgettext_lazy('phonenumber', 'International area code'),
'autocomplete': 'tel-country-code',
+76 -90
View File
@@ -22,9 +22,7 @@
import datetime
import logging
import math
import re
import textwrap
import unicodedata
from collections import defaultdict
from decimal import Decimal
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.templatetags.money import money_filter
from pretix.helpers.reportlab import (
FontFallbackParagraph, ThumbnailingImageReader, register_ttf_font_if_new,
reshaper,
FontFallbackParagraph, PlainTextParagraph, ThumbnailingImageReader,
normalize_text, register_ttf_font_if_new, reshaper,
)
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']))
def _normalize(self, text):
# 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 = "<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
# alias kept for plugin compatibility
return normalize_text(text)
def _upper(self, val):
# We uppercase labels, but not in every language
@@ -351,10 +339,15 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
return 'invoice.pdf', 'application/pdf', buffer.read()
def _clean_text(self, text, tags=None):
return self._normalize(bleach.clean(
text,
tags=set(tags) if tags else set()
).strip().replace('<br>', '<br />').replace('\n', '<br />\n'))
# For backwards compatibility with customer content, we need to support tags like <br> and <b> in a few text
# fields. Therefore, we can't use PlainTextParagraph for these, but run bleach instead to limit the allowed
# tags.
return self._normalize(
bleach.clean(
text,
tags=set(tags) if tags else set()
).strip().replace('<br>', '<br />').replace('\n', '<br />\n')
)
class PaidMarker(Flowable):
@@ -405,8 +398,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
invoice_to_top = 52 * mm
def _draw_invoice_to(self, canvas):
p = FontFallbackParagraph(self._clean_text(self.invoice.address_invoice_to),
style=self.stylesheet['Normal'])
p = PlainTextParagraph(self.invoice.address_invoice_to, style=self.stylesheet['Normal'])
p.wrapOn(canvas, 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)
@@ -417,8 +409,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
invoice_from_top = 17 * mm
def _draw_invoice_from(self, canvas):
p = FontFallbackParagraph(
self._clean_text(self.invoice.full_invoice_from),
p = PlainTextParagraph(
self.invoice.full_invoice_from,
style=self.stylesheet['InvoiceFrom']
)
p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height)
@@ -548,13 +540,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_event(self, canvas):
def shorten(txt):
txt = str(txt)
txt = bleach.clean(txt, tags=set()).strip()
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)
while p_size[1] > 2 * self.stylesheet['Normal'].leading:
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)
return txt
@@ -572,7 +563,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
else:
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_size = p.wrap(self.event_width, self.event_height)
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()
if type_info_text:
story.append(FontFallbackParagraph(
story.append(PlainTextParagraph(
type_info_text,
self.stylesheet['WarningBlock']
))
if self.invoice.custom_field:
story.append(FontFallbackParagraph(
story.append(PlainTextParagraph(
'{}: {}'.format(
self._clean_text(str(self.invoice.event.settings.invoice_address_custom_field)),
self._clean_text(self.invoice.custom_field),
str(self.invoice.event.settings.invoice_address_custom_field),
self.invoice.custom_field,
),
self.stylesheet['Normal']
))
if self.invoice.internal_reference:
story.append(FontFallbackParagraph(
self._normalize(pgettext('invoice', 'Customer reference: {reference}').format(
reference=self._clean_text(self.invoice.internal_reference),
)),
story.append(PlainTextParagraph(
pgettext('invoice', 'Customer reference: {reference}').format(
reference=self.invoice.internal_reference,
),
self.stylesheet['Normal']
))
if self.invoice.invoice_to_vat_id:
story.append(FontFallbackParagraph(
self._normalize(pgettext('invoice', 'Customer VAT ID')) + ': ' +
self._clean_text(self.invoice.invoice_to_vat_id),
story.append(PlainTextParagraph(
pgettext('invoice', 'Customer VAT ID') + ': ' + self.invoice.invoice_to_vat_id,
self.stylesheet['Normal']
))
if self.invoice.invoice_to_beneficiary:
story.append(FontFallbackParagraph(
self._normalize(pgettext('invoice', 'Beneficiary')) + ':<br />' +
self._clean_text(self.invoice.invoice_to_beneficiary),
story.append(PlainTextParagraph(
pgettext('invoice', 'Beneficiary') + ':\n' + self.invoice.invoice_to_beneficiary,
self.stylesheet['Normal']
))
@@ -707,11 +696,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story = [
NextPageTemplate('FirstPage'),
FontFallbackParagraph(
self._normalize(
PlainTextParagraph(
(
pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU'
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']
),
Spacer(1, 5 * mm),
@@ -733,17 +722,17 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
]
if has_taxes:
tdata = [(
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Net')), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Gross')), self.stylesheet['BoldRightNoSplit']),
PlainTextParagraph(pgettext('invoice', 'Description'), self.stylesheet['Bold']),
PlainTextParagraph(pgettext('invoice', 'Qty'), self.stylesheet['BoldRightNoSplit']),
PlainTextParagraph(pgettext('invoice', 'Tax rate'), self.stylesheet['BoldRightNoSplit']),
PlainTextParagraph(pgettext('invoice', 'Net'), self.stylesheet['BoldRightNoSplit']),
PlainTextParagraph(pgettext('invoice', 'Gross'), self.stylesheet['BoldRightNoSplit']),
)]
else:
tdata = [(
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Amount')), self.stylesheet['BoldRightNoSplit']),
PlainTextParagraph(pgettext('invoice', 'Description'), self.stylesheet['Bold']),
PlainTextParagraph(pgettext('invoice', 'Qty'), self.stylesheet['BoldRightNoSplit']),
PlainTextParagraph(pgettext('invoice', 'Amount'), self.stylesheet['BoldRightNoSplit']),
)]
def _group_key(line):
@@ -780,8 +769,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
max_height = self.stylesheet['Normal'].leading * 5
p_style = self.stylesheet['Normal']
for __ in range(1000):
p = FontFallbackParagraph(
self._clean_text(curr_description, tags=['br']),
p = PlainTextParagraph(
curr_description,
p_style
)
h = p.wrap(max_width, doc.height)[1]
@@ -862,7 +851,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
# Group together at the end of the invoice
request_show_service_date = period_line
elif period_line:
description_p_list.append(FontFallbackParagraph(
description_p_list.append(PlainTextParagraph(
period_line,
self.stylesheet['Fineprint']
))
@@ -874,7 +863,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
net_price=money_filter(net_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,
self.stylesheet['Fineprint']
))
@@ -883,11 +872,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
description_p_list.pop(0),
str(len(lines)),
localize(tax_rate) + " %",
FontFallbackParagraph(
PlainTextParagraph(
money_filter(net_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
self.stylesheet['NormalRight']
),
FontFallbackParagraph(
PlainTextParagraph(
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
self.stylesheet['NormalRight']
),
@@ -904,14 +893,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
single_price_line = pgettext('invoice', 'Single price: {price}').format(
price=money_filter(gross_value, self.invoice.event.currency),
)
description_p_list.append(FontFallbackParagraph(
description_p_list.append(PlainTextParagraph(
single_price_line,
self.stylesheet['Fineprint']
))
tdata.append((
description_p_list.pop(0),
str(len(lines)),
FontFallbackParagraph(
PlainTextParagraph(
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
self.stylesheet['NormalRight']
),
@@ -944,12 +933,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if has_taxes:
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)
])
else:
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)
])
@@ -958,12 +947,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
pending_sum = self.invoice.order.pending_sum
if pending_sum != total:
tdata.append(
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Received payments')), self.stylesheet['Normal'])] +
[PlainTextParagraph(pgettext('invoice', 'Received payments'), self.stylesheet['Normal'])] +
(['', '', ''] if has_taxes else ['']) +
[money_filter(pending_sum - total, self.invoice.event.currency)]
)
tdata.append(
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Outstanding payments')), self.stylesheet['Bold'])] +
[PlainTextParagraph(pgettext('invoice', 'Outstanding payments'), self.stylesheet['Bold'])] +
(['', '', ''] if has_taxes else ['']) +
[money_filter(pending_sum, self.invoice.event.currency)]
)
@@ -980,12 +969,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
s=Sum('amount')
)['s'] or Decimal('0.00')
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 ['']) +
[money_filter(giftcard_sum, self.invoice.event.currency)]
)
tdata.append(
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Remaining amount')), self.stylesheet['Bold'])] +
[PlainTextParagraph(pgettext('invoice', 'Remaining amount'), self.stylesheet['Bold'])] +
(['', '', ''] if has_taxes else ['']) +
[money_filter(total - giftcard_sum, self.invoice.event.currency)]
)
@@ -1008,14 +997,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story.append(Spacer(1, 10 * mm))
if request_show_service_date:
story.append(FontFallbackParagraph(
self._normalize(pgettext('invoice', 'Invoice period: {daterange}').format(daterange=request_show_service_date)),
story.append(PlainTextParagraph(
pgettext('invoice', 'Invoice period: {daterange}').format(daterange=request_show_service_date),
self.stylesheet['Normal']
))
if self.invoice.payment_provider_text:
story.append(FontFallbackParagraph(
self._normalize(self.invoice.payment_provider_text),
self._clean_text(self.invoice.payment_provider_text, tags=['br', 'b']),
self.stylesheet['Normal']
))
@@ -1039,10 +1028,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
('FONTNAME', (0, 0), (-1, -1), self.font_regular),
]
thead = [
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['Fineprint']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Net value')), self.stylesheet['FineprintRight']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Gross value')), self.stylesheet['FineprintRight']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax')), self.stylesheet['FineprintRight']),
PlainTextParagraph(pgettext('invoice', 'Tax rate'), self.stylesheet['Fineprint']),
PlainTextParagraph(pgettext('invoice', 'Net value'), self.stylesheet['FineprintRight']),
PlainTextParagraph(pgettext('invoice', 'Gross value'), self.stylesheet['FineprintRight']),
PlainTextParagraph(pgettext('invoice', 'Tax'), self.stylesheet['FineprintRight']),
''
]
tdata = [thead]
@@ -1053,7 +1042,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
continue
tax = taxvalue_map[idx]
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, self.invoice.event.currency),
money_filter(tax, self.invoice.event.currency),
@@ -1072,7 +1061,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
table.setStyle(TableStyle(tstyledata))
story.append(Spacer(5 * mm, 5 * mm))
story.append(KeepTogether([
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Included taxes')), self.stylesheet['FineprintHeading']),
PlainTextParagraph(pgettext('invoice', 'Included taxes'), self.stylesheet['FineprintHeading']),
table
]))
@@ -1089,7 +1078,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
net = gross - tax
tdata.append([
FontFallbackParagraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
PlainTextParagraph(localize(rate) + " % " + name, self.stylesheet['Fineprint']),
fmt(net), fmt(gross), fmt(tax), ''
])
@@ -1098,13 +1087,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story.append(KeepTogether([
Spacer(1, height=2 * mm),
FontFallbackParagraph(
self._normalize(pgettext(
PlainTextParagraph(
pgettext(
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
'{date}, this corresponds to:'
).format(rate=localize(self.invoice.foreign_currency_rate),
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']
),
Spacer(1, height=3 * mm),
@@ -1113,14 +1102,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate:
foreign_total = round_decimal(total * self.invoice.foreign_currency_rate)
story.append(Spacer(1, 5 * mm))
story.append(FontFallbackParagraph(self._normalize(
story.append(PlainTextParagraph(
pgettext(
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
'{date}, the invoice total corresponds to {total}.'
).format(rate=localize(self.invoice.foreign_currency_rate),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"),
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
total=fmt(foreign_total))),
total=fmt(foreign_total)),
self.stylesheet['Fineprint']
))
@@ -1162,11 +1151,8 @@ class Modern1Renderer(ClassicInvoiceRenderer):
def _draw_invoice_from(self, canvas):
if not self.invoice.address_invoice_from:
return
c = [
self._clean_text(l)
for l in self.invoice.address_invoice_from.strip().split('\n')
]
p = FontFallbackParagraph(self._normalize(' · '.join(c)), style=self.stylesheet['Sender'])
c = self.invoice.address_invoice_from.strip().split('\n')
p = PlainTextParagraph(' · '.join(c), style=self.stylesheet['Sender'])
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)
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)
]
p = FontFallbackParagraph(
self._normalize(date_format(self.invoice.date, "DATE_FORMAT")),
p = PlainTextParagraph(
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)
)
w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize)
@@ -1283,7 +1269,7 @@ class Modern1SimplifiedRenderer(Modern1Renderer):
i = []
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(
date_range=self.invoice.event.get_date_range_display(),
),
@@ -0,0 +1,29 @@
#
# 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/>.
#
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = "Do nothing. Useful for startup performance testing."
def handle(self, *args, **options):
pass
@@ -44,7 +44,8 @@ class Command(Parent):
# Start the vite server in the background
vite_server = subprocess.Popen(
["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():
+20 -14
View File
@@ -26,6 +26,7 @@ from django.utils.translation import gettext_lazy as _
class BaseMediaType:
medium_created_by_server = False
medium_created_from_unknown_supported = False
supports_orderposition = False
supports_giftcard = False
@@ -56,7 +57,7 @@ class BaseMediaType:
def is_active(self, organizer):
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
def handle_new(self, organizer, medium, user, auth):
@@ -88,23 +89,32 @@ class NfcUidMediaType(BaseMediaType):
verbose_name = _('NFC UID-based')
icon = 'pretixbase/img/media/nfc_uid.svg'
medium_created_by_server = False
medium_created_from_unknown_supported = 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
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"):
# 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.
return
with transaction.atomic():
gc = GiftCard.objects.create(
issuer=organizer,
expires=organizer.default_gift_card_expiry,
currency=organizer.settings.get(f'reusable_media_type_{self.identifier}_autocreate_giftcard_currency'),
)
if create_giftcard:
gc = GiftCard.objects.create(
issuer=organizer,
expires=organizer.default_gift_card_expiry,
currency=organizer.settings.get(f'reusable_media_type_{self.identifier}_autocreate_giftcard_currency'),
)
gc.log_action(
'pretix.giftcards.created',
user=user, auth=auth,
)
else:
gc = None
m = ReusableMedium.objects.create(
type=self.identifier,
identifier=identifier,
@@ -116,10 +126,6 @@ class NfcUidMediaType(BaseMediaType):
'pretix.reusable_medium.created.auto',
user=user, auth=auth,
)
gc.log_action(
'pretix.giftcards.created',
user=user, auth=auth,
)
return m
@@ -129,7 +135,7 @@ class NfcMf0aesMediaType(BaseMediaType):
icon = 'pretixbase/img/media/nfc_secure.svg'
medium_created_by_server = False
supports_giftcard = True
supports_orderposition = False
supports_orderposition = True
def handle_new(self, organizer, medium, user, auth):
from pretix.base.models import GiftCard
+4 -2
View File
@@ -282,10 +282,12 @@ def metric_values():
# Throwaway metrics
exact_tables = [
Order, OrderPosition, Invoice, Event, Organizer
Order, Invoice, Event, Organizer
]
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()
else:
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = estimate_count_fast(m)
+4 -2
View File
@@ -74,6 +74,7 @@ class LocaleMiddleware(MiddlewareMixin):
def process_request(self, request: HttpRequest):
language = get_language_from_request(request)
region = None
# 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
# 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:
language += '-' + settings_holder.settings.region
if settings_holder.settings.region:
set_region(settings_holder.settings.region)
region = settings_holder.settings.region
else:
gs = global_settings_object(request)
if '-' not in language and gs.settings.region:
language += '-' + gs.settings.region
if gs.settings.region:
set_region(gs.settings.region)
region = gs.settings.region
translation.activate(language)
set_region(region)
request.LANGUAGE_CODE = get_language_without_region()
tzname = None
+3 -3
View File
@@ -57,7 +57,7 @@ from django_otp.models import Device
from django_scopes import scopes_disabled
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.u2f import pub_key_from_der, websafe_decode
@@ -378,7 +378,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
{
'user': self,
'messages': msg,
'url': build_absolute_uri('control:user.settings'),
'url': mainreverse_absolute('control:user.settings'),
'instance': settings.PRETIX_INSTANCE_NAME,
},
event=None,
@@ -466,7 +466,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
{
'instance': settings.PRETIX_INSTANCE_NAME,
'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)))
},
None, locale=self.locale, user=self
+6
View File
@@ -346,11 +346,14 @@ class Checkin(models.Model):
REASON_INCOMPLETE = 'incomplete'
REASON_ALREADY_REDEEMED = 'already_redeemed'
REASON_AMBIGUOUS = 'ambiguous'
REASON_MEDIUM_INVALID = 'medium_invalid'
REASON_MEDIUM_EXISTS = 'medium_exists'
REASON_ERROR = 'error'
REASON_BLOCKED = 'blocked'
REASON_UNAPPROVED = 'unapproved'
REASON_INVALID_TIME = 'invalid_time'
REASON_ANNULLED = 'annulled'
REASON_ALREADY_EXCHANGED = 'already_exchanged'
REASONS = (
(REASON_CANCELED, _('Order canceled')),
(REASON_INVALID, _('Unknown ticket')),
@@ -366,6 +369,9 @@ class Checkin(models.Model):
(REASON_UNAPPROVED, _('Order not approved')),
(REASON_INVALID_TIME, _('Ticket not valid at this time')),
(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(
+5 -5
View File
@@ -167,7 +167,7 @@ class Customer(LoggedModel):
def send_security_notice(self, message, email=None):
from pretix.base.services.mail import SendMailException, mail
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.multidomain.urlreverse import eventreverse_absolute
try:
with language(self.locale):
@@ -178,7 +178,7 @@ class Customer(LoggedModel):
{
**self.get_email_context(),
'message': str(message),
'url': build_absolute_uri(self.organizer, 'presale:organizer.customer.index')
'url': eventreverse_absolute(self.organizer, 'presale:organizer.customer.index')
},
customer=self,
organizer=self.organizer,
@@ -299,12 +299,12 @@ class Customer(LoggedModel):
def send_activation_mail(self):
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
ctx = self.get_email_context()
token = TokenGenerator().make_token(self)
ctx['url'] = build_absolute_uri(
ctx['url'] = eventreverse_absolute(
self.organizer,
'presale:organizer.customer.activate'
) + '?id=' + self.identifier + '&token=' + token
@@ -395,7 +395,7 @@ class AttendeeProfile(models.Model):
self.company,
self.street,
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
self.country.name,
self.country.name if self.country else None,
]
for a in self.answers:
value = a.get('value')
+18 -5
View File
@@ -724,7 +724,7 @@ class Event(EventMixin, LoggedModel):
@property
def social_image(self):
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.multidomain.urlreverse import eventreverse_absolute
img = None
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}')
img = default_storage.url(logo_file)
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):
from .seating import Seat
@@ -883,6 +883,8 @@ class Event(EventMixin, LoggedModel):
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.
# 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
@@ -911,6 +913,15 @@ class Event(EventMixin, LoggedModel):
for emv in EventMetaValue.objects.filter(event=other):
emv.pk = None
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)
for fl in EventFooterLink.objects.filter(event=other):
@@ -964,13 +975,13 @@ class Event(EventMixin, LoggedModel):
if 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.save() # no force_insert since i.picture.save could have already inserted
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)
if not i.all_sales_channels:
@@ -985,7 +996,7 @@ class Event(EventMixin, LoggedModel):
v._prefetched_objects_cache = {}
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)
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]))
@@ -1869,6 +1880,8 @@ class EventMetaValue(LoggedModel):
self.event.cache.clear()
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)
if self.event:
self.event.cache.clear()
+16 -6
View File
@@ -452,11 +452,16 @@ class Item(LoggedModel):
MEDIA_POLICY_REUSE = 'reuse'
MEDIA_POLICY_NEW = 'new'
MEDIA_POLICY_REUSE_OR_NEW = 'reuse_or_new'
MEDIA_POLICY_APPEND = 'append'
MEDIA_POLICY_APPEND_OR_NEW = 'append_or_new'
MEDIA_POLICIES = (
(None, _("Don't use re-usable media, use regular one-off tickets")),
(MEDIA_POLICY_REUSE, _('Require an existing medium to be re-used')),
(None, _("Don't use reusable media, use regular one-off tickets")),
(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()
@@ -769,7 +774,7 @@ class Item(LoggedModel):
null=True, blank=True, max_length=16,
verbose_name=_('Reusable media policy'),
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 '
'renewable season tickets or re-chargeable gift card wristbands. '
'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(
max_length=100,
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'),
help_text=_(
'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.'))
if not mt.supports_giftcard and issue_giftcard:
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:
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 '
@@ -2220,7 +2230,7 @@ class Quota(LoggedModel):
class ItemMetaProperty(LoggedModel):
"""
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.
:type event: Event
+4 -1
View File
@@ -129,7 +129,10 @@ class ReusableMedium(LoggedModel):
@property
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:
unique_together = (("identifier", "type", "organizer"),)
+52 -30
View File
@@ -354,38 +354,60 @@ class Order(LockModel, LoggedModel):
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
def gracefully_delete(self, user=None, auth=None):
from . import GiftCard, GiftCardTransaction, Membership, Voucher
if not self.testmode:
raise TypeError("Only test mode orders can be deleted.")
self.log_action(
'pretix.event.order.deleted', user=user, auth=auth,
data={
'code': self.code,
}
@classmethod
def gracefully_delete_bulk(cls, event, orders, user=None, auth=None):
# Expects to be called in a transaction
from . import (
GiftCard, GiftCardTransaction, LogEntry, Membership, Voucher,
)
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:
for position in self.positions.all():
if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
logs_create = []
for o in orders:
if not o.testmode:
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)
GiftCardTransaction.objects.filter(refund__in=self.refunds.all()).update(refund=None)
GiftCardTransaction.objects.filter(order=self).update(order=None)
GiftCard.objects.filter(issued_in__in=self.positions.all()).update(issued_in=None)
Membership.objects.filter(granted_in__order=self, testmode=True).update(granted_in=None)
OrderPosition.all.filter(order=self, addon_to__isnull=False).delete()
OrderPosition.all.filter(order=self).delete()
OrderFee.all.filter(order=self).delete()
Transaction.objects.filter(order=self).delete()
self.refunds.all().delete()
self.payments.all().delete()
self.event.cache.delete('complain_testmode_orders')
self.delete()
voucher_ids = OrderPosition.objects.filter(
order__in=orders,
voucher__isnull=False
).exclude(order__status=Order.STATUS_CANCELED).values_list("voucher_id", flat=True)
voucher_usages = Counter(voucher_ids)
for v_id, usage_count in voucher_usages.items():
Voucher.objects.filter(pk=v_id).update(redeemed=Greatest(0, F('redeemed') - usage_count))
GiftCardTransaction.objects.filter(payment__order__in=orders).update(payment=None)
GiftCardTransaction.objects.filter(refund__order__in=orders).update(refund=None)
GiftCardTransaction.objects.filter(order__in=orders).update(order=None)
GiftCard.objects.filter(issued_in__order__in=orders).update(issued_in=None)
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):
return self.tagged_secret("email_confirm", 9)
@@ -1675,7 +1697,7 @@ class AbstractPosition(RoundingCorrectionMixin, models.Model):
self.company,
self.street,
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
self.country.name
self.country.name if self.country else ''
]
lines = [r.strip() for r in lines if r]
return '\n'.join(lines).strip()
@@ -3416,7 +3438,7 @@ class InvoiceAddress(models.Model):
self.name,
self.street,
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
self.country.name,
self.country.name if self.country else '',
self.vat_id,
self.custom_field,
self.internal_reference,
+6 -3
View File
@@ -118,7 +118,10 @@ class SeatingPlan(LoggedModel):
for zi, z in enumerate(self.layout_data['zones']):
zpos = (z['position']['x'], z['position']['y'])
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
if r.get('row_label'):
row_label = r['row_label'].replace("%s", r.get('row_number', str(ri)))
@@ -147,8 +150,8 @@ class SeatingPlan(LoggedModel):
zone=z['name'],
category=s['category'],
sorting_rank=rank,
x=rpos[0] + s['position']['x'],
y=rpos[1] + s['position']['y'],
x=rpos[0] + s.get('position', {}).get('x', 0),
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.signals import register_notification_types
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__)
_ALL_TYPES = None
@@ -170,7 +170,7 @@ class ParametrizedOrderNotificationType(NotificationType):
def build_notification(self, logentry: LogEntry):
order = logentry.content_object
order_url = build_absolute_uri(
order_url = mainreverse_absolute(
'control:event.order',
kwargs={
'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.format import format_map
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.cart import cart_session, get_or_create_cart_id
@@ -379,7 +379,7 @@ class BasePaymentProvider:
if not self.settings.get('_hidden_seed'):
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(),
})
+6 -1
View File
@@ -372,6 +372,11 @@ DEFAULT_VARIABLES = OrderedDict((
"editor_sample": _("Atlantis"),
"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", {
"label": _("List of Add-Ons"),
"editor_sample": _("Add-on 1\n2x Add-on 2"),
@@ -1062,7 +1067,7 @@ class Renderer:
except:
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
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'])
# 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]
if not mt.medium_created_by_server:
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'])
# Item removed from sales channel
+30 -2
View File
@@ -867,6 +867,15 @@ class RequiredQuestionsError(Exception):
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 _create_answer(question, answer):
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,
user=None, auth=None, canceled_supported=False, type=Checkin.TYPE_ENTRY,
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
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 simulate: If true, the check-in is not saved.
: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():
# 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:
opqs = opqs.select_for_update(of=OF_SELF)
op = opqs.get(pk=op.pk)
@@ -1101,6 +1111,24 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
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
if isinstance(auth, Device):
device = auth
+3 -3
View File
@@ -51,7 +51,7 @@ from pretix.base.signals import (
)
from pretix.celery_app import app
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__)
@@ -455,7 +455,7 @@ def scheduled_organizer_export(self, organizer: Organizer, schedule: int) -> Non
schedule,
organizer,
exporter,
build_absolute_uri(
mainreverse_absolute(
'control:organizer.export',
kwargs={
'organizer': organizer.slug,
@@ -481,7 +481,7 @@ def scheduled_event_export(self, event: Event, schedule: int) -> None:
schedule,
event,
exporter,
build_absolute_uri(
mainreverse_absolute(
'control:event.orders.export',
kwargs={
'event': event.slug,
+3 -3
View File
@@ -85,7 +85,7 @@ from pretix.helpers.format import (
FormattedString, PlainHtmlAlternativeString, SafeFormatter, format_map,
)
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
logger = logging.getLogger('pretix.base.mail')
@@ -997,7 +997,7 @@ def _wrap_plain_body(content_plain, signature, event, order, position, no_order_
body_plain += _(
"You can view your order details at the following URL:\n{orderurl}."
).replace("\n", "\r\n").format(
orderurl=build_absolute_uri(
orderurl=eventreverse_absolute(
order.event, 'presale:event.order.position', kwargs={
'order': order.code,
'secret': position.web_secret,
@@ -1013,7 +1013,7 @@ def _wrap_plain_body(content_plain, signature, event, order, position, no_order_
body_plain += _(
"You can view your order details at the following URL:\n{orderurl}."
).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': order.code,
'secret': order.secret,
+176 -2
View File
@@ -23,10 +23,13 @@ import secrets
from django.db import IntegrityError
from django.db.models import Q
from django.utils.translation import gettext as _
from django_scopes import scopes_disabled
from pretix.base.models import GiftCardAcceptance
from pretix.base.models.media import MediumKeySet
from pretix.base.media import MEDIA_TYPES
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):
@@ -70,3 +73,174 @@ def get_keysets_for_organizer(organizer):
if new_set:
sets.append(new_set)
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
+33 -3
View File
@@ -26,8 +26,9 @@ from typing import List
from django.conf import settings as django_settings
from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.utils import IntegrityError
from django.utils.timezone import now
from django.utils.translation import gettext as _
from django.utils.translation import gettext as _, ngettext
from pretix.base.i18n import language
from pretix.base.modelimport import DataImportError, ImportColumn, parse_csv
@@ -260,6 +261,7 @@ def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user
# Prepare model objects. Yes, this might consume lots of RAM, but allows us to make the actual SQL transaction
# shorter. We'll see what works better in reality…
vouchers = []
codes = set()
lock_seats = []
for i, record in enumerate(data):
try:
@@ -268,6 +270,14 @@ def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user
if not record.get("code"):
raise ValidationError(_('A voucher cannot be created without a code.'))
code = record.get("code")
if code.upper() in codes:
raise ValidationError(
_('Voucher codes must be unique. Code "{code}" already exists in this import.').format(
code=code,
)
)
codes.add(code.upper())
Voucher.clean_item_properties(
record,
event,
@@ -286,8 +296,22 @@ def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user
lock_seats.append(voucher.seat)
except (ValidationError, ImportError) as e:
raise DataImportError(
_('Invalid data in row {row}: {message}').format(row=i, message=str(e))
_('Invalid data in row {row}: {message}').format(row=i + 1, message=str(e))
)
existing_codes = Voucher.objects.filter(
event=event,
code__in=codes,
).values_list("code", flat=True)
if len(existing_codes):
raise DataImportError(
ngettext(
'Voucher codes must be unique. Import contains existing voucher code {code}.',
'Voucher codes must be unique. Import contains existing voucher codes {code}.',
len(existing_codes)
).format(
code=", ".join(existing_codes)
)
)
with transaction.atomic():
# We don't support quotas here, so we only need to lock if seats are in use
@@ -300,7 +324,13 @@ def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user
save_logentries = []
for v in vouchers:
v.save()
try:
v.save()
except IntegrityError:
# should not happen as we check existing codes before, but we did not lock so we might have a race-condition
raise DataImportError(
_('Vouchers could not be imported, probably due to a voucher code already being in use.')
)
save_logentries.append(v.log_action(
'pretix.voucher.added',
user=user,
+3 -3
View File
@@ -37,7 +37,7 @@ from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask
from pretix.base.signals import notification
from pretix.celery_app import app
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)
@@ -136,10 +136,10 @@ def send_notification_mail(notification: Notification, user: User):
'site_url': settings.SITE_URL,
'color': settings.PRETIX_PRIMARY_COLOR,
'notification': notification,
'settings_url': build_absolute_uri(
'settings_url': mainreverse_absolute(
'control:user.settings.notifications',
),
'disable_url': build_absolute_uri(
'disable_url': mainreverse_absolute(
'control:user.settings.notifications.off',
kwargs={
'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
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]
if mt.medium_created_by_server and not p.linked_media.exists():
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")
def base_placeholders(sender, **kwargs):
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.multidomain.urlreverse import eventreverse_absolute
def _event_sample(event):
if event.has_subevents:
@@ -388,14 +388,14 @@ def base_placeholders(sender, **kwargs):
lambda event: LazyDate(now() + timedelta(days=15))
),
SimpleFunctionalTextPlaceholder(
'url', ['order', 'event'], lambda order, event: build_absolute_uri(
'url', ['order', 'event'], lambda order, event: eventreverse_absolute(
event,
'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_secret()
}
), lambda event: build_absolute_uri(
), lambda event: eventreverse_absolute(
event,
'presale:event.order.open', kwargs={
'order': 'F8VVL',
@@ -406,7 +406,7 @@ def base_placeholders(sender, **kwargs):
),
SimpleButtonPlaceholder(
'url_button', ['order', 'event'],
url_func=lambda order, event: build_absolute_uri(
url_func=lambda order, event: eventreverse_absolute(
event,
'presale:event.order.open', kwargs={
'order': order.code,
@@ -415,7 +415,7 @@ def base_placeholders(sender, **kwargs):
}
),
text_func=lambda order, event: _("View order details"),
sample_url_func=lambda event: build_absolute_uri(
sample_url_func=lambda event: eventreverse_absolute(
event,
'presale:event.order.open', kwargs={
'order': 'F8VVL',
@@ -426,13 +426,13 @@ def base_placeholders(sender, **kwargs):
sample_text_func=lambda event: _("View order details"),
),
SimpleFunctionalTextPlaceholder(
'url_info_change', ['order', 'event'], lambda order, event: build_absolute_uri(
'url_info_change', ['order', 'event'], lambda order, event: eventreverse_absolute(
event,
'presale:event.order.modify', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: build_absolute_uri(
), lambda event: eventreverse_absolute(
event,
'presale:event.order.modify', kwargs={
'order': 'F8VVL',
@@ -441,13 +441,13 @@ def base_placeholders(sender, **kwargs):
),
),
SimpleFunctionalTextPlaceholder(
'url_products_change', ['order', 'event'], lambda order, event: build_absolute_uri(
'url_products_change', ['order', 'event'], lambda order, event: eventreverse_absolute(
event,
'presale:event.order.change', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: build_absolute_uri(
), lambda event: eventreverse_absolute(
event,
'presale:event.order.change', kwargs={
'order': 'F8VVL',
@@ -456,13 +456,13 @@ def base_placeholders(sender, **kwargs):
),
),
SimpleFunctionalTextPlaceholder(
'url_cancel', ['order', 'event'], lambda order, event: build_absolute_uri(
'url_cancel', ['order', 'event'], lambda order, event: eventreverse_absolute(
event,
'presale:event.order.cancel', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: build_absolute_uri(
), lambda event: eventreverse_absolute(
event,
'presale:event.order.cancel', kwargs={
'order': 'F8VVL',
@@ -471,7 +471,7 @@ def base_placeholders(sender, **kwargs):
),
),
SimpleFunctionalTextPlaceholder(
'url', ['event', 'position'], lambda event, position: build_absolute_uri(
'url', ['event', 'position'], lambda event, position: eventreverse_absolute(
event,
'presale:event.order.position',
kwargs={
@@ -480,7 +480,7 @@ def base_placeholders(sender, **kwargs):
'position': position.positionid
}
),
lambda event: build_absolute_uri(
lambda event: eventreverse_absolute(
event,
'presale:event.order.position', kwargs={
'order': 'F8VVL',
@@ -491,7 +491,7 @@ def base_placeholders(sender, **kwargs):
),
SimpleButtonPlaceholder(
'url_button', ['event', 'position'],
url_func=lambda event, position: build_absolute_uri(
url_func=lambda event, position: eventreverse_absolute(
event,
'presale:event.order.position', kwargs={
'order': position.order.code,
@@ -500,7 +500,7 @@ def base_placeholders(sender, **kwargs):
}
),
text_func=lambda event, position: _("View registration details"),
sample_url_func=lambda event: build_absolute_uri(
sample_url_func=lambda event: eventreverse_absolute(
event,
'presale:event.order.position', kwargs={
'order': 'F8VVL',
@@ -511,14 +511,14 @@ def base_placeholders(sender, **kwargs):
sample_text_func=lambda event: _("View registration details"),
),
SimpleFunctionalTextPlaceholder(
'url_info_change', ['position', 'event'], lambda position, event: build_absolute_uri(
'url_info_change', ['position', 'event'], lambda position, event: eventreverse_absolute(
event,
'presale:event.order.position.modify', kwargs={
'order': position.order.code,
'secret': position.web_secret,
'position': position.positionid
}
), lambda event: build_absolute_uri(
), lambda event: eventreverse_absolute(
event,
'presale:event.order.position.modify', kwargs={
'order': 'F8VVL',
@@ -528,14 +528,14 @@ def base_placeholders(sender, **kwargs):
),
),
SimpleFunctionalTextPlaceholder(
'url_products_change', ['position', 'event'], lambda position, event: build_absolute_uri(
'url_products_change', ['position', 'event'], lambda position, event: eventreverse_absolute(
event,
'presale:event.order.position.change', kwargs={
'order': position.order.code,
'secret': position.web_secret,
'position': position.positionid
}
), lambda event: build_absolute_uri(
), lambda event: eventreverse_absolute(
event,
'presale:event.order.position.change', kwargs={
'order': 'F8VVL',
@@ -581,20 +581,20 @@ def base_placeholders(sender, **kwargs):
),
SimpleFunctionalTextPlaceholder(
'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'
) + '?voucher=' + waiting_list_voucher.code,
lambda event: build_absolute_uri(
lambda event: eventreverse_absolute(
event,
'presale:event.waitinglist.remove',
) + '?voucher=68CYU2H6ZTP3WLK5',
),
SimpleFunctionalTextPlaceholder(
'url', ['waiting_list_voucher', 'event'],
lambda waiting_list_voucher, event: build_absolute_uri(
lambda waiting_list_voucher, event: eventreverse_absolute(
event, 'presale:event.redeem'
) + '?voucher=' + waiting_list_voucher.code,
lambda event: build_absolute_uri(
lambda event: eventreverse_absolute(
event,
'presale:event.redeem',
) + '?voucher=68CYU2H6ZTP3WLK5',
@@ -611,7 +611,7 @@ def base_placeholders(sender, **kwargs):
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
'* {} - {}'.format(
order.full_code,
build_absolute_uri(event, 'presale:event.order.open', kwargs={
eventreverse_absolute(event, 'presale:event.order.open', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'order': order.code,
@@ -623,7 +623,7 @@ def base_placeholders(sender, **kwargs):
), lambda event: '\n' + '\n\n'.join(
'* {} - {}'.format(
'{}-{}'.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,
'organizer': event.organizer.slug,
'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>
'voucher_url_list', ['event', 'voucher_list'],
lambda event, voucher_list: ' \n'.join([
build_absolute_uri(
eventreverse_absolute(
event, 'presale:event.redeem'
) + '?voucher=' + c
for c in voucher_list
]),
lambda event: ' \n'.join([
build_absolute_uri(
eventreverse_absolute(
event, 'presale:event.redeem'
) + '?voucher=' + c
for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2']
@@ -676,10 +676,10 @@ def base_placeholders(sender, **kwargs):
inline=False,
),
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,
'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,
'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.signals import periodic_task
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)
@@ -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,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Activate re-usable media"),
help_text=_("The re-usable 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 "
label=_("Activate reusable 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 reused for different tickets or gift cards "
"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': {
'default': 'False',
'type': bool,
+1 -1
View File
@@ -11,6 +11,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8">
<link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}">
<script type="text/javascript" src="{% static "pretixbase/js/errors.js" %}"></script>
{% block custom_header %}{% endblock %}
{% if css_theme %}
<link rel="stylesheet" type="text/css" href="{{ css_theme }}" />
@@ -21,5 +22,4 @@
{% block content %}{% endblock %}
</div>
</body>
<script src="{% static "pretixbase/js/errors.js" %}"></script>
</html>
@@ -2,13 +2,14 @@
{% load i18n %}
{% load rich_text %}
{% load static %}
{% load wrap_in %}
{% block title %}{% trans "Redirect" %}{% endblock %}
{% block content %}
<i class="fa fa-link fa-fw big-icon"></i>
<div class="error-details">
<h1>{% trans "Redirect" %}</h1>
<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 }}.
{% endblocktrans %}
{% blocktrans trimmed %}
+3 -3
View File
@@ -80,7 +80,7 @@ from pretix.control.forms.widgets import Select2
from pretix.helpers.countries import CachedCountries
from pretix.multidomain.models import AlternativeDomainAssignment, KnownDomain
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.presale.style import get_fonts
@@ -219,7 +219,7 @@ class EventWizardBasicsForm(I18nModelForm):
self.fields['location'].widget.attrs['placeholder'] = _(
'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
if self.has_subevents:
del self.fields['presale_start']
@@ -1673,7 +1673,7 @@ class CountriesAndEUAndStates(CountriesAndEU):
class TaxRuleLineForm(I18nForm):
country = LazyTypedChoiceField(
choices=CountriesAndEUAndStates(),
choices=lazy(lambda: CountriesAndEUAndStates(), CountriesAndEUAndStates),
required=False
)
address_type = forms.ChoiceField(
+7 -1
View File
@@ -1342,7 +1342,13 @@ class QuestionAnswerFilterForm(forms.Form):
opqs = opqs.filter(canceled=False)
if 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
+3 -2
View File
@@ -88,7 +88,7 @@ from pretix.control.forms.event import (
)
from pretix.control.forms.widgets import Select2, Select2Multiple
from pretix.multidomain.models import KnownDomain
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.multidomain.urlreverse import eventreverse_absolute
class OrganizerForm(I18nModelForm):
@@ -636,6 +636,7 @@ class OrganizerSettingsForm(SettingsForm):
'cookie_consent_dialog_button_yes',
'cookie_consent_dialog_button_no',
'reusable_media_active',
'reusable_media_usage_enforced',
'reusable_media_type_barcode',
'reusable_media_type_barcode_identifier_length',
'reusable_media_type_nfc_uid',
@@ -790,7 +791,7 @@ class MailSettingsForm(SettingsForm):
}
if 'url' in base_parameters:
placeholders['url'] = build_absolute_uri(
placeholders['url'] = eventreverse_absolute(
self.organizer,
'presale:organizer.customer.activate'
) + '?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.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.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.email.error': _('Sending of an email has failed.'),
'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.plugins import get_all_plugins
from pretix.base.templatetags.money import money_filter
from pretix.helpers.reportlab import PlainTextParagraph
from pretix.plugins.reports.exporters import ReportlabExportMixin
from pretix.settings import DATA_DIR
@@ -79,23 +80,23 @@ class SysReport(ReportlabExportMixin):
style_small.fontSize = 6
story = [
Paragraph("System report", headlinestyle),
PlainTextParagraph("System report", headlinestyle),
Spacer(1, 5 * mm),
Paragraph("Usage", subheadlinestyle),
PlainTextParagraph("Usage", subheadlinestyle),
Spacer(1, 5 * mm),
self._usage_table(),
Spacer(1, 5 * mm),
Paragraph("Installed versions", subheadlinestyle),
PlainTextParagraph("Installed versions", subheadlinestyle),
Spacer(1, 5 * mm),
self._tech_table(),
Spacer(1, 5 * mm),
Paragraph("Plugins", subheadlinestyle),
PlainTextParagraph("Plugins", subheadlinestyle),
Spacer(1, 5 * mm),
Paragraph(self._get_plugin_versions(), style_small),
PlainTextParagraph(self._get_plugin_versions(), style_small),
Spacer(1, 5 * mm),
Paragraph("Custom templates", subheadlinestyle),
PlainTextParagraph("Custom templates", subheadlinestyle),
Spacer(1, 5 * mm),
Paragraph(self._get_custom_templates(), style_small),
PlainTextParagraph(self._get_custom_templates(), style_small),
Spacer(1, 5 * mm),
]
@@ -121,13 +122,13 @@ class SysReport(ReportlabExportMixin):
("RIGHTPADDING", (-1, 0), (-1, -1), 0),
]
tdata = [
[Paragraph("Site URL:", style), Paragraph(settings.SITE_URL, style)],
[Paragraph("pretix version:", style), Paragraph(__version__, style)],
[Paragraph("Python version:", style), Paragraph(sys.version, style)],
[Paragraph("Platform:", style), Paragraph(platform.platform(), style)],
[PlainTextParagraph("Site URL:", style), Paragraph(settings.SITE_URL, style)],
[PlainTextParagraph("pretix version:", style), Paragraph(__version__, style)],
[PlainTextParagraph("Python version:", style), Paragraph(sys.version, style)],
[PlainTextParagraph("Platform:", style), Paragraph(platform.platform(), style)],
[
Paragraph("Database engine:", style),
Paragraph(settings.DATABASES["default"]["ENGINE"], style),
PlainTextParagraph("Database engine:", style),
PlainTextParagraph(settings.DATABASES["default"]["ENGINE"], style),
],
]
table = Table(tdata, colWidths=colwidths, repeatRows=0)
@@ -206,7 +207,7 @@ class SysReport(ReportlabExportMixin):
year_last = now().year
tdata = [
[
Paragraph(l, style_small_head)
PlainTextParagraph(l, style_small_head)
for l in (
"Time frame",
"Currency",
@@ -257,19 +258,19 @@ class SysReport(ReportlabExportMixin):
tdata.append(
(
Paragraph(
PlainTextParagraph(
date_format(first_day, "M Y")
+ " "
+ date_format(after_day - timedelta(days=1), "M Y"),
style_small,
),
Paragraph(c, style_small),
Paragraph(str(orders_count), style_small) if i == 0 else "",
Paragraph(money_filter(revenue_data.get("s_net") or 0, c), style_small),
Paragraph(str(testmode_count), style_small) if i == 0 else "",
Paragraph(str(unconfirmed_count), style_small) if i == 0 else "",
Paragraph(str(revenue_data.get("c") or 0), style_small),
Paragraph(money_filter(revenue_data.get("s_gross") or 0, c), style_small),
PlainTextParagraph(c, style_small),
PlainTextParagraph(str(orders_count), style_small) if i == 0 else "",
PlainTextParagraph(money_filter(revenue_data.get("s_net") or 0, c), style_small),
PlainTextParagraph(str(testmode_count), style_small) if i == 0 else "",
PlainTextParagraph(str(unconfirmed_count), style_small) if i == 0 else "",
PlainTextParagraph(str(revenue_data.get("c") or 0), 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 "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/errors.js" %}"></script>
{% endcompress %}
{{ html_head|safe }}
@@ -54,6 +54,8 @@
<span class="fa fa-check-circle"></span>
{% elif result.status == "incomplete" %}
<span class="fa fa-question-circle"></span>
{% elif result.status == "exchange" %}
<span class="fa fa-recycle"></span>
{% elif result.status == "error" %}
{% if result.reason == "already_redeemed" %}
<span class="fa fa-warning"></span>
@@ -79,6 +81,14 @@
</li>
{% endfor %}
</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" %}
<h3 class="nomargin-top">{{ reason_labels|getitem:result.reason }}</h3>
{% if result.reason_explanation %}
@@ -222,6 +222,7 @@
<fieldset>
<legend>{% trans "Reusable media" %}</legend>
{% 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 class="panel panel-default">
@@ -2,6 +2,7 @@
{% load i18n %}
{% load bootstrap3 %}
{% load getitem %}
{% load icon %}
{% block inner %}
{% if team %}
<h1>{% trans "Team:" %} {{ team.name }}</h1>
@@ -25,6 +26,18 @@
<legend>{% trans "Organizer permissions" %}</legend>
{% 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>
<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 %}
{% bootstrap_field form|getitem:f layout="control" %}
{% endfor %}
@@ -37,6 +50,17 @@
{% bootstrap_field form.limit_events 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>
<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 %}
{% bootstrap_field form|getitem:f layout="control" %}
{% endfor %}
@@ -19,9 +19,7 @@
{% endif %}
</h1>
<script type="application/json" id="editor-data">
{{ layout|safe }}
</script>
{{ layout|json_script:"editor-data" }}
<div class="row">
<div class="col-md-9">
<div class="panel panel-default panel-pdf-editor">
+5 -2
View File
@@ -50,7 +50,7 @@ from i18nfield.strings import LazyI18nString
from pretix.api.views.checkin import _redeem_process
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.orders import PrintLog
from pretix.base.permissions import AnyPermissionOf
@@ -401,13 +401,14 @@ class CheckinListUpdate(EventPermissionRequiredMixin, UpdateView):
{
'id': i.pk,
'name': str(i),
'active': i.active,
'variations': [
{
'id': v.pk,
'name': str(v.value)
} for v in i.variations.all()
]
} for i in self.request.event.items.filter(active=True).prefetch_related('variations')
} for i in self.request.event.items.prefetch_related('variations')
],
**super().get_context_data(),
}
@@ -532,6 +533,8 @@ class CheckInListSimulator(EventPermissionRequiredMixin, FormView):
checkinlist=self.list,
result=self.result,
reason_labels=dict(Checkin.REASONS),
media_policies=dict(Item.MEDIA_POLICIES),
media_types=dict(MEDIA_TYPES),
)
def form_valid(self, form):
+11 -7
View File
@@ -41,7 +41,7 @@ from collections import OrderedDict, defaultdict
from decimal import Decimal
from io import BytesIO
from itertools import groupby
from urllib.parse import urlparse, urlsplit
from urllib.parse import urlsplit
from zoneinfo import ZoneInfo
import bleach
@@ -64,7 +64,6 @@ from django.shortcuts import get_object_or_404, redirect
from django.urls import NoReverseMatch, reverse
from django.utils.functional import cached_property
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.timezone import now
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.user import RecentAuthenticationRequiredMixin
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 (
version_default as widget_version_default,
)
@@ -1148,8 +1149,11 @@ class EventLive(EventPermissionRequiredMixin, TemplateView):
if request.POST.get("delete") == "yes":
try:
with transaction.atomic():
for order in request.event.orders.filter(testmode=True):
order.gracefully_delete(user=self.request.user)
Order.gracefully_delete_bulk(
request.event,
request.event.orders.filter(testmode=True),
user=self.request.user
)
except ProtectedError:
messages.error(self.request, _('An order could not be deleted as some constraints (e.g. data '
'created by plug-ins) do not allow it.'))
@@ -1734,10 +1738,10 @@ class EventQRCode(EventPermissionRequiredMixin, View):
permission = None
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_has_allowed_host_and_scheme(request.GET["url"], allowed_hosts=[urlparse(url).netloc]):
if request.GET["url"].startswith(url):
url = request.GET["url"]
else:
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.format import SafeFormatter, format_map
from pretix.helpers.hierarkey import clean_filename
from pretix.helpers.iter import chunked_iterable
from pretix.helpers.json import CustomJSONEncoder
from pretix.helpers.safedownload import check_token
from pretix.presale.signals import question_form_fields
@@ -240,7 +241,7 @@ class BaseOrderBulkActionView(OrderSearchMixin, EventPermissionRequiredMixin, As
raise NotImplementedError()
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()
orders_with_successful_action = 0
for i, o in enumerate(qs):
@@ -394,8 +395,21 @@ class OrderDeleteBulkActionView(BaseOrderBulkActionView):
testmode=True,
)
def execute_single(self, instance, form: forms.Form):
instance.gracefully_delete(user=self.request.user)
def execute_bulk(self, queryset: QuerySet, form: forms.Form):
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):
+10 -8
View File
@@ -139,8 +139,8 @@ from pretix.helpers import OF_SELF, GroupConcat
from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.dicts import merge_dicts
from pretix.helpers.format import SafeFormatter, format_map
from pretix.helpers.urls import build_absolute_uri as build_global_uri
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.helpers.urls import mainreverse_absolute
from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.presale.forms.customer import TokenGenerator
logger = logging.getLogger(__name__)
@@ -1039,7 +1039,7 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
'user': self,
'organizer': self.request.organizer.name,
'team': instance.team.name,
'url': build_global_uri('control:auth.invite', kwargs={
'url': mainreverse_absolute('control:auth.invite', kwargs={
'token': instance.token
})
},
@@ -2851,10 +2851,12 @@ class SSOProviderUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequire
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['redirect_uri'] = build_absolute_uri(self.request.organizer, 'presale:organizer.customer.login.return',
kwargs={
'provider': self.object.pk
})
ctx['redirect_uri'] = eventreverse_absolute(
self.request.organizer, 'presale:organizer.customer.login.return',
kwargs={
'provider': self.object.pk
}
)
return ctx
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)
ctx = self.customer.get_email_context()
token = TokenGenerator().make_token(self.customer)
ctx['url'] = build_absolute_uri(
ctx['url'] = eventreverse_absolute(
self.request.organizer,
'presale:organizer.customer.recoverpw'
) + '?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['variables'] = self.get_variables()
ctx['images'] = self.get_images()
ctx['layout'] = json.dumps(self.get_current_layout())
ctx['layout'] = self.get_current_layout()
ctx['title'] = self.title
ctx['locales'] = [p for p in settings.LANGUAGES if p[0] in self.request.event.settings.locales]
ctx['maxfilesize'] = self.maxfilesize
+1
View File
@@ -537,6 +537,7 @@ class SubEventDetail(EventPermissionRequiredMixin, DetailView):
pcnt=Subquery(
OrderPosition.objects.filter(
subevent=self.object,
order_id=OuterRef("id"),
).values("subevent").annotate(c=Count("*")).values("c")
),
has_cancellation_request=Exists(CancellationRequest.objects.filter(order=OuterRef("pk"))),
+1 -1
View File
@@ -200,7 +200,7 @@ def giftcard_select2(request, **kwargs):
except ValueError:
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(
Q(secret__icontains=query)
).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.format import SafeFormatter, format_map
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):
@@ -338,7 +338,7 @@ class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
}
if 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
+5
View File
@@ -29,3 +29,8 @@ class PretixHelpersConfig(AppConfig):
def ready(self):
from .monkeypatching import 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']
+23 -1
View File
@@ -27,6 +27,7 @@ from datetime import datetime
from http import cookies
from django.conf import settings
from django.core.exceptions import SuspiciousFileOperation
from PIL import Image
from requests.adapters import HTTPAdapter
from urllib3.connection import HTTPConnection, HTTPSConnection
@@ -40,6 +41,8 @@ from urllib3.util.connection import (
)
from urllib3.util.timeout import _DEFAULT_TIMEOUT
from pretix.helpers.reportlab import ThumbnailingImageReader
_cgnat_net = ipaddress.ip_network('100.64.0.0/10')
@@ -148,13 +151,14 @@ def monkeypatch_urllib3_ssrf_protection():
if not getattr(settings, "ALLOW_HTTP_TO_PRIVATE_NETWORKS", False):
ip_addr = ipaddress.ip_address(sa[0])
check_ip4 = ip_addr.ipv4_mapped if getattr(ip_addr, "ipv4_mapped", None) else ip_addr
if ip_addr.is_multicast:
raise HTTPError(f"Request to multicast address {sa[0]} blocked")
if ip_addr.is_loopback or ip_addr.is_link_local:
raise HTTPError(f"Request to local address {sa[0]} blocked")
if ip_addr.is_private:
raise HTTPError(f"Request to private address {sa[0]} blocked")
if ip_addr in _cgnat_net:
if check_ip4 in _cgnat_net:
raise HTTPError(f"Request to RFC 6598 address {sa[0]} blocked")
sock = None
@@ -230,9 +234,27 @@ def monkeypatch_cookie_morsel():
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():
monkeypatch_vobject_performance()
monkeypatch_pillow_safer()
monkeypatch_requests_timeout()
monkeypatch_urllib3_ssrf_protection()
monkeypatch_cookie_morsel()
monkeypatch_reportlab_imagereader()
+39
View File
@@ -20,14 +20,19 @@
# <https://www.gnu.org/licenses/>.
#
import logging
import re
import unicodedata
from arabic_reshaper import ArabicReshaper
from bidi import get_display
from django.conf import settings
from django.utils.functional import SimpleLazyObject
from django.utils.html import escape
from PIL import Image
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.utils import ImageReader
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import Paragraph
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):
def __init__(self, text, style=None, *args, **kwargs):
if style is None:
@@ -87,6 +106,8 @@ class FontFallbackParagraph(Paragraph):
if not text:
return True
font = pdfmetrics.getFont(font_name)
if not isinstance(font, TTFont):
return True
return all(
ord(c) in font.face.charToGlyph or not c.isprintable()
for c in text
@@ -102,6 +123,24 @@ class FontFallbackParagraph(Paragraph):
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):
from reportlab.pdfbase import pdfmetrics
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.services.mail import mail
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__)
@@ -175,7 +175,7 @@ def handle_login_source(user, request):
'source': src,
'country': Country(str(country)).name if country else _('Unknown country'),
'instance': settings.PRETIX_INSTANCE_NAME,
'url': build_absolute_uri('control:user.settings')
'url': mainreverse_absolute('control:user.settings')
},
event=None,
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
# <https://www.gnu.org/licenses/>.
#
import warnings
from urllib.parse import urljoin
from django.conf import settings
@@ -26,6 +27,15 @@ from django.urls import reverse
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
return urljoin(settings.SITE_URL, reverse(urlname, args=args, kwargs=kwargs, urlconf=maindomain_urlconf))
File diff suppressed because it is too large Load Diff
+423 -23
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-27 14:47+0000\n"
"POT-Creation-Date: 2026-06-28 15:49+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -130,6 +130,7 @@ msgid "Mercado Pago"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:54
#: pretix/static/pretixpresale/js/ui/cart.js:89
msgid "Continue"
msgstr ""
@@ -183,6 +184,172 @@ msgstr ""
msgid "Contacting your bank …"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:34
msgid "Select a check-in list"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:35
msgid "No active check-in lists found."
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:36
msgid "Switch check-in list"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:37
msgid "Search results"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:38
msgid "No tickets found"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:39
msgid "Result"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:40
msgid "This ticket requires special attention"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:41
msgid "Switch direction"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:42
msgid "Entry"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:43
msgid "Exit"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:44
msgid "Scan a ticket or search and press return…"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:45
msgid "Load more"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:46
msgid "Valid"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:47
msgid "Unpaid"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:48
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:49
msgid "Canceled"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:50
msgid "Confirmed"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:51
msgid "Approval pending"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:52
msgid "Redeemed"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:53
msgid "Cancel"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:55
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:64
msgid "Ticket not paid"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:56
msgid "This ticket is not yet paid. Do you want to continue anyways?"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:57
msgid "Additional information required"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:58
msgid "Valid ticket"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:59
msgid "Exit recorded"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:60
msgid "Ticket already used"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:61
msgid "Information required"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:62
msgid "Unknown ticket"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:63
msgid "Ticket type not allowed here"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:65
msgid "Entry not allowed"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:66
msgid "Ticket code revoked/changed"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:67
msgid "Ticket blocked"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:68
msgid "Ticket not valid at this time"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:69
msgid "Order canceled"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:70
msgid "Ticket code is ambiguous on list"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:71
msgid "Order not approved"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:72
msgid "Checked-in Tickets"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:73
msgid "Valid Tickets"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:74
msgid "Currently inside"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:75
#: pretix/static/pretixcontrol/js/ui/question.js:136
#: pretix/static/pretixpresale/js/ui/questions.js:271
msgid "Yes"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:76
#: pretix/static/pretixcontrol/js/ui/question.js:137
#: pretix/static/pretixpresale/js/ui/questions.js:271
msgid "No"
msgstr ""
#: pretix/static/lightbox/js/lightbox.js:96
msgid "close"
msgstr ""
@@ -210,8 +377,8 @@ msgid ""
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:135
#: pretix/static/pretixbase/js/asynctask.js:192
#: pretix/static/pretixbase/js/asynctask.js:196
#: pretix/static/pretixbase/js/asynctask.js:193
#: pretix/static/pretixbase/js/asynctask.js:197
#: pretix/static/pretixcontrol/js/ui/mail.js:24
msgid "An error of type {code} occurred."
msgstr ""
@@ -227,28 +394,28 @@ msgstr ""
msgid "The request took too long. Please try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:198
#: pretix/static/pretixbase/js/asynctask.js:199
#: pretix/static/pretixcontrol/js/ui/mail.js:26
msgid ""
"We currently cannot reach the server. Please try again. Error code: {code}"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:226
#: pretix/static/pretixbase/js/asynctask.js:227
msgid "We are processing your request …"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:229
#: pretix/static/pretixbase/js/asynctask.js:230
msgid ""
"We are currently sending your request to the server. If this takes longer "
"than one minute, please check your internet connection and then reload this "
"page and try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:286
#: pretix/static/pretixbase/js/asynctask.js:287
msgid "If this takes longer than a few minutes, please contact us."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:341
#: pretix/static/pretixbase/js/asynctask.js:342
msgid "Close message"
msgstr ""
@@ -260,6 +427,165 @@ msgstr ""
msgid "Press Ctrl-C to copy!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:80
msgid "Edit"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:86
msgid "Visualize"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:96
msgid ""
"Your rule always filters by product or variation, but the following products "
"or variations are not contained in any of your rule parts so people with "
"these tickets will not get in:"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:99
msgid "Please double-check if this was intentional."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:4
msgid "All of the conditions below (AND)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:5
msgid "At least one of the conditions below (OR)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:6
msgid "Event start"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:7
msgid "Event end"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:8
msgid "Event admission"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:9
msgid "custom date and time"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:10
msgid "custom time"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:11
msgid "Tolerance (minutes)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:12
msgid "Add condition"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:13
msgid "minutes"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:14
msgid "Duplicate"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:15
msgctxt "entry_status"
msgid "present"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:16
msgctxt "entry_status"
msgid "absent"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:29
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:35
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:41
msgid "is one of"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:47
msgid "is before"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:51
msgid "is after"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:57
msgid "="
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:117
msgid "Product"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:121
msgid "Product variation"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:125
msgid "Gate"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:129
msgid "Current date and time"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:133
msgid "Current day of the week (1 = Monday, 7 = Sunday)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:137
msgid "Current entry status"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:141
msgid "Number of previous entries"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:145
msgid "Number of previous entries since midnight"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:149
msgid "Number of previous entries since"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:153
msgid "Number of previous entries before"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:157
msgid "Number of days with a previous entry"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:161
msgid "Number of days with a previous entry since"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:165
msgid "Number of days with a previous entry before"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:169
msgid "Minutes since last entry (-1 on first entry)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:173
msgid "Minutes since first entry (-1 on first entry)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/django-interop.ts:47
msgid "Error: Product not found!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/django-interop.ts:54
msgid "Error: Variation not found!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:171
msgid "Check-in QR"
msgstr ""
@@ -358,23 +684,23 @@ msgstr ""
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:842
#: pretix/static/pretixcontrol/js/ui/main.js:844
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:845
#: pretix/static/pretixcontrol/js/ui/main.js:847
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1003
#: pretix/static/pretixcontrol/js/ui/main.js:1005
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1043
#: pretix/static/pretixcontrol/js/ui/main.js:1045
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1124
#: pretix/static/pretixcontrol/js/ui/main.js:1126
msgid "You have unsaved changes!"
msgstr ""
@@ -394,16 +720,6 @@ msgstr ""
msgid "Count"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:136
#: pretix/static/pretixpresale/js/ui/questions.js:271
msgid "Yes"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:137
#: pretix/static/pretixpresale/js/ui/questions.js:271
msgid "No"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/subevent.js:112
msgid "(one more date)"
msgid_plural "({num} more dates)"
@@ -470,164 +786,196 @@ msgid "Google Pay"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:16
#: pretix/static/pretixpresale/widget/src/i18n.ts:30
msgctxt "widget"
msgid "Quantity"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:17
#: pretix/static/pretixpresale/widget/src/i18n.ts:31
msgctxt "widget"
msgid "Decrease quantity"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:18
#: pretix/static/pretixpresale/widget/src/i18n.ts:32
msgctxt "widget"
msgid "Increase quantity"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:19
#: pretix/static/pretixpresale/widget/src/i18n.ts:33
msgctxt "widget"
msgid "Filter events by"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:20
#: pretix/static/pretixpresale/widget/src/i18n.ts:34
msgctxt "widget"
msgid "Filter"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:21
#: pretix/static/pretixpresale/widget/src/i18n.ts:35
msgctxt "widget"
msgid "Price"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:22
#: pretix/static/pretixpresale/widget/src/i18n.ts:36
#, javascript-format
msgctxt "widget"
msgid "Original price: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:23
#: pretix/static/pretixpresale/widget/src/i18n.ts:37
#, javascript-format
msgctxt "widget"
msgid "New price: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:24
#: pretix/static/pretixpresale/widget/src/i18n.ts:38
msgctxt "widget"
msgid "Select"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:25
#: pretix/static/pretixpresale/widget/src/i18n.ts:39
#, javascript-format
msgctxt "widget"
msgid "Select %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:26
#: pretix/static/pretixpresale/widget/src/i18n.ts:40
#, javascript-format
msgctxt "widget"
msgid "Select variant %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:27
#: pretix/static/pretixpresale/widget/src/i18n.ts:41
msgctxt "widget"
msgid "Sold out"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:28
#: pretix/static/pretixpresale/widget/src/i18n.ts:42
msgctxt "widget"
msgid "Buy"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:29
#: pretix/static/pretixpresale/widget/src/i18n.ts:43
msgctxt "widget"
msgid "Register"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:30
#: pretix/static/pretixpresale/widget/src/i18n.ts:44
msgctxt "widget"
msgid "Reserved"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:31
#: pretix/static/pretixpresale/widget/src/i18n.ts:45
msgctxt "widget"
msgid "FREE"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:32
#: pretix/static/pretixpresale/widget/src/i18n.ts:46
msgctxt "widget"
msgid "from %(currency)s %(price)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:33
#: pretix/static/pretixpresale/widget/src/i18n.ts:47
#, javascript-format
msgctxt "widget"
msgid "Image of %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:34
#: pretix/static/pretixpresale/widget/src/i18n.ts:48
msgctxt "widget"
msgid "incl. %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:35
#: pretix/static/pretixpresale/widget/src/i18n.ts:49
msgctxt "widget"
msgid "plus %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:36
#: pretix/static/pretixpresale/widget/src/i18n.ts:50
msgctxt "widget"
msgid "incl. taxes"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:37
#: pretix/static/pretixpresale/widget/src/i18n.ts:51
msgctxt "widget"
msgid "plus taxes"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:38
#: pretix/static/pretixpresale/widget/src/i18n.ts:52
#, javascript-format
msgctxt "widget"
msgid "currently available: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:39
#: pretix/static/pretixpresale/widget/src/i18n.ts:53
msgctxt "widget"
msgid "Only available with a voucher"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:40
#: pretix/static/pretixpresale/js/widget/widget.js:43
#: pretix/static/pretixpresale/widget/src/i18n.ts:54
#: pretix/static/pretixpresale/widget/src/i18n.ts:57
msgctxt "widget"
msgid "Not yet available"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:41
#: pretix/static/pretixpresale/widget/src/i18n.ts:55
msgctxt "widget"
msgid "Not available anymore"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:42
#: pretix/static/pretixpresale/widget/src/i18n.ts:56
msgctxt "widget"
msgid "Currently not available"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:44
#: pretix/static/pretixpresale/widget/src/i18n.ts:58
#, javascript-format
msgctxt "widget"
msgid "minimum amount to order: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:45
#: pretix/static/pretixpresale/widget/src/i18n.ts:59
msgctxt "widget"
msgid "Close ticket shop"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:46
#: pretix/static/pretixpresale/widget/src/i18n.ts:60
msgctxt "widget"
msgid "The ticket shop could not be loaded."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:47
#: pretix/static/pretixpresale/widget/src/i18n.ts:61
msgctxt "widget"
msgid ""
"There are currently a lot of users in this ticket shop. Please open the shop "
@@ -635,21 +983,25 @@ msgid ""
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:49
#: pretix/static/pretixpresale/widget/src/i18n.ts:62
msgctxt "widget"
msgid "Open ticket shop"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:50
#: pretix/static/pretixpresale/widget/src/i18n.ts:63
msgctxt "widget"
msgid "Checkout"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:51
#: pretix/static/pretixpresale/widget/src/i18n.ts:64
msgctxt "widget"
msgid "The cart could not be created. Please try again later"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:52
#: pretix/static/pretixpresale/widget/src/i18n.ts:65
msgctxt "widget"
msgid ""
"We could not create your cart, since there are currently too many users in "
@@ -657,11 +1009,13 @@ msgid ""
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:54
#: pretix/static/pretixpresale/widget/src/i18n.ts:66
msgctxt "widget"
msgid "Waiting list"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:55
#: pretix/static/pretixpresale/widget/src/i18n.ts:67
msgctxt "widget"
msgid ""
"You currently have an active cart for this event. If you select more "
@@ -669,96 +1023,115 @@ msgid ""
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:57
#: pretix/static/pretixpresale/widget/src/i18n.ts:68
msgctxt "widget"
msgid "Resume checkout"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:58
#: pretix/static/pretixpresale/widget/src/i18n.ts:69
msgctxt "widget"
msgid "Redeem a voucher"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:59
#: pretix/static/pretixpresale/widget/src/i18n.ts:70
msgctxt "widget"
msgid "Redeem"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:60
#: pretix/static/pretixpresale/widget/src/i18n.ts:71
msgctxt "widget"
msgid "Voucher code"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:61
#: pretix/static/pretixpresale/widget/src/i18n.ts:72
msgctxt "widget"
msgid "Close"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:62
#: pretix/static/pretixpresale/widget/src/i18n.ts:73
msgctxt "widget"
msgid "Close checkout"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:63
#: pretix/static/pretixpresale/widget/src/i18n.ts:74
msgctxt "widget"
msgid "You cannot cancel this operation. Please wait for loading to finish."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:64
#: pretix/static/pretixpresale/widget/src/i18n.ts:75
msgctxt "widget"
msgid "Continue"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:65
#: pretix/static/pretixpresale/widget/src/i18n.ts:76
msgctxt "widget"
msgid "Show variants"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:66
#: pretix/static/pretixpresale/widget/src/i18n.ts:77
msgctxt "widget"
msgid "Hide variants"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:67
#: pretix/static/pretixpresale/widget/src/i18n.ts:78
msgctxt "widget"
msgid "Choose a different event"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:68
#: pretix/static/pretixpresale/widget/src/i18n.ts:79
msgctxt "widget"
msgid "Choose a different date"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:69
#: pretix/static/pretixpresale/widget/src/i18n.ts:80
msgctxt "widget"
msgid "Back"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:70
#: pretix/static/pretixpresale/widget/src/i18n.ts:81
msgctxt "widget"
msgid "Next month"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:71
#: pretix/static/pretixpresale/widget/src/i18n.ts:82
msgctxt "widget"
msgid "Previous month"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:72
#: pretix/static/pretixpresale/widget/src/i18n.ts:83
msgctxt "widget"
msgid "Next week"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:73
#: pretix/static/pretixpresale/widget/src/i18n.ts:84
msgctxt "widget"
msgid "Previous week"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:74
#: pretix/static/pretixpresale/widget/src/i18n.ts:85
msgctxt "widget"
msgid "Open seat selection"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:75
#: pretix/static/pretixpresale/widget/src/i18n.ts:86
msgctxt "widget"
msgid ""
"Some or all ticket categories are currently sold out. If you want, you can "
@@ -767,110 +1140,137 @@ msgid ""
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:76
#: pretix/static/pretixpresale/widget/src/i18n.ts:87
msgctxt "widget"
msgid "Load more"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:78
#: pretix/static/pretixpresale/widget/src/i18n.ts:89
msgid "Mo"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:79
#: pretix/static/pretixpresale/widget/src/i18n.ts:90
msgid "Tu"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:80
#: pretix/static/pretixpresale/widget/src/i18n.ts:91
msgid "We"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:81
#: pretix/static/pretixpresale/widget/src/i18n.ts:92
msgid "Th"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:82
#: pretix/static/pretixpresale/widget/src/i18n.ts:93
msgid "Fr"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:83
#: pretix/static/pretixpresale/widget/src/i18n.ts:94
msgid "Sa"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:84
#: pretix/static/pretixpresale/widget/src/i18n.ts:95
msgid "Su"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:85
#: pretix/static/pretixpresale/widget/src/i18n.ts:96
msgid "Monday"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:86
#: pretix/static/pretixpresale/widget/src/i18n.ts:97
msgid "Tuesday"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:87
#: pretix/static/pretixpresale/widget/src/i18n.ts:98
msgid "Wednesday"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:88
#: pretix/static/pretixpresale/widget/src/i18n.ts:99
msgid "Thursday"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:89
#: pretix/static/pretixpresale/widget/src/i18n.ts:100
msgid "Friday"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:90
#: pretix/static/pretixpresale/widget/src/i18n.ts:101
msgid "Saturday"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:91
#: pretix/static/pretixpresale/widget/src/i18n.ts:102
msgid "Sunday"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:94
#: pretix/static/pretixpresale/widget/src/i18n.ts:105
msgid "January"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:95
#: pretix/static/pretixpresale/widget/src/i18n.ts:106
msgid "February"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:96
#: pretix/static/pretixpresale/widget/src/i18n.ts:107
msgid "March"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:97
#: pretix/static/pretixpresale/widget/src/i18n.ts:108
msgid "April"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:98
#: pretix/static/pretixpresale/widget/src/i18n.ts:109
msgid "May"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:99
#: pretix/static/pretixpresale/widget/src/i18n.ts:110
msgid "June"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:100
#: pretix/static/pretixpresale/widget/src/i18n.ts:111
msgid "July"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:101
#: pretix/static/pretixpresale/widget/src/i18n.ts:112
msgid "August"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:102
#: pretix/static/pretixpresale/widget/src/i18n.ts:113
msgid "September"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:103
#: pretix/static/pretixpresale/widget/src/i18n.ts:114
msgid "October"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:104
#: pretix/static/pretixpresale/widget/src/i18n.ts:115
msgid "November"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:105
#: pretix/static/pretixpresale/widget/src/i18n.ts:116
msgid "December"
msgstr ""
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+423 -23
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-27 14:47+0000\n"
"POT-Creation-Date: 2026-06-28 15:49+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -130,6 +130,7 @@ msgid "Mercado Pago"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:54
#: pretix/static/pretixpresale/js/ui/cart.js:89
msgid "Continue"
msgstr ""
@@ -183,6 +184,172 @@ msgstr ""
msgid "Contacting your bank …"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:34
msgid "Select a check-in list"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:35
msgid "No active check-in lists found."
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:36
msgid "Switch check-in list"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:37
msgid "Search results"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:38
msgid "No tickets found"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:39
msgid "Result"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:40
msgid "This ticket requires special attention"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:41
msgid "Switch direction"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:42
msgid "Entry"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:43
msgid "Exit"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:44
msgid "Scan a ticket or search and press return…"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:45
msgid "Load more"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:46
msgid "Valid"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:47
msgid "Unpaid"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:48
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:49
msgid "Canceled"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:50
msgid "Confirmed"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:51
msgid "Approval pending"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:52
msgid "Redeemed"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:53
msgid "Cancel"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:55
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:64
msgid "Ticket not paid"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:56
msgid "This ticket is not yet paid. Do you want to continue anyways?"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:57
msgid "Additional information required"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:58
msgid "Valid ticket"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:59
msgid "Exit recorded"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:60
msgid "Ticket already used"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:61
msgid "Information required"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:62
msgid "Unknown ticket"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:63
msgid "Ticket type not allowed here"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:65
msgid "Entry not allowed"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:66
msgid "Ticket code revoked/changed"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:67
msgid "Ticket blocked"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:68
msgid "Ticket not valid at this time"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:69
msgid "Order canceled"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:70
msgid "Ticket code is ambiguous on list"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:71
msgid "Order not approved"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:72
msgid "Checked-in Tickets"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:73
msgid "Valid Tickets"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:74
msgid "Currently inside"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:75
#: pretix/static/pretixcontrol/js/ui/question.js:136
#: pretix/static/pretixpresale/js/ui/questions.js:271
msgid "Yes"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:76
#: pretix/static/pretixcontrol/js/ui/question.js:137
#: pretix/static/pretixpresale/js/ui/questions.js:271
msgid "No"
msgstr ""
#: pretix/static/lightbox/js/lightbox.js:96
msgid "close"
msgstr ""
@@ -210,8 +377,8 @@ msgid ""
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:135
#: pretix/static/pretixbase/js/asynctask.js:192
#: pretix/static/pretixbase/js/asynctask.js:196
#: pretix/static/pretixbase/js/asynctask.js:193
#: pretix/static/pretixbase/js/asynctask.js:197
#: pretix/static/pretixcontrol/js/ui/mail.js:24
msgid "An error of type {code} occurred."
msgstr ""
@@ -227,28 +394,28 @@ msgstr ""
msgid "The request took too long. Please try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:198
#: pretix/static/pretixbase/js/asynctask.js:199
#: pretix/static/pretixcontrol/js/ui/mail.js:26
msgid ""
"We currently cannot reach the server. Please try again. Error code: {code}"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:226
#: pretix/static/pretixbase/js/asynctask.js:227
msgid "We are processing your request …"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:229
#: pretix/static/pretixbase/js/asynctask.js:230
msgid ""
"We are currently sending your request to the server. If this takes longer "
"than one minute, please check your internet connection and then reload this "
"page and try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:286
#: pretix/static/pretixbase/js/asynctask.js:287
msgid "If this takes longer than a few minutes, please contact us."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:341
#: pretix/static/pretixbase/js/asynctask.js:342
msgid "Close message"
msgstr ""
@@ -260,6 +427,165 @@ msgstr ""
msgid "Press Ctrl-C to copy!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:80
msgid "Edit"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:86
msgid "Visualize"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:96
msgid ""
"Your rule always filters by product or variation, but the following products "
"or variations are not contained in any of your rule parts so people with "
"these tickets will not get in:"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:99
msgid "Please double-check if this was intentional."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:4
msgid "All of the conditions below (AND)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:5
msgid "At least one of the conditions below (OR)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:6
msgid "Event start"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:7
msgid "Event end"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:8
msgid "Event admission"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:9
msgid "custom date and time"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:10
msgid "custom time"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:11
msgid "Tolerance (minutes)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:12
msgid "Add condition"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:13
msgid "minutes"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:14
msgid "Duplicate"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:15
msgctxt "entry_status"
msgid "present"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:16
msgctxt "entry_status"
msgid "absent"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:29
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:35
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:41
msgid "is one of"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:47
msgid "is before"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:51
msgid "is after"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:57
msgid "="
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:117
msgid "Product"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:121
msgid "Product variation"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:125
msgid "Gate"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:129
msgid "Current date and time"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:133
msgid "Current day of the week (1 = Monday, 7 = Sunday)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:137
msgid "Current entry status"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:141
msgid "Number of previous entries"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:145
msgid "Number of previous entries since midnight"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:149
msgid "Number of previous entries since"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:153
msgid "Number of previous entries before"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:157
msgid "Number of days with a previous entry"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:161
msgid "Number of days with a previous entry since"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:165
msgid "Number of days with a previous entry before"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:169
msgid "Minutes since last entry (-1 on first entry)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:173
msgid "Minutes since first entry (-1 on first entry)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/django-interop.ts:47
msgid "Error: Product not found!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/django-interop.ts:54
msgid "Error: Variation not found!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:171
msgid "Check-in QR"
msgstr ""
@@ -358,23 +684,23 @@ msgstr ""
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:842
#: pretix/static/pretixcontrol/js/ui/main.js:844
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:845
#: pretix/static/pretixcontrol/js/ui/main.js:847
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1003
#: pretix/static/pretixcontrol/js/ui/main.js:1005
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1043
#: pretix/static/pretixcontrol/js/ui/main.js:1045
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1124
#: pretix/static/pretixcontrol/js/ui/main.js:1126
msgid "You have unsaved changes!"
msgstr ""
@@ -394,16 +720,6 @@ msgstr ""
msgid "Count"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:136
#: pretix/static/pretixpresale/js/ui/questions.js:271
msgid "Yes"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:137
#: pretix/static/pretixpresale/js/ui/questions.js:271
msgid "No"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/subevent.js:112
msgid "(one more date)"
msgid_plural "({num} more dates)"
@@ -470,164 +786,196 @@ msgid "Google Pay"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:16
#: pretix/static/pretixpresale/widget/src/i18n.ts:30
msgctxt "widget"
msgid "Quantity"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:17
#: pretix/static/pretixpresale/widget/src/i18n.ts:31
msgctxt "widget"
msgid "Decrease quantity"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:18
#: pretix/static/pretixpresale/widget/src/i18n.ts:32
msgctxt "widget"
msgid "Increase quantity"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:19
#: pretix/static/pretixpresale/widget/src/i18n.ts:33
msgctxt "widget"
msgid "Filter events by"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:20
#: pretix/static/pretixpresale/widget/src/i18n.ts:34
msgctxt "widget"
msgid "Filter"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:21
#: pretix/static/pretixpresale/widget/src/i18n.ts:35
msgctxt "widget"
msgid "Price"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:22
#: pretix/static/pretixpresale/widget/src/i18n.ts:36
#, javascript-format
msgctxt "widget"
msgid "Original price: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:23
#: pretix/static/pretixpresale/widget/src/i18n.ts:37
#, javascript-format
msgctxt "widget"
msgid "New price: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:24
#: pretix/static/pretixpresale/widget/src/i18n.ts:38
msgctxt "widget"
msgid "Select"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:25
#: pretix/static/pretixpresale/widget/src/i18n.ts:39
#, javascript-format
msgctxt "widget"
msgid "Select %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:26
#: pretix/static/pretixpresale/widget/src/i18n.ts:40
#, javascript-format
msgctxt "widget"
msgid "Select variant %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:27
#: pretix/static/pretixpresale/widget/src/i18n.ts:41
msgctxt "widget"
msgid "Sold out"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:28
#: pretix/static/pretixpresale/widget/src/i18n.ts:42
msgctxt "widget"
msgid "Buy"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:29
#: pretix/static/pretixpresale/widget/src/i18n.ts:43
msgctxt "widget"
msgid "Register"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:30
#: pretix/static/pretixpresale/widget/src/i18n.ts:44
msgctxt "widget"
msgid "Reserved"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:31
#: pretix/static/pretixpresale/widget/src/i18n.ts:45
msgctxt "widget"
msgid "FREE"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:32
#: pretix/static/pretixpresale/widget/src/i18n.ts:46
msgctxt "widget"
msgid "from %(currency)s %(price)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:33
#: pretix/static/pretixpresale/widget/src/i18n.ts:47
#, javascript-format
msgctxt "widget"
msgid "Image of %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:34
#: pretix/static/pretixpresale/widget/src/i18n.ts:48
msgctxt "widget"
msgid "incl. %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:35
#: pretix/static/pretixpresale/widget/src/i18n.ts:49
msgctxt "widget"
msgid "plus %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:36
#: pretix/static/pretixpresale/widget/src/i18n.ts:50
msgctxt "widget"
msgid "incl. taxes"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:37
#: pretix/static/pretixpresale/widget/src/i18n.ts:51
msgctxt "widget"
msgid "plus taxes"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:38
#: pretix/static/pretixpresale/widget/src/i18n.ts:52
#, javascript-format
msgctxt "widget"
msgid "currently available: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:39
#: pretix/static/pretixpresale/widget/src/i18n.ts:53
msgctxt "widget"
msgid "Only available with a voucher"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:40
#: pretix/static/pretixpresale/js/widget/widget.js:43
#: pretix/static/pretixpresale/widget/src/i18n.ts:54
#: pretix/static/pretixpresale/widget/src/i18n.ts:57
msgctxt "widget"
msgid "Not yet available"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:41
#: pretix/static/pretixpresale/widget/src/i18n.ts:55
msgctxt "widget"
msgid "Not available anymore"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:42
#: pretix/static/pretixpresale/widget/src/i18n.ts:56
msgctxt "widget"
msgid "Currently not available"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:44
#: pretix/static/pretixpresale/widget/src/i18n.ts:58
#, javascript-format
msgctxt "widget"
msgid "minimum amount to order: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:45
#: pretix/static/pretixpresale/widget/src/i18n.ts:59
msgctxt "widget"
msgid "Close ticket shop"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:46
#: pretix/static/pretixpresale/widget/src/i18n.ts:60
msgctxt "widget"
msgid "The ticket shop could not be loaded."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:47
#: pretix/static/pretixpresale/widget/src/i18n.ts:61
msgctxt "widget"
msgid ""
"There are currently a lot of users in this ticket shop. Please open the shop "
@@ -635,21 +983,25 @@ msgid ""
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:49
#: pretix/static/pretixpresale/widget/src/i18n.ts:62
msgctxt "widget"
msgid "Open ticket shop"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:50
#: pretix/static/pretixpresale/widget/src/i18n.ts:63
msgctxt "widget"
msgid "Checkout"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:51
#: pretix/static/pretixpresale/widget/src/i18n.ts:64
msgctxt "widget"
msgid "The cart could not be created. Please try again later"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:52
#: pretix/static/pretixpresale/widget/src/i18n.ts:65
msgctxt "widget"
msgid ""
"We could not create your cart, since there are currently too many users in "
@@ -657,11 +1009,13 @@ msgid ""
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:54
#: pretix/static/pretixpresale/widget/src/i18n.ts:66
msgctxt "widget"
msgid "Waiting list"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:55
#: pretix/static/pretixpresale/widget/src/i18n.ts:67
msgctxt "widget"
msgid ""
"You currently have an active cart for this event. If you select more "
@@ -669,96 +1023,115 @@ msgid ""
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:57
#: pretix/static/pretixpresale/widget/src/i18n.ts:68
msgctxt "widget"
msgid "Resume checkout"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:58
#: pretix/static/pretixpresale/widget/src/i18n.ts:69
msgctxt "widget"
msgid "Redeem a voucher"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:59
#: pretix/static/pretixpresale/widget/src/i18n.ts:70
msgctxt "widget"
msgid "Redeem"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:60
#: pretix/static/pretixpresale/widget/src/i18n.ts:71
msgctxt "widget"
msgid "Voucher code"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:61
#: pretix/static/pretixpresale/widget/src/i18n.ts:72
msgctxt "widget"
msgid "Close"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:62
#: pretix/static/pretixpresale/widget/src/i18n.ts:73
msgctxt "widget"
msgid "Close checkout"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:63
#: pretix/static/pretixpresale/widget/src/i18n.ts:74
msgctxt "widget"
msgid "You cannot cancel this operation. Please wait for loading to finish."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:64
#: pretix/static/pretixpresale/widget/src/i18n.ts:75
msgctxt "widget"
msgid "Continue"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:65
#: pretix/static/pretixpresale/widget/src/i18n.ts:76
msgctxt "widget"
msgid "Show variants"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:66
#: pretix/static/pretixpresale/widget/src/i18n.ts:77
msgctxt "widget"
msgid "Hide variants"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:67
#: pretix/static/pretixpresale/widget/src/i18n.ts:78
msgctxt "widget"
msgid "Choose a different event"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:68
#: pretix/static/pretixpresale/widget/src/i18n.ts:79
msgctxt "widget"
msgid "Choose a different date"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:69
#: pretix/static/pretixpresale/widget/src/i18n.ts:80
msgctxt "widget"
msgid "Back"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:70
#: pretix/static/pretixpresale/widget/src/i18n.ts:81
msgctxt "widget"
msgid "Next month"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:71
#: pretix/static/pretixpresale/widget/src/i18n.ts:82
msgctxt "widget"
msgid "Previous month"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:72
#: pretix/static/pretixpresale/widget/src/i18n.ts:83
msgctxt "widget"
msgid "Next week"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:73
#: pretix/static/pretixpresale/widget/src/i18n.ts:84
msgctxt "widget"
msgid "Previous week"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:74
#: pretix/static/pretixpresale/widget/src/i18n.ts:85
msgctxt "widget"
msgid "Open seat selection"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:75
#: pretix/static/pretixpresale/widget/src/i18n.ts:86
msgctxt "widget"
msgid ""
"Some or all ticket categories are currently sold out. If you want, you can "
@@ -767,110 +1140,137 @@ msgid ""
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:76
#: pretix/static/pretixpresale/widget/src/i18n.ts:87
msgctxt "widget"
msgid "Load more"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:78
#: pretix/static/pretixpresale/widget/src/i18n.ts:89
msgid "Mo"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:79
#: pretix/static/pretixpresale/widget/src/i18n.ts:90
msgid "Tu"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:80
#: pretix/static/pretixpresale/widget/src/i18n.ts:91
msgid "We"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:81
#: pretix/static/pretixpresale/widget/src/i18n.ts:92
msgid "Th"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:82
#: pretix/static/pretixpresale/widget/src/i18n.ts:93
msgid "Fr"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:83
#: pretix/static/pretixpresale/widget/src/i18n.ts:94
msgid "Sa"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:84
#: pretix/static/pretixpresale/widget/src/i18n.ts:95
msgid "Su"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:85
#: pretix/static/pretixpresale/widget/src/i18n.ts:96
msgid "Monday"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:86
#: pretix/static/pretixpresale/widget/src/i18n.ts:97
msgid "Tuesday"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:87
#: pretix/static/pretixpresale/widget/src/i18n.ts:98
msgid "Wednesday"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:88
#: pretix/static/pretixpresale/widget/src/i18n.ts:99
msgid "Thursday"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:89
#: pretix/static/pretixpresale/widget/src/i18n.ts:100
msgid "Friday"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:90
#: pretix/static/pretixpresale/widget/src/i18n.ts:101
msgid "Saturday"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:91
#: pretix/static/pretixpresale/widget/src/i18n.ts:102
msgid "Sunday"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:94
#: pretix/static/pretixpresale/widget/src/i18n.ts:105
msgid "January"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:95
#: pretix/static/pretixpresale/widget/src/i18n.ts:106
msgid "February"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:96
#: pretix/static/pretixpresale/widget/src/i18n.ts:107
msgid "March"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:97
#: pretix/static/pretixpresale/widget/src/i18n.ts:108
msgid "April"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:98
#: pretix/static/pretixpresale/widget/src/i18n.ts:109
msgid "May"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:99
#: pretix/static/pretixpresale/widget/src/i18n.ts:110
msgid "June"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:100
#: pretix/static/pretixpresale/widget/src/i18n.ts:111
msgid "July"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:101
#: pretix/static/pretixpresale/widget/src/i18n.ts:112
msgid "August"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:102
#: pretix/static/pretixpresale/widget/src/i18n.ts:113
msgid "September"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:103
#: pretix/static/pretixpresale/widget/src/i18n.ts:114
msgid "October"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:104
#: pretix/static/pretixpresale/widget/src/i18n.ts:115
msgid "November"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:105
#: pretix/static/pretixpresale/widget/src/i18n.ts:116
msgid "December"
msgstr ""
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+423 -23
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-27 14:47+0000\n"
"POT-Creation-Date: 2026-06-28 15:49+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -131,6 +131,7 @@ msgid "Mercado Pago"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:54
#: pretix/static/pretixpresale/js/ui/cart.js:89
msgid "Continue"
msgstr ""
@@ -184,6 +185,172 @@ msgstr ""
msgid "Contacting your bank …"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:34
msgid "Select a check-in list"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:35
msgid "No active check-in lists found."
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:36
msgid "Switch check-in list"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:37
msgid "Search results"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:38
msgid "No tickets found"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:39
msgid "Result"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:40
msgid "This ticket requires special attention"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:41
msgid "Switch direction"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:42
msgid "Entry"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:43
msgid "Exit"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:44
msgid "Scan a ticket or search and press return…"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:45
msgid "Load more"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:46
msgid "Valid"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:47
msgid "Unpaid"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:48
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:49
msgid "Canceled"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:50
msgid "Confirmed"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:51
msgid "Approval pending"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:52
msgid "Redeemed"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:53
msgid "Cancel"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:55
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:64
msgid "Ticket not paid"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:56
msgid "This ticket is not yet paid. Do you want to continue anyways?"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:57
msgid "Additional information required"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:58
msgid "Valid ticket"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:59
msgid "Exit recorded"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:60
msgid "Ticket already used"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:61
msgid "Information required"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:62
msgid "Unknown ticket"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:63
msgid "Ticket type not allowed here"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:65
msgid "Entry not allowed"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:66
msgid "Ticket code revoked/changed"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:67
msgid "Ticket blocked"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:68
msgid "Ticket not valid at this time"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:69
msgid "Order canceled"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:70
msgid "Ticket code is ambiguous on list"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:71
msgid "Order not approved"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:72
msgid "Checked-in Tickets"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:73
msgid "Valid Tickets"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:74
msgid "Currently inside"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:75
#: pretix/static/pretixcontrol/js/ui/question.js:136
#: pretix/static/pretixpresale/js/ui/questions.js:271
msgid "Yes"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts:76
#: pretix/static/pretixcontrol/js/ui/question.js:137
#: pretix/static/pretixpresale/js/ui/questions.js:271
msgid "No"
msgstr ""
#: pretix/static/lightbox/js/lightbox.js:96
msgid "close"
msgstr ""
@@ -211,8 +378,8 @@ msgid ""
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:135
#: pretix/static/pretixbase/js/asynctask.js:192
#: pretix/static/pretixbase/js/asynctask.js:196
#: pretix/static/pretixbase/js/asynctask.js:193
#: pretix/static/pretixbase/js/asynctask.js:197
#: pretix/static/pretixcontrol/js/ui/mail.js:24
msgid "An error of type {code} occurred."
msgstr ""
@@ -228,28 +395,28 @@ msgstr ""
msgid "The request took too long. Please try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:198
#: pretix/static/pretixbase/js/asynctask.js:199
#: pretix/static/pretixcontrol/js/ui/mail.js:26
msgid ""
"We currently cannot reach the server. Please try again. Error code: {code}"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:226
#: pretix/static/pretixbase/js/asynctask.js:227
msgid "We are processing your request …"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:229
#: pretix/static/pretixbase/js/asynctask.js:230
msgid ""
"We are currently sending your request to the server. If this takes longer "
"than one minute, please check your internet connection and then reload this "
"page and try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:286
#: pretix/static/pretixbase/js/asynctask.js:287
msgid "If this takes longer than a few minutes, please contact us."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:341
#: pretix/static/pretixbase/js/asynctask.js:342
msgid "Close message"
msgstr ""
@@ -261,6 +428,165 @@ msgstr ""
msgid "Press Ctrl-C to copy!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:80
msgid "Edit"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:86
msgid "Visualize"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:96
msgid ""
"Your rule always filters by product or variation, but the following products "
"or variations are not contained in any of your rule parts so people with "
"these tickets will not get in:"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:99
msgid "Please double-check if this was intentional."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:4
msgid "All of the conditions below (AND)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:5
msgid "At least one of the conditions below (OR)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:6
msgid "Event start"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:7
msgid "Event end"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:8
msgid "Event admission"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:9
msgid "custom date and time"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:10
msgid "custom time"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:11
msgid "Tolerance (minutes)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:12
msgid "Add condition"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:13
msgid "minutes"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:14
msgid "Duplicate"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:15
msgctxt "entry_status"
msgid "present"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:16
msgctxt "entry_status"
msgid "absent"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:29
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:35
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:41
msgid "is one of"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:47
msgid "is before"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:51
msgid "is after"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:57
msgid "="
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:117
msgid "Product"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:121
msgid "Product variation"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:125
msgid "Gate"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:129
msgid "Current date and time"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:133
msgid "Current day of the week (1 = Monday, 7 = Sunday)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:137
msgid "Current entry status"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:141
msgid "Number of previous entries"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:145
msgid "Number of previous entries since midnight"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:149
msgid "Number of previous entries since"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:153
msgid "Number of previous entries before"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:157
msgid "Number of days with a previous entry"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:161
msgid "Number of days with a previous entry since"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:165
msgid "Number of days with a previous entry before"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:169
msgid "Minutes since last entry (-1 on first entry)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:173
msgid "Minutes since first entry (-1 on first entry)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/django-interop.ts:47
msgid "Error: Product not found!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/django-interop.ts:54
msgid "Error: Variation not found!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:171
msgid "Check-in QR"
msgstr ""
@@ -359,23 +685,23 @@ msgstr ""
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:842
#: pretix/static/pretixcontrol/js/ui/main.js:844
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:845
#: pretix/static/pretixcontrol/js/ui/main.js:847
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1003
#: pretix/static/pretixcontrol/js/ui/main.js:1005
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1043
#: pretix/static/pretixcontrol/js/ui/main.js:1045
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1124
#: pretix/static/pretixcontrol/js/ui/main.js:1126
msgid "You have unsaved changes!"
msgstr ""
@@ -395,16 +721,6 @@ msgstr ""
msgid "Count"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:136
#: pretix/static/pretixpresale/js/ui/questions.js:271
msgid "Yes"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:137
#: pretix/static/pretixpresale/js/ui/questions.js:271
msgid "No"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/subevent.js:112
msgid "(one more date)"
msgid_plural "({num} more dates)"
@@ -471,164 +787,196 @@ msgid "Google Pay"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:16
#: pretix/static/pretixpresale/widget/src/i18n.ts:30
msgctxt "widget"
msgid "Quantity"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:17
#: pretix/static/pretixpresale/widget/src/i18n.ts:31
msgctxt "widget"
msgid "Decrease quantity"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:18
#: pretix/static/pretixpresale/widget/src/i18n.ts:32
msgctxt "widget"
msgid "Increase quantity"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:19
#: pretix/static/pretixpresale/widget/src/i18n.ts:33
msgctxt "widget"
msgid "Filter events by"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:20
#: pretix/static/pretixpresale/widget/src/i18n.ts:34
msgctxt "widget"
msgid "Filter"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:21
#: pretix/static/pretixpresale/widget/src/i18n.ts:35
msgctxt "widget"
msgid "Price"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:22
#: pretix/static/pretixpresale/widget/src/i18n.ts:36
#, javascript-format
msgctxt "widget"
msgid "Original price: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:23
#: pretix/static/pretixpresale/widget/src/i18n.ts:37
#, javascript-format
msgctxt "widget"
msgid "New price: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:24
#: pretix/static/pretixpresale/widget/src/i18n.ts:38
msgctxt "widget"
msgid "Select"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:25
#: pretix/static/pretixpresale/widget/src/i18n.ts:39
#, javascript-format
msgctxt "widget"
msgid "Select %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:26
#: pretix/static/pretixpresale/widget/src/i18n.ts:40
#, javascript-format
msgctxt "widget"
msgid "Select variant %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:27
#: pretix/static/pretixpresale/widget/src/i18n.ts:41
msgctxt "widget"
msgid "Sold out"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:28
#: pretix/static/pretixpresale/widget/src/i18n.ts:42
msgctxt "widget"
msgid "Buy"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:29
#: pretix/static/pretixpresale/widget/src/i18n.ts:43
msgctxt "widget"
msgid "Register"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:30
#: pretix/static/pretixpresale/widget/src/i18n.ts:44
msgctxt "widget"
msgid "Reserved"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:31
#: pretix/static/pretixpresale/widget/src/i18n.ts:45
msgctxt "widget"
msgid "FREE"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:32
#: pretix/static/pretixpresale/widget/src/i18n.ts:46
msgctxt "widget"
msgid "from %(currency)s %(price)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:33
#: pretix/static/pretixpresale/widget/src/i18n.ts:47
#, javascript-format
msgctxt "widget"
msgid "Image of %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:34
#: pretix/static/pretixpresale/widget/src/i18n.ts:48
msgctxt "widget"
msgid "incl. %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:35
#: pretix/static/pretixpresale/widget/src/i18n.ts:49
msgctxt "widget"
msgid "plus %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:36
#: pretix/static/pretixpresale/widget/src/i18n.ts:50
msgctxt "widget"
msgid "incl. taxes"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:37
#: pretix/static/pretixpresale/widget/src/i18n.ts:51
msgctxt "widget"
msgid "plus taxes"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:38
#: pretix/static/pretixpresale/widget/src/i18n.ts:52
#, javascript-format
msgctxt "widget"
msgid "currently available: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:39
#: pretix/static/pretixpresale/widget/src/i18n.ts:53
msgctxt "widget"
msgid "Only available with a voucher"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:40
#: pretix/static/pretixpresale/js/widget/widget.js:43
#: pretix/static/pretixpresale/widget/src/i18n.ts:54
#: pretix/static/pretixpresale/widget/src/i18n.ts:57
msgctxt "widget"
msgid "Not yet available"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:41
#: pretix/static/pretixpresale/widget/src/i18n.ts:55
msgctxt "widget"
msgid "Not available anymore"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:42
#: pretix/static/pretixpresale/widget/src/i18n.ts:56
msgctxt "widget"
msgid "Currently not available"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:44
#: pretix/static/pretixpresale/widget/src/i18n.ts:58
#, javascript-format
msgctxt "widget"
msgid "minimum amount to order: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:45
#: pretix/static/pretixpresale/widget/src/i18n.ts:59
msgctxt "widget"
msgid "Close ticket shop"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:46
#: pretix/static/pretixpresale/widget/src/i18n.ts:60
msgctxt "widget"
msgid "The ticket shop could not be loaded."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:47
#: pretix/static/pretixpresale/widget/src/i18n.ts:61
msgctxt "widget"
msgid ""
"There are currently a lot of users in this ticket shop. Please open the shop "
@@ -636,21 +984,25 @@ msgid ""
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:49
#: pretix/static/pretixpresale/widget/src/i18n.ts:62
msgctxt "widget"
msgid "Open ticket shop"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:50
#: pretix/static/pretixpresale/widget/src/i18n.ts:63
msgctxt "widget"
msgid "Checkout"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:51
#: pretix/static/pretixpresale/widget/src/i18n.ts:64
msgctxt "widget"
msgid "The cart could not be created. Please try again later"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:52
#: pretix/static/pretixpresale/widget/src/i18n.ts:65
msgctxt "widget"
msgid ""
"We could not create your cart, since there are currently too many users in "
@@ -658,11 +1010,13 @@ msgid ""
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:54
#: pretix/static/pretixpresale/widget/src/i18n.ts:66
msgctxt "widget"
msgid "Waiting list"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:55
#: pretix/static/pretixpresale/widget/src/i18n.ts:67
msgctxt "widget"
msgid ""
"You currently have an active cart for this event. If you select more "
@@ -670,96 +1024,115 @@ msgid ""
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:57
#: pretix/static/pretixpresale/widget/src/i18n.ts:68
msgctxt "widget"
msgid "Resume checkout"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:58
#: pretix/static/pretixpresale/widget/src/i18n.ts:69
msgctxt "widget"
msgid "Redeem a voucher"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:59
#: pretix/static/pretixpresale/widget/src/i18n.ts:70
msgctxt "widget"
msgid "Redeem"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:60
#: pretix/static/pretixpresale/widget/src/i18n.ts:71
msgctxt "widget"
msgid "Voucher code"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:61
#: pretix/static/pretixpresale/widget/src/i18n.ts:72
msgctxt "widget"
msgid "Close"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:62
#: pretix/static/pretixpresale/widget/src/i18n.ts:73
msgctxt "widget"
msgid "Close checkout"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:63
#: pretix/static/pretixpresale/widget/src/i18n.ts:74
msgctxt "widget"
msgid "You cannot cancel this operation. Please wait for loading to finish."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:64
#: pretix/static/pretixpresale/widget/src/i18n.ts:75
msgctxt "widget"
msgid "Continue"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:65
#: pretix/static/pretixpresale/widget/src/i18n.ts:76
msgctxt "widget"
msgid "Show variants"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:66
#: pretix/static/pretixpresale/widget/src/i18n.ts:77
msgctxt "widget"
msgid "Hide variants"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:67
#: pretix/static/pretixpresale/widget/src/i18n.ts:78
msgctxt "widget"
msgid "Choose a different event"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:68
#: pretix/static/pretixpresale/widget/src/i18n.ts:79
msgctxt "widget"
msgid "Choose a different date"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:69
#: pretix/static/pretixpresale/widget/src/i18n.ts:80
msgctxt "widget"
msgid "Back"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:70
#: pretix/static/pretixpresale/widget/src/i18n.ts:81
msgctxt "widget"
msgid "Next month"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:71
#: pretix/static/pretixpresale/widget/src/i18n.ts:82
msgctxt "widget"
msgid "Previous month"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:72
#: pretix/static/pretixpresale/widget/src/i18n.ts:83
msgctxt "widget"
msgid "Next week"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:73
#: pretix/static/pretixpresale/widget/src/i18n.ts:84
msgctxt "widget"
msgid "Previous week"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:74
#: pretix/static/pretixpresale/widget/src/i18n.ts:85
msgctxt "widget"
msgid "Open seat selection"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:75
#: pretix/static/pretixpresale/widget/src/i18n.ts:86
msgctxt "widget"
msgid ""
"Some or all ticket categories are currently sold out. If you want, you can "
@@ -768,110 +1141,137 @@ msgid ""
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:76
#: pretix/static/pretixpresale/widget/src/i18n.ts:87
msgctxt "widget"
msgid "Load more"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:78
#: pretix/static/pretixpresale/widget/src/i18n.ts:89
msgid "Mo"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:79
#: pretix/static/pretixpresale/widget/src/i18n.ts:90
msgid "Tu"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:80
#: pretix/static/pretixpresale/widget/src/i18n.ts:91
msgid "We"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:81
#: pretix/static/pretixpresale/widget/src/i18n.ts:92
msgid "Th"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:82
#: pretix/static/pretixpresale/widget/src/i18n.ts:93
msgid "Fr"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:83
#: pretix/static/pretixpresale/widget/src/i18n.ts:94
msgid "Sa"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:84
#: pretix/static/pretixpresale/widget/src/i18n.ts:95
msgid "Su"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:85
#: pretix/static/pretixpresale/widget/src/i18n.ts:96
msgid "Monday"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:86
#: pretix/static/pretixpresale/widget/src/i18n.ts:97
msgid "Tuesday"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:87
#: pretix/static/pretixpresale/widget/src/i18n.ts:98
msgid "Wednesday"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:88
#: pretix/static/pretixpresale/widget/src/i18n.ts:99
msgid "Thursday"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:89
#: pretix/static/pretixpresale/widget/src/i18n.ts:100
msgid "Friday"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:90
#: pretix/static/pretixpresale/widget/src/i18n.ts:101
msgid "Saturday"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:91
#: pretix/static/pretixpresale/widget/src/i18n.ts:102
msgid "Sunday"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:94
#: pretix/static/pretixpresale/widget/src/i18n.ts:105
msgid "January"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:95
#: pretix/static/pretixpresale/widget/src/i18n.ts:106
msgid "February"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:96
#: pretix/static/pretixpresale/widget/src/i18n.ts:107
msgid "March"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:97
#: pretix/static/pretixpresale/widget/src/i18n.ts:108
msgid "April"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:98
#: pretix/static/pretixpresale/widget/src/i18n.ts:109
msgid "May"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:99
#: pretix/static/pretixpresale/widget/src/i18n.ts:110
msgid "June"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:100
#: pretix/static/pretixpresale/widget/src/i18n.ts:111
msgid "July"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:101
#: pretix/static/pretixpresale/widget/src/i18n.ts:112
msgid "August"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:102
#: pretix/static/pretixpresale/widget/src/i18n.ts:113
msgid "September"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:103
#: pretix/static/pretixpresale/widget/src/i18n.ts:114
msgid "October"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:104
#: pretix/static/pretixpresale/widget/src/i18n.ts:115
msgid "November"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:105
#: pretix/static/pretixpresale/widget/src/i18n.ts:116
msgid "December"
msgstr ""
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2068 -1827
View File
File diff suppressed because it is too large Load Diff
+13 -13
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-27 15:49+0000\n"
"POT-Creation-Date: 2026-06-28 14:44+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -210,8 +210,8 @@ msgid ""
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:135
#: pretix/static/pretixbase/js/asynctask.js:192
#: pretix/static/pretixbase/js/asynctask.js:196
#: pretix/static/pretixbase/js/asynctask.js:193
#: pretix/static/pretixbase/js/asynctask.js:197
#: pretix/static/pretixcontrol/js/ui/mail.js:24
msgid "An error of type {code} occurred."
msgstr ""
@@ -227,28 +227,28 @@ msgstr ""
msgid "The request took too long. Please try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:198
#: pretix/static/pretixbase/js/asynctask.js:199
#: pretix/static/pretixcontrol/js/ui/mail.js:26
msgid ""
"We currently cannot reach the server. Please try again. Error code: {code}"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:226
#: pretix/static/pretixbase/js/asynctask.js:227
msgid "We are processing your request …"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:229
#: pretix/static/pretixbase/js/asynctask.js:230
msgid ""
"We are currently sending your request to the server. If this takes longer "
"than one minute, please check your internet connection and then reload this "
"page and try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:286
#: pretix/static/pretixbase/js/asynctask.js:287
msgid "If this takes longer than a few minutes, please contact us."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:341
#: pretix/static/pretixbase/js/asynctask.js:342
msgid "Close message"
msgstr ""
@@ -358,23 +358,23 @@ msgstr ""
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:842
#: pretix/static/pretixcontrol/js/ui/main.js:844
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:845
#: pretix/static/pretixcontrol/js/ui/main.js:847
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1003
#: pretix/static/pretixcontrol/js/ui/main.js:1005
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1043
#: pretix/static/pretixcontrol/js/ui/main.js:1045
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1124
#: pretix/static/pretixcontrol/js/ui/main.js:1126
msgid "You have unsaved changes!"
msgstr ""
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

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