Compare commits

...

115 Commits

Author SHA1 Message Date
Kara Engelhardt 72e3438632 Ignore expiry date for payments created via api (Z#23232671) 2026-04-27 15:10:09 +02:00
Raphael Michel 771f4f5d1e Turn attendee emails on by default for new events (Z#23213656) (#5598)
* Turn attendee emails on by default for new events (Z#23213656)

I think the thing that makes me most unhappy is that *most* organizers will
probably want to turn off mail_send_order_paid_attendee when they set
ticket_download_pending and I don't think organizers will remember that, but
it also seems complex and weird to create an automatism for it?

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

Co-authored-by: Martin Gross <gross@rami.io>

---------

Co-authored-by: Martin Gross <gross@rami.io>
2026-04-27 15:00:48 +02:00
Raphael Michel 496591bb3b Navigation: suggest event or organizer by domain (Z#23231404) (#6107) 2026-04-27 14:55:59 +02:00
Raphael Michel 88165c098e Subevents: Allow to skip conflicting dates in bulk-creation (Z#23217384) (#6079)
* Subevents: Allow to skip conflicting dates in bulk-creation

* Update src/pretix/control/templates/pretixcontrol/subevents/bulk.html

* Fix overlap calc for consecutive subevents

* Add test for skipping conflicting dates in bulk-creation

---------

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
Co-authored-by: Richard Schreiber <schreiber@rami.io>
Co-authored-by: Kara Engelhardt <engelhardt@pretix.eu>
2026-04-27 14:52:49 +02:00
dependabot[bot] 82a14a4f83 Update pytest-asyncio requirement from >=0.24 to >=1.3.0 (#6108)
Updates the requirements on [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) to permit the latest version.
- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases)
- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.24.0...v1.3.0)

---
updated-dependencies:
- dependency-name: pytest-asyncio
  dependency-version: 1.3.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-04-27 12:39:36 +02:00
Kara Engelhardt ff77a2125a Limit widget frame inner height to 100dvh (Z#23231969)
Fixes a bug where the submit buttons were obscured by the browsers elements on some ios devices
2026-04-27 12:38:32 +02:00
Raphael Michel 97904d8567 Backend: Support are-you-sure for dynamically added form parts (Z#23232506) (#6109) 2026-04-27 12:24:55 +02:00
Raphael Michel a6a9eb6a6a Subevent selection: Order by date before name (Z#23231460) (#6111) 2026-04-27 12:23:17 +02:00
Raphael Michel b000dff134 Invoices: Allow to use currency rates from National Bank of Poland (#6100) 2026-04-21 15:14:10 +02:00
Kara Engelhardt ba75de7e7d Handle existing cart with empty session in presale views (PRETIXEU-D9Y) 2026-04-21 13:05:42 +02:00
Raphael Michel 35e1df28d9 Overhaul contribution guide & add a AI policy (#6038)
* Overhaul contribution guide & add a AI policy

* Fix broken links
2026-04-21 11:32:56 +02:00
Martin Gross 7e457f7430 Set max_length to 70 but for all name fields together and not only every single one. 2026-04-21 10:45:00 +02:00
Martin Gross 5faa85ed40 isort 2026-04-21 10:45:00 +02:00
Martin Gross 1b88a84a83 Move validation into form field. 2026-04-21 10:45:00 +02:00
Martin Gross 447cffa7a8 Customer Accounts: Limit length; reject URLs in name 2026-04-21 10:45:00 +02:00
dependabot[bot] 6d255bb9cc Update defusedcsv requirement from >=1.1.0 to >=3.0.0 (#6105)
Updates the requirements on [defusedcsv](https://github.com/raphaelm/defusedcsv) to permit the latest version.
- [Commits](https://github.com/raphaelm/defusedcsv/compare/v1.1.0...v3.0.0)

---
updated-dependencies:
- dependency-name: defusedcsv
  dependency-version: 3.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-04-20 23:50:50 +02:00
dependabot[bot] 4fe405886e Update tlds requirement from >=2026021400 to >=2026041800 (#6104)
Updates the requirements on [tlds](https://github.com/kichik/tlds) to permit the latest version.
- [Commits](https://github.com/kichik/tlds/commits)

---
updated-dependencies:
- dependency-name: tlds
  dependency-version: '2026041800'
  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-04-20 22:44:03 +02:00
Richard Schreiber b7d3e8a80a Add invoice numbers to paymentlist export (Z#23227966) (#6097) 2026-04-20 17:55:44 +02:00
Raphael Michel d0d76ffddc Delete unused code (#6026)
* Delete unused code

* Delete template
2026-04-20 16:56:50 +02:00
dependabot[bot] c04be5c0d9 Update cryptography requirement from >=44.0.0 to >=46.0.7 (#6084)
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/44.0.0...46.0.7)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 46.0.7
  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-04-20 15:09:49 +02:00
dependabot[bot] ee1a8420a5 Update sentry-sdk requirement from ==2.57.* to ==2.58.* (#6095)
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.57.0...2.58.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-version: 2.58.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-04-20 15:09:33 +02:00
dependabot[bot] d9000c2a66 Update tlds requirement from >=2020041600 to >=2026021400 (#6088) 2026-04-20 15:09:04 +02:00
Yasunobu YesNo Kawaguchi 4530d864d3 Translations: Update Japanese
Currently translated at 100.0% (6287 of 6287 strings)

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

powered by weblate
2026-04-20 10:09:50 +02:00
Yasunobu YesNo Kawaguchi b968266611 Translations: Update Japanese
Currently translated at 100.0% (6287 of 6287 strings)

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

powered by weblate
2026-04-20 10:09:50 +02:00
Yasunobu YesNo Kawaguchi 640518c1b3 Translations: Update Japanese
Currently translated at 100.0% (6287 of 6287 strings)

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

powered by weblate
2026-04-20 10:09:50 +02:00
Nikolai 0715144a31 Translations: Update Danish
Currently translated at 48.5% (3055 of 6287 strings)

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

powered by weblate
2026-04-20 10:09:50 +02:00
Tim 58ea7c8656 Translations: Update Spanish
Currently translated at 100.0% (6287 of 6287 strings)

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

powered by weblate
2026-04-20 10:09:50 +02:00
Tim a8fe6f505e Translations: Update Spanish
Currently translated at 100.0% (6287 of 6287 strings)

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

powered by weblate
2026-04-20 10:09:50 +02:00
Nikolai baeec92203 Translations: Update Danish
Currently translated at 47.7% (3004 of 6287 strings)

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

powered by weblate
2026-04-20 10:09:50 +02:00
Tim 2f9ac05184 Translations: Update Spanish
Currently translated at 100.0% (6287 of 6287 strings)

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

powered by weblate
2026-04-20 10:09:50 +02:00
Mie Frydensbjerg 4beea63b49 Translations: Update Danish
Currently translated at 46.5% (2925 of 6287 strings)

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

powered by weblate
2026-04-16 11:01:56 +02:00
Nikolai 5e49df0ef6 Translations: Update Danish
Currently translated at 46.1% (2904 of 6287 strings)

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

powered by weblate
2026-04-16 11:01:56 +02:00
pajowu b3bb9fccb5 Translations: Update Danish
Currently translated at 44.2% (2784 of 6287 strings)

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

powered by weblate
2026-04-16 11:01:56 +02:00
Lukas Bockstaller e3ffd66691 Giftcard/Reusable Media API: fix expand permission check (Z#23230608) (#6091)
* add failing tests

* add permission checks in to_representation

* only overwrite final representation not the serializer

* styling

* include review
2026-04-15 15:59:08 +02:00
Martin Gross 0f2ebb8687 PPv2: Fix permission-check for ISU (event.settings.general:write to event.settings.payment:write) 2026-04-14 17:02:47 +02:00
Richard Schreiber efd887b439 API: fix PDF-download name (Z#23231496) 2026-04-14 14:13:14 +02:00
pajowu 8690d65e99 Do not show payment text of canceled and failed payments on invoice (Z#23231070) (#6075) 2026-04-14 13:02:12 +02:00
Richard Schreiber 5682d3ed56 Do not force PDFs to be downloaded (Z#23225892) (#5994)
* Display invoice and tickets inline in browser (Z#23225892)

* Use FileResponse filename for AnswerDownload

* Use inline for PDF-view in pretix-control editor

* use as_attachment for API FileResponses

* do not ignore csp even for disposition=inline

* use as_attachment for file responses in control

* remove unused code

* improve code style

* Invoice preview inline

* do not force download on tickets in backend

* do not force download on AnswerDownload

* imrpove code style

* improve code style

* fix missing int str conversion

* Apply suggestions from code review

Co-authored-by: luelista <mira@teamwiki.de>

---------

Co-authored-by: luelista <mira@teamwiki.de>
2026-04-14 09:12:09 +02:00
pajowu 059ff6c99b Allow buttons to reuse cart (Z#23226853) (#6047)
* Allow buttons to reuse cart (Z#23226853)

* Always keep cart of buttons with items set
2026-04-13 19:32:33 +02:00
Mie Frydensbjerg f46fc7fa69 Translations: Update Danish
Currently translated at 44.2% (2784 of 6287 strings)

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

powered by weblate
2026-04-13 16:02:34 +02:00
pajowu 3473fa738d Fix AttributeError in CheckPrivateNetworkMixin (#6076) 2026-04-10 12:47:53 +02:00
Ruud Hendrickx 6c7163406e Translations: Update Dutch (Belgium)
Currently translated at 82.9% (5214 of 6287 strings)

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

powered by weblate
2026-04-10 11:47:38 +02:00
Hijiri Umemoto 49729d2c87 Translations: Update Japanese
Currently translated at 100.0% (6287 of 6287 strings)

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

powered by weblate
2026-04-10 11:47:38 +02:00
pajowu e80b4b560b customer login: open pw reset link in new tab (Z#23231027) (#6074)
This way customers don't have to break their checkout flow and the link works in a widgets iframe
2026-04-10 11:44:36 +02:00
pajowu 0bb04ca8f0 Email: Check custom SMTP IP at usage time 2026-04-10 10:57:08 +02:00
Raphael Michel f50548cd02 Fix crash on build 2026-04-10 10:34:15 +02:00
Raphael Michel bb450e1be9 Add default protection for SSRF 2026-04-10 10:34:15 +02:00
Kian Cross 6d07530d2b Waiting list: group product choices by category (#6006)
* Group waiting list product choices by category

Use optgroups to group products by category in the waiting list selection
dropdown.

Products are normally separated in the UI by category grouping, but this
context is lost in the waiting list form. When multiple products share the
same name, this can make it difficult for customers to distinguish between
them.

* Add tests for waiting list initial selection with optgroups

Verify that the initial product selection (via `?item=` and `?var=`
query parameters) works correctly when choices are grouped by category
into `<optgroup>`s. Covers both plain items and items with variations.
2026-04-10 09:14:34 +02:00
Kara Engelhardt 5d7ee584d9 Fix AttributeError when running tests with debug toolbar installed 2026-04-09 13:21:54 +02:00
Lukas Bockstaller 58cce4b85e adds fallback to PaymentDetailsField (PRETIXEU-D6V) (#6041)
* adds fallback to PaymentDetailsField

* return empty object instead of info_data
2026-04-09 12:32:46 +02:00
luelista aa420d4353 Do not reset event list type automatically (Z#23226325) (#6068)
Co-authored-by: Kara Engelhardt <engelhardt@pretix.eu>
2026-04-08 18:47:45 +02:00
dependabot[bot] d2ca217cd8 Bump brace-expansion from 1.1.12 to 1.1.13 in /src/pretix/static/npm_dir (#6050)
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.12 to 1.1.13.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.12...v1.1.13)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.13
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-08 15:30:15 +02:00
dependabot[bot] cb6d3967a0 Bump picomatch from 2.3.1 to 2.3.2 in /src/pretix/static/npm_dir (#6030)
Bumps [picomatch](https://github.com/micromatch/picomatch) from 2.3.1 to 2.3.2.
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-08 15:30:05 +02:00
Raphael Michel 221cbd15ab [SECURITY] API: Add missing event filter for check-ins 2026-04-08 13:57:55 +02:00
Lukas Bockstaller 5c7104634e Order import: handle mixed endings of last line (Z#23230806) (#6066)
* handle mixed line endings in import

* formatting
2026-04-08 13:25:38 +02:00
Richard Schreiber c037fd865b Fix multi-product order edit with seats (#6063) 2026-04-08 11:02:58 +02:00
Kara Engelhardt 12171e0665 Fix copy-and-paste errors 2026-04-07 14:39:33 +02:00
Kara Engelhardt 444963e952 tests: Remove on_commit monkeypatch 2026-04-07 14:39:33 +02:00
Kara Engelhardt a57810cf41 tests: replace broken monkeypatching with TransactionTestCase 2026-04-07 14:39:33 +02:00
Kara Engelhardt 2e2e57d231 Fix typo in test detection, improve check
A non-empty string is truthy, making the the for-loop useless, as the first item in inspect.stack() is always the for-loop itself, which then lead to the function returning immediately.
This commit
* fixes this typo
* changes the loop to ignore the first element of instpect.stack() (which is the loop itself)
* ignores django-internal code

This should create something similar to what I suspect the code was intended to do originally.
2026-04-07 14:39:33 +02:00
Kara Engelhardt fc7e8ea67a Log new properties when changing device 2026-04-07 13:28:38 +02:00
Raphael Michel 23d1673403 Fix typo 2026-04-02 21:43:36 +02:00
Raphael Michel 92d1830f3b Exporters: Pass state about staff_session 2026-04-02 21:03:42 +02:00
Raphael Michel d411c36414 Exporters: Give access to authentication infos and allow empty permissions (#5979)
* Exporters: Give access to authentication infos

* Allow exporters to have empty permission

* Use a protocol
2026-04-02 15:44:36 +02:00
Raphael Michel 84e12fea32 Dockerfile: Use Python 3.13 (#6028) 2026-04-02 13:18:04 +02:00
Kara Engelhardt b6518449d6 Add placeholder for checked in addons (Z#23230009) 2026-04-02 12:06:00 +02:00
Ruud Hendrickx 50c99e1239 Translations: Update Dutch (Belgium)
Currently translated at 82.8% (5210 of 6287 strings)

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

powered by weblate
2026-04-02 12:01:26 +02:00
dependabot[bot] e70452ee47 Update pillow requirement from ==12.1.* to ==12.2.*
Updates the requirements on [pillow](https://github.com/python-pillow/Pillow) to permit the latest version.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/12.1.0...12.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-02 12:01:19 +02:00
Raphael Michel 666b496ab4 Allow to configure a readonly DB connection (#5978) 2026-04-01 13:46:52 +02:00
Richard Schreiber 8bd0665f37 Fix password-manager username not saved on customer account creation (#6043)
* Fix password-manager username not saved on customer account creation

* Fix tests/make email not required
2026-04-01 12:00:03 +02:00
Raphael Michel ed1459b1dd Order change form: Allow to add multiple identical positions (Z#23227479) (#6044)
* Order change form: Allow to add multiple identical positions (Z#23227479)

* New implementation
2026-04-01 11:54:48 +02:00
Raphael Michel 8c251029b9 Fix useless cart sessions being created (#6045)
* Do not create useless cart session accessing invoice address

* Skip useless code paths in CartMixin

* Do not create cart session on view with active session

* Create regression tests
2026-04-01 09:29:14 +02:00
dependabot[bot] 531f697b9a Update redis requirement from ==7.1.* to ==7.4.*
Updates the requirements on [redis](https://github.com/redis/redis-py) to permit the latest version.
- [Release notes](https://github.com/redis/redis-py/releases)
- [Changelog](https://github.com/redis/redis-py/blob/master/CHANGES)
- [Commits](https://github.com/redis/redis-py/compare/v7.1.0...v7.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-01 09:20:14 +02:00
Ruud Hendrickx 719ad7104d Translations: Update Dutch (Belgium)
Currently translated at 82.1% (5164 of 6287 strings)

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

powered by weblate
2026-04-01 09:19:49 +02:00
Ruud Hendrickx dcb0eb765f Translations: Update Dutch (informal) (nl_Informal)
Currently translated at 100.0% (6287 of 6287 strings)

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

powered by weblate
2026-04-01 09:19:49 +02:00
CVZ-es 86b5191e8b Translations: Update Spanish
Currently translated at 100.0% (6287 of 6287 strings)

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

powered by weblate
2026-04-01 09:19:49 +02:00
Ruud Hendrickx b0714886bc Translations: Update Dutch
Currently translated at 100.0% (6287 of 6287 strings)

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

powered by weblate
2026-04-01 09:19:49 +02:00
CVZ-es 438f70c730 Translations: Update French
Currently translated at 100.0% (6287 of 6287 strings)

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

powered by weblate
2026-04-01 09:19:49 +02:00
Ruud Hendrickx 608b150bf8 Translations: Update Dutch (Belgium)
Currently translated at 79.6% (5007 of 6287 strings)

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

powered by weblate
2026-04-01 09:19:49 +02:00
Renne Rocha c0df7c6142 Translations: Update Portuguese (Brazil)
Currently translated at 95.1% (5980 of 6287 strings)

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

powered by weblate
2026-04-01 09:19:49 +02:00
dependabot[bot] b2ea172a60 Update sentry-sdk requirement from ==2.56.* to ==2.57.*
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.56.0...2.57.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-01 09:19:35 +02:00
Raphael Michel a2cef22ea8 Bump version to 2026.4.0.dev0 2026-03-30 15:01:39 +02:00
Raphael Michel 3843448812 Bump version to 2026.3.0 2026-03-30 15:01:30 +02:00
Kara Engelhardt 49893ca9df Fix crash in mail_send_task for nonexistant mails 2026-03-30 14:57:56 +02:00
Raphael Michel 4eade5070e Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (6287 of 6287 strings)

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

powered by weblate
2026-03-30 14:01:13 +02:00
Raphael Michel 32b1997208 Translations: Update German
Currently translated at 100.0% (6287 of 6287 strings)

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

powered by weblate
2026-03-30 14:01:13 +02:00
Raphael Michel eaf4a310f6 Translations: Update wordlist 2026-03-30 13:59:37 +02:00
Raphael Michel 8dc0f7c1b2 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2026-03-30 13:26:02 +02:00
CVZ-es dd3e6c4692 Translations: Update Spanish
Currently translated at 100.0% (256 of 256 strings)

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

powered by weblate
2026-03-30 13:21:45 +02:00
Kara Engelhardt c7437336b4 Add length help text to customer password forms
Also cleans up dead code, as `validate_password` always returns None or raises a ValidationError.
2026-03-30 11:25:14 +02:00
luelista 4c0c775baa Improve 2fa type selection UI (#6031) 2026-03-27 13:47:10 +01:00
Linnea Thelander 394652a5ff Translations: Update Swedish
Currently translated at 88.0% (5530 of 6283 strings)

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

powered by weblate
2026-03-27 10:05:21 +01:00
Ivano Voghera 3f50d065ec Translations: Update Italian
Currently translated at 40.0% (2515 of 6283 strings)

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

powered by weblate
2026-03-27 10:05:21 +01:00
Ivano Voghera 4121061267 Translations: Update Italian
Currently translated at 40.0% (2515 of 6283 strings)

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

powered by weblate
2026-03-26 18:50:24 +01:00
Linnea Thelander aed2220139 Translations: Update Swedish
Currently translated at 76.9% (197 of 256 strings)

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

powered by weblate
2026-03-26 18:50:24 +01:00
Linnea Thelander 4b2c54d38e Translations: Update Swedish
Currently translated at 88.0% (5530 of 6283 strings)

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

powered by weblate
2026-03-26 18:50:24 +01:00
Ivano Voghera 0113a3dc1f Translations: Update Italian
Currently translated at 39.9% (2507 of 6283 strings)

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

powered by weblate
2026-03-26 18:50:24 +01:00
Linnea Thelander c12a8935f1 Translations: Update Swedish
Currently translated at 87.9% (5529 of 6283 strings)

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

powered by weblate
2026-03-26 11:48:45 +01:00
Pietro Isotti a86a6cc2c7 Translations: Update Italian
Currently translated at 39.5% (2486 of 6283 strings)

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

powered by weblate
2026-03-26 11:48:45 +01:00
Pietro Isotti fec2b9a2fc Translations: Update Italian
Currently translated at 68.3% (175 of 256 strings)

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

powered by weblate
2026-03-26 11:48:45 +01:00
Pietro Isotti d847a7e8f8 Translations: Update Italian
Currently translated at 39.2% (2463 of 6283 strings)

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

powered by weblate
2026-03-26 11:48:45 +01:00
Ruud Hendrickx c58a968196 Translations: Update Dutch (Belgium)
Currently translated at 79.1% (4970 of 6283 strings)

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

powered by weblate
2026-03-26 11:48:45 +01:00
Renne Rocha 81cbaca162 Translations: Update Portuguese (Brazil)
Currently translated at 100.0% (256 of 256 strings)

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

powered by weblate
2026-03-26 11:48:45 +01:00
Renne Rocha 218df7a49f Translations: Update Portuguese (Brazil)
Currently translated at 95.1% (5979 of 6283 strings)

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

powered by weblate
2026-03-26 11:48:45 +01:00
Ruud Hendrickx f64343d977 Translations: Update Dutch (Belgium)
Currently translated at 78.7% (4948 of 6283 strings)

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

powered by weblate
2026-03-26 11:48:45 +01:00
Hijiri Umemoto b36c7cbef3 Translations: Update Japanese
Currently translated at 100.0% (256 of 256 strings)

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

powered by weblate
2026-03-26 11:48:45 +01:00
Hijiri Umemoto 18b39ba7cd Translations: Update Japanese
Currently translated at 100.0% (6283 of 6283 strings)

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

powered by weblate
2026-03-26 11:48:45 +01:00
Raphael Michel 1383e967df Hotfix font select in organizer 2026-03-25 15:14:20 +01:00
dependabot[bot] c743e9fd3f Update sentry-sdk requirement from ==2.54.* to ==2.56.* (#6023)
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.54.0...2.56.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-version: 2.56.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-03-25 13:41:13 +01:00
Raphael Michel a71efa6747 Event settings: Workaround for Django 5.2 change (#6025) 2026-03-24 22:00:05 +01:00
Richard Schreiber 4fed47fb9b Fix live_receivers for django 5 2026-03-24 17:14:05 +01:00
Phin Wolkwitz c143d50290 Update django to 5.2 2026-03-24 16:33:28 +01:00
luelista 88cd715ece Always show Organizers and Events menu entries for staff (#6011) 2026-03-24 11:26:54 +01:00
dependabot[bot] 3513de6a45 Update importlib-metadata requirement from ==8.* to ==9.* (#6016)
Updates the requirements on [importlib-metadata](https://github.com/python/importlib_metadata) to permit the latest version.
- [Release notes](https://github.com/python/importlib_metadata/releases)
- [Changelog](https://github.com/python/importlib_metadata/blob/main/NEWS.rst)
- [Commits](https://github.com/python/importlib_metadata/compare/v8.0.0...v9.0.0)

---
updated-dependencies:
- dependency-name: importlib-metadata
  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-03-24 09:09:07 +01:00
Richard Schreiber fd6d3934c0 Remove invoice_address_from_vat_id on save if it is not used 2026-03-23 14:33:17 +01:00
166 changed files with 20011 additions and 17321 deletions
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
name: Packaging
strategy:
matrix:
python-version: ["3.11"]
python-version: ["3.13"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
+4 -4
View File
@@ -24,10 +24,10 @@ jobs:
name: Check gettext syntax
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
python-version: 3.11
python-version: 3.13
- uses: actions/cache@v4
with:
path: ~/.cache/pip
@@ -49,10 +49,10 @@ jobs:
name: Spellcheck
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
python-version: 3.11
python-version: 3.13
- uses: actions/cache@v4
with:
path: ~/.cache/pip
+6 -6
View File
@@ -24,10 +24,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
python-version: 3.11
python-version: 3.13
- uses: actions/cache@v4
with:
path: ~/.cache/pip
@@ -44,10 +44,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
python-version: 3.11
python-version: 3.13
- uses: actions/cache@v4
with:
path: ~/.cache/pip
@@ -64,10 +64,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
python-version: 3.11
python-version: 3.13
- name: Install Dependencies
run: pip3 install licenseheaders
- name: Run licenseheaders
+4 -2
View File
@@ -23,13 +23,15 @@ jobs:
name: Tests
strategy:
matrix:
python-version: ["3.10", "3.11", "3.13"]
python-version: ["3.11", "3.13", "3.14"]
database: [sqlite, postgres]
exclude:
- database: sqlite
python-version: "3.10"
- database: sqlite
python-version: "3.11"
- database: sqlite
python-version: "3.12"
services:
postgres:
image: postgres:15
@@ -81,4 +83,4 @@ jobs:
file: src/coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
if: matrix.database == 'postgres' && matrix.python-version == '3.11'
if: matrix.database == 'postgres' && matrix.python-version == '3.13'
+10 -5
View File
@@ -1,11 +1,16 @@
Contributing to pretix
======================
Hey there and welcome to pretix!
Welcome to pretix, we are happy that you would like to contribute.
Before you do so, please make sure to read the following documents:
* We've got a contributors guide in [our documentation](https://docs.pretix.eu/dev/development/contribution/) together with notes on the [development setup](https://docs.pretix.eu/dev/development/setup.html).
- [Contribution workflow](https://docs.pretix.eu/dev/development/contribution/general.html)
- [AI-assisted contribution policy](https://docs.pretix.eu/dev/development/contribution/ai.html)
- [Coding style and quality](https://docs.pretix.eu/dev/development/contribution/style.html)
- [Development setup](https://docs.pretix.eu/dev/development/setup.html)
- [Code of Conduct](https://docs.pretix.eu/dev/development/contribution/codeofconduct.html)
* Please note that we have a [Code of Conduct](https://docs.pretix.eu/dev/development/contribution/codeofconduct.html) in place that applies to all project contributions, including issues, pull requests, etc.
* Before we can accept a PR from you we'll need you to sign [our CLA](https://pretix.eu/about/en/cla). You can find more information about the how and why in our [License FAQ](https://docs.pretix.eu/trust/licensing/faq/) and in our [license change blog post](https://pretix.eu/about/en/blog/20210412-license/).
Before we can accept your first PR we'll need you to sign [our **Contributor License Agreement** (CLA)](https://pretix.eu/about/en/cla).
You can find more information about the how and why in our [License FAQ](https://docs.pretix.eu/trust/licensing/faq/) and in our [license change blog post](https://pretix.eu/about/en/blog/20210412-license/).
**Before contributing new functionality, always open a discussion first.**
+1 -1
View File
@@ -1,4 +1,4 @@
FROM python:3.11-bookworm
FROM python:3.13-trixie
RUN apt-get update && \
apt-get install -y --no-install-recommends \
+24
View File
@@ -0,0 +1,24 @@
.. _`aipolicy`:
AI-assisted contribution policy
===============================
pretix is maintained by humans.
Every discussion, issue, and pull request is read and reviewed by humans (and sometimes machines, too).
We ask you to respect the time and effort put in by these humans by not sending low-effort, unqualified work, since it puts the burden of validation on the maintainer.
Therefore, the pretix project has strict rules for AI usage:
- **All AI usage in any form must be disclosed.** You must state the tool you used (e.g. Claude Code, Cursor, Amp) along with the extent that the work was AI-assisted.
- **The human-in-the-loop must fully understand all code.** If you can't explain what your changes do and how they interact with the greater system without the aid of AI tools, do not contribute to this project.
- **Issues and discussions can use AI assistance but must have a full human-in-the-loop.** This means that any content generated with AI must have been reviewed and edited by a human before submission. AI is very good at being overly verbose and including noise that distracts from the main point. Humans must do their research and trim this down.
- **No AI-generated media is allowed (art, images, videos, audio, etc.).** Text and code are the only acceptable AI-generated content, per the other rules in this policy.
- **Bad AI drivers will be excluded from the project.** People who produce bad contributions that are clearly AI (slop) will be blocked from our organization without warning.
This policy was inspired by the `ghostty project`_.
.. _ghostty project: https://github.com/ghostty-org/ghostty/blob/main/AI_POLICY.md
+30 -11
View File
@@ -1,23 +1,39 @@
General remarks
===============
Contribution workflow
=====================
You are interested in contributing to pretix? That is awesome!
If youre new to contributing to open source software, dont be afraid. Well happily review your code and give you
constructive and friendly feedback on your changes.
constructive and friendly feedback on your changes. Every contribution should go through the following steps.
First of all, you'll need pretix running locally on your machine. Head over to :ref:`devsetup` to learn how to do this.
Discussion & Design
-------------------
pretix is a large and mature project with more of a decade of history and hopefully many more decades to come.
Keeping pretix in good shape over long timeframes is first and foremost a fight against complexity.
With every additional feature, complexity grows, and both features and complexity are hard to remove.
Even if you are doing the initial work of the contribution, accepting the contribution is not free for us.
Not only will we need to maintain the feature, but every feature adds cost to the maintenance of every other feature it interacts with, and every feature adds effort for users to understand how pretix works.
Therefore, we must carefully select what features we add, based on how well they fit the system in general and of how much use they will be to our larger user base.
We strongly ask you to **create a discussion on GitHub for every new feature idea** outlining the use case and the proposed implementation design.
Pull requests without prior discussion will likely just be closed.
For bug fixes and very minor changes, you can skip this step and open a PR right away.
Development
-----------
To develop your contribution, you'll need pretix running locally on your machine. Head over to :ref:`devsetup` to learn how to do this.
If you run into any problems on your way, please do not hesitate to ask us anytime!
Please note that we bound ourselves to a :ref:`coc` that applies to all communication around the project. You can be
assured that we will not tolerate any form of harassment.
While developing, please have a look at our :ref:`aipolicy` and our guidelines on :ref:`codestyle`.
Sending a patch
---------------
If you improved pretix in any way, we'd be very happy if you contribute it
back to the main code base! The easiest way to do so is to `create a pull request`_
on our `GitHub repository`_.
Once you have a first draft of your changes, please `create a pull request`_ on our `GitHub repository`_.
We recommend that you create a feature branch for every issue you work on so the changes can
be reviewed individually.
@@ -25,14 +41,17 @@ Please use the test suite to check whether your changes break any existing featu
the code style checks to confirm you are consistent with pretix's coding style. You'll
find instructions on this in the :ref:`checksandtests` section of the development setup guide.
We automatically run the tests and the code style check on every pull request on Travis CI and we wont
We automatically run the tests and the code style check on every pull request through GitHub Actions and we wont
accept any pull requests without all tests passing. However, if you don't find out *why* they are not passing,
just send the pull request and tell us we'll be glad to help.
If you add a new feature, please include appropriate documentation into your patch. If you fix a bug,
please include a regression test, i.e. a test that fails without your changes and passes after applying your changes.
Again: If you get stuck, do not hesitate to contact any of us, or Raphael personally at mail@raphaelmichel.de.
Again: If you get stuck, do not hesitate to contact us through GitHub discussions.
Please note that we bound ourselves to a :ref:`coc` that applies to all communication around the project. You can be
assured that we will not tolerate any form of harassment.
.. _create a pull request: https://help.github.com/articles/creating-a-pull-request/
.. _GitHub repository: https://github.com/pretix/pretix
+1
View File
@@ -6,4 +6,5 @@ Contributing to pretix
general
style
ai
codeofconduct
+2 -2
View File
@@ -1,5 +1,7 @@
.. spelling:word-list:: Rebase rebasing
.. _`codestyle`:
Coding style and quality
========================
@@ -28,8 +30,6 @@ Code
Commits and Pull Requests
-------------------------
Most commits should start as pull requests, therefore this applies to the titles of pull requests as well since
the pull request title will become the commit message on merge. We prefer merging with GitHub's "Squash and merge"
feature if the PR contains multiple commits that do not carry value to keep. If there is value in keeping the
+14 -13
View File
@@ -3,7 +3,7 @@ name = "pretix"
dynamic = ["version"]
description = "Reinventing presales, one ticket at a time"
readme = "README.rst"
requires-python = ">=3.10"
requires-python = ">=3.11"
license = {file = "LICENSE"}
keywords = ["tickets", "web", "shop", "ecommerce"]
authors = [
@@ -19,10 +19,11 @@ classifiers = [
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
"Environment :: Web Environment",
"License :: OSI Approved :: GNU Affero General Public License v3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Framework :: Django :: 4.2",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Framework :: Django :: 5.2",
]
dependencies = [
@@ -32,11 +33,11 @@ dependencies = [
"bleach==6.3.*",
"celery==5.6.*",
"chardet==5.2.*",
"cryptography>=44.0.0",
"cryptography>=46.0.7",
"css-inline==0.20.*",
"defusedcsv>=1.1.0",
"defusedcsv>=3.0.0",
"dnspython==2.*",
"Django[argon2]==4.2.*,>=4.2.26",
"Django[argon2]==5.2.*",
"django-bootstrap3==26.1",
"django-compressor==4.6.0",
"django-countries==8.2.*",
@@ -59,7 +60,7 @@ dependencies = [
"dnspython==2.8.*",
"drf_ujson2==1.7.*",
"geoip2==5.*",
"importlib_metadata==8.*", # Polyfill, we can probably drop this once we require Python 3.10+
"importlib_metadata==9.*", # Polyfill, we can probably drop this once we require Python 3.10+
"isoweek",
"jsonschema",
"kombu==5.6.*",
@@ -75,7 +76,7 @@ dependencies = [
"paypal-checkout-serversdk==1.0.*",
"PyJWT==2.12.*",
"phonenumberslite==9.0.*",
"Pillow==12.1.*",
"Pillow==12.2.*",
"pretix-plugin-build",
"protobuf==7.34.*",
"psycopg2-binary",
@@ -89,14 +90,14 @@ dependencies = [
"pytz-deprecation-shim==0.1.*",
"pyuca",
"qrcode==8.2",
"redis==7.1.*",
"redis==7.4.*",
"reportlab==4.4.*",
"requests==2.32.*",
"sentry-sdk==2.54.*",
"sentry-sdk==2.58.*",
"sepaxml==2.7.*",
"stripe==7.9.*",
"text-unidecode==1.*",
"tlds>=2020041600",
"tlds>=2026041800",
"tqdm==4.*",
"ua-parser==1.0.*",
"vobject==0.9.*",
@@ -116,7 +117,7 @@ dev = [
"isort==8.0.*",
"pep8-naming==0.15.*",
"potypo",
"pytest-asyncio>=0.24",
"pytest-asyncio>=1.3.0",
"pytest-cache",
"pytest-cov",
"pytest-django==4.*",
+1 -1
View File
@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "2026.3.0.dev0"
__version__ = "2026.4.0.dev0"
+25 -3
View File
@@ -31,7 +31,9 @@ from pretix.api.serializers.order import OrderPositionSerializer
from pretix.api.serializers.organizer import (
CustomerSerializer, GiftCardSerializer,
)
from pretix.base.models import Order, OrderPosition, ReusableMedium
from pretix.base.models import (
Device, Order, OrderPosition, ReusableMedium, TeamAPIToken,
)
logger = logging.getLogger(__name__)
@@ -80,8 +82,7 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
)
if 'linked_orderposition' in self.context['request'].query_params.getlist('expand'):
# No additional permission check performed, documented limitation of the permission system
# Would get to complex/unusable otherwise since the permission depends on the event
# Permission Check performed in to_representation
self.fields['linked_orderposition'] = NestedOrderPositionSerializer(read_only=True)
else:
self.fields['linked_orderposition'] = serializers.PrimaryKeyRelatedField(
@@ -117,6 +118,27 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
)
return data
def to_representation(self, instance):
r = super().to_representation(instance)
request = self.context.get('request')
# late permission evaluations for checks that depend on the actual linked events
expand_nested = self.context['request'].query_params.getlist('expand')
perm_holder = request.auth if isinstance(request.auth, (Device, TeamAPIToken)) else request.user
if 'linked_orderposition' in expand_nested:
if instance.linked_orderposition is not None:
event = instance.linked_orderposition.order.event
if not perm_holder.has_event_permission(event.organizer, event, 'event.orders:read', request):
r['linked_orderposition'] = {'id': instance.linked_orderposition.id}
if 'linked_giftcard.owner_ticket' in expand_nested:
gc = instance.linked_giftcard
if gc is not None and gc.owner_ticket is not None:
event = gc.owner_ticket.order.event
if not perm_holder.has_event_permission(event.organizer, event, 'event.orders:read', request):
r['linked_giftcard']['owner_ticket'] = {'id': instance.linked_giftcard.owner_ticket.id}
return r
class Meta:
model = ReusableMedium
fields = (
+5 -1
View File
@@ -769,7 +769,11 @@ class PaymentDetailsField(serializers.Field):
pp = value.payment_provider
if not pp:
return {}
return pp.api_payment_details(value)
try:
return pp.api_payment_details(value)
except Exception:
logger.exception("Failed to retrieve payment_details")
return {}
class OrderPaymentSerializer(I18nAwareModelSerializer):
+13
View File
@@ -286,6 +286,19 @@ class GiftCardSerializer(I18nAwareModelSerializer):
)
return data
def to_representation(self, instance):
r = super().to_representation(instance)
request = self.context.get('request')
# late permission evaluations for checks that depend on the actual linked events
if 'owner_ticket' in self.context['request'].query_params.getlist('expand'):
owner_ticket = instance.owner_ticket
if owner_ticket:
event = owner_ticket.order.event
perm_holder = request.auth if isinstance(request.auth, (Device, TeamAPIToken)) else request.user
if not perm_holder.has_event_permission(event.organizer, event, 'event.orders:read', request):
r['owner_ticket'] = {'id': instance.owner_ticket.id}
return r
class Meta:
model = GiftCard
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions', 'owner_ticket',
+1 -1
View File
@@ -1122,7 +1122,7 @@ class CheckinViewSet(viewsets.ReadOnlyModelViewSet):
permission = 'event.orders:read'
def get_queryset(self):
qs = Checkin.all.filter().select_related(
qs = Checkin.all.filter(list__event=self.request.event).select_related(
"position",
"device",
)
+46 -29
View File
@@ -381,12 +381,15 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list')
return resp
else:
resp = FileResponse(ct.file.file, content_type=ct.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
self.request.event.slug.upper(), order.code,
provider.identifier, ct.extension
return FileResponse(
ct.file.file,
filename='{}-{}-{}{}'.format(
self.request.event.slug.upper(), order.code,
provider.identifier, ct.extension
),
as_attachment=True,
content_type=ct.type
)
return resp
@action(detail=True, methods=['POST'])
def mark_paid(self, request, **kwargs):
@@ -1303,14 +1306,17 @@ class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet
raise NotFound()
ftype, ignored = mimetypes.guess_type(answer.file.name)
resp = FileResponse(answer.file, content_type=ftype or 'application/binary')
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}"'.format(
self.request.event.slug.upper(),
pos.order.code,
pos.positionid,
os.path.basename(answer.file.name).split('.', 1)[1]
return FileResponse(
answer.file,
filename='{}-{}-{}-{}'.format(
self.request.event.slug.upper(),
pos.order.code,
pos.positionid,
os.path.basename(answer.file.name).split('.', 1)[1]
),
as_attachment=True,
content_type=ftype or 'application/binary'
)
return resp
@action(detail=True, url_name="printlog", url_path="printlog", methods=["POST"])
def printlog(self, request, **kwargs):
@@ -1365,15 +1371,18 @@ class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet
if hasattr(image_file, 'seek'):
image_file.seek(0)
resp = FileResponse(image_file, content_type=ftype or 'application/binary')
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}.{}"'.format(
self.request.event.slug.upper(),
pos.order.code,
pos.positionid,
key,
extension,
return FileResponse(
image_file,
filename='{}-{}-{}-{}.{}'.format(
self.request.event.slug.upper(),
pos.order.code,
pos.positionid,
key,
extension,
),
as_attachment=True,
content_type=ftype or 'application/binary'
)
return resp
@action(detail=True, url_name='download', url_path='download/(?P<output>[^/]+)')
def download(self, request, output, **kwargs):
@@ -1399,12 +1408,15 @@ class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet
resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list')
return resp
else:
resp = FileResponse(ct.file.file, content_type=ct.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
self.request.event.slug.upper(), pos.order.code, pos.positionid,
provider.identifier, ct.extension
return FileResponse(
ct.file.file,
filename='{}-{}-{}-{}{}'.format(
self.request.event.slug.upper(), pos.order.code, pos.positionid,
provider.identifier, ct.extension
),
as_attachment=True,
content_type=ct.type
)
return resp
@action(detail=True, methods=['POST'])
def regenerate_secrets(self, request, **kwargs):
@@ -1646,6 +1658,7 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
count_waitinglist=False,
force=request.data.get('force', False),
send_mail=send_mail,
ignore_date=True,
)
except Quota.QuotaExceededException:
pass
@@ -1681,7 +1694,8 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
auth=self.request.auth,
count_waitinglist=False,
send_mail=send_mail,
force=force)
force=force,
ignore_date=True)
except Quota.QuotaExceededException as e:
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except PaymentException as e:
@@ -1986,9 +2000,12 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
if not invoice.file:
raise RetryException()
resp = FileResponse(invoice.file.file, content_type='application/pdf')
resp['Content-Disposition'] = 'attachment; filename="{}.pdf"'.format(invoice.number)
return resp
return FileResponse(
invoice.file.file,
filename='{}.pdf'.format(invoice.number),
as_attachment=True,
content_type='application/pdf'
)
@action(detail=True, methods=['POST'])
def transmit(self, request, **kwargs):
+80
View File
@@ -19,7 +19,10 @@
# 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 ipaddress
import logging
import smtplib
import socket
from itertools import groupby
from smtplib import SMTPResponseException
from typing import TypeVar
@@ -237,3 +240,80 @@ def base_renderers(sender, **kwargs):
def get_email_context(**kwargs):
return PlaceholderContext(**kwargs).render_all()
def create_connection(address, timeout=socket.getdefaulttimeout(),
source_address=None, *, all_errors=False):
# Taken from the python stdlib, extended with a check for local ips
host, port = address
exceptions = []
for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
af, socktype, proto, canonname, sa = res
if not getattr(settings, "MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS", False):
ip_addr = ipaddress.ip_address(sa[0])
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")
sock = None
try:
sock = socket.socket(af, socktype, proto)
if timeout is not socket.getdefaulttimeout():
sock.settimeout(timeout)
if source_address:
sock.bind(source_address)
sock.connect(sa)
# Break explicitly a reference cycle
exceptions.clear()
return sock
except socket.error as exc:
if not all_errors:
exceptions.clear() # raise only the last error
exceptions.append(exc)
if sock is not None:
sock.close()
if len(exceptions):
try:
if not all_errors:
raise exceptions[0]
raise ExceptionGroup("create_connection failed", exceptions)
finally:
# Break explicitly a reference cycle
exceptions.clear()
else:
raise socket.error("getaddrinfo returns an empty list")
class CheckPrivateNetworkMixin:
# _get_socket taken 1:1 from smtplib, just with a call to our own create_connection
def _get_socket(self, host, port, timeout):
# This makes it simpler for SMTP_SSL to use the SMTP connect code
# and just alter the socket connection bit.
if timeout is not None and not timeout:
raise ValueError('Non-blocking socket (timeout=0) is not supported')
if self.debuglevel > 0:
self._print_debug('connect: to', (host, port), self.source_address)
return create_connection((host, port), timeout, self.source_address)
class SMTP(CheckPrivateNetworkMixin, smtplib.SMTP):
pass
# SMTP used here instead of mixin, because smtp.SMTP_SSL._get_socket calls super()._get_socket and then wraps this socket
# super()._get_socket needs to be our version from the mixin
class SMTP_SSL(smtplib.SMTP_SSL, SMTP): # noqa: N801
pass
class CheckPrivateNetworkSmtpBackend(EmailBackend):
@property
def connection_class(self):
return SMTP_SSL if self.use_ssl else SMTP
+13 -3
View File
@@ -47,6 +47,7 @@ from django.utils.formats import localize
from django.utils.translation import gettext, gettext_lazy as _
from pretix.base.models import Event
from pretix.base.models.auth import PermissionHolder
from pretix.helpers.safe_openpyxl import ( # NOQA: backwards compatibility for plugins using excel_safe
SafeWorkbook, remove_invalid_excel_chars as excel_safe,
)
@@ -59,11 +60,20 @@ class BaseExporter:
This is the base class for all data exporters
"""
def __init__(self, event, organizer, progress_callback=lambda v: None):
def __init__(self, event, organizer, permission_holder: PermissionHolder=None, progress_callback=lambda v: None):
"""
:param event: Event context, can also be a queryset of events for multi-event exports
:param organizer: Organizer context
:param user: The user who triggered the export (or None).
:param token: The API token that triggered the export (or None).
:param device: The device that triggered the export (or None)
:param progress_callback: Callback function with progress
"""
self.event = event
self.organizer = organizer
self.progress_callback = progress_callback
self.is_multievent = isinstance(event, QuerySet)
self.permission_holder = permission_holder
if isinstance(event, QuerySet):
self.events = event
self.event = None
@@ -180,7 +190,7 @@ class BaseExporter:
return True
@classmethod
def get_required_event_permission(cls) -> str:
def get_required_event_permission(cls) -> Optional[str]:
"""
The permission level required to use this exporter for events. For multi-event-exports, this will be used
to limit the selection of events. Will be ignored if the ``OrganizerLevelExportMixin`` mixin is used.
@@ -195,7 +205,7 @@ class OrganizerLevelExportMixin:
raise TypeError("required_event_permission may not be called on OrganizerLevelExportMixin")
@classmethod
def get_required_organizer_permission(cls) -> str:
def get_required_organizer_permission(cls) -> Optional[str]:
"""
The permission level required to use this exporter. Must be set for organizer-level exports. Set to `None` to
allow everyone with any access to the organizer.
+14
View File
@@ -1103,13 +1103,25 @@ class PaymentListExporter(ListExporter):
def iterate_list(self, form_data):
provider_names = dict(get_all_payment_providers())
i_numbers = Invoice.objects.filter(
order=OuterRef('order_id'),
).values('order').annotate(
m=GroupConcat('full_invoice_no', delimiter=', ')
).values(
'm'
).order_by()
payments = OrderPayment.objects.filter(
order__event__in=self.events,
state__in=form_data.get('payment_states', [])
).annotate(
order_invoice_numbers=Subquery(i_numbers, output_field=CharField()),
).select_related('order').prefetch_related('order__event').order_by('created')
refunds = OrderRefund.objects.filter(
order__event__in=self.events,
state__in=form_data.get('refund_states', [])
).annotate(
order_invoice_numbers=Subquery(i_numbers, output_field=CharField()),
).select_related('order').prefetch_related('order__event').order_by('created')
if form_data.get('end_date_range'):
@@ -1135,6 +1147,7 @@ class PaymentListExporter(ListExporter):
headers = [
_('Event slug'), _('Order'), _('Payment ID'), _('Creation date'), _('Completion date'), _('Status'),
_('Status code'), _('Amount'), _('Payment method'), _('Comment'), _('Matching ID'), _('Payment details'),
_('Invoice numbers'),
]
yield headers
@@ -1172,6 +1185,7 @@ class PaymentListExporter(ListExporter):
obj.comment if isinstance(obj, OrderRefund) else "",
matching_id,
payment_details,
obj.order_invoice_numbers,
]
yield row
+1 -2
View File
@@ -196,8 +196,7 @@ class RegistrationForm(forms.Form):
def clean_password(self):
password1 = self.cleaned_data.get('password', '')
user = User(email=self.cleaned_data.get('email'))
if validate_password(password1, user=user) is not None:
raise forms.ValidationError(_(password_validators_help_texts()), code='pw_invalid')
validate_password(password1, user=user)
return password1
def clean_email(self):
+10 -4
View File
@@ -45,7 +45,6 @@ import pycountry
from django import forms
from django.conf import settings
from django.contrib import messages
from django.contrib.gis.geoip2 import GeoIP2
from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.validators import (
@@ -91,7 +90,7 @@ from pretix.base.settings import (
COUNTRIES_WITH_STATE_IN_ADDRESS, COUNTRY_STATE_LABEL,
PERSON_NAME_SALUTATIONS, PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS,
)
from pretix.base.templatetags.rich_text import rich_text
from pretix.base.templatetags.rich_text import URL_RE, rich_text
from pretix.base.timemachine import time_machine_now
from pretix.control.forms import (
ExtFileField, ExtValidationMixin, SizeValidationMixin, SplitDateTimeField,
@@ -102,6 +101,7 @@ from pretix.helpers.countries import (
from pretix.helpers.escapejson import escapejson_attr
from pretix.helpers.http import get_client_ip
from pretix.helpers.i18n import get_format_without_seconds
from pretix.helpers.security import get_geoip
from pretix.presale.signals import question_form_fields
logger = logging.getLogger(__name__)
@@ -227,9 +227,15 @@ class NamePartsFormField(forms.MultiValueField):
# bots.
r'^[^$€/%§{}<>~]*$',
message=_('Please do not use special characters in names.')
),
RegexValidator(
URL_RE,
inverse_match=True,
message=_('Please do not use special characters in names.')
)
]
}
self.max_length = defaults['max_length']
self.scheme_name = kwargs.pop('scheme')
self.titles = kwargs.pop('titles')
self.scheme = PERSON_NAME_SCHEMES.get(self.scheme_name)
@@ -287,7 +293,7 @@ class NamePartsFormField(forms.MultiValueField):
if self.require_all_fields and not all(v for v in value):
raise forms.ValidationError(self.error_messages['incomplete'], code='required')
if sum(len(v) for v in value.values() if v) > 250:
if sum(len(v) for v in value.values() if v) > (self.max_length or 250):
raise forms.ValidationError(_('Please enter a shorter name.'), code='max_length')
if value.get("salutation") == "empty":
@@ -393,7 +399,7 @@ class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
def guess_country_from_request(request, event):
if settings.HAS_GEOIP:
g = GeoIP2()
g = get_geoip()
try:
res = g.country(get_client_ip(request))
if res['country_code'] and len(res['country_code']) == 2:
@@ -36,8 +36,9 @@ from django.core.management.commands.makemigrations import Command as Parent
from ._migrations import monkeypatch_migrations
monkeypatch_migrations()
class Command(Parent):
pass
def handle(self, *args, **kwargs):
monkeypatch_migrations()
return super().handle(*args, **kwargs)
@@ -64,7 +64,7 @@ class Command(BaseCommand):
if not periodic_task.receivers or periodic_task.sender_receivers_cache.get(self) is NO_RECEIVERS:
return
for receiver in periodic_task._live_receivers(self):
for receiver in periodic_task._live_receivers(self)[0]:
name = f'{receiver.__module__}.{receiver.__name__}'
if options['list_tasks']:
print(name)
@@ -41,16 +41,20 @@ class Migration(migrations.Migration):
name='datetime',
field=models.DateTimeField(),
),
migrations.AlterIndexTogether(
name='logentry',
index_together={('datetime', 'id')},
migrations.AddIndex(
'logentry',
models.Index(fields=('datetime', 'id'), name="pretixbase__datetim_b1fe5a_idx"),
),
migrations.AlterIndexTogether(
name='order',
index_together={('datetime', 'id'), ('last_modified', 'id')},
migrations.AddIndex(
'order',
models.Index(fields=["datetime", "id"], name="pretixbase__datetim_66aff0_idx"),
),
migrations.AlterIndexTogether(
name='transaction',
index_together={('datetime', 'id')},
migrations.AddIndex(
'order',
models.Index(fields=["last_modified", "id"], name="pretixbase__last_mo_4ebf8b_idx"),
),
migrations.AddIndex(
'transaction',
models.Index(fields=('datetime', 'id'), name="pretixbase__datetim_b20405_idx"),
),
]
@@ -61,7 +61,10 @@ class Migration(migrations.Migration):
options={
'ordering': ('identifier', 'type', 'organizer'),
'unique_together': {('identifier', 'type', 'organizer')},
'index_together': {('identifier', 'type', 'organizer'), ('updated', 'id')},
'indexes': [
models.Index(fields=('identifier', 'type', 'organizer'), name='reusable_medium_organizer_index'),
models.Index(fields=('updated', 'id'), name="pretixbase__updated_093277_idx")
],
},
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
-25
View File
@@ -9,31 +9,6 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RenameIndex(
model_name="logentry",
new_name="pretixbase__datetim_b1fe5a_idx",
old_fields=("datetime", "id"),
),
migrations.RenameIndex(
model_name="order",
new_name="pretixbase__datetim_66aff0_idx",
old_fields=("datetime", "id"),
),
migrations.RenameIndex(
model_name="order",
new_name="pretixbase__last_mo_4ebf8b_idx",
old_fields=("last_modified", "id"),
),
migrations.RenameIndex(
model_name="reusablemedium",
new_name="pretixbase__updated_093277_idx",
old_fields=("updated", "id"),
),
migrations.RenameIndex(
model_name="transaction",
new_name="pretixbase__datetim_b20405_idx",
old_fields=("datetime", "id"),
),
migrations.AlterField(
model_name="attendeeprofile",
name="id",
@@ -1,6 +1,6 @@
# Generated by Django 4.2.10 on 2024-04-02 15:16
from django.db import migrations
from django.db import migrations, models
class Migration(migrations.Migration):
@@ -10,8 +10,8 @@ class Migration(migrations.Migration):
]
operations = [
migrations.AlterIndexTogether(
name="reusablemedium",
index_together=set(),
migrations.RemoveIndex(
"reusablemedium",
'reusable_medium_organizer_index',
),
]
+4
View File
@@ -70,6 +70,10 @@ def parse_csv(file, length=None, mode="strict", charset=None):
except ImportError:
charset = file.charset
data = data.decode(charset or "utf-8", mode)
# remove stray linebreaks from the end of the file
data = data.rstrip("\n")
# If the file was modified on a Mac, it only contains \r as line breaks
if '\r' in data and '\n' not in data:
data = data.replace('\r', '\n')
+9 -3
View File
@@ -29,7 +29,9 @@ import inspect
import logging
import os
import threading
from pathlib import Path
import django
from django.conf import settings
from django.db import transaction
@@ -74,10 +76,14 @@ def _transactions_mark_order_dirty(order_id, using=None):
if "PYTEST_CURRENT_TEST" in os.environ:
# We don't care about Order.objects.create() calls in test code so let's try to figure out if this is test code
# or not.
for frame in inspect.stack():
if 'pretix/base/models/orders' in frame.filename:
for frame in inspect.stack()[1:]:
if (
'pretix/base/models/orders' in frame.filename
or Path(frame.filename).is_relative_to(Path(django.__file__).parent)
):
# Ignore model- and django-internal code
continue
elif 'test_' in frame.filename or 'conftest.py in frame.filename':
elif 'test_' in frame.filename or 'conftest.py' in frame.filename:
return
elif 'pretix/' in frame.filename or 'pretix_' in frame.filename:
# This went through non-test code, let's consider it non-test
+21
View File
@@ -38,6 +38,7 @@ import operator
import secrets
from datetime import timedelta
from functools import reduce
from typing import Protocol
from django.conf import settings
from django.contrib.auth.models import (
@@ -67,6 +68,14 @@ class EmailAddressTakenError(IntegrityError):
pass
class PermissionHolder(Protocol):
def has_event_permission(self, organizer, event, perm_name=None, request=None, session_key=None) -> bool:
...
def has_organizer_permission(self, organizer, perm_name=None, request=None):
...
class UserManager(BaseUserManager):
"""
This is the user manager for our custom user model. See the User
@@ -696,6 +705,18 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
return self.teams.exists()
class UserWithStaffSession:
# Wrapper around a User object with a staff session, implementing the PermissionHolder Protocol
def __init__(self, user):
self.user = user
def has_event_permission(self, organizer, event, perm_name=None, request=None, session_key=None) -> bool:
return True
def has_organizer_permission(self, organizer, perm_name=None, request=None):
return True
class UserKnownLoginSource(models.Model):
user = models.ForeignKey('User', on_delete=models.CASCADE, related_name="known_login_sources")
agent_type = models.CharField(max_length=255, null=True, blank=True)
+2 -1
View File
@@ -229,7 +229,7 @@ class Device(LoggedModel):
"""
return self._organizer_permission_set() if self.organizer == organizer else set()
def has_event_permission(self, organizer, event, perm_name=None, request=None) -> bool:
def has_event_permission(self, organizer, event, perm_name=None, request=None, session_key=None) -> bool:
"""
Checks if this token is part of a team that grants access of type ``perm_name``
to the event ``event``.
@@ -238,6 +238,7 @@ class Device(LoggedModel):
:param event: The event to check
:param perm_name: The permission, e.g. ``event.orders:read``
:param request: This parameter is ignored and only defined for compatibility reasons.
:param session_key: This parameter is ignored and only defined for compatibility reasons.
:return: bool
"""
has_event_access = (self.all_events and organizer == self.organizer) or (
+6
View File
@@ -715,6 +715,12 @@ class Event(EventMixin, LoggedModel):
self.settings.name_scheme = 'given_family'
self.settings.payment_banktransfer_invoice_immediately = True
self.settings.low_availability_percentage = 10
self.settings.mail_send_order_free_attendee = True
self.settings.mail_send_order_placed_attendee = True
self.settings.mail_send_order_paid_attendee = True
self.settings.mail_send_order_approved_attendee = True
self.settings.mail_send_order_approved_free_attendee = True
self.settings.mail_text_download_reminder_attendee = True
@property
def social_image(self):
+1 -1
View File
@@ -88,7 +88,7 @@ class LogEntry(models.Model):
class Meta:
ordering = ('-datetime', '-id')
indexes = [models.Index(fields=["datetime", "id"])]
indexes = [models.Index(fields=["datetime", "id"], name="pretixbase__datetim_b1fe5a_idx")]
def display(self):
from pretix.base.logentrytype_registry import log_entry_types
+1 -1
View File
@@ -122,7 +122,7 @@ class ReusableMedium(LoggedModel):
class Meta:
unique_together = (("identifier", "type", "organizer"),)
indexes = [
models.Index(fields=("updated", "id")),
models.Index(fields=("updated", "id"), name="pretixbase__updated_093277_idx"),
]
ordering = "identifier", "type", "organizer"
+5 -5
View File
@@ -336,8 +336,8 @@ class Order(LockModel, LoggedModel):
verbose_name_plural = _("Orders")
ordering = ("-datetime", "-pk")
indexes = [
models.Index(fields=["datetime", "id"]),
models.Index(fields=["last_modified", "id"]),
models.Index(fields=["datetime", "id"], name="pretixbase__datetim_66aff0_idx"),
models.Index(fields=["last_modified", "id"], name="pretixbase__last_mo_4ebf8b_idx"),
]
constraints = [
models.UniqueConstraint(fields=["organizer", "code"], name="order_organizer_code_uniq"),
@@ -590,7 +590,7 @@ class Order(LockModel, LoggedModel):
not kwargs.get('force_save_with_deferred_fields', None) and
(not update_fields or ('require_approval' not in update_fields and 'status' not in update_fields))
):
_fail("It is unsafe to call save() on an OrderFee with deferred fields since we can't check if you missed "
_fail("It is unsafe to call save() on an Order with deferred fields since we can't check if you missed "
"creating a transaction. Call save(force_save_with_deferred_fields=True) if you really want to do "
"this.")
@@ -2841,7 +2841,7 @@ class OrderPosition(AbstractPosition):
if Transaction.key(self) != self.__initial_transaction_key or self.canceled != self.__initial_canceled or not self.pk:
_transactions_mark_order_dirty(self.order_id, using=kwargs.get('using', None))
elif not kwargs.get('force_save_with_deferred_fields', None):
_fail("It is unsafe to call save() on an OrderFee with deferred fields since we can't check if you missed "
_fail("It is unsafe to call save() on an OrderPosition with deferred fields since we can't check if you missed "
"creating a transaction. Call save(force_save_with_deferred_fields=True) if you really want to do "
"this.")
@@ -3080,7 +3080,7 @@ class Transaction(models.Model):
class Meta:
ordering = 'datetime', 'pk'
indexes = [
models.Index(fields=['datetime', 'id'])
models.Index(fields=['datetime', 'id'], name="pretixbase__datetim_b20405_idx")
]
def save(self, *args, **kwargs):
+8 -1
View File
@@ -319,6 +319,9 @@ class TeamQuerySet(models.QuerySet):
def event_permission_q(cls, perm_name):
from ..permissions import assert_valid_event_permission
if perm_name is None:
return Q()
if perm_name.startswith('can_') and perm_name in OLD_TO_NEW_EVENT_COMPAT: # legacy
return reduce(operator.and_, [cls.event_permission_q(p) for p in OLD_TO_NEW_EVENT_COMPAT[perm_name]])
assert_valid_event_permission(perm_name, allow_legacy=False)
@@ -331,6 +334,9 @@ class TeamQuerySet(models.QuerySet):
def organizer_permission_q(cls, perm_name):
from ..permissions import assert_valid_organizer_permission
if perm_name is None:
return Q()
if perm_name.startswith('can_') and perm_name in OLD_TO_NEW_ORGANIZER_COMPAT: # legacy
return reduce(operator.and_, [cls.organizer_permission_q(p) for p in OLD_TO_NEW_ORGANIZER_COMPAT[perm_name]])
assert_valid_organizer_permission(perm_name, allow_legacy=False)
@@ -550,7 +556,7 @@ class TeamAPIToken(models.Model):
"""
return self.team.organizer_permission_set() if self.team.organizer == organizer else set()
def has_event_permission(self, organizer, event, perm_name=None, request=None) -> bool:
def has_event_permission(self, organizer, event, perm_name=None, request=None, session_key=None) -> bool:
"""
Checks if this token is part of a team that grants access of type ``perm_name``
to the event ``event``.
@@ -559,6 +565,7 @@ class TeamAPIToken(models.Model):
:param event: The event to check
:param perm_name: The permission, e.g. ``event.orders:read``
:param request: This parameter is ignored and only defined for compatibility reasons.
:param session_key: This parameter is ignored and only defined for compatibility reasons.
:return: bool
"""
has_event_access = (self.team.all_events and organizer == self.team.organizer) or (
+16 -5
View File
@@ -54,7 +54,7 @@ from bidi import get_display
from django.conf import settings
from django.contrib.staticfiles import finders
from django.core.exceptions import ValidationError
from django.db.models import Max, Min
from django.db.models import Exists, Max, Min, OuterRef
from django.db.models.fields.files import FieldFile
from django.dispatch import receiver
from django.utils.deconstruct import deconstructible
@@ -76,7 +76,7 @@ from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import Paragraph
from pretix.base.i18n import language
from pretix.base.models import Event, Order, OrderPosition, Question
from pretix.base.models import Checkin, Event, Order, OrderPosition, Question
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import layout_image_variables, layout_text_variables
from pretix.base.templatetags.money import money_filter
@@ -379,6 +379,13 @@ DEFAULT_VARIABLES = OrderedDict((
str(p) for p in generate_compressed_addon_list(op, order, ev)
])
}),
("checked_in_addons", {
"label": _("List of Checked-In Add-Ons"),
"editor_sample": _("Add-on 1\n2x Add-on 2"),
"evaluate": lambda op, order, ev: "\n".join([
str(p) for p in generate_compressed_addon_list(op, order, ev, only_checked_in=True)
])
}),
("organizer", {
"label": _("Organizer name"),
"editor_sample": _("Event organizer company"),
@@ -750,12 +757,16 @@ def get_program_times(op: OrderPosition, ev: Event):
])
def generate_compressed_addon_list(op, order, event):
def generate_compressed_addon_list(op, order, event, only_checked_in=False):
itemcount = defaultdict(int)
addons = [p for p in (
addon_qs = (
op.addons.all() if 'addons' in getattr(op, '_prefetched_objects_cache', {})
else op.addons.select_related('item', 'variation')
) if not p.canceled]
)
if only_checked_in:
addon_qs = addon_qs.filter(Exists(Checkin.objects.filter(position=OuterRef('pk'))), canceled=False)
addons = [p for p in addon_qs if not p.canceled]
for pos in addons:
itemcount[pos.item, pos.variation] += 1
+28
View File
@@ -38,6 +38,7 @@ SOURCE_NAMES = {
None: _('European Central Bank'), # backwards-compatibility
'eu:ecb:eurofxref-daily': _('European Central Bank'),
'cz:cnb:rate-fixing-daily': _('Czech National Bank'),
'pl:nbp:table-a': _('National Bank of Poland'),
}
@@ -49,6 +50,7 @@ def fetch_rates(sender, **kwargs):
source_tasks = {
'eu:ecb:eurofxref-daily': fetch_ecb_rates,
'cz:cnb:rate-fixing-daily': fetch_cnb_cz_rates,
'pl:nbp:table-a': fetch_nbp_pl_rates,
}
for source_name, task in source_tasks.items():
@@ -144,3 +146,29 @@ def fetch_cnb_cz_rates():
rate=rate,
)
)
@app.task()
def fetch_nbp_pl_rates():
"""
Fetches currency rates from the Polish National Bank.
"""
r = requests.get("https://api.nbp.pl/api/exchangerates/tables/A/", headers={
"Accept": "application/json",
})
r.raise_for_status()
data = r.json()[0]
source_date = datetime.strptime(data["effectiveDate"], "%Y-%m-%d").date()
for r in data["rates"]:
rate = Decimal(r["mid"]).quantize(Decimal('0.000001'))
ExchangeRate.objects.update_or_create(
source='pl:nbp:table-a',
source_currency=r["code"],
other_currency='PLN',
defaults=dict(
source_date=source_date,
rate=rate,
)
)
+19 -3
View File
@@ -40,6 +40,7 @@ from pretix.base.models import (
CachedFile, Device, Event, Organizer, ScheduledEventExport, TeamAPIToken,
User, cachedfile_name,
)
from pretix.base.models.auth import UserWithStaffSession
from pretix.base.models.exports import ScheduledOrganizerExport
from pretix.base.services.mail import mail
from pretix.base.services.tasks import (
@@ -211,7 +212,12 @@ def init_event_exporters(event, user=None, token=None, device=None, request=None
if not perm_holder.has_event_permission(event.organizer, event, permission_name, request) and not staff_session:
continue
exporter: BaseExporter = response(event=event, organizer=event.organizer, **kwargs)
exporter: BaseExporter = response(
event=event,
organizer=event.organizer,
permission_holder=token or device or (UserWithStaffSession(user) if staff_session else user),
**kwargs
)
if not exporter.available_for_user(user if user and user.is_authenticated else None):
continue
@@ -243,7 +249,12 @@ def init_organizer_exporters(
continue
if issubclass(response, OrganizerLevelExportMixin):
exporter: BaseExporter = response(event=Event.objects.none(), organizer=organizer, **kwargs)
exporter: BaseExporter = response(
event=Event.objects.none(),
organizer=organizer,
permission_holder=token or device or (UserWithStaffSession(user) if staff_session else user),
**kwargs,
)
try:
if not perm_holder.has_organizer_permission(organizer, response.get_required_organizer_permission(), request) and not staff_session:
@@ -295,7 +306,12 @@ def init_organizer_exporters(
if not _has_permission_on_any_team_cache[permission_name] and not staff_session:
continue
exporter: BaseExporter = response(event=_event_list_cache[permission_name], organizer=organizer, **kwargs)
exporter: BaseExporter = response(
event=_event_list_cache[permission_name],
organizer=organizer,
permission_holder=token or device or (UserWithStaffSession(user) if staff_session else user),
**kwargs,
)
if not exporter.available_for_user(user if user and user.is_authenticated else None):
continue
+15 -1
View File
@@ -58,6 +58,7 @@ from pretix.base.invoicing.transmission import (
from pretix.base.models import (
ExchangeRate, Invoice, InvoiceAddress, InvoiceLine, Order, OrderFee,
)
from pretix.base.models.orders import OrderPayment
from pretix.base.models.tax import EU_CURRENCIES
from pretix.base.services.tasks import (
TransactionAwareProfiledEventTask, TransactionAwareTask,
@@ -102,7 +103,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
introductory = invoice.event.settings.get('invoice_introductory_text', as_type=LazyI18nString)
additional = invoice.event.settings.get('invoice_additional_text', as_type=LazyI18nString)
footer = invoice.event.settings.get('invoice_footer_text', as_type=LazyI18nString)
if lp and lp.payment_provider:
if lp and lp.payment_provider and lp.state not in (OrderPayment.PAYMENT_STATE_FAILED, OrderPayment.PAYMENT_STATE_CANCELED):
if 'payment' in inspect.signature(lp.payment_provider.render_invoice_text).parameters:
payment = str(lp.payment_provider.render_invoice_text(invoice.order, lp))
else:
@@ -204,6 +205,19 @@ def build_invoice(invoice: Invoice) -> Invoice:
invoice.foreign_currency_rate = rate.rate.quantize(Decimal('0.0001'), ROUND_HALF_UP)
invoice.foreign_currency_rate_date = rate.source_date
invoice.foreign_currency_source = 'cz:cnb:rate-fixing-daily'
elif invoice.event.settings.invoice_eu_currencies == 'PLN' and invoice.event.currency != 'PLN':
invoice.foreign_currency_display = 'PLN'
if settings.FETCH_ECB_RATES:
rate = ExchangeRate.objects.filter(
source='pl:nbp:table-a',
source_currency=invoice.event.currency,
other_currency=invoice.foreign_currency_display,
source_date__gt=now().date() - timedelta(days=7)
).first()
if rate:
invoice.foreign_currency_rate = rate.rate.quantize(Decimal('0.0001'), ROUND_HALF_UP)
invoice.foreign_currency_rate_date = rate.source_date
invoice.foreign_currency_source = 'pl:nbp:table-a'
except InvoiceAddress.DoesNotExist:
ia = None
+1 -1
View File
@@ -411,7 +411,7 @@ def mail_send_task(self, **kwargs) -> bool:
try:
outgoing_mail = OutgoingMail.objects.select_for_update(of=OF_SELF).get(pk=outgoing_mail)
except OutgoingMail.DoesNotExist:
logger.info(f"Ignoring job for non existing email {outgoing_mail.guid}")
logger.info(f"Ignoring job for non existing email {outgoing_mail}")
return False
if outgoing_mail.status == OutgoingMail.STATUS_INFLIGHT:
logger.info(f"Ignoring job for inflight email {outgoing_mail.guid}")
+67 -48
View File
@@ -67,9 +67,9 @@ from pretix.base.email import get_email_context
from pretix.base.i18n import get_language_without_region, language
from pretix.base.media import MEDIA_TYPES
from pretix.base.models import (
CartPosition, Device, Event, GiftCard, Item, ItemVariation, Membership,
Order, OrderPayment, OrderPosition, Quota, Seat, SeatCategoryMapping, User,
Voucher,
CartPosition, Device, Event, GiftCard, Item, ItemVariation, LogEntry,
Membership, Order, OrderPayment, OrderPosition, Quota, Seat,
SeatCategoryMapping, User, Voucher,
)
from pretix.base.models.event import SubEvent
from pretix.base.models.orders import (
@@ -1618,7 +1618,7 @@ class OrderChangeManager:
MembershipOperation = namedtuple('MembershipOperation', ('position', 'membership'))
CancelOperation = namedtuple('CancelOperation', ('position', 'price_diff'))
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent', 'seat', 'membership',
'valid_from', 'valid_until', 'is_bundled', 'result'))
'valid_from', 'valid_until', 'is_bundled', 'result', 'count'))
SplitOperation = namedtuple('SplitOperation', ('position',))
FeeValueOperation = namedtuple('FeeValueOperation', ('fee', 'value', 'price_diff'))
AddFeeOperation = namedtuple('AddFeeOperation', ('fee', 'price_diff'))
@@ -1632,16 +1632,24 @@ class OrderChangeManager:
ForceRecomputeOperation = namedtuple('ForceRecomputeOperation', tuple())
class AddPositionResult:
_position: Optional[OrderPosition]
_positions: Optional[List[OrderPosition]]
def __init__(self):
self._position = None
self._positions = None
@property
def position(self) -> OrderPosition:
if self._position is None:
if self._positions is None:
raise RuntimeError("Order position has not been created yet. Call commit() first on OrderChangeManager.")
return self._position
if len(self._positions) != 1:
raise RuntimeError("More than one position created.")
return self._positions[0]
@property
def positions(self) -> List[OrderPosition]:
if self._positions is None:
raise RuntimeError("Order position has not been created yet. Call commit() first on OrderChangeManager.")
return self._positions
def __init__(self, order: Order, user=None, auth=None, notify=True, reissue_invoice=True, allow_blocked_seats=False):
self.order = order
@@ -1848,8 +1856,12 @@ class OrderChangeManager:
def add_position(self, item: Item, variation: ItemVariation, price: Decimal, addon_to: OrderPosition = None,
subevent: SubEvent = None, seat: Seat = None, membership: Membership = None,
valid_from: datetime = None, valid_until: datetime = None) -> 'OrderChangeManager.AddPositionResult':
valid_from: datetime = None, valid_until: datetime = None, count: int = 1) -> 'OrderChangeManager.AddPositionResult':
if count < 1:
raise ValueError("Count must be positive")
if isinstance(seat, str):
if count > 1:
raise ValueError("Cannot combine count > 1 with seat")
if not seat:
seat = None
else:
@@ -1903,14 +1915,14 @@ class OrderChangeManager:
if self.order.event.settings.invoice_include_free or price.gross != Decimal('0.00'):
self._invoice_dirty = True
self._totaldiff_guesstimate += price.gross
self._quotadiff.update(new_quotas)
self._totaldiff_guesstimate += price.gross * count
self._quotadiff.update({q: count for q in new_quotas})
if seat:
self._seatdiff.update([seat])
result = self.AddPositionResult()
self._operations.append(self.AddOperation(item, variation, price, addon_to, subevent, seat, membership,
valid_from, valid_until, is_bundled, result))
valid_from, valid_until, is_bundled, result, count))
return result
def split(self, position: OrderPosition):
@@ -2530,29 +2542,35 @@ class OrderChangeManager:
secret_dirty.remove(position)
position.save(update_fields=['canceled', 'secret'])
elif isinstance(op, self.AddOperation):
pos = OrderPosition.objects.create(
item=op.item, variation=op.variation, addon_to=op.addon_to,
price=op.price.gross, order=self.order, tax_rate=op.price.rate, tax_code=op.price.code,
tax_value=op.price.tax, tax_rule=op.item.tax_rule,
positionid=nextposid, subevent=op.subevent, seat=op.seat,
used_membership=op.membership, valid_from=op.valid_from, valid_until=op.valid_until,
is_bundled=op.is_bundled,
)
nextposid += 1
self.order.log_action('pretix.event.order.changed.add', user=self.user, auth=self.auth, data={
'position': pos.pk,
'item': op.item.pk,
'variation': op.variation.pk if op.variation else None,
'addon_to': op.addon_to.pk if op.addon_to else None,
'price': op.price.gross,
'positionid': pos.positionid,
'membership': pos.used_membership_id,
'subevent': op.subevent.pk if op.subevent else None,
'seat': op.seat.pk if op.seat else None,
'valid_from': op.valid_from.isoformat() if op.valid_from else None,
'valid_until': op.valid_until.isoformat() if op.valid_until else None,
})
op.result._position = pos
new_pos = []
new_logs = []
for i in range(op.count):
pos = OrderPosition.objects.create(
item=op.item, variation=op.variation, addon_to=op.addon_to,
price=op.price.gross, order=self.order, tax_rate=op.price.rate, tax_code=op.price.code,
tax_value=op.price.tax, tax_rule=op.item.tax_rule,
positionid=nextposid, subevent=op.subevent, seat=op.seat,
used_membership=op.membership, valid_from=op.valid_from, valid_until=op.valid_until,
is_bundled=op.is_bundled,
)
nextposid += 1
new_pos.append(pos)
new_logs.append(self.order.log_action('pretix.event.order.changed.add', user=self.user, auth=self.auth, data={
'position': pos.pk,
'item': op.item.pk,
'variation': op.variation.pk if op.variation else None,
'addon_to': op.addon_to.pk if op.addon_to else None,
'price': op.price.gross,
'positionid': pos.positionid,
'membership': pos.used_membership_id,
'subevent': op.subevent.pk if op.subevent else None,
'seat': op.seat.pk if op.seat else None,
'valid_from': op.valid_from.isoformat() if op.valid_from else None,
'valid_until': op.valid_until.isoformat() if op.valid_until else None,
}, save=False))
op.result._positions = new_pos
LogEntry.bulk_create_and_postprocess(new_logs)
elif isinstance(op, self.SplitOperation):
position = position_cache.setdefault(op.position.pk, op.position)
split_positions.append(position)
@@ -2877,7 +2895,7 @@ class OrderChangeManager:
return total
def _check_order_size(self):
if (len(self.order.positions.all()) + len([op for op in self._operations if isinstance(op, self.AddOperation)])) > settings.PRETIX_MAX_ORDER_SIZE:
if (len(self.order.positions.all()) + sum([op.count for op in self._operations if isinstance(op, self.AddOperation)])) > settings.PRETIX_MAX_ORDER_SIZE:
raise OrderError(
self.error_messages['max_order_size'] % {
'max': settings.PRETIX_MAX_ORDER_SIZE,
@@ -2938,7 +2956,7 @@ class OrderChangeManager:
]) + len([
o for o in self._operations if isinstance(o, self.SplitOperation)
])
adds = len([o for o in self._operations if isinstance(o, self.AddOperation)])
adds = sum([o.count for o in self._operations if isinstance(o, self.AddOperation)])
if current > 0 and current - cancels + adds < 1:
raise OrderError(self.error_messages['complete_cancel'])
@@ -2985,17 +3003,18 @@ class OrderChangeManager:
elif isinstance(op, self.CancelOperation) and op.position in positions_to_fake_cart:
fake_cart.remove(positions_to_fake_cart[op.position])
elif isinstance(op, self.AddOperation):
cp = CartPosition(
event=self.event,
item=op.item,
variation=op.variation,
used_membership=op.membership,
subevent=op.subevent,
seat=op.seat,
)
cp.override_valid_from = op.valid_from
cp.override_valid_until = op.valid_until
fake_cart.append(cp)
for i in range(op.count):
cp = CartPosition(
event=self.event,
item=op.item,
variation=op.variation,
used_membership=op.membership,
subevent=op.subevent,
seat=op.seat,
)
cp.override_valid_from = op.valid_from
cp.override_valid_until = op.valid_until
fake_cart.append(cp)
try:
validate_memberships_in_order(self.order.customer, fake_cart, self.event, lock=True, ignored_order=self.order, testmode=self.order.testmode)
except ValidationError as e:
+11 -1
View File
@@ -100,7 +100,7 @@ def primary_font_kwargs():
choices = [('Open Sans', 'Open Sans')]
choices += sorted([
(a, {"title": a, "data": v}) for a, v in get_fonts(pdf_support_required=False).items()
(a, FontSelect.FontOption(title=a, data=v)) for a, v in get_fonts(pdf_support_required=False).items()
], key=lambda a: a[0])
return {
'choices': choices,
@@ -574,6 +574,7 @@ DEFAULTS = {
('True', _('Based on European Central Bank daily rates, whenever the invoice recipient is in an EU '
'country that uses a different currency.')),
('CZK', _('Based on Czech National Bank daily rates, whenever the invoice amount is not in CZK.')),
('PLN', _('Based on National Bank of Poland daily rates, whenever the invoice amount is not in PLN.')),
),
),
'serializer_kwargs': dict(
@@ -582,6 +583,7 @@ DEFAULTS = {
('True', _('Based on European Central Bank daily rates, whenever the invoice recipient is in an EU '
'country that uses a different currency.')),
('CZK', _('Based on Czech National Bank daily rates, whenever the invoice amount is not in CZK.')),
('PLN', _('Based on National Bank of Poland daily rates, whenever the invoice amount is not in PLN.')),
),
),
},
@@ -4148,6 +4150,14 @@ def validate_event_settings(event, settings_dict):
)
]}
)
if (
settings_dict.get('invoice_address_from_vat_id') and
settings_dict.get('invoice_address_from_country') and
settings_dict.get('invoice_address_from_country') not in VAT_ID_COUNTRIES
):
raise ValidationError({
'invoice_address_from_vat_id': _('VAT-ID is not supported for "{}".').format(settings_dict.get('invoice_address_from_country'))
})
payment_term_last = settings_dict.get('payment_term_last')
if payment_term_last and event.presale_end:
+60 -19
View File
@@ -32,6 +32,7 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import logging
import warnings
from typing import Any, Callable, Generic, List, Tuple, TypeVar
@@ -48,6 +49,8 @@ from .plugins import (
PLUGIN_LEVEL_ORGANIZER,
)
logger = logging.getLogger(__name__)
app_cache = {}
T = TypeVar('T')
@@ -60,23 +63,25 @@ def _populate_app_cache():
def get_defining_app(o):
# If sentry packed this in a wrapper, unpack that
if "sentry" in o.__module__:
module = getattr(o, "__module__", None)
if module and "sentry" in module:
o = o.__wrapped__
if hasattr(o, "__mocked_app"):
return o.__mocked_app
# Find the Django application this belongs to
searchpath = o.__module__
searchpath = module or getattr(o.__class__, "__module__", None) or ""
# Core modules are always active
if any(searchpath.startswith(cm) for cm in settings.CORE_MODULES):
if searchpath and any(searchpath.startswith(cm) for cm in settings.CORE_MODULES):
return 'CORE'
if not app_cache:
_populate_app_cache()
while True:
app = None
while searchpath:
app = app_cache.get(searchpath)
if "." not in searchpath or app:
break
@@ -157,7 +162,7 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
if not app_cache:
_populate_app_cache()
for receiver in self._sorted_receivers(sender):
for receiver in self._live_receivers(sender)[0]:
if self._is_receiver_active(sender, receiver):
response = receiver(signal=self, sender=sender, **named)
responses.append((receiver, response))
@@ -179,7 +184,7 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
if not app_cache:
_populate_app_cache()
for receiver in self._sorted_receivers(sender):
for receiver in self._live_receivers(sender)[0]:
if self._is_receiver_active(sender, receiver):
named[chain_kwarg_name] = response
response = receiver(signal=self, sender=sender, **named)
@@ -204,7 +209,7 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
if not app_cache:
_populate_app_cache()
for receiver in self._sorted_receivers(sender):
for receiver in self._live_receivers(sender)[0]:
if self._is_receiver_active(sender, receiver):
try:
response = receiver(signal=self, sender=sender, **named)
@@ -214,17 +219,35 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
responses.append((receiver, response))
return responses
def _sorted_receivers(self, sender):
orig_list = self._live_receivers(sender)
def asend(self, sender: T, **named):
raise NotImplementedError() # NOQA
def asend_robust(self, sender: T, **named):
raise NotImplementedError() # NOQA
def _live_receivers(self, sender):
orig_list, orig_async_list = super()._live_receivers(sender)
if orig_async_list:
logger.error('Async receivers are not supported.')
raise NotImplementedError
def _getattr_fallback_to_class(obj, key):
return getattr(obj, key, getattr(obj.__class__, key))
def _is_core_module(receiver):
m = _getattr_fallback_to_class(receiver, "__module__")
return any(m.startswith(c) for c in settings.CORE_MODULES)
sorted_list = sorted(
orig_list,
key=lambda receiver: (
0 if any(receiver.__module__.startswith(m) for m in settings.CORE_MODULES) else 1,
receiver.__module__,
receiver.__name__,
0 if _is_core_module(receiver) else 1,
_getattr_fallback_to_class(receiver, "__module__"),
_getattr_fallback_to_class(receiver, "__name__"),
)
)
return sorted_list
return sorted_list, []
class EventPluginSignal(PluginSignal[Event]):
@@ -300,23 +323,41 @@ class GlobalSignal(django.dispatch.Signal):
if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS:
return response
for receiver in self._live_receivers(sender):
for receiver in self._live_receivers(sender)[0]:
named[chain_kwarg_name] = response
response = receiver(signal=self, sender=sender, **named)
return response
def asend(self, sender: T, **named):
raise NotImplementedError() # NOQA
def asend_robust(self, sender: T, **named):
raise NotImplementedError() # NOQA
def _live_receivers(self, sender):
# Ensure consistent sorting of receivers
orig_list = super()._live_receivers(sender)
orig_list, orig_async_list = super()._live_receivers(sender)
if orig_async_list:
logger.error('Async receivers are not supported.')
raise NotImplementedError
def _getattr_fallback_to_class(obj, key):
return getattr(obj, key, getattr(obj.__class__, key))
def _is_core_module(receiver):
m = _getattr_fallback_to_class(receiver, "__module__")
return any(m.startswith(c) for c in settings.CORE_MODULES)
sorted_list = sorted(
orig_list,
key=lambda receiver: (
0 if any(receiver.__module__.startswith(m) for m in settings.CORE_MODULES) else 1,
receiver.__module__,
receiver.__name__,
0 if _is_core_module(receiver) else 1,
_getattr_fallback_to_class(receiver, "__module__"),
_getattr_fallback_to_class(receiver, "__name__"),
)
)
return sorted_list
return sorted_list, []
class DeprecatedSignal(GlobalSignal):
+6
View File
@@ -34,6 +34,7 @@
import datetime
import os
from dataclasses import dataclass
from django import forms
from django.conf import settings
@@ -420,6 +421,11 @@ class SplitDateTimeField(forms.SplitDateTimeField):
class FontSelect(forms.RadioSelect):
option_template_name = 'pretixcontrol/font_option.html'
@dataclass
class FontOption:
title: str
data: str
class ItemMultipleChoiceField(SafeModelMultipleChoiceField):
def label_from_instance(self, obj):
+11 -4
View File
@@ -63,7 +63,7 @@ from pretix.base.forms import (
from pretix.base.models import Event, Organizer, TaxRule, Team
from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent
from pretix.base.models.organizer import TeamQuerySet
from pretix.base.models.tax import TAX_CODE_LISTS
from pretix.base.models.tax import TAX_CODE_LISTS, VAT_ID_COUNTRIES
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from pretix.base.services.placeholders import FormPlaceholderMixin
from pretix.base.settings import (
@@ -73,8 +73,8 @@ from pretix.base.settings import (
)
from pretix.base.validators import multimail_validate
from pretix.control.forms import (
MultipleLanguagesWidget, SalesChannelCheckboxSelectMultiple, SlugWidget,
SplitDateTimeField, SplitDateTimePickerWidget,
FontSelect, MultipleLanguagesWidget, SalesChannelCheckboxSelectMultiple,
SlugWidget, SplitDateTimeField, SplitDateTimePickerWidget,
)
from pretix.control.forms.widgets import Select2
from pretix.helpers.countries import CachedCountries
@@ -531,6 +531,13 @@ class EventUpdateForm(I18nModelForm):
class EventSettingsValidationMixin:
def clean_invoice_address_from_vat_id(self):
value = self.cleaned_data.get('invoice_address_from_vat_id')
country = self.cleaned_data.get('invoice_address_from_country')
if value and country and country not in VAT_ID_COUNTRIES:
return None
return value
def clean(self):
data = super().clean()
settings_dict = self.obj.settings.freeze()
@@ -722,7 +729,7 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
del self.fields['event_list_filters']
del self.fields['event_calendar_future_only']
self.fields['primary_font'].choices = [('Open Sans', 'Open Sans')] + sorted([
(a, {"title": a, "data": v}) for a, v in get_fonts(self.event, pdf_support_required=False).items()
(a, FontSelect.FontOption(title=a, data=v)) for a, v in get_fonts(self.event, pdf_support_required=False).items()
], key=lambda a: a[0])
# create "virtual" fields for better UX when editing <name>_asked and <name>_required fields
+8
View File
@@ -331,6 +331,10 @@ class OtherOperationsForm(forms.Form):
class OrderPositionAddForm(forms.Form):
count = forms.IntegerField(
label=_('Number of products to add'),
initial=1,
)
itemvar = forms.ChoiceField(
label=_('Product')
)
@@ -432,6 +436,10 @@ class OrderPositionAddForm(forms.Form):
d['used_membership'] = [m for m in self.memberships if str(m.pk) == d['used_membership']][0]
else:
d['used_membership'] = None
if d.get("count", 1) > 1 and d.get("seat"):
raise ValidationError({
"seat": _("You can not choose a seat when adding multiple products at once.")
})
return d
+11 -1
View File
@@ -28,7 +28,7 @@ from django.forms import formset_factory
from django.forms.utils import ErrorDict
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from i18nfield.forms import I18nInlineFormSet
from pretix.base.forms import I18nModelForm
@@ -102,6 +102,16 @@ class SubEventBulkForm(SubEventForm):
required=False,
limit_choices=('date_from', 'date_to'),
)
skip_if_overlap = forms.BooleanField(
label=pgettext_lazy('subevent', 'Skip dates that overlap with any existing date'),
help_text=pgettext_lazy(
'subevent',
'This can be useful if all your dates happen in the same location and no repeated dates should '
'be created in conflict with existing special events. This respects even inactive dates and works best if '
'all dates have both a start and end time.'
),
required=False,
)
def __init__(self, *args, **kwargs):
self.event = kwargs['event']
+1 -1
View File
@@ -363,7 +363,7 @@ def get_global_navigation(request):
'icon': 'dashboard',
},
]
if request.user.is_in_any_teams:
if request.user.is_in_any_teams or request.user.is_staff:
nav += [
{
'label': _('Events'),
@@ -329,6 +329,7 @@
{{ add_form.custom_error }}
</div>
{% endif %}
{% bootstrap_field add_form.count layout="control" %}
{% bootstrap_field add_form.itemvar layout="control" %}
{% bootstrap_field add_form.price addon_after=request.event.currency layout="control" %}
{% if add_form.addon_to %}
@@ -364,6 +365,7 @@
</div>
<div class="panel-body">
<div class="form-horizontal">
{% bootstrap_field add_position_formset.empty_form.count layout="control" %}
{% bootstrap_field add_position_formset.empty_form.itemvar layout="control" %}
{% bootstrap_field add_position_formset.empty_form.price addon_after=request.event.currency layout="control" %}
{% if add_position_formset.empty_form.addon_to %}
@@ -379,6 +379,8 @@
<i class="fa fa-calendar"></i> {% trans "Add many time slots" %}</button>
</p>
</div>
<hr />
{% bootstrap_field form.skip_if_overlap layout="control" horizontal_label_class='sr-only' horizontal_field_class='col-md-12' %}
</fieldset>
<fieldset>
<legend>{% trans "General information" %}</legend>
@@ -8,7 +8,45 @@
{% csrf_token %}
{% bootstrap_form_errors form %}
{% bootstrap_field form.name layout='horizontal' %}
{% bootstrap_field form.devicetype layout='horizontal' %}
<div class="form-group{% if form.devicetype.errors %} has-error{% endif %}">
<label class="col-md-3 control-label">{% trans "Device type" %}</label>
<div class="col-md-9">
<div>
<div class="big-radio radio">
<label>
<input type="radio" required value="totp" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "totp" %}checked{% endif %}>
<strong>{% trans "Smartphone with Authenticator app" %}</strong><br>
<div class="help-block">
{% blocktrans trimmed %}
Use your smartphone with any Time-based One-Time-Password app like freeOTP, Google Authenticator or Proton Authenticator.
{% endblocktrans %}
</div>
</label>
</div>
<div class="big-radio radio">
<label>
<input type="radio" required value="webauthn" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "webauthn" %}checked{% endif %}>
<strong>{% trans "WebAuthn-compatible hardware token" %}</strong><br>
<div class="help-block">
{% blocktrans trimmed %}
Use a hardware token like the Yubikey, or other biometric authentication like fingerprint or face recognition.
{% endblocktrans %}
</div>
</label>
</div>
</div>
{% if form.devicetype.errors %}
<div class="help-block">
{% for error in form.devicetype.errors %}
<p>{{ error|escape }}</p>
{% endfor %}
</div>
{% endif %}
</div>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Continue" %}
@@ -28,11 +28,6 @@
{% trans "iOS (iTunes)" %}
</a>
</li>
<li>
<a href="https://m.google.com/authenticator">
{% trans "Blackberry (Link via Google)" %}
</a>
</li>
</ul>
</li>
<li>
+1 -1
View File
@@ -641,7 +641,7 @@ def user_index(request):
ctx = {
'widgets': rearrange(widgets),
'can_create_event': request.user.teams.with_organizer_permission("organizer.events:create").exists(),
'can_create_event': request.user.teams.with_organizer_permission("organizer.events:create").exists() or request.user.is_staff,
'upcoming': widgets_for_event_qs(
request,
annotated_event_query(request, lazy=True).filter(
+1 -6
View File
@@ -763,12 +763,7 @@ class InvoicePreview(EventPermissionRequiredMixin, View):
def get(self, request, *args, **kwargs):
fname, ftype, fcontent = build_preview_invoice_pdf(request.event)
resp = HttpResponse(fcontent, content_type=ftype)
if settings.DEBUG:
# attachment is more secure as we're dealing with user-generated stuff here, but inline is much more convenient during debugging
resp['Content-Disposition'] = 'inline; filename="{}"'.format(fname)
resp._csp_ignore = True
else:
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(fname)
resp['Content-Disposition'] = 'inline; filename="{}"'.format(fname)
return resp
@@ -300,5 +300,4 @@ class SysReportView(AdministratorPermissionRequiredMixin, TemplateView):
resp = HttpResponse(data)
resp['Content-Type'] = mime
resp['Content-Disposition'] = 'inline; filename="{}"'.format(name)
resp._csp_ignore = True
return resp
+25 -20
View File
@@ -710,22 +710,26 @@ class OrderDownload(AsyncAction, OrderView):
resp = HttpResponseRedirect(value.file.file.read())
return resp
else:
resp = FileResponse(value.file.file, content_type=value.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
self.request.event.slug.upper(), self.order.code, self.order_position.positionid,
self.output.identifier, value.extension
return FileResponse(
value.file.file,
filename='{}-{}-{}-{}{}'.format(
self.request.event.slug.upper(), self.order.code, self.order_position.positionid,
self.output.identifier, value.extension
),
content_type=value.type
)
return resp
elif isinstance(value, CachedCombinedTicket):
if value.type == 'text/uri-list':
resp = HttpResponseRedirect(value.file.file.read())
return resp
else:
resp = FileResponse(value.file.file, content_type=value.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
self.request.event.slug.upper(), self.order.code, self.output.identifier, value.extension
return FileResponse(
value.file.file,
filename='{}-{}-{}{}'.format(
self.request.event.slug.upper(), self.order.code, self.output.identifier, value.extension
),
content_type=value.type
)
return resp
else:
return redirect(self.get_self_url())
@@ -1831,15 +1835,15 @@ class InvoiceDownload(EventPermissionRequiredMixin, View):
return redirect(self.get_order_url())
try:
resp = FileResponse(self.invoice.file.file, content_type='application/pdf')
return FileResponse(
self.invoice.file.file,
filename='{}.pdf'.format(re.sub("[^a-zA-Z0-9-_.]+", "_", self.invoice.number)),
content_type='application/pdf'
)
except FileNotFoundError:
invoice_pdf_task.apply(args=(self.invoice.pk,))
return self.get(request, *args, **kwargs)
resp['Content-Disposition'] = 'inline; filename="{}.pdf"'.format(re.sub("[^a-zA-Z0-9-_.]+", "_", self.invoice.number))
resp._csp_ignore = True # Some browser's PDF readers do not work with CSP
return resp
class OrderExtend(OrderView):
permission = 'event.orders:write'
@@ -2059,12 +2063,13 @@ class OrderChange(OrderView):
else:
variation = None
try:
ocm.add_position(item, variation,
f.cleaned_data['price'],
f.cleaned_data.get('addon_to'),
f.cleaned_data.get('subevent'),
f.cleaned_data.get('seat'),
f.cleaned_data.get('used_membership'))
for i in range(f.cleaned_data.get("count", 1)):
ocm.add_position(item, variation,
f.cleaned_data['price'],
f.cleaned_data.get('addon_to'),
f.cleaned_data.get('subevent'),
f.cleaned_data.get('seat'),
f.cleaned_data.get('used_membership'))
except OrderError as e:
f.custom_error = str(e)
return False
+1 -1
View File
@@ -1322,7 +1322,7 @@ class DeviceUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
def form_valid(self, form):
if form.has_changed():
self.object.log_action('pretix.device.changed', user=self.request.user, data={
k: getattr(self.object, k) if k != 'limit_events' else [e.id for e in getattr(self.object, k).all()]
k: form.cleaned_data[k] if k != 'limit_events' else [e.id for e in form.cleaned_data[k]]
for k in form.changed_data
})
+2 -8
View File
@@ -263,12 +263,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
resp = HttpResponse(data, content_type=mimet)
ftype = fname.split(".")[-1]
if settings.DEBUG:
# attachment is more secure as we're dealing with user-generated stuff here, but inline is much more convenient during debugging
resp['Content-Disposition'] = 'inline; filename="ticket-preview.{}"'.format(ftype)
resp._csp_ignore = True
else:
resp['Content-Disposition'] = 'attachment; filename="ticket-preview.{}"'.format(ftype)
resp['Content-Disposition'] = 'inline; filename="ticket-preview.{}"'.format(ftype)
return resp
elif "data" in request.POST:
if cf:
@@ -309,6 +304,5 @@ class FontsCSSView(TemplateView):
class PdfView(TemplateView):
def get(self, request, *args, **kwargs):
cf = get_object_or_404(CachedFile, id=kwargs.get("filename"), filename="background_preview.pdf")
resp = FileResponse(cf.file, content_type='application/pdf')
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename)
resp = FileResponse(cf.file, filename=cf.filename, content_type='application/pdf')
return resp
+29
View File
@@ -917,6 +917,35 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Asyn
if len(subevents) > 100_000:
raise ValidationError(_('Please do not create more than 100.000 dates at once.'))
if form.cleaned_data.get("skip_if_overlap") and subevents:
def overlaps(a_from, a_to, b_from, b_to):
if a_from == b_from:
return True
if a_from > b_from:
# a starts after b
# check if it starts before b ends
return b_to and a_from < b_to
# a starts before b
# check if it ends before b starts
return a_to and a_to > b_from
date_min = min(se.date_from for se in subevents)
date_max = max(se.date_to or se.date_from for se in subevents)
dates_existing = list(self.request.event.subevents.annotate(
date_fromto=Coalesce('date_to', 'date_from'),
).filter(
date_from__lte=date_max,
date_fromto__gte=date_min,
).values('date_from', 'date_to'))
subevents = [
se for se in subevents if not any(
overlaps(se.date_from, se.date_to, other['date_from'], other['date_to'])
for other in dates_existing
)
]
if not subevents:
raise ValidationError(_('All dates would be skipped because they conflict with existing dates.'))
for i, se in enumerate(subevents):
se.save(clear_cache=False)
if i % 100 == 0:
+3 -3
View File
@@ -316,7 +316,7 @@ def nav_context_list(request):
page = 1
qs_events = request.user.get_events_with_any_permission(request).filter(
Q(name__icontains=i18ncomp(query)) | Q(slug__icontains=query)
Q(name__icontains=i18ncomp(query)) | Q(slug__icontains=query) | Q(domain__domainname__iexact=query)
).annotate(
min_from=Min('subevents__date_from'),
max_from=Max('subevents__date_from'),
@@ -331,7 +331,7 @@ def nav_context_list(request):
else:
qs_orga = Organizer.objects.filter(pk__in=request.user.teams.values_list('organizer', flat=True))
if query:
qs_orga = qs_orga.filter(Q(name__icontains=query) | Q(slug__icontains=query))
qs_orga = qs_orga.filter(Q(name__icontains=query) | Q(slug__icontains=query) | Q(domains__domainname__iexact=query))
qs_orga = qs_orga.annotate(
n_events=Count("events")
).order_by("-n_events")
@@ -619,7 +619,7 @@ def checkinlist_select2(request, **kwargs):
qs = request.event.checkin_lists.select_related('subevent').filter(
qf
).order_by('name')
).order_by('subevent__date_from', 'name', 'pk')
total = qs.count()
pagesize = 20
+132
View File
@@ -19,12 +19,26 @@
# 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 ipaddress
import socket
import sys
import types
from datetime import datetime
from http import cookies
from django.conf import settings
from PIL import Image
from requests.adapters import HTTPAdapter
from urllib3.connection import HTTPConnection, HTTPSConnection
from urllib3.connectionpool import HTTPConnectionPool, HTTPSConnectionPool
from urllib3.exceptions import (
ConnectTimeoutError, HTTPError, LocationParseError, NameResolutionError,
NewConnectionError,
)
from urllib3.util.connection import (
_TYPE_SOCKET_OPTIONS, _set_socket_options, allowed_gai_family,
)
from urllib3.util.timeout import _DEFAULT_TIMEOUT
def monkeypatch_vobject_performance():
@@ -89,6 +103,123 @@ def monkeypatch_requests_timeout():
HTTPAdapter.send = httpadapter_send
def monkeypatch_urllib3_ssrf_protection():
"""
pretix allows HTTP requests to untrusted URLs, e.g. through webhooks or external API URLs. This is dangerous since
it can allow access to private networks that should not be reachable by users ("server-side request forgery", SSRF).
Validating URLs at submission is not sufficient, since with DNS rebinding an attacker can make a domain name pass
validation and then resolve to a private IP address on actual execution. Unfortunately, there seems no clean solution
to this in Python land, so we monkeypatch urllib3's connection management to check the IP address to be external
*after* the DNS resolution.
This does not work when a global http(s) proxy is used, but in that scenario the proxy can perform the validation.
"""
if getattr(settings, "ALLOW_HTTP_TO_PRIVATE_NETWORKS", False):
# Settings are not supposed to change during runtime, so we can optimize performance and complexity by skipping
# this if not needed.
return
def create_connection(
address: tuple[str, int],
timeout=_DEFAULT_TIMEOUT,
source_address: tuple[str, int] | None = None,
socket_options: _TYPE_SOCKET_OPTIONS | None = None,
) -> socket.socket:
# This is copied from urllib3.util.connection v2.3.0
host, port = address
if host.startswith("["):
host = host.strip("[]")
err = None
# Using the value from allowed_gai_family() in the context of getaddrinfo lets
# us select whether to work with IPv4 DNS records, IPv6 records, or both.
# The original create_connection function always returns all records.
family = allowed_gai_family()
try:
host.encode("idna")
except UnicodeError:
raise LocationParseError(f"'{host}', label empty or too long") from None
for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM):
af, socktype, proto, canonname, sa = res
if not getattr(settings, "ALLOW_HTTP_TO_PRIVATE_NETWORKS", False):
ip_addr = ipaddress.ip_address(sa[0])
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")
sock = None
try:
sock = socket.socket(af, socktype, proto)
# If provided, set socket level options before connecting.
_set_socket_options(sock, socket_options)
if timeout is not _DEFAULT_TIMEOUT:
sock.settimeout(timeout)
if source_address:
sock.bind(source_address)
sock.connect(sa)
# Break explicitly a reference cycle
err = None
return sock
except OSError as _:
err = _
if sock is not None:
sock.close()
if err is not None:
try:
raise err
finally:
# Break explicitly a reference cycle
err = None
else:
raise OSError("getaddrinfo returns an empty list")
class ProtectionMixin:
def _new_conn(self) -> socket.socket:
# This is 1:1 the version from urllib3.connection.HTTPConnection._new_conn v2.3.0
# just with a call to our own create_connection
try:
sock = create_connection(
(self._dns_host, self.port),
self.timeout,
source_address=self.source_address,
socket_options=self.socket_options,
)
except socket.gaierror as e:
raise NameResolutionError(self.host, self, e) from e
except socket.timeout as e:
raise ConnectTimeoutError(
self,
f"Connection to {self.host} timed out. (connect timeout={self.timeout})",
) from e
except OSError as e:
raise NewConnectionError(
self, f"Failed to establish a new connection: {e}"
) from e
sys.audit("http.client.connect", self, self.host, self.port)
return sock
class ProtectedHTTPConnection(ProtectionMixin, HTTPConnection):
pass
class ProtectedHTTPSConnection(ProtectionMixin, HTTPSConnection):
pass
HTTPConnectionPool.ConnectionCls = ProtectedHTTPConnection
HTTPSConnectionPool.ConnectionCls = ProtectedHTTPSConnection
def monkeypatch_cookie_morsel():
# See https://code.djangoproject.com/ticket/34613
cookies.Morsel._flags.add("partitioned")
@@ -99,4 +230,5 @@ def monkeypatch_all_at_ready():
monkeypatch_vobject_performance()
monkeypatch_pillow_safer()
monkeypatch_requests_timeout()
monkeypatch_urllib3_ssrf_protection()
monkeypatch_cookie_morsel()
+11 -5
View File
@@ -25,7 +25,7 @@ import time
from django.conf import settings
from django.contrib.auth import login as auth_login
from django.contrib.gis.geoip2 import GeoIP2
from django.contrib.gis import geoip2
from django.core.cache import cache
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
@@ -63,14 +63,20 @@ def get_user_agent_hash(request):
_geoip = None
def _get_country(request):
def get_geoip() -> geoip2.GeoIP2:
# See https://code.djangoproject.com/ticket/36988#ticket
global _geoip
if not _geoip:
_geoip = GeoIP2()
geoip2.SUPPORTED_DATABASE_TYPES.add("Geoacumen-Country")
if not _geoip:
_geoip = geoip2.GeoIP2()
return _geoip
def _get_country(request):
try:
res = _geoip.country(get_client_ip(request))
res = get_geoip().country(get_client_ip(request))
except AddressNotFoundError:
return None
return res['country_code']
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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4
View File
@@ -32,6 +32,7 @@ ausgecheckt
ausgeklappt
auswahl
Authentication
Authenticator
Authenticator-App
Autorisierungscode
Autorisierungs-Endpunktes
@@ -130,6 +131,7 @@ Eingangsscan
Einlassbuchung
Einlassdatum
Einlasskontrolle
Einmalpasswörter
einzuchecken
email
E-Mail-Renderer
@@ -163,6 +165,7 @@ Explorer
FA
Favicon
F-Droid
freeOTP
Footer
Footer-Link
Footer-Text
@@ -557,6 +560,7 @@ Zahlungs-ID
Zahlungspflichtig
Zehnerkarten
Zeitbasiert
zeitbasierte
Zeitslotbuchung
Zimpler
ZIP-Datei
File diff suppressed because it is too large Load Diff
@@ -32,6 +32,7 @@ ausgecheckt
ausgeklappt
auswahl
Authentication
Authenticator
Authenticator-App
Autorisierungscode
Autorisierungs-Endpunktes
@@ -130,6 +131,7 @@ Eingangsscan
Einlassbuchung
Einlassdatum
Einlasskontrolle
Einmalpasswörter
einzuchecken
email
E-Mail-Renderer
@@ -163,6 +165,7 @@ Explorer
FA
Favicon
F-Droid
freeOTP
Footer
Footer-Link
Footer-Text
@@ -557,6 +560,7 @@ Zahlungs-ID
Zahlungspflichtig
Zehnerkarten
Zeitbasiert
zeitbasierte
Zeitslotbuchung
Zimpler
ZIP-Datei
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
"POT-Creation-Date: 2026-03-30 11:25+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"
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
+2 -2
View File
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
"PO-Revision-Date: 2026-03-18 12:23+0000\n"
"PO-Revision-Date: 2026-03-30 03:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
"js/es/>\n"
@@ -329,7 +329,7 @@ msgstr "Pedido no aprobado"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
msgid "Checked-in Tickets"
msgstr "Registro de código QR"
msgstr "Billetes registrados"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
msgid "Valid Tickets"
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
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
+5 -6
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
"PO-Revision-Date: 2026-02-10 16:49+0000\n"
"Last-Translator: Raffaele Doretto <ced@comune.portogruaro.ve.it>\n"
"PO-Revision-Date: 2026-03-25 14:14+0000\n"
"Last-Translator: Pietro Isotti <isottipietro@gmail.com>\n"
"Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix-"
"js/it/>\n"
"Language: it\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.15.2\n"
"X-Generator: Weblate 5.16.2\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -310,9 +310,8 @@ msgid "Ticket code revoked/changed"
msgstr "Codice biglietto annullato/modificato"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
#, fuzzy
msgid "Ticket blocked"
msgstr "Biglietto non pagato"
msgstr "Biglietto bloccato"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
msgid "Ticket not valid at this time"
@@ -429,7 +428,7 @@ msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:276
msgid "If this takes longer than a few minutes, please contact us."
msgstr ""
msgstr "Se questa operazione richiede alcuni minuti, si prega di contattarci."
#: pretix/static/pretixbase/js/asynctask.js:331
msgid "Close message"
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
"PO-Revision-Date: 2026-02-23 10:00+0000\n"
"PO-Revision-Date: 2026-03-23 21:00+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix-"
"js/ja/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.16\n"
"X-Generator: Weblate 5.16.2\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -60,7 +60,7 @@ msgstr "PayPal後払い"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:41
msgid "iDEAL | Wero"
msgstr ""
msgstr "iDEAL | Wero"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:42
msgid "SEPA Direct Debit"
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

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