Compare commits

...
Author SHA1 Message Date
Raphael Michel 55dc2a23b4 Add missing file 2026-07-08 19:23:24 +02:00
Raphael Michel 28de4554e5 API: Allow to simulate check-ins 2026-07-07 19:20:19 +02:00
Raphael Michel 2bd5e90a86 Fix isort 2026-07-07 15:43:00 +02:00
Raphael Michel 8095134400 Add query tagging for periodic tasks 2026-07-07 14:55:32 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
62f2ed55b2 Update webauthn requirement from ==2.8.* to ==3.0.* (#6350)
Updates the requirements on [webauthn](https://github.com/duo-labs/py_webauthn) to permit the latest version.
- [Release notes](https://github.com/duo-labs/py_webauthn/releases)
- [Changelog](https://github.com/duo-labs/py_webauthn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/duo-labs/py_webauthn/compare/v2.8.0-alpha1...v3.0.0)

---
updated-dependencies:
- dependency-name: webauthn
  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-07-07 12:13:40 +02:00
3533450601 Add trace IDs from request to celery task (#6333)
* Add trace IDs from request to celery task

Celery tasks will log the request ID they were triggered from:

    [2026-07-02 10:33:17,614: INFO/MainProcess] Task pretix.base.services.orders.cancel_order[5f3104b3-0a54-4e49-921e-c866c4dc4c6d] received
    [2026-07-02 10:33:17,614: INFO/MainProcess] Task 5f3104b3-0a54-4e49-921e-c866c4dc4c6d has trace 4d389638-00ab-4c4d-bdec-d73ac322bf44

Nested celery tasks will then contain both the request ID as well as the previous tasks:

    [2026-07-02 10:33:18,354: INFO/MainProcess] Task pretix.base.services.notifications.notify[d52a3a49-9c89-4f67-bdde-9f773586fc07] received
    [2026-07-02 10:33:18,354: INFO/MainProcess] Task d52a3a49-9c89-4f67-bdde-9f773586fc07 has trace 4d389638-00ab-4c4d-bdec-d73ac322bf44 5f3104b3-0a54-4e49-921e-c866c4dc4c6d

* Apply suggestion from @luelista

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

---------

Co-authored-by: luelista <weller@rami.io>
2026-07-07 12:09:27 +02:00
pajowuandGitHub 5fb827c8f5 CheckInListPDF export: remove double html escaping with PlainTextParagraph (Z#23239571) (#6347) 2026-07-07 12:00:17 +02:00
Raphael MichelandRaphael Michel af6727bbe2 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (6362 of 6362 strings)

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

powered by weblate
2026-07-07 11:57:51 +02:00
Raphael MichelandRaphael Michel f0d2733e8d Translations: Update German
Currently translated at 100.0% (6362 of 6362 strings)

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

powered by weblate
2026-07-07 11:57:51 +02:00
Raphael Michel 90bca935de Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2026-07-07 11:34:15 +02:00
Raphael Michel 9c752b7263 Translations: Fix typo 2026-07-07 11:29:03 +02:00
CVZ-esandRaphael Michel bfdd61a14c Translations: Update French
Currently translated at 99.9% (6355 of 6360 strings)

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

powered by weblate
2026-07-07 11:28:33 +02:00
CVZ-esandRaphael Michel 6aca806239 Translations: Update Spanish
Currently translated at 100.0% (6360 of 6360 strings)

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

powered by weblate
2026-07-07 11:28:33 +02:00
CVZ-esandRaphael Michel 8512a8c4a5 Translations: Update French
Currently translated at 99.9% (6354 of 6360 strings)

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

powered by weblate
2026-07-07 11:28:33 +02:00
Raphael MichelandRaphael Michel ecee6f6c30 Translations: Update German (informal) (de_Informal)
Currently translated at 99.9% (6359 of 6360 strings)

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

powered by weblate
2026-07-07 11:28:33 +02:00
Raphael MichelandRaphael Michel 846ec77c50 Translations: Update German
Currently translated at 99.9% (6359 of 6360 strings)

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

powered by weblate
2026-07-07 11:28:33 +02:00
CVZ-esandRaphael Michel 49c69c816c Translations: Update French
Currently translated at 99.8% (6348 of 6360 strings)

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

powered by weblate
2026-07-07 11:28:33 +02:00
Hijiri UmemotoandRaphael Michel f3868576c5 Translations: Update Japanese
Currently translated at 100.0% (260 of 260 strings)

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

powered by weblate
2026-07-07 11:28:33 +02:00
Hijiri UmemotoandRaphael Michel 3857361559 Translations: Update Japanese
Currently translated at 100.0% (6360 of 6360 strings)

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

powered by weblate
2026-07-07 11:28:33 +02:00
CVZ-esandRaphael Michel b0c50de331 Translations: Update Spanish
Currently translated at 100.0% (6360 of 6360 strings)

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

powered by weblate
2026-07-07 11:28:33 +02:00
CVZ-esandRaphael Michel 1dad6bf801 Translations: Update French
Currently translated at 99.7% (6347 of 6360 strings)

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

powered by weblate
2026-07-07 11:28:33 +02:00
Raphael MichelandRaphael Michel d94d4bd57e Translations: Update German (informal) (de_Informal)
Currently translated at 99.9% (6358 of 6360 strings)

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

powered by weblate
2026-07-07 11:28:33 +02:00
CVZ-esandRaphael Michel 2437f768c9 Translations: Update German (informal) (de_Informal)
Currently translated at 99.9% (6358 of 6360 strings)

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

powered by weblate
2026-07-07 11:28:33 +02:00
Raphael MichelandRaphael Michel ab621890a2 Translations: Update German
Currently translated at 99.9% (6359 of 6360 strings)

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

powered by weblate
2026-07-07 11:28:33 +02:00
Raphael Michel a9135919a1 Translations: Update wordlists 2026-07-07 11:21:59 +02:00
Raphael Michel e2c437fd43 Update confusing verbiage after #6216 2026-07-07 10:58:27 +02:00
Raphael MichelandGitHub ba10fc8041 Event product list: Fix another performance issue for large series (#6331)
This is a follow-up for #6318, basically the same problem but in a
neighboring query.

Performance comparison for real-world event with 13k subevents:

In [15]: %time get_grouped_items(e, subevent=se, channel=e.organizer.sales_channels.get(identifier="web"))
CPU times: user 49.1 ms, sys: 3.79 ms, total: 52.9 ms
Wall time: 1.12 s
Out[15]: ([<Item: xxx>], True

In [16]: %time get_grouped_items_patched(e, subevent=se, channel=e.organizer.sales_channels.get(identifier="web"))
CPU times: user 30.2 ms, sys: 445 μs, total: 30.6 ms
Wall time: 45.3 ms
Out[16]: ([<Item: xxx>], True)
2026-07-07 10:48:50 +02:00
Raphael MichelandGitHub 7732794317 Do not use redis cache at import time (#6321)
During our [2026-06-27 incident](https://pretix.eu/about/en/blog/20260630-pretix-hosted-outage/),
we noticed that pretix is using redis at import time. This means that
gunicorn and celery process were unable to start on servers who could
currently not reach redis. This is kinda mitigated through auto-restart
on systemd or docker level, but that's not really how it is supposed to
work. Celery even has smart retry/reconnect logic that becomes pointless
this way.
2026-07-07 10:47:12 +02:00
Raphael Michel 8a47ba7ff5 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2026-07-06 17:53:13 +02:00
dd2a74557d Add contact URL setting (#6132)
* Add contact URL setting

* Apply suggestions from code review

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

* Apply suggestion from @raphaelm

---------

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
2026-07-06 17:49:55 +02:00
Richard SchreiberandGitHub bb9c4fa18d Event calendar: Improve iOS VoiceOver month dropdown selection (Z#23234442) (#6320) 2026-07-06 17:38:23 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Raphael Michel
ac356acd29 Update django-redis requirement from ==6.0.* to ==7.0.* (#6244)
Updates the requirements on [django-redis](https://github.com/jazzband/django-redis) to permit the latest version.
- [Release notes](https://github.com/jazzband/django-redis/releases)
- [Changelog](https://github.com/jazzband/django-redis/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jazzband/django-redis/compare/6.0.0...7.0.0)

---
updated-dependencies:
- dependency-name: django-redis
  dependency-version: 7.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>
Co-authored-by: Raphael Michel <michel@pretix.eu>
2026-07-06 17:35:52 +02:00
54eadaffcc Improve admin-facing email templates (#6216)
* Improve subject lines for admin-facing emails

A few of the current subjects are ambiguous about the expected
action, and some omit context that would help in an inbox preview
(which event, which address). The rewrites bring them closer to
common conventions in modern transactional email (verb-led,
recipient-addressed, with recipient-meaningful variables). Two
themes:

- Action-required emails lead with the action verb. "Reset your
  password", "Confirm event cancellation and bulk refund", and
  "Confirm <address> as a sender address" tell the recipient up
  front what's expected, where "Password recovery", "Bulk-refund
  confirmation" and "Sender address verification" did not.

- Surface the relevant variable when the email is about something
  specific. "Data shredding completed for <event>" is more useful
  than the generic version when an admin manages several events.
  "You've been invited to join <organizer>" names the inviting
  organizer. "Confirm <address> as a sender address" names the
  address.

The remaining rewrites are lighter rewordings. "New sign-in to
your account" replaces "Login from new source detected" because
"source" is jargon a non-technical recipient wouldn't recognise.
"Changes to your account" replaces "Account information changed"
because the possessive frames the email as being about the
recipient's own account.

Also fixes a hardcoded "pretix" in the confirmation-code subject.

* Standardise admin email sign-offs as "Thanks, The <instance> Team"

The current sign-offs ("Best regards, Your <instance> team") have
a formal tone. A review of the last ~20 transactional emails in
my inbox showed most senders use something friendlier:

- Thanks: Deliveroo, Starling Bank, GitHub, Cloudflare
- Thank you: AWS
- Sincerely: Google Workspace

A small minority (e.g., Sentry) had no sign-off at all. "Thanks"
was the most common, and among that group "The <instance> Team"
was the consistent phrasing rather than "Your <instance> team".

Two templates (cancel_confirm, export_failed) didn't have a
sign-off; they now get one for consistency. Notification emails
are deliberately excluded: they're system alerts rather than
direct correspondence.

* Add anti-phishing notice to admin emails containing confirmation codes

Three admin emails send the recipient a confirmation code to
enter back into a form: confirmation_code, email_setup, and
cancel_confirm. Only confirmation_code had an anti-phishing
warning, and its wording was awkward ("Please do never give this
code to another person. Our support team will never ask for this
code.").

This commit standardises the warning across all three:

> Don't share this code with anyone. The <instance> team will
> never ask you for it.

* Add structured details to login-notice email

The single-sentence body ("The login was performed using <agent>
on <os> from <country>.") is replaced with a labelled bullet list:
Time, Browser, Operating system, Device, Country.

Time and Device are new fields. Device is omitted when ua-parser
can't identify the device, Country when GeoIP isn't available,
so the user only sees fields with real values.

* Restructure notification.txt for clearer layout

- Attributes: bullet list instead of paragraph-per-attribute.

- Actions: label gets a colon, URL on its own paragraph (was
  4-space-indented code block).

- Footer: separated by --- and bulleted (manage / disable
  links). "Click here X" phrasing dropped (incidentally moots
  a missing-"to" typo).

- Minor whitespace fix: detail-block endif now matches the
  placement of the rest of the template.

notification.html's footer text is also updated, only to match
the new .txt wording (link labels and intro line). No
structural changes to the HTML template.

* Improve confirmation-code email reason strings

- Drop the redundant "to confirm" opener.

- Replace hardcoded "your pretix account" in email_verify
  with "{instance}".

* Polish admin email body copy

A small wording and formatting pass on the admin email bodies,
in three loosely-grouped themes:

1. Sentence case for body text (previously lowercase after
   "Hello,"), matching standard English convention.

2. Light restructuring where helpful: bullet lists for sets
   of labelled facts; 4-space-indented code blocks for codes
   the recipient is meant to type back.

3. Phrasing polish. Some sentences tightened or shortened.
   Largely matters of taste, but generally read smoother.

---------

Co-authored-by: Raphael Michel <michel@pretix.eu>
2026-07-06 17:25:52 +02:00
617a548b19 Add event placeholder support to more fields (#6098)
* Add event placeholder support to more fields

I found it useful to be able to use the `{event}` placeholder in
some fields such as the "End of presale text" because I don't need
to fix the texts creating a new event by cloning another event.
I made placeholders available to the other fields as well (where not too
difficult). Specifically `presale_has_ended_text`, `voucher_explanation_text`
`banner_text`, `banner_text_bottom` and `event_info_text`.
In addition, I grouped them under a new `texts` variable in the `context`
(including `frontpage_text` which was part of the root `context` previously).

* change compute location

---------

Co-authored-by: Raphael Michel <michel@rami.io>
2026-07-06 17:18:04 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fe0f7864e7 Update reportlab requirement from ==4.5.* to ==5.0.* (#6307)
Updates the requirements on [reportlab](https://www.reportlab.com/) to permit the latest version.

---
updated-dependencies:
- dependency-name: reportlab
  dependency-version: 5.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-07-06 17:03:12 +02:00
fc4a2f5508 Customer SSO: Improve one-time token error message (#5841)
* Improve SSO one-time token error message

Replace the generic 'invalid one-time token' message shown after failed
SSO login attempts with a clearer, user-facing explanation of what went
wrong and how to recover.

* Remove unneeded classes from headings

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

* Apply suggestions from code review

Added `trimmed` to translation blocks

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

* Reword SSO error template for improved translation clarity

---------

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
2026-07-06 16:59:59 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Raphael Michel
4cb32a753f Update requests requirement from ==2.32.* to ==2.34.* (#6178)
Updates the requirements on [requests](https://github.com/psf/requests) to permit the latest version.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.32.0...v2.34.0)

---
updated-dependencies:
- dependency-name: requests
  dependency-version: 2.34.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>
Co-authored-by: Raphael Michel <michel@pretix.eu>
2026-07-06 16:57:27 +02:00
KarlKeu00andGitHub 3ae9aabcfb Add Docker secrets support in config (#6250)
* Add Docker secrets support in config

* ruff format

* Remove gracefully fallback exception handling

* Add support for loading secret fallbacks from environment file
2026-07-06 15:09:31 +02:00
3270c4e583 Bank transfer: Fix incorrect HTML escaping in QR Code (fix #4780) (#6201)
* Fix EPC QR beneficiary escaping

* Fix EPC QR script encoding

Keep EPC QR helper output as a plain string and serialize payment QR payloads as JSON script data before the QR replacement JavaScript parses them. This preserves apostrophes without relying on mark_safe in the helper.

Assisted-by: OpenAI GPT-5 <noreply@openai.com>

* "type safety"

---------

Co-authored-by: Puneet Dixit <236133619+puneetdixit200@users.noreply.github.com>
Co-authored-by: Raphael Michel <michel@rami.io>
2026-07-06 12:52:29 +02:00
3b285a89dd Sentry: Optionally enable sending logs (#6222)
* Enable sending logs to Sentry

* change to info

---------

Co-authored-by: Raphael Michel <michel@rami.io>
2026-07-06 12:38:28 +02:00
sweenuandGitHub c1683df1fd Allow to update -> Allow updating (#6131) 2026-07-06 11:54:33 +02:00
Kian CrossandGitHub 6fdcbcebd2 Add typeahead suggestions for voucher tag field (#6058)
Suggest existing tags as the user types in the voucher tag field.
Waiting list voucher tags are excluded from suggestions.
2026-07-06 11:42:05 +02:00
Kian CrossandGitHub ddbea7c32e Customer login during checkout: Display SSO login errors inline (#5840)
Replace the JavaScript alert used for SSO popup login errors during the
checkout flow with an inline HTML error message.
2026-07-06 11:38:43 +02:00
luelistaandGitHub 3f8ed0f722 Only set vat_id_validated if vat_id non-empty (#6210) 2026-07-06 11:21:53 +02:00
sweenuandGitHub 388eb7de96 BasePaymentProvider: Fix type hinting on execute_payment() (#6078) 2026-07-06 11:19:53 +02:00
dependabot[bot]andRaphael Michel df4c83d849 Update pillow requirement from ==12.2.* to ==12.3.*
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.2.0...12.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-06 11:12:11 +02:00
Benedikt BormannandRaphael Michel fa4cec8a2d Translations: Update German
Currently translated at 100.0% (260 of 260 strings)

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

powered by weblate
2026-07-06 11:11:51 +02:00
943b319557 use cookieretry only on presale event pages (Z#23236752) (#6297)
* use cookieretry only on presale event pages

* use csrfcookieretry only on event index page

* include static tag

* include csrfcookieretry in order.html as well

* Update src/pretix/static/pretixpresale/js/csrfcookieretry.js

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

---------

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
2026-07-03 13:56:47 +02:00
Richard SchreiberandGitHub 28b13667ce Widget: add beta-flag to URL (#6338) 2026-07-03 12:04:54 +02:00
Raphael MichelandGitHub b00d1c9156 Bump django-querytagger 2026-07-03 11:53:34 +02:00
Raphael Michel 120317a8f2 Fix linter issue 2026-07-03 11:00:48 +02:00
Raphael Michel 7d5b00a610 Fix linter issues 2026-07-03 10:54:44 +02:00
Raphael Michel 83612c7d65 Merge branch 'security/harden-staffsession' into 'master'
Harden StaffSession handling

See merge request pretix/pretix!42
2026-07-03 10:41:39 +02:00
Mira WellerandRaphael Michel 7a5f96369a Harden StaffSession handling 2026-07-03 10:41:39 +02:00
Raphael Michel 7fd6bf41f9 Merge branch 'csp-refactor' into 'master'
CSP refactor

See merge request pretix/pretix!35
2026-07-03 10:33:35 +02:00
Mira WellerandRaphael Michel 458c3d4b83 CSP refactor 2026-07-03 10:33:35 +02:00
Raphael Michel d10d061e45 Merge branch 'check-csp' into 'master'
Check CSP components before rendering, prevent format string traversal

See merge request pretix/pretix!33
2026-07-03 10:26:23 +02:00
Mira WellerandRaphael Michel d30bca50f7 Check CSP components before rendering, prevent format string traversal 2026-07-03 10:26:23 +02:00
Phin WolkwitzandGitHub 3903aca7c9 Add Thai translations to community languages (Z#23239401) (#6334) 2026-07-02 17:44:28 +02:00
Raphael MichelandGitHub 493c920aba Install django-querytagger (#6332)
* Install django-querytagger

* Update pyproject.toml
2026-07-02 14:40:02 +02:00
Raphael MichelandGitHub 09b7bc00b0 Organizer calendar: Respect event_calendar_future_only (Z#23238776) (#6326)
We initially didn't do this for two reasons:

- Performance implications of calling the settings store for every event
  that shows up in the calendar. As of d43e85da, we need that anyways.
- Performance implications of filtering in Python except SQL but... it
  can't really be worse than not filtering at all.
- We don't easily know if it's valid for all events so we can't stop
  rendering the unused calendar rows. That's an acceptable issue for
  now, still better than nothing. We can always optimize later.

So we might as well implement it.
2026-07-02 14:31:44 +02:00
dependabot[bot]andRaphael Michel 2e195c0274 Update sentry-sdk requirement from ==2.63.* to ==2.64.*
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.63.0...2.64.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-02 13:53:33 +02:00
Raphael MichelandGitHub 18cb9c1816 Drop line numbers from gettext .po files (#6330)
Knowing what file a string comes from is useful, but the line number is less
useful and changes a lot, causing very unreadable diffs of translation
files. I propose we drop them and only include the file names
2026-07-02 10:21:09 +02:00
Richard SchreiberandGitHub c3e0120f9f Improve calendar explorability for VoiceOver on iOS 2026-07-02 08:13:09 +02:00
Raphael Michel 67f7fec134 Fix flake8 issue 2026-07-01 18:01:50 +02:00
Raphael Michel c2c97f31ca Bump version to 2026.7.0.dev0 2026-07-01 16:33:10 +02:00
Raphael Michel fd565ecdb2 Bump version to 2026.6.0 2026-07-01 16:33:07 +02:00
Nikita MitasovandRaphael Michel f35b13b686 Translations: Update Russian
Currently translated at 18.9% (1200 of 6343 strings)

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

powered by weblate
2026-07-01 16:32:06 +02:00
CVZ-esandRaphael Michel 550bb675f5 Translations: Update Spanish
Currently translated at 100.0% (260 of 260 strings)

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

powered by weblate
2026-07-01 16:32:06 +02:00
CVZ-esandRaphael Michel adc9c9d514 Translations: Update Spanish
Currently translated at 100.0% (6343 of 6343 strings)

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

powered by weblate
2026-07-01 16:32:06 +02:00
CVZ-esandRaphael Michel 8441c4bc7a Translations: Update French
Currently translated at 100.0% (260 of 260 strings)

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

powered by weblate
2026-07-01 16:32:06 +02:00
CVZ-esandRaphael Michel 97ff252c09 Translations: Update French
Currently translated at 100.0% (6343 of 6343 strings)

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

powered by weblate
2026-07-01 16:32:06 +02:00
CVZ-esandRaphael Michel d3ca2ac1e5 Translations: Update German
Currently translated at 100.0% (6343 of 6343 strings)

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

powered by weblate
2026-07-01 16:32:06 +02:00
Raphael MichelandMira Weller 6eebaaa563 [SECURITY] Hardening for user impersonation feature (CVE-2026-13602)
---------

Co-authored-by: Mira Weller <weller@pretix.eu>
2026-07-01 15:15:43 +02:00
Raphael MichelandMira Weller c9781f012b [SECURITY] Centralize framebreaking logic from payment plugins to core (CVE-2026-13602)
- Add central framebreaker page via safelink helper
- Update paypal, paypal2 and stripe plugins to use central framebreaker
- Add CSP header to cookies.html

---------

Co-authored-by: Mira Weller <weller@pretix.eu>
2026-07-01 15:15:43 +02:00
000bf54105 [SECURITY] Allowlisting and changed salts for safelink and safelink_callback (CVE-2026-13602)
---------

Co-authored-by: Raphael Michel <michel@pretix.eu>
2026-07-01 15:15:43 +02:00
Lukas BockstallerandGitHub e42d3d632f filter out the 404 log records from django.request (#6324) 2026-07-01 11:33:04 +02:00
Lukas BockstallerandGitHub 3bf5a5e478 include settings attribute during type checking (#6323)
* include settings attribute during type checking

* isort
2026-07-01 11:32:54 +02:00
Raphael Michel a6f31df0d4 Do not assign domain across organizers when copying events 2026-06-30 18:51:49 +02:00
Raphael MichelandGitHub d40492748a Event product list: Hotfix for pathological performance in large event series (#6318)
* Event product list: Hotfix for pathological performance

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

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

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

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

* Update checkoutflow.py

* fix more occurences

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

* Apply suggestion from @pajowu

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

* Fix style issue

---------

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

* new name

* fix old call

* Revert "new name"

This reverts commit a6e9a488b6.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* scope down event

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

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

* drop the jquery dependency for error.js

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

* include errors.js in error.html

* include errors.js in control base.html

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

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

* put errors.js in an IIFE call

---------

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

* Accountingreport: Fix crash for single events

---------

Co-authored-by: Kara Engelhardt <engelhardt@pretix.eu>
2026-06-12 14:36:10 +02:00
Raphael Michel 034722fa39 Skip e2e tests on gitlab for now 2026-06-12 14:07:42 +02:00
275 changed files with 523540 additions and 558128 deletions
+1 -2
View File
@@ -10,8 +10,7 @@ tests:
- cd src - cd src
- python manage.py check - python manage.py check
- make all compress - make all compress
- playwright install - PRETIX_CONFIG_FILE=tests/ci_sqlite.cfg py.test -n 3 tests --ignore=tests/e2e --maxfail=100
- PRETIX_CONFIG_FILE=tests/ci_sqlite.cfg py.test -n 3 tests --maxfail=100
except: except:
- '/^v.*$/' - '/^v.*$/'
pypi: pypi:
+2
View File
@@ -71,6 +71,8 @@ Checking a ticket in
:>json object questions: List of questions to be answered for check-in, only set on status ``"incomplete"``. :>json object questions: List of questions to be answered for check-in, only set on status ``"incomplete"``.
:>json object media_policy: Reusable media policy (see documentation on items), only set on status ``"exchange"``. :>json object media_policy: Reusable media policy (see documentation on items), only set on status ``"exchange"``.
:>json object media_type: Reusable media type (see documentation on items), only set on status ``"exchange"``. :>json object media_type: Reusable media type (see documentation on items), only set on status ``"exchange"``.
:>json boolean simulate: Do not actually perform the check-in, only simulate the response. The ``position`` response
object will not reflect the simulated changes.
**Example request**: **Example request**:
+1 -1
View File
@@ -81,7 +81,7 @@ is a python method that emulates a behavior similar to ``reverse``:
If you need to communicate the URL externally, you can use a different method to ensure that it is always an absolute URL: If you need to communicate the URL externally, you can use a different method to ensure that it is always an absolute URL:
.. autofunction:: pretix.multidomain.urlreverse.build_absolute_uri .. autofunction:: pretix.multidomain.urlreverse.eventreverse_absolute
In addition, there is a template tag that works similar to ``url`` but takes an event or organizer object In addition, there is a template tag that works similar to ``url`` but takes an event or organizer object
as its first argument and can be used like this:: as its first argument and can be used like this::
+109 -91
View File
@@ -370,14 +370,14 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@napi-rs/wasm-runtime": { "node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.4", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@tybys/wasm-util": "^0.10.1" "@tybys/wasm-util": "^0.10.2"
}, },
"funding": { "funding": {
"type": "github", "type": "github",
@@ -427,9 +427,9 @@
} }
}, },
"node_modules/@oxc-project/types": { "node_modules/@oxc-project/types": {
"version": "0.129.0", "version": "0.133.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.129.0.tgz", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
"integrity": "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==", "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@@ -758,9 +758,9 @@
} }
}, },
"node_modules/@rolldown/binding-android-arm64": { "node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
"integrity": "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==", "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -775,9 +775,9 @@
} }
}, },
"node_modules/@rolldown/binding-darwin-arm64": { "node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
"integrity": "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==", "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -792,9 +792,9 @@
} }
}, },
"node_modules/@rolldown/binding-darwin-x64": { "node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
"integrity": "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==", "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -809,9 +809,9 @@
} }
}, },
"node_modules/@rolldown/binding-freebsd-x64": { "node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
"integrity": "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==", "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -826,9 +826,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm-gnueabihf": { "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
"integrity": "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==", "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -843,13 +843,16 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm64-gnu": { "node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
"integrity": "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==", "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -860,13 +863,16 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm64-musl": { "node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
"integrity": "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==", "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -877,13 +883,16 @@
} }
}, },
"node_modules/@rolldown/binding-linux-ppc64-gnu": { "node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
"integrity": "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==", "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -894,13 +903,16 @@
} }
}, },
"node_modules/@rolldown/binding-linux-s390x-gnu": { "node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
"integrity": "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==", "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -911,13 +923,16 @@
} }
}, },
"node_modules/@rolldown/binding-linux-x64-gnu": { "node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
"integrity": "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==", "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -928,13 +943,16 @@
} }
}, },
"node_modules/@rolldown/binding-linux-x64-musl": { "node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
"integrity": "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==", "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -945,9 +963,9 @@
} }
}, },
"node_modules/@rolldown/binding-openharmony-arm64": { "node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
"integrity": "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==", "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -962,9 +980,9 @@
} }
}, },
"node_modules/@rolldown/binding-wasm32-wasi": { "node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
"integrity": "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==", "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
"cpu": [ "cpu": [
"wasm32" "wasm32"
], ],
@@ -981,9 +999,9 @@
} }
}, },
"node_modules/@rolldown/binding-win32-arm64-msvc": { "node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
"integrity": "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==", "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -998,9 +1016,9 @@
} }
}, },
"node_modules/@rolldown/binding-win32-x64-msvc": { "node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
"integrity": "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==", "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -3158,9 +3176,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -3334,9 +3352,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.14", "version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -3353,7 +3371,7 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.12",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"source-map-js": "^1.2.1" "source-map-js": "^1.2.1"
}, },
@@ -3610,14 +3628,14 @@
} }
}, },
"node_modules/rolldown": { "node_modules/rolldown": {
"version": "1.0.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0.tgz", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
"integrity": "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==", "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@oxc-project/types": "=0.129.0", "@oxc-project/types": "=0.133.0",
"@rolldown/pluginutils": "1.0.0" "@rolldown/pluginutils": "^1.0.0"
}, },
"bin": { "bin": {
"rolldown": "bin/cli.mjs" "rolldown": "bin/cli.mjs"
@@ -3626,27 +3644,27 @@
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.0", "@rolldown/binding-android-arm64": "1.0.3",
"@rolldown/binding-darwin-arm64": "1.0.0", "@rolldown/binding-darwin-arm64": "1.0.3",
"@rolldown/binding-darwin-x64": "1.0.0", "@rolldown/binding-darwin-x64": "1.0.3",
"@rolldown/binding-freebsd-x64": "1.0.0", "@rolldown/binding-freebsd-x64": "1.0.3",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0", "@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
"@rolldown/binding-linux-arm64-gnu": "1.0.0", "@rolldown/binding-linux-arm64-gnu": "1.0.3",
"@rolldown/binding-linux-arm64-musl": "1.0.0", "@rolldown/binding-linux-arm64-musl": "1.0.3",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0", "@rolldown/binding-linux-ppc64-gnu": "1.0.3",
"@rolldown/binding-linux-s390x-gnu": "1.0.0", "@rolldown/binding-linux-s390x-gnu": "1.0.3",
"@rolldown/binding-linux-x64-gnu": "1.0.0", "@rolldown/binding-linux-x64-gnu": "1.0.3",
"@rolldown/binding-linux-x64-musl": "1.0.0", "@rolldown/binding-linux-x64-musl": "1.0.3",
"@rolldown/binding-openharmony-arm64": "1.0.0", "@rolldown/binding-openharmony-arm64": "1.0.3",
"@rolldown/binding-wasm32-wasi": "1.0.0", "@rolldown/binding-wasm32-wasi": "1.0.3",
"@rolldown/binding-win32-arm64-msvc": "1.0.0", "@rolldown/binding-win32-arm64-msvc": "1.0.3",
"@rolldown/binding-win32-x64-msvc": "1.0.0" "@rolldown/binding-win32-x64-msvc": "1.0.3"
} }
}, },
"node_modules/rolldown/node_modules/@rolldown/pluginutils": { "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
"integrity": "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==", "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@@ -4325,9 +4343,9 @@
} }
}, },
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.16", "version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -4465,17 +4483,17 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "8.0.12", "version": "8.0.16",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.12.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
"integrity": "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg==", "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"lightningcss": "^1.32.0", "lightningcss": "^1.32.0",
"picomatch": "^4.0.4", "picomatch": "^4.0.4",
"postcss": "^8.5.14", "postcss": "^8.5.15",
"rolldown": "1.0.0", "rolldown": "1.0.3",
"tinyglobby": "^0.2.16" "tinyglobby": "^0.2.17"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"
+10 -9
View File
@@ -33,8 +33,8 @@ dependencies = [
"bleach==6.4.*", "bleach==6.4.*",
"celery==5.6.*", "celery==5.6.*",
"chardet==5.2.*", "chardet==5.2.*",
"cryptography>=48.0.1", "cryptography>=49.0.0",
"css-inline==0.20.*", "css-inline==0.21.*",
"defusedcsv>=3.0.0", "defusedcsv>=3.0.0",
"dnspython==2.*", "dnspython==2.*",
"Django[argon2]==5.2.*", "Django[argon2]==5.2.*",
@@ -53,7 +53,8 @@ dependencies = [
"django-oauth-toolkit==2.3.*", "django-oauth-toolkit==2.3.*",
"django-otp==1.7.*", "django-otp==1.7.*",
"django-phonenumber-field==8.4.*", "django-phonenumber-field==8.4.*",
"django-redis==6.0.*", "django-querytagger==0.0.3",
"django-redis==7.0.*",
"django-scopes==2.0.*", "django-scopes==2.0.*",
"django-statici18n==2.7.*", "django-statici18n==2.7.*",
"djangorestframework==3.17.*", "djangorestframework==3.17.*",
@@ -76,7 +77,7 @@ dependencies = [
"paypal-checkout-serversdk==1.0.*", "paypal-checkout-serversdk==1.0.*",
"PyJWT==2.13.*", "PyJWT==2.13.*",
"phonenumberslite==9.0.*", "phonenumberslite==9.0.*",
"Pillow==12.2.*", "Pillow==12.3.*",
"pretix-plugin-build", "pretix-plugin-build",
"protobuf==7.35.*", "protobuf==7.35.*",
"psycopg2-binary", "psycopg2-binary",
@@ -91,9 +92,9 @@ dependencies = [
"pyuca", "pyuca",
"qrcode==8.2", "qrcode==8.2",
"redis==7.4.*", "redis==7.4.*",
"reportlab==4.5.*", "reportlab==5.0.*",
"requests==2.32.*", "requests==2.34.*",
"sentry-sdk==2.62.*", "sentry-sdk==2.64.*",
"sepaxml==2.7.*", "sepaxml==2.7.*",
"stripe==7.9.*", "stripe==7.9.*",
"text-unidecode==1.*", "text-unidecode==1.*",
@@ -101,7 +102,7 @@ dependencies = [
"tqdm==4.*", "tqdm==4.*",
"ua-parser==1.0.*", "ua-parser==1.0.*",
"vobject==0.9.*", "vobject==0.9.*",
"webauthn==2.7.*", "webauthn==3.0.*",
"zeep==4.3.*" "zeep==4.3.*"
] ]
@@ -125,7 +126,7 @@ dev = [
"pytest-sugar", "pytest-sugar",
"pytest-xdist==3.8.*", "pytest-xdist==3.8.*",
"pytest-playwright", "pytest-playwright",
"pytest==9.0.*", "pytest==9.1.*",
"playwright", "playwright",
"responses", "responses",
] ]
+2 -2
View File
@@ -6,8 +6,8 @@ localecompile:
./manage.py compilemessages ./manage.py compilemessages
localegen: localegen:
./manage.py makemessages --keep-pot --ignore "pretix/static/npm_dir/*" $(LNGS) ./manage.py makemessages --keep-pot --add-location file --ignore "pretix/static/npm_dir/*" $(LNGS)
./manage.py makemessages --keep-pot -d djangojs --ignore "pretix/static/npm_dir/*" --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS) ./manage.py makemessages --keep-pot --add-location file -e js,ts,vue -d djangojs --ignore "pretix/static/npm_dir/*" --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS)
staticfiles: npminstall npmbuild jsi18n staticfiles: npminstall npmbuild jsi18n
./manage.py collectstatic --noinput ./manage.py collectstatic --noinput
+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 # You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
__version__ = "2026.6.0.dev0" __version__ = "2026.7.0.dev0"
+1
View File
@@ -118,6 +118,7 @@ ALL_LANGUAGES = [
('sv', _('Swedish')), ('sv', _('Swedish')),
('es', _('Spanish')), ('es', _('Spanish')),
('es-419', _('Spanish (Latin America)')), ('es-419', _('Spanish (Latin America)')),
('th', _('Thai')),
('tr', _('Turkish')), ('tr', _('Turkish')),
('uk', _('Ukrainian')), ('uk', _('Ukrainian')),
] ]
+1
View File
@@ -90,6 +90,7 @@ class CheckinRPCRedeemInputSerializer(serializers.Serializer):
answers = serializers.JSONField(required=False, allow_null=True) answers = serializers.JSONField(required=False, allow_null=True)
exchange_medium_type = serializers.ChoiceField(required=False, choices=MEDIA_TYPES) exchange_medium_type = serializers.ChoiceField(required=False, choices=MEDIA_TYPES)
exchange_medium_identifier = serializers.CharField(required=False) exchange_medium_identifier = serializers.CharField(required=False)
simulate = serializers.BooleanField(default=False, required=False)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
+3 -2
View File
@@ -73,7 +73,7 @@ from pretix.base.settings import (
LazyI18nStringList, validate_event_settings, LazyI18nStringList, validate_event_settings,
) )
from pretix.base.signals import api_event_settings_fields from pretix.base.signals import api_event_settings_fields
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -173,7 +173,7 @@ class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
) )
def get_event_url(self, event): def get_event_url(self, event):
return build_absolute_uri(event, 'presale:event.index') return eventreverse_absolute(event, 'presale:event.index')
class Meta: class Meta:
model = Event model = Event
@@ -747,6 +747,7 @@ class EventSettingsSerializer(SettingsSerializer):
'max_items_per_order', 'max_items_per_order',
'reservation_time', 'reservation_time',
'contact_mail', 'contact_mail',
'contact_url',
'show_variations_expanded', 'show_variations_expanded',
'hide_sold_out', 'hide_sold_out',
'meta_noindex', 'meta_noindex',
+3 -3
View File
@@ -76,7 +76,7 @@ from pretix.base.settings import (
) )
from pretix.base.signals import register_ticket_outputs from pretix.base.signals import register_ticket_outputs
from pretix.helpers.countries import CachedCountries from pretix.helpers.countries import CachedCountries
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -757,7 +757,7 @@ class PaymentURLField(serializers.URLField):
def to_representation(self, instance: OrderPayment): def to_representation(self, instance: OrderPayment):
if instance.state != OrderPayment.PAYMENT_STATE_CREATED: if instance.state != OrderPayment.PAYMENT_STATE_CREATED:
return None return None
return build_absolute_uri(instance.order.event, 'presale:event.order.pay', kwargs={ return eventreverse_absolute(instance.order.event, 'presale:event.order.pay', kwargs={
'order': instance.order.code, 'order': instance.order.code,
'secret': instance.order.secret, 'secret': instance.order.secret,
'payment': instance.pk, 'payment': instance.pk,
@@ -806,7 +806,7 @@ class OrderRefundSerializer(I18nAwareModelSerializer):
class OrderURLField(serializers.URLField): class OrderURLField(serializers.URLField):
def to_representation(self, instance: Order): def to_representation(self, instance: Order):
return build_absolute_uri(instance.event, 'presale:event.order', kwargs={ return eventreverse_absolute(instance.event, 'presale:event.order', kwargs={
'order': instance.code, 'order': instance.code,
'secret': instance.secret, 'secret': instance.secret,
}) })
+9 -6
View File
@@ -27,7 +27,7 @@ from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction from django.db import transaction
from django.db.models import Q from django.db.models import Q
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext, gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
@@ -58,8 +58,8 @@ from pretix.helpers.permission_migration import (
OLD_TO_NEW_EVENT_COMPAT, OLD_TO_NEW_EVENT_MIGRATION, OLD_TO_NEW_EVENT_COMPAT, OLD_TO_NEW_EVENT_MIGRATION,
OLD_TO_NEW_ORGANIZER_COMPAT, OLD_TO_NEW_ORGANIZER_MIGRATION, OLD_TO_NEW_ORGANIZER_COMPAT, OLD_TO_NEW_ORGANIZER_MIGRATION,
) )
from pretix.helpers.urls import build_absolute_uri as build_global_uri from pretix.helpers.urls import mainreverse_absolute
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -71,7 +71,7 @@ class OrganizerSerializer(I18nAwareModelSerializer):
slug = serializers.CharField(read_only=True) slug = serializers.CharField(read_only=True)
def get_organizer_url(self, organizer): def get_organizer_url(self, organizer):
return build_absolute_uri(organizer, 'presale:organizer.index') return eventreverse_absolute(organizer, 'presale:organizer.index')
class Meta: class Meta:
model = Organizer model = Organizer
@@ -492,14 +492,16 @@ class TeamInviteSerializer(serializers.ModelSerializer):
def _send_invite(self, instance): def _send_invite(self, instance):
mail( mail(
instance.email, instance.email,
_('Account invitation'), gettext('You\'ve been invited to join %(organizer)s') % {
'organizer': self.context['organizer'].name,
},
'pretixcontrol/email/invitation.txt', 'pretixcontrol/email/invitation.txt',
{ {
'instance': settings.PRETIX_INSTANCE_NAME, 'instance': settings.PRETIX_INSTANCE_NAME,
'user': self, 'user': self,
'organizer': self.context['organizer'].name, 'organizer': self.context['organizer'].name,
'team': instance.team.name, 'team': instance.team.name,
'url': build_global_uri('control:auth.invite', kwargs={ 'url': mainreverse_absolute('control:auth.invite', kwargs={
'token': instance.token 'token': instance.token
}) })
}, },
@@ -574,6 +576,7 @@ class OrganizerSettingsSerializer(SettingsSerializer):
'customer_accounts_require_login_for_order_access', 'customer_accounts_require_login_for_order_access',
'invoice_regenerate_allowed', 'invoice_regenerate_allowed',
'contact_mail', 'contact_mail',
'contact_url',
'imprint_url', 'imprint_url',
'organizer_info_text', 'organizer_info_text',
'event_list_type', 'event_list_type',
+7
View File
@@ -837,6 +837,11 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
) )
if exchange_medium_identifier: # other fields are filled, see CheckinRPCRedeemInputSerializer.validate if exchange_medium_identifier: # other fields are filled, see CheckinRPCRedeemInputSerializer.validate
if simulate:
raise CheckInError(
gettext('You cannot simulate a medium exchange.'),
'error'
)
with transaction.atomic(): with transaction.atomic():
# Do exchange and check-in atomically, i.e. both succeed or both fail # Do exchange and check-in atomically, i.e. both succeed or both fail
medium = perform_media_exchange( medium = perform_media_exchange(
@@ -872,6 +877,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'media_policy': e.media_policy, 'media_policy': e.media_policy,
'media_type': e.media_type, 'media_type': e.media_type,
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data, 'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
'reason': e.code,
'reason_explanation': e.msg, 'reason_explanation': e.msg,
}, status=400) }, status=400)
except CheckInError as e: except CheckInError as e:
@@ -1063,6 +1069,7 @@ class CheckinRPCRedeemView(views.APIView):
legacy_url_support=False, legacy_url_support=False,
exchange_medium_type=s.validated_data.get('exchange_medium_type'), exchange_medium_type=s.validated_data.get('exchange_medium_type'),
exchange_medium_identifier=s.validated_data.get('exchange_medium_identifier'), exchange_medium_identifier=s.validated_data.get('exchange_medium_identifier'),
simulate=s.validated_data.get('simulate'),
) )
+2 -2
View File
@@ -36,7 +36,7 @@ from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from requests import RequestException from requests import RequestException
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -313,7 +313,7 @@ def _get_or_create_server_keypair(organizer):
def generate_id_token(customer, client, auth_time, nonce, scope, expires: datetime, scope_claims=False, with_code=None, with_access_token=None): def generate_id_token(customer, client, auth_time, nonce, scope, expires: datetime, scope_claims=False, with_code=None, with_access_token=None):
payload = { payload = {
'iss': build_absolute_uri(client.organizer, 'presale:organizer.index').rstrip('/'), 'iss': eventreverse_absolute(client.organizer, 'presale:organizer.index').rstrip('/'),
'aud': client.client_id, 'aud': client.client_id,
'exp': int(expires.timestamp()), 'exp': int(expires.timestamp()),
'iat': int(time.time()), 'iat': int(time.time()),
+3 -3
View File
@@ -28,7 +28,7 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.models import Checkin, InvoiceAddress, Order, Question from pretix.base.models import Checkin, InvoiceAddress, Order, Question
from pretix.base.settings import PERSON_NAME_SCHEMES from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
def get_answer(op, question_identifier=None): def get_answer(op, question_identifier=None):
@@ -545,7 +545,7 @@ def get_data_fields(event, for_model=None):
_("Order link"), _("Order link"),
Question.TYPE_STRING, Question.TYPE_STRING,
None, None,
lambda order: build_absolute_uri( lambda order: eventreverse_absolute(
event, event,
'presale:event.order', kwargs={ 'presale:event.order', kwargs={
'order': order.code, 'order': order.code,
@@ -560,7 +560,7 @@ def get_data_fields(event, for_model=None):
_("Ticket link"), _("Ticket link"),
Question.TYPE_STRING, Question.TYPE_STRING,
None, None,
lambda op: build_absolute_uri( lambda op: eventreverse_absolute(
event, event,
'presale:event.order.position', kwargs={ 'presale:event.order.position', kwargs={
'order': op.order.code, 'order': op.order.code,
+5
View File
@@ -57,6 +57,8 @@ logger = logging.getLogger('pretix.base.email')
T = TypeVar("T", bound=EmailBackend) T = TypeVar("T", bound=EmailBackend)
_cgnat_net = ipaddress.ip_network('100.64.0.0/10')
def test_custom_smtp_backend(backend: T, from_addr: str) -> None: def test_custom_smtp_backend(backend: T, from_addr: str) -> None:
try: try:
@@ -253,12 +255,15 @@ def create_connection(address, timeout=socket.getdefaulttimeout(),
if not getattr(settings, "MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS", False): if not getattr(settings, "MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS", False):
ip_addr = ipaddress.ip_address(sa[0]) ip_addr = ipaddress.ip_address(sa[0])
check_ip4 = ip_addr.ipv4_mapped if getattr(ip_addr, "ipv4_mapped", None) else ip_addr
if ip_addr.is_multicast: if ip_addr.is_multicast:
raise socket.error(f"Request to multicast address {sa[0]} blocked") raise socket.error(f"Request to multicast address {sa[0]} blocked")
if ip_addr.is_loopback or ip_addr.is_link_local: if ip_addr.is_loopback or ip_addr.is_link_local:
raise socket.error(f"Request to local address {sa[0]} blocked") raise socket.error(f"Request to local address {sa[0]} blocked")
if ip_addr.is_private: if ip_addr.is_private:
raise socket.error(f"Request to private address {sa[0]} blocked") raise socket.error(f"Request to private address {sa[0]} blocked")
if check_ip4 in _cgnat_net:
raise socket.error(f"Request to RFC 6598 address {sa[0]} blocked")
sock = None sock = None
try: try:
+3 -3
View File
@@ -68,7 +68,7 @@ from ...control.forms.filter import get_all_payment_providers
from ...helpers import GroupConcat from ...helpers import GroupConcat
from ...helpers.iter import chunked_iterable from ...helpers.iter import chunked_iterable
from ...helpers.safe_openpyxl import remove_invalid_excel_chars from ...helpers.safe_openpyxl import remove_invalid_excel_chars
from ...multidomain.urlreverse import build_absolute_uri from ...multidomain.urlreverse import eventreverse_absolute
from ..exporter import ( from ..exporter import (
ListExporter, MultiSheetListExporter, OrganizerLevelExportMixin, ListExporter, MultiSheetListExporter, OrganizerLevelExportMixin,
) )
@@ -429,7 +429,7 @@ class OrderListExporter(MultiSheetListExporter):
])) ]))
row.append( row.append(
build_absolute_uri(order.event, 'presale:event.order', kwargs={ eventreverse_absolute(order.event, 'presale:event.order', kwargs={
'order': order.code, 'order': order.code,
'secret': order.secret, 'secret': order.secret,
}) })
@@ -855,7 +855,7 @@ class OrderListExporter(MultiSheetListExporter):
])) ]))
row.append( row.append(
build_absolute_uri(order.event, 'presale:event.order.position', kwargs={ eventreverse_absolute(order.event, 'presale:event.order.position', kwargs={
'order': order.code, 'order': order.code,
'secret': op.web_secret, 'secret': op.web_secret,
'position': op.positionid 'position': op.positionid
+16 -10
View File
@@ -53,6 +53,7 @@ from django.db.models import QuerySet
from django.forms import Select, widgets from django.forms import Select, widgets
from django.forms.widgets import FILE_INPUT_CONTRADICTION from django.forms.widgets import FILE_INPUT_CONTRADICTION
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.functional import lazy
from django.utils.html import escape from django.utils.html import escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.text import format_lazy from django.utils.text import format_lazy
@@ -324,16 +325,21 @@ class WrappedPhonePrefixSelect(Select):
initial = None initial = None
def __init__(self, initial=None): def __init__(self, initial=None):
choices = [("", "---------")] def _get_choices():
choices = [("", "---------")]
if initial:
for prefix, values in COUNTRY_CODE_TO_REGION_CODE.items():
if all(v == REGION_CODE_FOR_NON_GEO_ENTITY for v in values):
continue
if initial in values:
self.initial = "+%d" % prefix
break
choices += get_phone_prefixes_sorted_and_localized()
return choices
choices = lazy(_get_choices, list)()
if initial:
for prefix, values in COUNTRY_CODE_TO_REGION_CODE.items():
if all(v == REGION_CODE_FOR_NON_GEO_ENTITY for v in values):
continue
if initial in values:
self.initial = "+%d" % prefix
break
choices += get_phone_prefixes_sorted_and_localized()
super().__init__(choices=choices, attrs={ super().__init__(choices=choices, attrs={
'aria-label': pgettext_lazy('phonenumber', 'International area code'), 'aria-label': pgettext_lazy('phonenumber', 'International area code'),
'autocomplete': 'tel-country-code', 'autocomplete': 'tel-country-code',
@@ -1398,7 +1404,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
elif self.validate_vat_id and vat_id_applicable: elif self.validate_vat_id and vat_id_applicable:
try: try:
normalized_id = validate_vat_id(data.get('vat_id'), str(data.get('country'))) normalized_id = validate_vat_id(data.get('vat_id'), str(data.get('country')))
self.instance.vat_id_validated = True self.instance.vat_id_validated = bool(normalized_id)
self.instance.vat_id = data['vat_id'] = normalized_id self.instance.vat_id = data['vat_id'] = normalized_id
except VATIDFinalError as e: except VATIDFinalError as e:
if self.all_optional: if self.all_optional:
+76 -90
View File
@@ -22,9 +22,7 @@
import datetime import datetime
import logging import logging
import math import math
import re
import textwrap import textwrap
import unicodedata
from collections import defaultdict from collections import defaultdict
from decimal import Decimal from decimal import Decimal
from io import BytesIO from io import BytesIO
@@ -58,8 +56,8 @@ from pretix.base.services.currencies import SOURCE_NAMES
from pretix.base.signals import register_invoice_renderers from pretix.base.signals import register_invoice_renderers
from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.money import money_filter
from pretix.helpers.reportlab import ( from pretix.helpers.reportlab import (
FontFallbackParagraph, ThumbnailingImageReader, register_ttf_font_if_new, FontFallbackParagraph, PlainTextParagraph, ThumbnailingImageReader,
reshaper, normalize_text, register_ttf_font_if_new, reshaper,
) )
from pretix.presale.style import get_fonts from pretix.presale.style import get_fonts
@@ -259,18 +257,8 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
register_ttf_font_if_new(family + ' B I', finders.find(styles['bolditalic']['truetype'])) register_ttf_font_if_new(family + ' B I', finders.find(styles['bolditalic']['truetype']))
def _normalize(self, text): def _normalize(self, text):
# reportlab does not support unicode combination characters # alias kept for plugin compatibility
# It's important we do this before we use ArabicReshaper return normalize_text(text)
text = unicodedata.normalize("NFKC", text)
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
# to resolve all ligatures and python-bidi to switch RTL texts.
try:
text = "<br />".join(get_display(reshaper.reshape(l)) for l in re.split("<br ?/>", text))
except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
return text
def _upper(self, val): def _upper(self, val):
# We uppercase labels, but not in every language # We uppercase labels, but not in every language
@@ -351,10 +339,15 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
return 'invoice.pdf', 'application/pdf', buffer.read() return 'invoice.pdf', 'application/pdf', buffer.read()
def _clean_text(self, text, tags=None): def _clean_text(self, text, tags=None):
return self._normalize(bleach.clean( # For backwards compatibility with customer content, we need to support tags like <br> and <b> in a few text
text, # fields. Therefore, we can't use PlainTextParagraph for these, but run bleach instead to limit the allowed
tags=set(tags) if tags else set() # tags.
).strip().replace('<br>', '<br />').replace('\n', '<br />\n')) return self._normalize(
bleach.clean(
text,
tags=set(tags) if tags else set()
).strip().replace('<br>', '<br />').replace('\n', '<br />\n')
)
class PaidMarker(Flowable): class PaidMarker(Flowable):
@@ -405,8 +398,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
invoice_to_top = 52 * mm invoice_to_top = 52 * mm
def _draw_invoice_to(self, canvas): def _draw_invoice_to(self, canvas):
p = FontFallbackParagraph(self._clean_text(self.invoice.address_invoice_to), p = PlainTextParagraph(self.invoice.address_invoice_to, style=self.stylesheet['Normal'])
style=self.stylesheet['Normal'])
p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height) p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height)
p_size = p.wrap(self.invoice_to_width, self.invoice_to_height) p_size = p.wrap(self.invoice_to_width, self.invoice_to_height)
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - p_size[1] - self.invoice_to_top) p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - p_size[1] - self.invoice_to_top)
@@ -417,8 +409,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
invoice_from_top = 17 * mm invoice_from_top = 17 * mm
def _draw_invoice_from(self, canvas): def _draw_invoice_from(self, canvas):
p = FontFallbackParagraph( p = PlainTextParagraph(
self._clean_text(self.invoice.full_invoice_from), self.invoice.full_invoice_from,
style=self.stylesheet['InvoiceFrom'] style=self.stylesheet['InvoiceFrom']
) )
p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height) p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height)
@@ -548,13 +540,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_event(self, canvas): def _draw_event(self, canvas):
def shorten(txt): def shorten(txt):
txt = str(txt) txt = str(txt)
txt = bleach.clean(txt, tags=set()).strip() p = PlainTextParagraph(txt, style=self.stylesheet['Normal'])
p = FontFallbackParagraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
p_size = p.wrap(self.event_width, self.event_height) p_size = p.wrap(self.event_width, self.event_height)
while p_size[1] > 2 * self.stylesheet['Normal'].leading: while p_size[1] > 2 * self.stylesheet['Normal'].leading:
txt = ' '.join(txt.replace('', '').split()[:-1]) + '' txt = ' '.join(txt.replace('', '').split()[:-1]) + ''
p = FontFallbackParagraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal']) p = PlainTextParagraph(txt, style=self.stylesheet['Normal'])
p_size = p.wrap(self.event_width, self.event_height) p_size = p.wrap(self.event_width, self.event_height)
return txt return txt
@@ -572,7 +563,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
else: else:
p_str = shorten(self.invoice.event.name) p_str = shorten(self.invoice.event.name)
p = FontFallbackParagraph(self._normalize(p_str.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal']) p = PlainTextParagraph(p_str, style=self.stylesheet['Normal'])
p.wrapOn(canvas, self.event_width, self.event_height) p.wrapOn(canvas, self.event_width, self.event_height)
p_size = p.wrap(self.event_width, self.event_height) p_size = p.wrap(self.event_width, self.event_height)
p.drawOn(canvas, self.event_left, self.pagesize[1] - self.event_top - p_size[1]) p.drawOn(canvas, self.event_left, self.pagesize[1] - self.event_top - p_size[1])
@@ -645,39 +636,37 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
type_info_text = self.invoice.transmission_type_instance.pdf_info_text() type_info_text = self.invoice.transmission_type_instance.pdf_info_text()
if type_info_text: if type_info_text:
story.append(FontFallbackParagraph( story.append(PlainTextParagraph(
type_info_text, type_info_text,
self.stylesheet['WarningBlock'] self.stylesheet['WarningBlock']
)) ))
if self.invoice.custom_field: if self.invoice.custom_field:
story.append(FontFallbackParagraph( story.append(PlainTextParagraph(
'{}: {}'.format( '{}: {}'.format(
self._clean_text(str(self.invoice.event.settings.invoice_address_custom_field)), str(self.invoice.event.settings.invoice_address_custom_field),
self._clean_text(self.invoice.custom_field), self.invoice.custom_field,
), ),
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
if self.invoice.internal_reference: if self.invoice.internal_reference:
story.append(FontFallbackParagraph( story.append(PlainTextParagraph(
self._normalize(pgettext('invoice', 'Customer reference: {reference}').format( pgettext('invoice', 'Customer reference: {reference}').format(
reference=self._clean_text(self.invoice.internal_reference), reference=self.invoice.internal_reference,
)), ),
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
if self.invoice.invoice_to_vat_id: if self.invoice.invoice_to_vat_id:
story.append(FontFallbackParagraph( story.append(PlainTextParagraph(
self._normalize(pgettext('invoice', 'Customer VAT ID')) + ': ' + pgettext('invoice', 'Customer VAT ID') + ': ' + self.invoice.invoice_to_vat_id,
self._clean_text(self.invoice.invoice_to_vat_id),
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
if self.invoice.invoice_to_beneficiary: if self.invoice.invoice_to_beneficiary:
story.append(FontFallbackParagraph( story.append(PlainTextParagraph(
self._normalize(pgettext('invoice', 'Beneficiary')) + ':<br />' + pgettext('invoice', 'Beneficiary') + ':\n' + self.invoice.invoice_to_beneficiary,
self._clean_text(self.invoice.invoice_to_beneficiary),
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
@@ -707,11 +696,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story = [ story = [
NextPageTemplate('FirstPage'), NextPageTemplate('FirstPage'),
FontFallbackParagraph( PlainTextParagraph(
self._normalize( (
pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU' pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU'
else pgettext('invoice', 'Invoice') else pgettext('invoice', 'Invoice')
) if not self.invoice.is_cancellation else self._normalize(pgettext('invoice', 'Cancellation')), ) if not self.invoice.is_cancellation else pgettext('invoice', 'Cancellation'),
self.stylesheet['Heading1'] self.stylesheet['Heading1']
), ),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
@@ -733,17 +722,17 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
] ]
if has_taxes: if has_taxes:
tdata = [( tdata = [(
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']), PlainTextParagraph(pgettext('invoice', 'Description'), self.stylesheet['Bold']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']), PlainTextParagraph(pgettext('invoice', 'Qty'), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['BoldRightNoSplit']), PlainTextParagraph(pgettext('invoice', 'Tax rate'), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Net')), self.stylesheet['BoldRightNoSplit']), PlainTextParagraph(pgettext('invoice', 'Net'), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Gross')), self.stylesheet['BoldRightNoSplit']), PlainTextParagraph(pgettext('invoice', 'Gross'), self.stylesheet['BoldRightNoSplit']),
)] )]
else: else:
tdata = [( tdata = [(
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']), PlainTextParagraph(pgettext('invoice', 'Description'), self.stylesheet['Bold']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']), PlainTextParagraph(pgettext('invoice', 'Qty'), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Amount')), self.stylesheet['BoldRightNoSplit']), PlainTextParagraph(pgettext('invoice', 'Amount'), self.stylesheet['BoldRightNoSplit']),
)] )]
def _group_key(line): def _group_key(line):
@@ -780,8 +769,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
max_height = self.stylesheet['Normal'].leading * 5 max_height = self.stylesheet['Normal'].leading * 5
p_style = self.stylesheet['Normal'] p_style = self.stylesheet['Normal']
for __ in range(1000): for __ in range(1000):
p = FontFallbackParagraph( p = PlainTextParagraph(
self._clean_text(curr_description, tags=['br']), curr_description,
p_style p_style
) )
h = p.wrap(max_width, doc.height)[1] h = p.wrap(max_width, doc.height)[1]
@@ -862,7 +851,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
# Group together at the end of the invoice # Group together at the end of the invoice
request_show_service_date = period_line request_show_service_date = period_line
elif period_line: elif period_line:
description_p_list.append(FontFallbackParagraph( description_p_list.append(PlainTextParagraph(
period_line, period_line,
self.stylesheet['Fineprint'] self.stylesheet['Fineprint']
)) ))
@@ -874,7 +863,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
net_price=money_filter(net_value, self.invoice.event.currency), net_price=money_filter(net_value, self.invoice.event.currency),
gross_price=money_filter(gross_value, self.invoice.event.currency), gross_price=money_filter(gross_value, self.invoice.event.currency),
) )
description_p_list.append(FontFallbackParagraph( description_p_list.append(PlainTextParagraph(
single_price_line, single_price_line,
self.stylesheet['Fineprint'] self.stylesheet['Fineprint']
)) ))
@@ -883,11 +872,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
description_p_list.pop(0), description_p_list.pop(0),
str(len(lines)), str(len(lines)),
localize(tax_rate) + " %", localize(tax_rate) + " %",
FontFallbackParagraph( PlainTextParagraph(
money_filter(net_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '), money_filter(net_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
self.stylesheet['NormalRight'] self.stylesheet['NormalRight']
), ),
FontFallbackParagraph( PlainTextParagraph(
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '), money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
self.stylesheet['NormalRight'] self.stylesheet['NormalRight']
), ),
@@ -904,14 +893,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
single_price_line = pgettext('invoice', 'Single price: {price}').format( single_price_line = pgettext('invoice', 'Single price: {price}').format(
price=money_filter(gross_value, self.invoice.event.currency), price=money_filter(gross_value, self.invoice.event.currency),
) )
description_p_list.append(FontFallbackParagraph( description_p_list.append(PlainTextParagraph(
single_price_line, single_price_line,
self.stylesheet['Fineprint'] self.stylesheet['Fineprint']
)) ))
tdata.append(( tdata.append((
description_p_list.pop(0), description_p_list.pop(0),
str(len(lines)), str(len(lines)),
FontFallbackParagraph( PlainTextParagraph(
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '), money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
self.stylesheet['NormalRight'] self.stylesheet['NormalRight']
), ),
@@ -944,12 +933,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if has_taxes: if has_taxes:
tdata.append([ tdata.append([
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', '', '', PlainTextParagraph(pgettext('invoice', 'Invoice total'), self.stylesheet['Bold']), '', '', '',
money_filter(total, self.invoice.event.currency) money_filter(total, self.invoice.event.currency)
]) ])
else: else:
tdata.append([ tdata.append([
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', PlainTextParagraph(pgettext('invoice', 'Invoice total'), self.stylesheet['Bold']), '',
money_filter(total, self.invoice.event.currency) money_filter(total, self.invoice.event.currency)
]) ])
@@ -958,12 +947,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
pending_sum = self.invoice.order.pending_sum pending_sum = self.invoice.order.pending_sum
if pending_sum != total: if pending_sum != total:
tdata.append( tdata.append(
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Received payments')), self.stylesheet['Normal'])] + [PlainTextParagraph(pgettext('invoice', 'Received payments'), self.stylesheet['Normal'])] +
(['', '', ''] if has_taxes else ['']) + (['', '', ''] if has_taxes else ['']) +
[money_filter(pending_sum - total, self.invoice.event.currency)] [money_filter(pending_sum - total, self.invoice.event.currency)]
) )
tdata.append( tdata.append(
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Outstanding payments')), self.stylesheet['Bold'])] + [PlainTextParagraph(pgettext('invoice', 'Outstanding payments'), self.stylesheet['Bold'])] +
(['', '', ''] if has_taxes else ['']) + (['', '', ''] if has_taxes else ['']) +
[money_filter(pending_sum, self.invoice.event.currency)] [money_filter(pending_sum, self.invoice.event.currency)]
) )
@@ -980,12 +969,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
s=Sum('amount') s=Sum('amount')
)['s'] or Decimal('0.00') )['s'] or Decimal('0.00')
tdata.append( tdata.append(
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Paid by gift card')), self.stylesheet['Normal'])] + [PlainTextParagraph(pgettext('invoice', 'Paid by gift card'), self.stylesheet['Normal'])] +
(['', '', ''] if has_taxes else ['']) + (['', '', ''] if has_taxes else ['']) +
[money_filter(giftcard_sum, self.invoice.event.currency)] [money_filter(giftcard_sum, self.invoice.event.currency)]
) )
tdata.append( tdata.append(
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Remaining amount')), self.stylesheet['Bold'])] + [PlainTextParagraph(pgettext('invoice', 'Remaining amount'), self.stylesheet['Bold'])] +
(['', '', ''] if has_taxes else ['']) + (['', '', ''] if has_taxes else ['']) +
[money_filter(total - giftcard_sum, self.invoice.event.currency)] [money_filter(total - giftcard_sum, self.invoice.event.currency)]
) )
@@ -1008,14 +997,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story.append(Spacer(1, 10 * mm)) story.append(Spacer(1, 10 * mm))
if request_show_service_date: if request_show_service_date:
story.append(FontFallbackParagraph( story.append(PlainTextParagraph(
self._normalize(pgettext('invoice', 'Invoice period: {daterange}').format(daterange=request_show_service_date)), pgettext('invoice', 'Invoice period: {daterange}').format(daterange=request_show_service_date),
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
if self.invoice.payment_provider_text: if self.invoice.payment_provider_text:
story.append(FontFallbackParagraph( story.append(FontFallbackParagraph(
self._normalize(self.invoice.payment_provider_text), self._clean_text(self.invoice.payment_provider_text, tags=['br', 'b']),
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
@@ -1039,10 +1028,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
('FONTNAME', (0, 0), (-1, -1), self.font_regular), ('FONTNAME', (0, 0), (-1, -1), self.font_regular),
] ]
thead = [ thead = [
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['Fineprint']), PlainTextParagraph(pgettext('invoice', 'Tax rate'), self.stylesheet['Fineprint']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Net value')), self.stylesheet['FineprintRight']), PlainTextParagraph(pgettext('invoice', 'Net value'), self.stylesheet['FineprintRight']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Gross value')), self.stylesheet['FineprintRight']), PlainTextParagraph(pgettext('invoice', 'Gross value'), self.stylesheet['FineprintRight']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax')), self.stylesheet['FineprintRight']), PlainTextParagraph(pgettext('invoice', 'Tax'), self.stylesheet['FineprintRight']),
'' ''
] ]
tdata = [thead] tdata = [thead]
@@ -1053,7 +1042,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
continue continue
tax = taxvalue_map[idx] tax = taxvalue_map[idx]
tdata.append([ tdata.append([
FontFallbackParagraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']), PlainTextParagraph(localize(rate) + " % " + name, self.stylesheet['Fineprint']),
money_filter(gross - tax, self.invoice.event.currency), money_filter(gross - tax, self.invoice.event.currency),
money_filter(gross, self.invoice.event.currency), money_filter(gross, self.invoice.event.currency),
money_filter(tax, self.invoice.event.currency), money_filter(tax, self.invoice.event.currency),
@@ -1072,7 +1061,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
table.setStyle(TableStyle(tstyledata)) table.setStyle(TableStyle(tstyledata))
story.append(Spacer(5 * mm, 5 * mm)) story.append(Spacer(5 * mm, 5 * mm))
story.append(KeepTogether([ story.append(KeepTogether([
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Included taxes')), self.stylesheet['FineprintHeading']), PlainTextParagraph(pgettext('invoice', 'Included taxes'), self.stylesheet['FineprintHeading']),
table table
])) ]))
@@ -1089,7 +1078,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
net = gross - tax net = gross - tax
tdata.append([ tdata.append([
FontFallbackParagraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']), PlainTextParagraph(localize(rate) + " % " + name, self.stylesheet['Fineprint']),
fmt(net), fmt(gross), fmt(tax), '' fmt(net), fmt(gross), fmt(tax), ''
]) ])
@@ -1098,13 +1087,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story.append(KeepTogether([ story.append(KeepTogether([
Spacer(1, height=2 * mm), Spacer(1, height=2 * mm),
FontFallbackParagraph( PlainTextParagraph(
self._normalize(pgettext( pgettext(
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on ' 'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
'{date}, this corresponds to:' '{date}, this corresponds to:'
).format(rate=localize(self.invoice.foreign_currency_rate), ).format(rate=localize(self.invoice.foreign_currency_rate),
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"), authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"))), date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT")),
self.stylesheet['Fineprint'] self.stylesheet['Fineprint']
), ),
Spacer(1, height=3 * mm), Spacer(1, height=3 * mm),
@@ -1113,14 +1102,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate: elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate:
foreign_total = round_decimal(total * self.invoice.foreign_currency_rate) foreign_total = round_decimal(total * self.invoice.foreign_currency_rate)
story.append(Spacer(1, 5 * mm)) story.append(Spacer(1, 5 * mm))
story.append(FontFallbackParagraph(self._normalize( story.append(PlainTextParagraph(
pgettext( pgettext(
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on ' 'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
'{date}, the invoice total corresponds to {total}.' '{date}, the invoice total corresponds to {total}.'
).format(rate=localize(self.invoice.foreign_currency_rate), ).format(rate=localize(self.invoice.foreign_currency_rate),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"), date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"),
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"), authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
total=fmt(foreign_total))), total=fmt(foreign_total)),
self.stylesheet['Fineprint'] self.stylesheet['Fineprint']
)) ))
@@ -1162,11 +1151,8 @@ class Modern1Renderer(ClassicInvoiceRenderer):
def _draw_invoice_from(self, canvas): def _draw_invoice_from(self, canvas):
if not self.invoice.address_invoice_from: if not self.invoice.address_invoice_from:
return return
c = [ c = self.invoice.address_invoice_from.strip().split('\n')
self._clean_text(l) p = PlainTextParagraph(' · '.join(c), style=self.stylesheet['Sender'])
for l in self.invoice.address_invoice_from.strip().split('\n')
]
p = FontFallbackParagraph(self._normalize(' · '.join(c)), style=self.stylesheet['Sender'])
p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm) p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm)
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm) p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm)
super()._draw_invoice_from(canvas) super()._draw_invoice_from(canvas)
@@ -1225,8 +1211,8 @@ class Modern1Renderer(ClassicInvoiceRenderer):
_draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm, **kwargs) _draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm, **kwargs)
] ]
p = FontFallbackParagraph( p = PlainTextParagraph(
self._normalize(date_format(self.invoice.date, "DATE_FORMAT")), date_format(self.invoice.date, "DATE_FORMAT"),
style=ParagraphStyle(name=f'Normal{value_size}', fontName=self.font_regular, fontSize=value_size, leading=value_size * 1.2) style=ParagraphStyle(name=f'Normal{value_size}', fontName=self.font_regular, fontSize=value_size, leading=value_size * 1.2)
) )
w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize) w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize)
@@ -1283,7 +1269,7 @@ class Modern1SimplifiedRenderer(Modern1Renderer):
i = [] i = []
if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage: if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage:
i.append(FontFallbackParagraph( i.append(PlainTextParagraph(
pgettext('invoice', 'Event date: {date_range}').format( pgettext('invoice', 'Event date: {date_range}').format(
date_range=self.invoice.event.get_date_range_display(), date_range=self.invoice.event.get_date_range_display(),
), ),
@@ -0,0 +1,29 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = "Do nothing. Useful for startup performance testing."
def handle(self, *args, **options):
pass
@@ -40,6 +40,7 @@ from django.core.cache import cache
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db import close_old_connections from django.db import close_old_connections
from django.dispatch.dispatcher import NO_RECEIVERS from django.dispatch.dispatcher import NO_RECEIVERS
from django_querytagger.tagging import with_tag
from pretix.helpers.periodic import SKIPPED from pretix.helpers.periodic import SKIPPED
@@ -82,7 +83,8 @@ class Command(BaseCommand):
try: try:
# Check if the DB connection is still good, it might be closed if the previous task took too long. # Check if the DB connection is still good, it might be closed if the previous task took too long.
close_old_connections() close_old_connections()
r = receiver(signal=periodic_task, sender=self) with with_tag(f"periodictask={name}"):
r = receiver(signal=periodic_task, sender=self)
except Exception as err: except Exception as err:
if isinstance(err, KeyboardInterrupt): if isinstance(err, KeyboardInterrupt):
raise err raise err
+4 -2
View File
@@ -282,10 +282,12 @@ def metric_values():
# Throwaway metrics # Throwaway metrics
exact_tables = [ exact_tables = [
Order, OrderPosition, Invoice, Event, Organizer Order, Invoice, Event, Organizer
] ]
for m in apps.get_models(): # Count all models for m in apps.get_models(): # Count all models
if any(issubclass(m, p) for p in exact_tables): if issubclass(m, OrderPosition):
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = m.all.count()
elif any(issubclass(m, p) for p in exact_tables):
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = m.objects.count() metrics['pretix_model_instances']['{model="%s"}' % m._meta] = m.objects.count()
else: else:
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = estimate_count_fast(m) metrics['pretix_model_instances']['{model="%s"}' % m._meta] = estimate_count_fast(m)
+80 -48
View File
@@ -19,6 +19,8 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
import logging
import re
from collections import OrderedDict from collections import OrderedDict
from urllib.parse import urlparse, urlsplit from urllib.parse import urlparse, urlsplit
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
@@ -43,6 +45,8 @@ from pretix.multidomain.urlreverse import (
) )
from pretix.presale.style import get_fonts from pretix.presale.style import get_fonts
logger = logging.getLogger(__name__)
_supported = None _supported = None
@@ -74,6 +78,7 @@ class LocaleMiddleware(MiddlewareMixin):
def process_request(self, request: HttpRequest): def process_request(self, request: HttpRequest):
language = get_language_from_request(request) language = get_language_from_request(request)
region = None
# Normally, this middleware runs *before* the event is set. However, on event frontend pages it # Normally, this middleware runs *before* the event is set. However, on event frontend pages it
# might be run a second time by pretix.presale.EventMiddleware and in this case the event is already # might be run a second time by pretix.presale.EventMiddleware and in this case the event is already
# set and can be taken into account for the decision. # set and can be taken into account for the decision.
@@ -94,15 +99,16 @@ class LocaleMiddleware(MiddlewareMixin):
if '-' not in language and settings_holder.settings.region: if '-' not in language and settings_holder.settings.region:
language += '-' + settings_holder.settings.region language += '-' + settings_holder.settings.region
if settings_holder.settings.region: if settings_holder.settings.region:
set_region(settings_holder.settings.region) region = settings_holder.settings.region
else: else:
gs = global_settings_object(request) gs = global_settings_object(request)
if '-' not in language and gs.settings.region: if '-' not in language and gs.settings.region:
language += '-' + gs.settings.region language += '-' + gs.settings.region
if gs.settings.region: if gs.settings.region:
set_region(gs.settings.region) region = gs.settings.region
translation.activate(language) translation.activate(language)
set_region(region)
request.LANGUAGE_CODE = get_language_without_region() request.LANGUAGE_CODE = get_language_without_region()
tzname = None tzname = None
@@ -221,7 +227,26 @@ def _parse_csp(header):
return h return h
VALID_CSP_DIRECTIVES = [
"child-src", "connect-src", "default-src", "fenced-frame-src", "font-src", "form-action", "frame-src", "img-src",
"manifest-src", "media-src", "object-src", "prefetch-src", "report-uri", "script-src", "script-src-elem",
"script-src-attr", "style-src", "style-src-elem", "style-src-attr", "worker-src",
]
CSP_ILLEGAL_CHARS = re.compile(r'[\s,;]')
def _sanitize_csp(h):
for k, v in h.items():
if k not in VALID_CSP_DIRECTIVES:
raise ValueError("Invalid CSP directive " + k)
if any(CSP_ILLEGAL_CHARS.search(el) for el in v):
logger.warning("Stripping invalid component from CSP: %r", h)
h[k] = [el for el in v if not CSP_ILLEGAL_CHARS.search(el)]
def _render_csp(h): def _render_csp(h):
_sanitize_csp(h)
return "; ".join(k + ' ' + ' '.join(v) for k, v in h.items() if v) return "; ".join(k + ' ' + ' '.join(v) for k, v in h.items() if v)
@@ -241,21 +266,7 @@ def _merge_csp(a, b):
class SecurityMiddleware(MiddlewareMixin): class SecurityMiddleware(MiddlewareMixin):
CSP_EXEMPT = (
'/api/v1/docs/',
)
def process_response(self, request, resp): def process_response(self, request, resp):
def nested_dict_values(d):
for v in d.values():
if isinstance(v, dict):
yield from nested_dict_values(v)
else:
if isinstance(v, str):
yield v
url = resolve(request.path_info)
if settings.DEBUG and resp.status_code >= 400: if settings.DEBUG and resp.status_code >= 400:
# Don't use CSP on debug error page as it breaks of Django's fancy error # Don't use CSP on debug error page as it breaks of Django's fancy error
# pages # pages
@@ -266,18 +277,15 @@ class SecurityMiddleware(MiddlewareMixin):
# https://github.com/pretix/pretix/issues/765 # https://github.com/pretix/pretix/issues/765
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"' resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
img_src = [] if not getattr(resp, '_csp_ignore', False):
gs = global_settings_object(request) resp['Content-Security-Policy'] = _render_csp(self._build_csp(request, resp))
if gs.settings.leaflet_tiles: elif 'Content-Security-Policy' in resp:
img_src.append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*")) del resp['Content-Security-Policy']
font_src = set() return resp
if hasattr(request, 'event'):
for font in get_fonts(request.event, pdf_support_required=False).values(): def _build_csp(self, request, resp):
for path in list(nested_dict_values(font)): url = resolve(request.path_info)
font_location = urlparse(path)
if font_location.scheme and font_location.netloc:
font_src.add('{}://{}'.format(font_location.scheme, font_location.netloc))
h = { h = {
'default-src': ["{static}"], 'default-src': ["{static}"],
@@ -286,8 +294,8 @@ class SecurityMiddleware(MiddlewareMixin):
'frame-src': ['{static}'], 'frame-src': ['{static}'],
'style-src': ["{static}", "{media}"], 'style-src': ["{static}", "{media}"],
'connect-src': ["{dynamic}", "{media}"], 'connect-src': ["{dynamic}", "{media}"],
'img-src': ["{static}", "{media}", "data:"] + img_src, 'img-src': ["{static}", "{media}", "data:"],
'font-src': ["{static}"] + list(font_src), 'font-src': ["{static}"],
'media-src': ["{static}", "data:"], 'media-src': ["{static}", "data:"],
# form-action is not only used to match on form actions, but also on URLs # form-action is not only used to match on form actions, but also on URLs
# form-actions redirect to. In the context of e.g. payment providers or # form-actions redirect to. In the context of e.g. payment providers or
@@ -296,6 +304,13 @@ class SecurityMiddleware(MiddlewareMixin):
'form-action': ["{dynamic}", "https:"] + (['http:'] if settings.SITE_URL.startswith('http://') else []), 'form-action': ["{dynamic}", "https:"] + (['http:'] if settings.SITE_URL.startswith('http://') else []),
} }
gs = global_settings_object(request)
if gs.settings.leaflet_tiles:
h['img-src'].append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*"))
if hasattr(request, 'event'):
h['font-src'] += list(self._get_font_origins(request.event))
if settings.VITE_DEV_MODE: if settings.VITE_DEV_MODE:
h['script-src'] += ["http://localhost:5173", "ws://localhost:5173"] h['script-src'] += ["http://localhost:5173", "ws://localhost:5173"]
h['style-src'] += ["'unsafe-inline'"] h['style-src'] += ["'unsafe-inline'"]
@@ -307,6 +322,7 @@ class SecurityMiddleware(MiddlewareMixin):
if not settings.VITE_DEV_MODE: if not settings.VITE_DEV_MODE:
# can't have 'unsafe-inline' and nonce at the same time # can't have 'unsafe-inline' and nonce at the same time
h['style-src'].append(nonce) h['style-src'].append(nonce)
# Only include pay.google.com for wallet detection purposes on the Payment selection page # Only include pay.google.com for wallet detection purposes on the Payment selection page
if ( if (
url.url_name == "event.order.pay.change" or url.url_name == "event.order.pay.change" or
@@ -315,27 +331,32 @@ class SecurityMiddleware(MiddlewareMixin):
h['script-src'].append('https://pay.google.com') h['script-src'].append('https://pay.google.com')
h['frame-src'].append('https://pay.google.com') h['frame-src'].append('https://pay.google.com')
h['connect-src'].append('https://google.com/pay') h['connect-src'].append('https://google.com/pay')
if settings.LOG_CSP: if settings.LOG_CSP:
h['report-uri'] = ["/csp_report/"] h['report-uri'] = ["/csp_report/"]
if 'Content-Security-Policy' in resp: if 'Content-Security-Policy' in resp:
_merge_csp(h, _parse_csp(resp['Content-Security-Policy'])) _merge_csp(h, _parse_csp(resp['Content-Security-Policy']))
if settings.CSP_ADDITIONAL_HEADER: if settings.CSP_ADDITIONAL_HEADER:
_merge_csp(h, _parse_csp(settings.CSP_ADDITIONAL_HEADER)) _merge_csp(h, _parse_csp(settings.CSP_ADDITIONAL_HEADER))
staticdomain = "'self'" placeholders = {
dynamicdomain = "'self'" "{static}": ["'self'"],
mediadomain = "'self'" "{dynamic}": ["'self'"],
"{media}": ["'self'"],
}
if settings.MEDIA_URL.startswith('http'): if settings.MEDIA_URL.startswith('http'):
mediadomain += " " + settings.MEDIA_URL[:settings.MEDIA_URL.find('/', 9)] placeholders["{media}"].append(settings.MEDIA_URL[:settings.MEDIA_URL.find('/', 9)])
if settings.STATIC_URL.startswith('http'): if settings.STATIC_URL.startswith('http'):
staticdomain += " " + settings.STATIC_URL[:settings.STATIC_URL.find('/', 9)] placeholders["{static}"].append(settings.STATIC_URL[:settings.STATIC_URL.find('/', 9)])
if settings.SITE_URL.startswith('http'): if settings.SITE_URL.startswith('http'):
if settings.SITE_URL.find('/', 9) > 0: if settings.SITE_URL.find('/', 9) > 0:
staticdomain += " " + settings.SITE_URL[:settings.SITE_URL.find('/', 9)] placeholders["{static}"].append(settings.SITE_URL[:settings.SITE_URL.find('/', 9)])
dynamicdomain += " " + settings.SITE_URL[:settings.SITE_URL.find('/', 9)] placeholders["{dynamic}"].append(settings.SITE_URL[:settings.SITE_URL.find('/', 9)])
else: else:
staticdomain += " " + settings.SITE_URL placeholders["{static}"].append(settings.SITE_URL)
dynamicdomain += " " + settings.SITE_URL placeholders["{dynamic}"].append(settings.SITE_URL)
if hasattr(request, 'organizer') and request.organizer: if hasattr(request, 'organizer') and request.organizer:
if hasattr(request, 'event') and request.event: if hasattr(request, 'event') and request.event:
@@ -346,18 +367,29 @@ class SecurityMiddleware(MiddlewareMixin):
siteurlsplit = urlsplit(settings.SITE_URL) siteurlsplit = urlsplit(settings.SITE_URL)
if siteurlsplit.port and siteurlsplit.port not in (80, 443): if siteurlsplit.port and siteurlsplit.port not in (80, 443):
domain = '%s:%d' % (domain, siteurlsplit.port) domain = '%s:%d' % (domain, siteurlsplit.port)
dynamicdomain += " " + domain placeholders["{dynamic}"].append(domain)
if request.path not in self.CSP_EXEMPT and not getattr(resp, '_csp_ignore', False): for k, v in h.items():
resp['Content-Security-Policy'] = _render_csp(h).format(static=staticdomain, dynamic=dynamicdomain, h[k] = sorted(set(result for part in v for result in placeholders.get(part, [part])))
media=mediadomain)
for k, v in h.items():
h[k] = sorted(set(' '.join(v).format(static=staticdomain, dynamic=dynamicdomain, media=mediadomain).split(' ')))
resp['Content-Security-Policy'] = _render_csp(h)
elif 'Content-Security-Policy' in resp:
del resp['Content-Security-Policy']
return resp return h
def _get_font_origins(self, event):
def nested_dict_values(d):
for v in d.values():
if isinstance(v, dict):
yield from nested_dict_values(v)
else:
if isinstance(v, str):
yield v
font_src = set()
for font in get_fonts(event, pdf_support_required=False).values():
for path in list(nested_dict_values(font)):
font_location = urlparse(path)
if font_location.scheme and font_location.netloc:
font_src.add('{}://{}'.format(font_location.scheme, font_location.netloc))
return font_src
class RejectInvalidInputMiddleware(MiddlewareMixin): class RejectInvalidInputMiddleware(MiddlewareMixin):
+17 -17
View File
@@ -57,7 +57,7 @@ from django_otp.models import Device
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.helpers.urls import build_absolute_uri from pretix.helpers.urls import mainreverse_absolute
from ...helpers.countries import FastCountryField from ...helpers.countries import FastCountryField
from ...helpers.u2f import pub_key_from_der, websafe_decode from ...helpers.u2f import pub_key_from_der, websafe_decode
@@ -373,12 +373,12 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
mail( mail(
email or self.email, email or self.email,
_('Account information changed'), _('Changes to your account'),
'pretixcontrol/email/security_notice.txt', 'pretixcontrol/email/security_notice.txt',
{ {
'user': self, 'user': self,
'messages': msg, 'messages': msg,
'url': build_absolute_uri('control:user.settings'), 'url': mainreverse_absolute('control:user.settings'),
'instance': settings.PRETIX_INSTANCE_NAME, 'instance': settings.PRETIX_INSTANCE_NAME,
}, },
event=None, event=None,
@@ -400,12 +400,13 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
with language(self.locale): with language(self.locale):
if reason == 'email_change': if reason == 'email_change':
msg = str(_('to confirm changing your email address from {old_email}\nto {new_email}, use the following code:').format( msg = str(_('To change your email address from {old_email} to {new_email}, use the following code:').format(
old_email=self.email, new_email=email, old_email=self.email, new_email=email,
)) ))
elif reason == 'email_verify': elif reason == 'email_verify':
msg = str(_('to confirm that your email address {email} belongs to your pretix account, use the following code:').format( msg = str(_('To verify your email address {email} on {instance}, use the following code:').format(
email=self.email, email=self.email,
instance=settings.PRETIX_INSTANCE_NAME,
)) ))
else: else:
raise Exception('Invalid confirmation code reason') raise Exception('Invalid confirmation code reason')
@@ -418,7 +419,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
} }
mail( mail(
email or self.email, email or self.email,
_('pretix confirmation code'), _('Your confirmation code'),
'pretixcontrol/email/confirmation_code.txt', 'pretixcontrol/email/confirmation_code.txt',
{ {
'user': self, 'user': self,
@@ -462,11 +463,13 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
from pretix.base.services.mail import mail from pretix.base.services.mail import mail
mail( mail(
self.email, _('Password recovery'), 'pretixcontrol/email/forgot.txt', self.email,
_('Reset your password'),
'pretixcontrol/email/forgot.txt',
{ {
'instance': settings.PRETIX_INSTANCE_NAME, 'instance': settings.PRETIX_INSTANCE_NAME,
'user': self, 'user': self,
'url': (build_absolute_uri('control:auth.forgot.recover') 'url': (mainreverse_absolute('control:auth.forgot.recover')
+ '?id=%d&token=%s' % (self.id, default_token_generator.make_token(self))) + '?id=%d&token=%s' % (self.id, default_token_generator.make_token(self)))
}, },
None, locale=self.locale, user=self None, locale=self.locale, user=self
@@ -647,25 +650,22 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
id__in=self.teams.filter(TeamQuerySet.organizer_permission_q(permission)).values_list('organizer', flat=True) id__in=self.teams.filter(TeamQuerySet.organizer_permission_q(permission)).values_list('organizer', flat=True)
) )
def has_active_staff_session(self, session_key=None): def has_active_staff_session(self, session_key):
""" """
Returns whether or not a user has an active staff session (formerly known as superuser session) Returns whether or not a user has an active staff session (formerly known as superuser session)
with the given session key. with the given session key.
""" """
return self.get_active_staff_session(session_key) is not None return self.get_active_staff_session(session_key) is not None
def get_active_staff_session(self, session_key=None): def get_active_staff_session(self, session_key):
if not self.is_staff: if not self.is_staff or not session_key:
return None return None
if not hasattr(self, '_staff_session_cache'): if not hasattr(self, '_staff_session_cache'):
self._staff_session_cache = {} self._staff_session_cache = {}
if session_key not in self._staff_session_cache: if session_key not in self._staff_session_cache:
qs = StaffSession.objects.filter( sess = StaffSession.objects.filter(
user=self, date_end__isnull=True user=self, date_end__isnull=True, session_key=session_key
) ).first()
if session_key:
qs = qs.filter(session_key=session_key)
sess = qs.first()
if sess: if sess:
if sess.date_start < now() - timedelta(seconds=settings.PRETIX_SESSION_TIMEOUT_ABSOLUTE): if sess.date_start < now() - timedelta(seconds=settings.PRETIX_SESSION_TIMEOUT_ABSOLUTE):
sess.date_end = now() sess.date_end = now()
+5 -5
View File
@@ -167,7 +167,7 @@ class Customer(LoggedModel):
def send_security_notice(self, message, email=None): def send_security_notice(self, message, email=None):
from pretix.base.services.mail import SendMailException, mail from pretix.base.services.mail import SendMailException, mail
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
try: try:
with language(self.locale): with language(self.locale):
@@ -178,7 +178,7 @@ class Customer(LoggedModel):
{ {
**self.get_email_context(), **self.get_email_context(),
'message': str(message), 'message': str(message),
'url': build_absolute_uri(self.organizer, 'presale:organizer.customer.index') 'url': eventreverse_absolute(self.organizer, 'presale:organizer.customer.index')
}, },
customer=self, customer=self,
organizer=self.organizer, organizer=self.organizer,
@@ -299,12 +299,12 @@ class Customer(LoggedModel):
def send_activation_mail(self): def send_activation_mail(self):
from pretix.base.services.mail import mail from pretix.base.services.mail import mail
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.presale.forms.customer import TokenGenerator from pretix.presale.forms.customer import TokenGenerator
ctx = self.get_email_context() ctx = self.get_email_context()
token = TokenGenerator().make_token(self) token = TokenGenerator().make_token(self)
ctx['url'] = build_absolute_uri( ctx['url'] = eventreverse_absolute(
self.organizer, self.organizer,
'presale:organizer.customer.activate' 'presale:organizer.customer.activate'
) + '?id=' + self.identifier + '&token=' + token ) + '?id=' + self.identifier + '&token=' + token
@@ -395,7 +395,7 @@ class AttendeeProfile(models.Model):
self.company, self.company,
self.street, self.street,
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''), (self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
self.country.name, self.country.name if self.country else None,
] ]
for a in self.answers: for a in self.answers:
value = a.get('value') value = a.get('value')
+26 -6
View File
@@ -40,6 +40,7 @@ import warnings
from collections import Counter, OrderedDict, defaultdict from collections import Counter, OrderedDict, defaultdict
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta
from operator import attrgetter from operator import attrgetter
from typing import TYPE_CHECKING
from urllib.parse import urljoin from urllib.parse import urljoin
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
@@ -79,10 +80,16 @@ from pretix.helpers.thumb import get_thumbnail
from ..settings import settings_hierarkey from ..settings import settings_hierarkey
from .organizer import Organizer, Team from .organizer import Organizer, Team
if TYPE_CHECKING:
from hierarkey.proxy import HierarkeyProxy
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class EventMixin: class EventMixin:
if TYPE_CHECKING:
settings: HierarkeyProxy
def clean(self): def clean(self):
if self.presale_start and self.presale_end and self.presale_start > self.presale_end: if self.presale_start and self.presale_end and self.presale_start > self.presale_end:
raise ValidationError({'presale_end': _('The end of the presale period has to be later than its start.')}) raise ValidationError({'presale_end': _('The end of the presale period has to be later than its start.')})
@@ -724,7 +731,7 @@ class Event(EventMixin, LoggedModel):
@property @property
def social_image(self): def social_image(self):
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
img = None img = None
logo_file = self.settings.get('logo_image', as_type=str, default='')[7:] logo_file = self.settings.get('logo_image', as_type=str, default='')[7:]
@@ -742,7 +749,7 @@ class Event(EventMixin, LoggedModel):
logger.exception(f'Failed to create thumbnail of {logo_file}') logger.exception(f'Failed to create thumbnail of {logo_file}')
img = default_storage.url(logo_file) img = default_storage.url(logo_file)
if img: if img:
return urljoin(build_absolute_uri(self, 'presale:event.index'), img) return urljoin(eventreverse_absolute(self, 'presale:event.index'), img)
def _seats(self, ignore_voucher=None): def _seats(self, ignore_voucher=None):
from .seating import Seat from .seating import Seat
@@ -883,6 +890,8 @@ class Event(EventMixin, LoggedModel):
ItemProgramTime, ItemVariationMetaValue, Question, Quota, ItemProgramTime, ItemVariationMetaValue, Question, Quota,
) )
is_cross_organizer = other.organizer_id != self.organizer_id
# Note: avoid self.set_active_plugins(), it causes trouble e.g. for the badges plugin. # Note: avoid self.set_active_plugins(), it causes trouble e.g. for the badges plugin.
# Plugins can create data in installed() hook based on existing data of the event. # Plugins can create data in installed() hook based on existing data of the event.
# Calling set_active_plugins() results in defaults being created while actually data # Calling set_active_plugins() results in defaults being created while actually data
@@ -897,7 +906,7 @@ class Event(EventMixin, LoggedModel):
self.save() self.save()
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk}) self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
if hasattr(other, 'alternative_domain_assignment'): if hasattr(other, 'alternative_domain_assignment') and not is_cross_organizer:
other.alternative_domain_assignment.domain.event_assignments.create(event=self) other.alternative_domain_assignment.domain.event_assignments.create(event=self)
if not self.all_sales_channels: if not self.all_sales_channels:
@@ -911,6 +920,15 @@ class Event(EventMixin, LoggedModel):
for emv in EventMetaValue.objects.filter(event=other): for emv in EventMetaValue.objects.filter(event=other):
emv.pk = None emv.pk = None
emv.event = self emv.event = self
if is_cross_organizer:
try:
emv.property = self.organizer.meta_properties.get(name=emv.property.name)
except EventMetaProperty.DoesNotExist:
meta_prop = emv.property
meta_prop.pk = None
meta_prop.organizer = self.organizer
meta_prop.save(force_insert=True)
emv.property = meta_prop
emv.save(force_insert=True) emv.save(force_insert=True)
for fl in EventFooterLink.objects.filter(event=other): for fl in EventFooterLink.objects.filter(event=other):
@@ -964,13 +982,13 @@ class Event(EventMixin, LoggedModel):
if i.tax_rule_id: if i.tax_rule_id:
i.tax_rule = tax_map[i.tax_rule_id] i.tax_rule = tax_map[i.tax_rule_id]
if i.grant_membership_type and other.organizer_id != self.organizer_id: if i.grant_membership_type and is_cross_organizer:
i.grant_membership_type = None i.grant_membership_type = None
i.save() # no force_insert since i.picture.save could have already inserted i.save() # no force_insert since i.picture.save could have already inserted
i.log_action('pretix.object.cloned') i.log_action('pretix.object.cloned')
if require_membership_types and other.organizer_id == self.organizer_id: if require_membership_types and not is_cross_organizer:
i.require_membership_types.set(require_membership_types) i.require_membership_types.set(require_membership_types)
if not i.all_sales_channels: if not i.all_sales_channels:
@@ -985,7 +1003,7 @@ class Event(EventMixin, LoggedModel):
v._prefetched_objects_cache = {} v._prefetched_objects_cache = {}
v.save(force_insert=True) v.save(force_insert=True)
if require_membership_types and other.organizer_id == self.organizer_id: if require_membership_types and not is_cross_organizer:
v.require_membership_types.set(require_membership_types) v.require_membership_types.set(require_membership_types)
if not v.all_sales_channels: if not v.all_sales_channels:
v.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels])) v.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels]))
@@ -1869,6 +1887,8 @@ class EventMetaValue(LoggedModel):
self.event.cache.clear() self.event.cache.clear()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.event and self.event.organizer != self.property.organizer:
raise ValidationError(_("Property and event must belong to the same organizer."))
super().save(*args, **kwargs) super().save(*args, **kwargs)
if self.event: if self.event:
self.event.cache.clear() self.event.cache.clear()
+52 -30
View File
@@ -354,38 +354,60 @@ class Order(LockModel, LoggedModel):
def _transaction_key_reset(self): def _transaction_key_reset(self):
self.__initial_status_paid_or_pending = self.status in (Order.STATUS_PENDING, Order.STATUS_PAID) and not self.require_approval self.__initial_status_paid_or_pending = self.status in (Order.STATUS_PENDING, Order.STATUS_PAID) and not self.require_approval
def gracefully_delete(self, user=None, auth=None): @classmethod
from . import GiftCard, GiftCardTransaction, Membership, Voucher def gracefully_delete_bulk(cls, event, orders, user=None, auth=None):
# Expects to be called in a transaction
if not self.testmode: from . import (
raise TypeError("Only test mode orders can be deleted.") GiftCard, GiftCardTransaction, LogEntry, Membership, Voucher,
self.log_action(
'pretix.event.order.deleted', user=user, auth=auth,
data={
'code': self.code,
}
) )
order_gracefully_delete.send(self.event, order=self) if not transaction.get_connection().in_atomic_block:
raise Exception('gracefully_delete_bulk should only be called in atomic transaction!')
if self.status != Order.STATUS_CANCELED: logs_create = []
for position in self.positions.all(): for o in orders:
if position.voucher: if not o.testmode:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1)) raise TypeError("Only test mode orders can be deleted.")
order_gracefully_delete.send(event, order=o)
logs_create.append(o.log_action(
'pretix.event.order.deleted', user=user, auth=auth,
data={
'code': o.code,
},
save=False,
))
LogEntry.bulk_create_and_postprocess(logs_create)
GiftCardTransaction.objects.filter(payment__in=self.payments.all()).update(payment=None) voucher_ids = OrderPosition.objects.filter(
GiftCardTransaction.objects.filter(refund__in=self.refunds.all()).update(refund=None) order__in=orders,
GiftCardTransaction.objects.filter(order=self).update(order=None) voucher__isnull=False
GiftCard.objects.filter(issued_in__in=self.positions.all()).update(issued_in=None) ).exclude(order__status=Order.STATUS_CANCELED).values_list("voucher_id", flat=True)
Membership.objects.filter(granted_in__order=self, testmode=True).update(granted_in=None) voucher_usages = Counter(voucher_ids)
OrderPosition.all.filter(order=self, addon_to__isnull=False).delete() for v_id, usage_count in voucher_usages.items():
OrderPosition.all.filter(order=self).delete() Voucher.objects.filter(pk=v_id).update(redeemed=Greatest(0, F('redeemed') - usage_count))
OrderFee.all.filter(order=self).delete()
Transaction.objects.filter(order=self).delete() GiftCardTransaction.objects.filter(payment__order__in=orders).update(payment=None)
self.refunds.all().delete() GiftCardTransaction.objects.filter(refund__order__in=orders).update(refund=None)
self.payments.all().delete() GiftCardTransaction.objects.filter(order__in=orders).update(order=None)
self.event.cache.delete('complain_testmode_orders') GiftCard.objects.filter(issued_in__order__in=orders).update(issued_in=None)
self.delete() Membership.objects.filter(granted_in__order__in=orders, testmode=True).update(granted_in=None)
OrderPosition.all.filter(order__in=orders, addon_to__isnull=False).delete()
OrderPosition.all.filter(order__in=orders).delete()
OrderFee.all.filter(order__in=orders).delete()
Transaction.objects.filter(order__in=orders).delete()
OrderRefund.objects.filter(order__in=orders).delete()
OrderPayment.objects.filter(order__in=orders).delete()
if isinstance(orders, models.QuerySet):
orders.delete()
else:
Order.objects.filter(pk__in=[o.pk for o in orders]).delete()
event.cache.delete('complain_testmode_orders')
def gracefully_delete(self, user=None, auth=None):
if not self.testmode:
raise TypeError("Only test mode orders can be deleted.")
Order.gracefully_delete_bulk(self.event, Order.objects.filter(pk=self.pk), user, auth)
def email_confirm_secret(self): def email_confirm_secret(self):
return self.tagged_secret("email_confirm", 9) return self.tagged_secret("email_confirm", 9)
@@ -1675,7 +1697,7 @@ class AbstractPosition(RoundingCorrectionMixin, models.Model):
self.company, self.company,
self.street, self.street,
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''), (self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
self.country.name self.country.name if self.country else ''
] ]
lines = [r.strip() for r in lines if r] lines = [r.strip() for r in lines if r]
return '\n'.join(lines).strip() return '\n'.join(lines).strip()
@@ -3416,7 +3438,7 @@ class InvoiceAddress(models.Model):
self.name, self.name,
self.street, self.street,
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''), (self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
self.country.name, self.country.name if self.country else '',
self.vat_id, self.vat_id,
self.custom_field, self.custom_field,
self.internal_reference, self.internal_reference,
+7
View File
@@ -35,6 +35,7 @@ import operator
import string import string
from datetime import date, datetime, time from datetime import date, datetime, time
from functools import reduce from functools import reduce
from typing import TYPE_CHECKING
import pytz_deprecation_shim import pytz_deprecation_shim
from django.conf import settings from django.conf import settings
@@ -61,6 +62,9 @@ from ...helpers.permission_migration import (
from ..settings import settings_hierarkey from ..settings import settings_hierarkey
from .auth import User from .auth import User
if TYPE_CHECKING:
from hierarkey.proxy import HierarkeyProxy
@settings_hierarkey.add(cache_namespace='organizer') @settings_hierarkey.add(cache_namespace='organizer')
class Organizer(LoggedModel): class Organizer(LoggedModel):
@@ -78,6 +82,9 @@ class Organizer(LoggedModel):
""" """
settings_namespace = 'organizer' settings_namespace = 'organizer'
if TYPE_CHECKING:
settings: HierarkeyProxy
name = models.CharField(max_length=200, name = models.CharField(max_length=200,
verbose_name=_("Name")) verbose_name=_("Name"))
slug = models.CharField( slug = models.CharField(
+6 -3
View File
@@ -118,7 +118,10 @@ class SeatingPlan(LoggedModel):
for zi, z in enumerate(self.layout_data['zones']): for zi, z in enumerate(self.layout_data['zones']):
zpos = (z['position']['x'], z['position']['y']) zpos = (z['position']['x'], z['position']['y'])
for ri, r in enumerate(z['rows']): for ri, r in enumerate(z['rows']):
rpos = (zpos[0] + r['position']['x'], zpos[1] + r['position']['y']) rpos = (
zpos[0] + r.get('position', {}).get('x', 0),
zpos[1] + r.get('position', {}).get('y', 0),
)
row_label = None row_label = None
if r.get('row_label'): if r.get('row_label'):
row_label = r['row_label'].replace("%s", r.get('row_number', str(ri))) row_label = r['row_label'].replace("%s", r.get('row_number', str(ri)))
@@ -147,8 +150,8 @@ class SeatingPlan(LoggedModel):
zone=z['name'], zone=z['name'],
category=s['category'], category=s['category'],
sorting_rank=rank, sorting_rank=rank,
x=rpos[0] + s['position']['x'], x=rpos[0] + s.get('position', {}).get('x', 0),
y=rpos[1] + s['position']['y'], y=rpos[1] + s.get('position', {}).get('y', 0),
) )
+2 -2
View File
@@ -43,7 +43,7 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.models import Event, LogEntry from pretix.base.models import Event, LogEntry
from pretix.base.signals import register_notification_types from pretix.base.signals import register_notification_types
from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.money import money_filter
from pretix.helpers.urls import build_absolute_uri from pretix.helpers.urls import mainreverse_absolute
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_ALL_TYPES = None _ALL_TYPES = None
@@ -170,7 +170,7 @@ class ParametrizedOrderNotificationType(NotificationType):
def build_notification(self, logentry: LogEntry): def build_notification(self, logentry: LogEntry):
order = logentry.content_object order = logentry.content_object
order_url = build_absolute_uri( order_url = mainreverse_absolute(
'control:event.order', 'control:event.order',
kwargs={ kwargs={
'organizer': logentry.event.organizer.slug, 'organizer': logentry.event.organizer.slug,
+3 -3
View File
@@ -71,7 +71,7 @@ from pretix.helpers import OF_SELF
from pretix.helpers.countries import CachedCountries from pretix.helpers.countries import CachedCountries
from pretix.helpers.format import format_map from pretix.helpers.format import format_map
from pretix.helpers.money import DecimalTextInput from pretix.helpers.money import DecimalTextInput
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.presale.views import get_cart from pretix.presale.views import get_cart
from pretix.presale.views.cart import cart_session, get_or_create_cart_id from pretix.presale.views.cart import cart_session, get_or_create_cart_id
@@ -379,7 +379,7 @@ class BasePaymentProvider:
if not self.settings.get('_hidden_seed'): if not self.settings.get('_hidden_seed'):
self.settings.set('_hidden_seed', get_random_string(64)) self.settings.set('_hidden_seed', get_random_string(64))
hidden_url = build_absolute_uri(self.event, 'presale:event.payment.unlock', kwargs={ hidden_url = eventreverse_absolute(self.event, 'presale:event.payment.unlock', kwargs={
'hash': hashlib.sha256((self.settings._hidden_seed + self.event.slug).encode()).hexdigest(), 'hash': hashlib.sha256((self.settings._hidden_seed + self.event.slug).encode()).hexdigest(),
}) })
@@ -834,7 +834,7 @@ class BasePaymentProvider:
""" """
raise NotImplementedError() # NOQA raise NotImplementedError() # NOQA
def execute_payment(self, request: HttpRequest, payment: OrderPayment) -> str: def execute_payment(self, request: HttpRequest, payment: OrderPayment) -> str | None:
""" """
After the user has confirmed their purchase, this method will be called to complete After the user has confirmed their purchase, this method will be called to complete
the payment process. This is the place to actually move the money if applicable. the payment process. This is the place to actually move the money if applicable.
+16 -6
View File
@@ -77,6 +77,7 @@ from reportlab.platypus import Paragraph
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import Checkin, Event, Order, OrderPosition, Question from pretix.base.models import Checkin, Event, Order, OrderPosition, Question
from pretix.base.services.placeholders import PlaceholderContext
from pretix.base.settings import PERSON_NAME_SCHEMES from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import layout_image_variables, layout_text_variables from pretix.base.signals import layout_image_variables, layout_text_variables
from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.money import money_filter
@@ -372,6 +373,11 @@ DEFAULT_VARIABLES = OrderedDict((
"editor_sample": _("Atlantis"), "editor_sample": _("Atlantis"),
"evaluate": lambda op, order, ev: str(getattr(order.invoice_address.country, 'name', '')) if getattr(order, 'invoice_address', None) else '' "evaluate": lambda op, order, ev: str(getattr(order.invoice_address.country, 'name', '')) if getattr(order, 'invoice_address', None) else ''
}), }),
("invoice_custom_field", {
"label": _("Invoice custom recipient field"),
"editor_sample": _("Custom recipient field"),
"evaluate": lambda op, order, ev: order.invoice_address.custom_field if getattr(order, 'invoice_address', None) else ''
}),
("addons", { ("addons", {
"label": _("List of Add-Ons"), "label": _("List of Add-Ons"),
"editor_sample": _("Add-on 1\n2x Add-on 2"), "editor_sample": _("Add-on 1\n2x Add-on 2"),
@@ -396,11 +402,7 @@ DEFAULT_VARIABLES = OrderedDict((
"editor_sample": _("Event organizer info text"), "editor_sample": _("Event organizer info text"),
"evaluate": lambda op, order, ev: str(order.event.settings.organizer_info_text) "evaluate": lambda op, order, ev: str(order.event.settings.organizer_info_text)
}), }),
("event_info_text", { ("event_info_text", {}), # Placeholder to "reserve" position, defined later in `get_variables`
"label": _("Event info text"),
"editor_sample": _("Event info text"),
"evaluate": lambda op, order, ev: str(order.event.settings.event_info_text)
}),
("now_date", { ("now_date", {
"label": _("Printing date"), "label": _("Printing date"),
"editor_sample": _("2017-05-31"), "editor_sample": _("2017-05-31"),
@@ -665,6 +667,14 @@ def get_images(event):
def get_variables(event): def get_variables(event):
v = copy.copy(DEFAULT_VARIABLES) v = copy.copy(DEFAULT_VARIABLES)
templating_context = PlaceholderContext(event=event)
v['event_info_text'] = {
"label": _("Event info text"),
"editor_sample": _("Event info text"),
"evaluate": lambda op, order, ev:
templating_context.format(str(order.event.settings.event_info_text))
}
scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme] scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
concatenation_for_salutation = scheme.get("concatenation_for_salutation", scheme["concatenation"]) concatenation_for_salutation = scheme.get("concatenation_for_salutation", scheme["concatenation"])
@@ -1062,7 +1072,7 @@ class Renderer:
except: except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text))) logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
p = Paragraph(text, style=style) p = Paragraph(text, style=style) # not using AutoEscapeParagraph is safe as we escape above
return p, ad, lineheight return p, ad, lineheight
def _draw_textcontainer(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict): def _draw_textcontainer(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
+3 -1
View File
@@ -22,6 +22,7 @@
import logging import logging
from decimal import Decimal from decimal import Decimal
from django.conf import settings
from django.db import transaction from django.db import transaction
from django.db.models import Count, Exists, IntegerField, OuterRef, Q, Subquery from django.db.models import Count, Exists, IntegerField, OuterRef, Q, Subquery
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
@@ -377,12 +378,13 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
confirmation_code = get_random_string(8, allowed_chars="01234567890") confirmation_code = get_random_string(8, allowed_chars="01234567890")
mail( mail(
user.email, user.email,
subject=gettext('Bulk-refund confirmation'), subject=gettext('Confirm event cancellation and bulk refund'),
template='pretixbase/email/cancel_confirm.txt', template='pretixbase/email/cancel_confirm.txt',
context={ context={
"event": str(event), "event": str(event),
"amount": money_filter(refund_total, event.currency), "amount": money_filter(refund_total, event.currency),
"confirmation_code": confirmation_code, "confirmation_code": confirmation_code,
"instance": settings.PRETIX_INSTANCE_NAME,
}, },
locale=user.locale, locale=user.locale,
) )
+4 -3
View File
@@ -40,7 +40,7 @@ import dateutil
import dateutil.parser import dateutil.parser
from dateutil.tz import datetime_exists from dateutil.tz import datetime_exists
from django.core.files import File from django.core.files import File
from django.db import IntegrityError, transaction from django.db import IntegrityError
from django.db.models import ( from django.db.models import (
BooleanField, Case, Count, ExpressionWrapper, F, IntegerField, Max, Min, BooleanField, Case, Count, ExpressionWrapper, F, IntegerField, Max, Min,
OuterRef, Q, Subquery, TextField, Value, When, OuterRef, Q, Subquery, TextField, Value, When,
@@ -59,6 +59,7 @@ from pretix.base.models import (
) )
from pretix.base.signals import checkin_created, periodic_task from pretix.base.signals import checkin_created, periodic_task
from pretix.helpers import OF_SELF from pretix.helpers import OF_SELF
from pretix.helpers.database import conditional_atomic
from pretix.helpers.jsonlogic import Logic from pretix.helpers.jsonlogic import Logic
from pretix.helpers.jsonlogic_boolalg import convert_to_dnf from pretix.helpers.jsonlogic_boolalg import convert_to_dnf
from pretix.helpers.jsonlogic_query import ( from pretix.helpers.jsonlogic_query import (
@@ -1043,10 +1044,10 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
if not simulate: if not simulate:
_save_answers(op, answers, given_answers) _save_answers(op, answers, given_answers)
with transaction.atomic(): with conditional_atomic(not simulate):
# Lock order positions, if it is an entry. We don't need it for exits, as a race condition wouldn't be problematic # Lock order positions, if it is an entry. We don't need it for exits, as a race condition wouldn't be problematic
opqs = OrderPosition.all.select_related("order", "item") opqs = OrderPosition.all.select_related("order", "item")
if type != Checkin.TYPE_EXIT: if type != Checkin.TYPE_EXIT and not simulate:
opqs = opqs.select_for_update(of=OF_SELF) opqs = opqs.select_for_update(of=OF_SELF)
op = opqs.get(pk=op.pk) op = opqs.get(pk=op.pk)
+5 -4
View File
@@ -51,7 +51,7 @@ from pretix.base.signals import (
) )
from pretix.celery_app import app from pretix.celery_app import app
from pretix.helpers import OF_SELF, repeatable_reads_transaction from pretix.helpers import OF_SELF, repeatable_reads_transaction
from pretix.helpers.urls import build_absolute_uri from pretix.helpers.urls import mainreverse_absolute
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -340,12 +340,13 @@ def _run_scheduled_export(schedule, context: Union[Event, Organizer], exporter,
if schedule.owner.is_active: if schedule.owner.is_active:
mail( mail(
email=schedule.owner.email, email=schedule.owner.email,
subject=gettext('Export failed'), subject=gettext('Scheduled export failed'),
template='pretixbase/email/export_failed.txt', template='pretixbase/email/export_failed.txt',
context={ context={
'configuration_url': config_url, 'configuration_url': config_url,
'reason': msg, 'reason': msg,
'soft': soft, 'soft': soft,
'instance': settings.PRETIX_INSTANCE_NAME,
}, },
event=context if isinstance(context, Event) else None, event=context if isinstance(context, Event) else None,
organizer=context.organizer if isinstance(context, Event) else context, organizer=context.organizer if isinstance(context, Event) else context,
@@ -455,7 +456,7 @@ def scheduled_organizer_export(self, organizer: Organizer, schedule: int) -> Non
schedule, schedule,
organizer, organizer,
exporter, exporter,
build_absolute_uri( mainreverse_absolute(
'control:organizer.export', 'control:organizer.export',
kwargs={ kwargs={
'organizer': organizer.slug, 'organizer': organizer.slug,
@@ -481,7 +482,7 @@ def scheduled_event_export(self, event: Event, schedule: int) -> None:
schedule, schedule,
event, event,
exporter, exporter,
build_absolute_uri( mainreverse_absolute(
'control:event.orders.export', 'control:event.orders.export',
kwargs={ kwargs={
'event': event.slug, 'event': event.slug,
+3 -3
View File
@@ -85,7 +85,7 @@ from pretix.helpers.format import (
FormattedString, PlainHtmlAlternativeString, SafeFormatter, format_map, FormattedString, PlainHtmlAlternativeString, SafeFormatter, format_map,
) )
from pretix.helpers.hierarkey import clean_filename from pretix.helpers.hierarkey import clean_filename
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.presale.ical import get_private_icals from pretix.presale.ical import get_private_icals
logger = logging.getLogger('pretix.base.mail') logger = logging.getLogger('pretix.base.mail')
@@ -997,7 +997,7 @@ def _wrap_plain_body(content_plain, signature, event, order, position, no_order_
body_plain += _( body_plain += _(
"You can view your order details at the following URL:\n{orderurl}." "You can view your order details at the following URL:\n{orderurl}."
).replace("\n", "\r\n").format( ).replace("\n", "\r\n").format(
orderurl=build_absolute_uri( orderurl=eventreverse_absolute(
order.event, 'presale:event.order.position', kwargs={ order.event, 'presale:event.order.position', kwargs={
'order': order.code, 'order': order.code,
'secret': position.web_secret, 'secret': position.web_secret,
@@ -1013,7 +1013,7 @@ def _wrap_plain_body(content_plain, signature, event, order, position, no_order_
body_plain += _( body_plain += _(
"You can view your order details at the following URL:\n{orderurl}." "You can view your order details at the following URL:\n{orderurl}."
).replace("\n", "\r\n").format( ).replace("\n", "\r\n").format(
event=event.name, orderurl=build_absolute_uri( event=event.name, orderurl=eventreverse_absolute(
order.event, 'presale:event.order.open', kwargs={ order.event, 'presale:event.order.open', kwargs={
'order': order.code, 'order': order.code,
'secret': order.secret, 'secret': order.secret,
+33 -3
View File
@@ -26,8 +26,9 @@ from typing import List
from django.conf import settings as django_settings from django.conf import settings as django_settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import transaction from django.db import transaction
from django.db.utils import IntegrityError
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _, ngettext
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.modelimport import DataImportError, ImportColumn, parse_csv from pretix.base.modelimport import DataImportError, ImportColumn, parse_csv
@@ -260,6 +261,7 @@ def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user
# Prepare model objects. Yes, this might consume lots of RAM, but allows us to make the actual SQL transaction # Prepare model objects. Yes, this might consume lots of RAM, but allows us to make the actual SQL transaction
# shorter. We'll see what works better in reality… # shorter. We'll see what works better in reality…
vouchers = [] vouchers = []
codes = set()
lock_seats = [] lock_seats = []
for i, record in enumerate(data): for i, record in enumerate(data):
try: try:
@@ -268,6 +270,14 @@ def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user
if not record.get("code"): if not record.get("code"):
raise ValidationError(_('A voucher cannot be created without a code.')) raise ValidationError(_('A voucher cannot be created without a code.'))
code = record.get("code")
if code.upper() in codes:
raise ValidationError(
_('Voucher codes must be unique. Code "{code}" already exists in this import.').format(
code=code,
)
)
codes.add(code.upper())
Voucher.clean_item_properties( Voucher.clean_item_properties(
record, record,
event, event,
@@ -286,8 +296,22 @@ def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user
lock_seats.append(voucher.seat) lock_seats.append(voucher.seat)
except (ValidationError, ImportError) as e: except (ValidationError, ImportError) as e:
raise DataImportError( raise DataImportError(
_('Invalid data in row {row}: {message}').format(row=i, message=str(e)) _('Invalid data in row {row}: {message}').format(row=i + 1, message=str(e))
) )
existing_codes = Voucher.objects.filter(
event=event,
code__in=codes,
).values_list("code", flat=True)
if len(existing_codes):
raise DataImportError(
ngettext(
'Voucher codes must be unique. Import contains existing voucher code {code}.',
'Voucher codes must be unique. Import contains existing voucher codes {code}.',
len(existing_codes)
).format(
code=", ".join(existing_codes)
)
)
with transaction.atomic(): with transaction.atomic():
# We don't support quotas here, so we only need to lock if seats are in use # We don't support quotas here, so we only need to lock if seats are in use
@@ -300,7 +324,13 @@ def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user
save_logentries = [] save_logentries = []
for v in vouchers: for v in vouchers:
v.save() try:
v.save()
except IntegrityError:
# should not happen as we check existing codes before, but we did not lock so we might have a race-condition
raise DataImportError(
_('Vouchers could not be imported, probably due to a voucher code already being in use.')
)
save_logentries.append(v.log_action( save_logentries.append(v.log_action(
'pretix.voucher.added', 'pretix.voucher.added',
user=user, user=user,
+3 -3
View File
@@ -37,7 +37,7 @@ from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask
from pretix.base.signals import notification from pretix.base.signals import notification
from pretix.celery_app import app from pretix.celery_app import app
from pretix.helpers.celery import get_task_priority from pretix.helpers.celery import get_task_priority
from pretix.helpers.urls import build_absolute_uri from pretix.helpers.urls import mainreverse_absolute
@app.task(base=TransactionAwareTask, acks_late=True, max_retries=9, default_retry_delay=900) @app.task(base=TransactionAwareTask, acks_late=True, max_retries=9, default_retry_delay=900)
@@ -136,10 +136,10 @@ def send_notification_mail(notification: Notification, user: User):
'site_url': settings.SITE_URL, 'site_url': settings.SITE_URL,
'color': settings.PRETIX_PRIMARY_COLOR, 'color': settings.PRETIX_PRIMARY_COLOR,
'notification': notification, 'notification': notification,
'settings_url': build_absolute_uri( 'settings_url': mainreverse_absolute(
'control:user.settings.notifications', 'control:user.settings.notifications',
), ),
'disable_url': build_absolute_uri( 'disable_url': mainreverse_absolute(
'control:user.settings.notifications.off', 'control:user.settings.notifications.off',
kwargs={ kwargs={
'token': user.notifications_token, 'token': user.notifications_token,
+29 -29
View File
@@ -327,7 +327,7 @@ def get_best_name(position_or_address, parts=False):
@receiver(register_text_placeholders, dispatch_uid="pretixbase_register_text_placeholders") @receiver(register_text_placeholders, dispatch_uid="pretixbase_register_text_placeholders")
def base_placeholders(sender, **kwargs): def base_placeholders(sender, **kwargs):
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
def _event_sample(event): def _event_sample(event):
if event.has_subevents: if event.has_subevents:
@@ -388,14 +388,14 @@ def base_placeholders(sender, **kwargs):
lambda event: LazyDate(now() + timedelta(days=15)) lambda event: LazyDate(now() + timedelta(days=15))
), ),
SimpleFunctionalTextPlaceholder( SimpleFunctionalTextPlaceholder(
'url', ['order', 'event'], lambda order, event: build_absolute_uri( 'url', ['order', 'event'], lambda order, event: eventreverse_absolute(
event, event,
'presale:event.order.open', kwargs={ 'presale:event.order.open', kwargs={
'order': order.code, 'order': order.code,
'secret': order.secret, 'secret': order.secret,
'hash': order.email_confirm_secret() 'hash': order.email_confirm_secret()
} }
), lambda event: build_absolute_uri( ), lambda event: eventreverse_absolute(
event, event,
'presale:event.order.open', kwargs={ 'presale:event.order.open', kwargs={
'order': 'F8VVL', 'order': 'F8VVL',
@@ -406,7 +406,7 @@ def base_placeholders(sender, **kwargs):
), ),
SimpleButtonPlaceholder( SimpleButtonPlaceholder(
'url_button', ['order', 'event'], 'url_button', ['order', 'event'],
url_func=lambda order, event: build_absolute_uri( url_func=lambda order, event: eventreverse_absolute(
event, event,
'presale:event.order.open', kwargs={ 'presale:event.order.open', kwargs={
'order': order.code, 'order': order.code,
@@ -415,7 +415,7 @@ def base_placeholders(sender, **kwargs):
} }
), ),
text_func=lambda order, event: _("View order details"), text_func=lambda order, event: _("View order details"),
sample_url_func=lambda event: build_absolute_uri( sample_url_func=lambda event: eventreverse_absolute(
event, event,
'presale:event.order.open', kwargs={ 'presale:event.order.open', kwargs={
'order': 'F8VVL', 'order': 'F8VVL',
@@ -426,13 +426,13 @@ def base_placeholders(sender, **kwargs):
sample_text_func=lambda event: _("View order details"), sample_text_func=lambda event: _("View order details"),
), ),
SimpleFunctionalTextPlaceholder( SimpleFunctionalTextPlaceholder(
'url_info_change', ['order', 'event'], lambda order, event: build_absolute_uri( 'url_info_change', ['order', 'event'], lambda order, event: eventreverse_absolute(
event, event,
'presale:event.order.modify', kwargs={ 'presale:event.order.modify', kwargs={
'order': order.code, 'order': order.code,
'secret': order.secret, 'secret': order.secret,
} }
), lambda event: build_absolute_uri( ), lambda event: eventreverse_absolute(
event, event,
'presale:event.order.modify', kwargs={ 'presale:event.order.modify', kwargs={
'order': 'F8VVL', 'order': 'F8VVL',
@@ -441,13 +441,13 @@ def base_placeholders(sender, **kwargs):
), ),
), ),
SimpleFunctionalTextPlaceholder( SimpleFunctionalTextPlaceholder(
'url_products_change', ['order', 'event'], lambda order, event: build_absolute_uri( 'url_products_change', ['order', 'event'], lambda order, event: eventreverse_absolute(
event, event,
'presale:event.order.change', kwargs={ 'presale:event.order.change', kwargs={
'order': order.code, 'order': order.code,
'secret': order.secret, 'secret': order.secret,
} }
), lambda event: build_absolute_uri( ), lambda event: eventreverse_absolute(
event, event,
'presale:event.order.change', kwargs={ 'presale:event.order.change', kwargs={
'order': 'F8VVL', 'order': 'F8VVL',
@@ -456,13 +456,13 @@ def base_placeholders(sender, **kwargs):
), ),
), ),
SimpleFunctionalTextPlaceholder( SimpleFunctionalTextPlaceholder(
'url_cancel', ['order', 'event'], lambda order, event: build_absolute_uri( 'url_cancel', ['order', 'event'], lambda order, event: eventreverse_absolute(
event, event,
'presale:event.order.cancel', kwargs={ 'presale:event.order.cancel', kwargs={
'order': order.code, 'order': order.code,
'secret': order.secret, 'secret': order.secret,
} }
), lambda event: build_absolute_uri( ), lambda event: eventreverse_absolute(
event, event,
'presale:event.order.cancel', kwargs={ 'presale:event.order.cancel', kwargs={
'order': 'F8VVL', 'order': 'F8VVL',
@@ -471,7 +471,7 @@ def base_placeholders(sender, **kwargs):
), ),
), ),
SimpleFunctionalTextPlaceholder( SimpleFunctionalTextPlaceholder(
'url', ['event', 'position'], lambda event, position: build_absolute_uri( 'url', ['event', 'position'], lambda event, position: eventreverse_absolute(
event, event,
'presale:event.order.position', 'presale:event.order.position',
kwargs={ kwargs={
@@ -480,7 +480,7 @@ def base_placeholders(sender, **kwargs):
'position': position.positionid 'position': position.positionid
} }
), ),
lambda event: build_absolute_uri( lambda event: eventreverse_absolute(
event, event,
'presale:event.order.position', kwargs={ 'presale:event.order.position', kwargs={
'order': 'F8VVL', 'order': 'F8VVL',
@@ -491,7 +491,7 @@ def base_placeholders(sender, **kwargs):
), ),
SimpleButtonPlaceholder( SimpleButtonPlaceholder(
'url_button', ['event', 'position'], 'url_button', ['event', 'position'],
url_func=lambda event, position: build_absolute_uri( url_func=lambda event, position: eventreverse_absolute(
event, event,
'presale:event.order.position', kwargs={ 'presale:event.order.position', kwargs={
'order': position.order.code, 'order': position.order.code,
@@ -500,7 +500,7 @@ def base_placeholders(sender, **kwargs):
} }
), ),
text_func=lambda event, position: _("View registration details"), text_func=lambda event, position: _("View registration details"),
sample_url_func=lambda event: build_absolute_uri( sample_url_func=lambda event: eventreverse_absolute(
event, event,
'presale:event.order.position', kwargs={ 'presale:event.order.position', kwargs={
'order': 'F8VVL', 'order': 'F8VVL',
@@ -511,14 +511,14 @@ def base_placeholders(sender, **kwargs):
sample_text_func=lambda event: _("View registration details"), sample_text_func=lambda event: _("View registration details"),
), ),
SimpleFunctionalTextPlaceholder( SimpleFunctionalTextPlaceholder(
'url_info_change', ['position', 'event'], lambda position, event: build_absolute_uri( 'url_info_change', ['position', 'event'], lambda position, event: eventreverse_absolute(
event, event,
'presale:event.order.position.modify', kwargs={ 'presale:event.order.position.modify', kwargs={
'order': position.order.code, 'order': position.order.code,
'secret': position.web_secret, 'secret': position.web_secret,
'position': position.positionid 'position': position.positionid
} }
), lambda event: build_absolute_uri( ), lambda event: eventreverse_absolute(
event, event,
'presale:event.order.position.modify', kwargs={ 'presale:event.order.position.modify', kwargs={
'order': 'F8VVL', 'order': 'F8VVL',
@@ -528,14 +528,14 @@ def base_placeholders(sender, **kwargs):
), ),
), ),
SimpleFunctionalTextPlaceholder( SimpleFunctionalTextPlaceholder(
'url_products_change', ['position', 'event'], lambda position, event: build_absolute_uri( 'url_products_change', ['position', 'event'], lambda position, event: eventreverse_absolute(
event, event,
'presale:event.order.position.change', kwargs={ 'presale:event.order.position.change', kwargs={
'order': position.order.code, 'order': position.order.code,
'secret': position.web_secret, 'secret': position.web_secret,
'position': position.positionid 'position': position.positionid
} }
), lambda event: build_absolute_uri( ), lambda event: eventreverse_absolute(
event, event,
'presale:event.order.position.change', kwargs={ 'presale:event.order.position.change', kwargs={
'order': 'F8VVL', 'order': 'F8VVL',
@@ -581,20 +581,20 @@ def base_placeholders(sender, **kwargs):
), ),
SimpleFunctionalTextPlaceholder( SimpleFunctionalTextPlaceholder(
'url_remove', ['waiting_list_voucher', 'event'], 'url_remove', ['waiting_list_voucher', 'event'],
lambda waiting_list_voucher, event: build_absolute_uri( lambda waiting_list_voucher, event: eventreverse_absolute(
event, 'presale:event.waitinglist.remove' event, 'presale:event.waitinglist.remove'
) + '?voucher=' + waiting_list_voucher.code, ) + '?voucher=' + waiting_list_voucher.code,
lambda event: build_absolute_uri( lambda event: eventreverse_absolute(
event, event,
'presale:event.waitinglist.remove', 'presale:event.waitinglist.remove',
) + '?voucher=68CYU2H6ZTP3WLK5', ) + '?voucher=68CYU2H6ZTP3WLK5',
), ),
SimpleFunctionalTextPlaceholder( SimpleFunctionalTextPlaceholder(
'url', ['waiting_list_voucher', 'event'], 'url', ['waiting_list_voucher', 'event'],
lambda waiting_list_voucher, event: build_absolute_uri( lambda waiting_list_voucher, event: eventreverse_absolute(
event, 'presale:event.redeem' event, 'presale:event.redeem'
) + '?voucher=' + waiting_list_voucher.code, ) + '?voucher=' + waiting_list_voucher.code,
lambda event: build_absolute_uri( lambda event: eventreverse_absolute(
event, event,
'presale:event.redeem', 'presale:event.redeem',
) + '?voucher=68CYU2H6ZTP3WLK5', ) + '?voucher=68CYU2H6ZTP3WLK5',
@@ -611,7 +611,7 @@ def base_placeholders(sender, **kwargs):
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join( 'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
'* {} - {}'.format( '* {} - {}'.format(
order.full_code, order.full_code,
build_absolute_uri(event, 'presale:event.order.open', kwargs={ eventreverse_absolute(event, 'presale:event.order.open', kwargs={
'event': event.slug, 'event': event.slug,
'organizer': event.organizer.slug, 'organizer': event.organizer.slug,
'order': order.code, 'order': order.code,
@@ -623,7 +623,7 @@ def base_placeholders(sender, **kwargs):
), lambda event: '\n' + '\n\n'.join( ), lambda event: '\n' + '\n\n'.join(
'* {} - {}'.format( '* {} - {}'.format(
'{}-{}'.format(event.slug.upper(), order['code']), '{}-{}'.format(event.slug.upper(), order['code']),
build_absolute_uri(event, 'presale:event.order.open', kwargs={ eventreverse_absolute(event, 'presale:event.order.open', kwargs={
'event': event.slug, 'event': event.slug,
'organizer': event.organizer.slug, 'organizer': event.organizer.slug,
'order': order['code'], 'order': order['code'],
@@ -662,13 +662,13 @@ def base_placeholders(sender, **kwargs):
# join vouchers with two spaces at end of line so markdown-parser inserts a <br> # join vouchers with two spaces at end of line so markdown-parser inserts a <br>
'voucher_url_list', ['event', 'voucher_list'], 'voucher_url_list', ['event', 'voucher_list'],
lambda event, voucher_list: ' \n'.join([ lambda event, voucher_list: ' \n'.join([
build_absolute_uri( eventreverse_absolute(
event, 'presale:event.redeem' event, 'presale:event.redeem'
) + '?voucher=' + c ) + '?voucher=' + c
for c in voucher_list for c in voucher_list
]), ]),
lambda event: ' \n'.join([ lambda event: ' \n'.join([
build_absolute_uri( eventreverse_absolute(
event, 'presale:event.redeem' event, 'presale:event.redeem'
) + '?voucher=' + c ) + '?voucher=' + c
for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2'] for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2']
@@ -676,10 +676,10 @@ def base_placeholders(sender, **kwargs):
inline=False, inline=False,
), ),
SimpleFunctionalTextPlaceholder( SimpleFunctionalTextPlaceholder(
'url', ['event', 'voucher_list'], lambda event, voucher_list: build_absolute_uri(event, 'presale:event.index', kwargs={ 'url', ['event', 'voucher_list'], lambda event, voucher_list: eventreverse_absolute(event, 'presale:event.index', kwargs={
'event': event.slug, 'event': event.slug,
'organizer': event.organizer.slug, 'organizer': event.organizer.slug,
}), lambda event: build_absolute_uri(event, 'presale:event.index', kwargs={ }), lambda event: eventreverse_absolute(event, 'presale:event.index', kwargs={
'event': event.slug, 'event': event.slug,
'organizer': event.organizer.slug, 'organizer': event.organizer.slug,
}) })
+4 -3
View File
@@ -44,7 +44,7 @@ from django.conf import settings
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext, gettext_lazy as _
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import CachedFile, Event, User, cachedfile_name from pretix.base.models import CachedFile, Event, User, cachedfile_name
@@ -171,15 +171,16 @@ def shred(self, event: Event, fileid: str, confirm_code: str, user: int=None, lo
if user: if user:
with language(user.locale): with language(user.locale):
event_name = str(event.name)
mail( mail(
user.email, user.email,
_('Data shredding completed'), gettext('Data shredding completed for %(event)s') % {'event': event_name},
'pretixbase/email/shred_completed.txt', 'pretixbase/email/shred_completed.txt',
{ {
'instance': settings.PRETIX_INSTANCE_NAME, 'instance': settings.PRETIX_INSTANCE_NAME,
'user': user, 'user': user,
'organizer': event.organizer.name, 'organizer': event.organizer.name,
'event': str(event.name), 'event': event_name,
'start_time': date_format(parse(indexdata['time']).astimezone(event.timezone), 'SHORT_DATETIME_FORMAT'), 'start_time': date_format(parse(indexdata['time']).astimezone(event.timezone), 'SHORT_DATETIME_FORMAT'),
'shredders': ', '.join([str(s.verbose_name) for s in shredders]) 'shredders': ', '.join([str(s.verbose_name) for s in shredders])
}, },
+2 -2
View File
@@ -37,7 +37,7 @@ from pretix.base.services.mail import mail
from pretix.base.settings import GlobalSettingsObject from pretix.base.settings import GlobalSettingsObject
from pretix.base.signals import periodic_task from pretix.base.signals import periodic_task
from pretix.celery_app import app from pretix.celery_app import app
from pretix.helpers.urls import build_absolute_uri from pretix.helpers.urls import mainreverse_absolute
@receiver(signal=periodic_task) @receiver(signal=periodic_task)
@@ -121,7 +121,7 @@ def send_update_notification_email():
) )
), ),
{ {
'url': build_absolute_uri('control:global.update') 'url': mainreverse_absolute('control:global.update')
}, },
) )
+12 -3
View File
@@ -1276,7 +1276,7 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'write_permission': 'event.settings.invoicing:write', 'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Allow to update existing invoices"), label=_("Allow updating existing invoices"),
help_text=_("By default, invoices can never again be changed once they are issued. In most countries, we " help_text=_("By default, invoices can never again be changed once they are issued. In most countries, we "
"recommend to leave this option turned off and always issue a new invoice if a change needs " "recommend to leave this option turned off and always issue a new invoice if a change needs "
"to be made."), "to be made."),
@@ -1924,8 +1924,6 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Hide all past dates from calendar"), label=_("Hide all past dates from calendar"),
help_text=_("This option currently only affects the calendar of this event series, not the organizer-wide "
"calendar.")
) )
}, },
'allow_modifications': { 'allow_modifications': {
@@ -2286,6 +2284,17 @@ DEFAULTS = {
help_text=_("We'll show this publicly to allow attendees to contact you.") help_text=_("We'll show this publicly to allow attendees to contact you.")
) )
}, },
'contact_url': {
'default': None,
'type': str,
'serializer_class': serializers.URLField,
'form_class': forms.URLField,
'form_kwargs': dict(
label=_("Contact URL"),
help_text=_("If you set this, the footer contact link will point here instead of using the email address above. "
"Please note that you still need to add a contact email address that will be shared with all emails you send.")
)
},
'imprint_url': { 'imprint_url': {
'default': None, 'default': None,
'type': str, 'type': str,
+1 -1
View File
@@ -11,6 +11,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}"> <link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}">
<script type="text/javascript" src="{% static "pretixbase/js/errors.js" %}"></script>
{% block custom_header %}{% endblock %} {% block custom_header %}{% endblock %}
{% if css_theme %} {% if css_theme %}
<link rel="stylesheet" type="text/css" href="{{ css_theme }}" /> <link rel="stylesheet" type="text/css" href="{{ css_theme }}" />
@@ -20,6 +21,5 @@
<div class="container"> <div class="container">
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>
<script src="{% static "pretixbase/js/errors.js" %}"></script>
</body> </body>
</html> </html>
@@ -1,10 +1,14 @@
{% load i18n %} {% load i18n %}
{% trans "You have requested us to cancel an event which includes a larger bulk-refund:" %} {% trans "You requested to cancel an event that involves a large bulk refund:" %}
{% trans "Event" %}: {{ event }} - {% trans "Event" %}: {{ event }}
- {% trans "Estimated refund" %}: **{{ amount }}**
{% trans "Estimated refund amount" %}: **{{ amount }}** {% trans "To confirm, paste the following code into the cancellation form:" %}
{% trans "Please confirm that you want to proceed by coping the following confirmation code into the cancellation form:" %} {{ confirmation_code }}
**{{ confirmation_code }}** {% blocktrans with instance=instance %}Don't share this code with anyone. The {{ instance }} team will never ask you for it.{% endblocktrans %}
{% blocktrans with instance=instance %}Thanks,
The {{ instance }} Team{% endblocktrans %}
@@ -1,12 +1,13 @@
{% load i18n %} {% load i18n %}
{% trans "Your export failed." %} {% trans "Your scheduled export failed." %}
{% trans "Reason:" %} {{ reason }} - {% trans "Reason" %}: {{ reason }}
{% if not soft %} {% if not soft %}{% trans "If an export fails five times in a row, we'll stop sending it." %}{% endif %}
{% trans "If your export fails five times in a row, it will no longer be sent." %}
{% endif %}
{% trans "Configuration link:" %} {% trans "You can adjust or remove this export here:" %}
{{ configuration_url }} {{ configuration_url }}
{% blocktrans with instance=instance %}Thanks,
The {{ instance }} Team{% endblocktrans %}
@@ -52,13 +52,13 @@
<table cellpadding="20"><tr><td> <table cellpadding="20"><tr><td>
<![endif]--> <![endif]-->
<div class="content"> <div class="content">
{% trans "You receive these emails based on your notification settings." %}<br> {% trans "You're receiving this email based on your notification settings." %}<br>
<a href="{{ settings_url }}"> <a href="{{ settings_url }}">
{% trans "Click here to view and change your notification settings" %} {% trans "Manage settings" %}
</a> </a>
{% if disable_url %}<br> {% if disable_url %}<br>
<a href="{{ disable_url }}"> <a href="{{ disable_url }}">
{% trans "Click here disable all notifications immediately." %} {% trans "Disable all notifications" %}
</a> </a>
{% endif %} {% endif %}
</div> </div>
@@ -1,19 +1,21 @@
{% load i18n %} {% load i18n %}
{{ notification.title }}{% if notification.detail %} {{ notification.title }}{% if notification.detail %}
{{ notification.detail }} {{ notification.detail }}{% endif %}{% if notification.url %}
{% endif %}{% if notification.url %}
{{ notification.url }}{% endif %}{% for attr in notification.attributes %} {{ notification.url }}{% endif %}{% if notification.attributes %}
{{ attr.title }}: {{ attr.value }}{% endfor %}{% for action in notification.actions %} {% for attr in notification.attributes %}- {{ attr.title }}: {{ attr.value }}
{% endfor %}{% endif %}{% for action in notification.actions %}
{{ action.label }} {{ action.label }}:
{{ action.url }}{% endfor %}
{% trans "You receive these emails based on your notification settings." %} {{ action.url }}{% endfor %}
{% trans "Click here to view and change your notification settings:" %}
{{ settings_url }} ---
{% if disable_url %}{% trans "Click here disable all notifications immediately:" %}
{{ disable_url }} {% trans "You're receiving this email based on your notification settings." %}
- {% trans "Manage settings" %}: {{ settings_url }}
{% if disable_url %}- {% trans "Disable all notifications" %}: {{ disable_url }}
{% endif %} {% endif %}
@@ -1,17 +1,14 @@
{% load i18n %} {% load i18n %}{% blocktrans %}Hello,
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
we hereby confirm that the following data shredding job has been completed: The following data shredding job has been completed:
Organizer: {{ organizer }} - Organizer: {{ organizer }}
- Event: {{ event }}
- Data selection: {{ shredders }}
- Start time: {{ start_time }}
Event: {{ event }} Any data added to the event after the start time may not have been deleted.
Data selection: {{ shredders }} Thanks,
The {{ instance }} Team
Start time: {{ start_time }} (new data added after this time might not have been deleted)
Best regards,
Your {{ instance }} team
{% endblocktrans %} {% endblocktrans %}
@@ -0,0 +1,28 @@
{% extends "error.html" %}
{% load i18n %}
{% load eventurl %}
{% load urlreplace %}
{% load static %}
{% block content %}
<h1>{% trans "Please continue in a new tab" %}</h1>
<p class="larger">
{% blocktrans trimmed %}
For security reasons, the following step is only possible in a new tab.
{% endblocktrans %}
</p>
<p class="larger">
{% blocktrans trimmed %}
If the new tab did not open automatically, please click the following button:
{% endblocktrans %}
</p>
<div class="text-center">
<a href="{{ url }}"
class="btn btn-primary btn-lg" target="_blank">
<span class="fa fa-external-link-square"></span>
{% trans "Continue in new tab" %}
</a>
{{ url|json_script:"framebreak-url" }}
<script type="text/javascript" src="{% static "pretixbase/js/framebreak.js" %}"></script>
</div>
{% endblock %}
@@ -2,13 +2,14 @@
{% load i18n %} {% load i18n %}
{% load rich_text %} {% load rich_text %}
{% load static %} {% load static %}
{% load wrap_in %}
{% block title %}{% trans "Redirect" %}{% endblock %} {% block title %}{% trans "Redirect" %}{% endblock %}
{% block content %} {% block content %}
<i class="fa fa-link fa-fw big-icon"></i> <i class="fa fa-link fa-fw big-icon"></i>
<div class="error-details"> <div class="error-details">
<h1>{% trans "Redirect" %}</h1> <h1>{% trans "Redirect" %}</h1>
<h3> <h3>
{% blocktrans trimmed with host="<strong>"|add:hostname|add:"</strong>"|safe %} {% blocktrans trimmed with host=hostname|wrap_in:'strong' %}
The link you clicked on wants to redirect you to a destination on the website {{ host }}. The link you clicked on wants to redirect you to a destination on the website {{ host }}.
{% endblocktrans %} {% endblocktrans %}
{% blocktrans trimmed %} {% blocktrans trimmed %}
+2 -4
View File
@@ -42,8 +42,6 @@ from bleach import DEFAULT_CALLBACKS, html5lib_shim
from bleach.linkifier import build_email_re from bleach.linkifier import build_email_re
from django import template from django import template
from django.conf import settings from django.conf import settings
from django.core import signing
from django.urls import reverse
from django.utils.functional import SimpleLazyObject from django.utils.functional import SimpleLazyObject
from django.utils.html import escape from django.utils.html import escape
from django.utils.http import url_has_allowed_host_and_scheme from django.utils.http import url_has_allowed_host_and_scheme
@@ -54,6 +52,7 @@ from markdown.postprocessors import Postprocessor
from markdown.treeprocessors import UnescapeTreeprocessor from markdown.treeprocessors import UnescapeTreeprocessor
from tlds import tld_set from tlds import tld_set
from pretix.base.views.redirect import safelink
from pretix.helpers.format import SafeFormatter, format_map from pretix.helpers.format import SafeFormatter, format_map
register = template.Library() register = template.Library()
@@ -158,8 +157,7 @@ def safelink_callback(attrs, new=False):
""" """
url = html.unescape(attrs.get((None, 'href'), '/')) url = html.unescape(attrs.get((None, 'href'), '/'))
if not url_has_allowed_host_and_scheme(url, allowed_hosts=None) and not url.startswith('mailto:') and not url.startswith('tel:'): if not url_has_allowed_host_and_scheme(url, allowed_hosts=None) and not url.startswith('mailto:') and not url.startswith('tel:'):
signer = signing.Signer(salt='safe-redirect') attrs[None, 'href'] = safelink(url)
attrs[None, 'href'] = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
attrs[None, 'target'] = '_blank' attrs[None, 'target'] = '_blank'
attrs[None, 'rel'] = 'noopener' attrs[None, 'rel'] = 'noopener'
return attrs return attrs
+29 -6
View File
@@ -19,6 +19,7 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
import logging
import urllib.parse import urllib.parse
from django.core import signing from django.core import signing
@@ -26,6 +27,8 @@ from django.http import HttpResponseBadRequest, HttpResponseRedirect
from django.shortcuts import render from django.shortcuts import render
from django.urls import reverse from django.urls import reverse
logger = logging.getLogger(__name__)
def _is_samesite_referer(request): def _is_samesite_referer(request):
referer = request.headers.get('referer') referer = request.headers.get('referer')
@@ -42,11 +45,16 @@ def _is_samesite_referer(request):
def redir_view(request): def redir_view(request):
signer = signing.Signer(salt='safe-redirect') framebreak = "framebreak" in request.GET
salt = 'framebreak-safelink-url' if framebreak else 'safelink-url'
try: try:
url = signer.unsign(request.GET.get('url', '')) url = signing.Signer(salt=salt).unsign(request.GET.get('url', ''))
except signing.BadSignature: except signing.BadSignature:
return HttpResponseBadRequest('Invalid parameter') try:
# Backwards-compatibility for a change in 2026-06, remove after a while
url = signing.Signer(salt='safe-redirect').unsign(request.GET.get('url', ''))
except signing.BadSignature:
return HttpResponseBadRequest('Invalid parameter')
if not _is_samesite_referer(request): if not _is_samesite_referer(request):
u = urllib.parse.urlparse(url) u = urllib.parse.urlparse(url)
@@ -55,11 +63,26 @@ def redir_view(request):
'url': url, 'url': url,
}) })
if framebreak:
r = render(request, 'pretixbase/framebreak.html', {
'url': url,
})
r.xframe_options_exempt = True
return r
r = HttpResponseRedirect(url) r = HttpResponseRedirect(url)
r['X-Robots-Tag'] = 'noindex' r['X-Robots-Tag'] = 'noindex'
return r return r
def safelink(url): def safelink(url, framebreak=False):
signer = signing.Signer(salt='safe-redirect') url = str(url)
return reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url)) if not (url.startswith('https://') or url.startswith('http://') or url.startswith("/")):
logger.warning('Invalid URL passed to safelink: %r', url)
return '#invalid-url'
salt = 'framebreak-safelink-url' if framebreak else 'safelink-url'
signer = signing.Signer(salt=salt)
u = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
if framebreak:
u += "&framebreak=true"
return u
+43 -1
View File
@@ -19,14 +19,56 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
import logging
import os import os
from celery import Celery from celery import Celery, signals
from django.dispatch import receiver
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pretix.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pretix.settings")
logger = logging.getLogger(__name__)
from django.conf import settings from django.conf import settings
app = Celery('pretix') app = Celery('pretix')
app.config_from_object('django.conf:settings', namespace='CELERY') app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
@receiver(signals.before_task_publish)
def on_before_task_publish(sender, body, exchange, routing_key, headers, properties, declare, retry_policy, **kwargs):
from pretix.helpers.logs import local
trace = getattr(local, 'trace', [])
request_id = getattr(local, 'request_id', None)
if request_id:
trace.append(request_id)
headers["X-Pretix-Trace"] = " ".join(trace)
@receiver(signals.task_received)
def on_task_received(sender, request, **kwargs):
trace = request._request_dict.get("X-Pretix-Trace")
if trace:
logger.info(f"Task {request.id} has trace {trace}")
@receiver(signals.task_prerun)
def on_task_prerun(sender, task_id, task, **kwargs):
from pretix.helpers.logs import local
local.request_id = task_id
if "X-Pretix-Trace" in task.request.headers:
local.trace = task.request.headers["X-Pretix-Trace"].split(" ")
else:
local.trace = []
local.trace.append(task_id)
@receiver(signals.task_postrun)
def on_task_postrun(sender, task_id, task, **kwargs):
from pretix.helpers.logs import local
local.request_id = None
local.trace = []
+15 -3
View File
@@ -80,7 +80,7 @@ from pretix.control.forms.widgets import Select2
from pretix.helpers.countries import CachedCountries from pretix.helpers.countries import CachedCountries
from pretix.multidomain.models import AlternativeDomainAssignment, KnownDomain from pretix.multidomain.models import AlternativeDomainAssignment, KnownDomain
from pretix.multidomain.urlreverse import ( from pretix.multidomain.urlreverse import (
build_absolute_uri, get_organizer_domain, eventreverse_absolute, get_organizer_domain,
) )
from pretix.plugins.banktransfer.payment import BankTransfer from pretix.plugins.banktransfer.payment import BankTransfer
from pretix.presale.style import get_fonts from pretix.presale.style import get_fonts
@@ -219,7 +219,7 @@ class EventWizardBasicsForm(I18nModelForm):
self.fields['location'].widget.attrs['placeholder'] = _( self.fields['location'].widget.attrs['placeholder'] = _(
'Sample Conference Center\nHeidelberg, Germany' 'Sample Conference Center\nHeidelberg, Germany'
) )
self.fields['slug'].widget.prefix = build_absolute_uri(self.organizer, 'presale:organizer.index') self.fields['slug'].widget.prefix = eventreverse_absolute(self.organizer, 'presale:organizer.index')
self.fields['tax_rate']._required = True # Do not render as optional because it is conditionally required self.fields['tax_rate']._required = True # Do not render as optional because it is conditionally required
if self.has_subevents: if self.has_subevents:
del self.fields['presale_start'] del self.fields['presale_start']
@@ -625,6 +625,7 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
'max_items_per_order', 'max_items_per_order',
'reservation_time', 'reservation_time',
'contact_mail', 'contact_mail',
'contact_url',
'show_variations_expanded', 'show_variations_expanded',
'hide_sold_out', 'hide_sold_out',
'meta_noindex', 'meta_noindex',
@@ -671,6 +672,11 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
base_context = { base_context = {
'frontpage_text': ['event'], 'frontpage_text': ['event'],
'presale_has_ended_text': ['event'],
'voucher_explanation_text': ['event'],
'banner_text': ['event'],
'banner_text_bottom': ['event'],
'event_info_text': ['event'],
} }
def _resolve_virtual_keys_input(self, data, prefix=''): def _resolve_virtual_keys_input(self, data, prefix=''):
@@ -1673,7 +1679,7 @@ class CountriesAndEUAndStates(CountriesAndEU):
class TaxRuleLineForm(I18nForm): class TaxRuleLineForm(I18nForm):
country = LazyTypedChoiceField( country = LazyTypedChoiceField(
choices=CountriesAndEUAndStates(), choices=lazy(lambda: CountriesAndEUAndStates(), CountriesAndEUAndStates),
required=False required=False
) )
address_type = forms.ChoiceField( address_type = forms.ChoiceField(
@@ -1899,6 +1905,12 @@ class QuickSetupForm(I18nForm):
required=False, required=False,
help_text=_("We'll show this publicly to allow attendees to contact you.") help_text=_("We'll show this publicly to allow attendees to contact you.")
) )
contact_url = forms.URLField(
label=_("Contact URL"),
required=False,
help_text=_("If you set this, the footer contact link will point here instead of using the email address above. "
"Please note that you still need to add a contact email address that will be shared with all emails you send.")
)
total_quota = forms.IntegerField( total_quota = forms.IntegerField(
label=_("Total capacity"), label=_("Total capacity"),
min_value=0, min_value=0,
+7 -1
View File
@@ -1342,7 +1342,13 @@ class QuestionAnswerFilterForm(forms.Form):
opqs = opqs.filter(canceled=False) opqs = opqs.filter(canceled=False)
if fdata.get("item", "") != "": if fdata.get("item", "") != "":
i = fdata.get("item", "") i = fdata.get("item", "")
opqs = opqs.filter(item_id__in=(i,)) if '-' in i:
opqs = opqs.filter(
item_id=i.split('-')[0],
variation_id=i.split('-')[1],
)
else:
opqs = opqs.filter(item_id=i)
return opqs return opqs
+3 -2
View File
@@ -88,7 +88,7 @@ from pretix.control.forms.event import (
) )
from pretix.control.forms.widgets import Select2, Select2Multiple from pretix.control.forms.widgets import Select2, Select2Multiple
from pretix.multidomain.models import KnownDomain from pretix.multidomain.models import KnownDomain
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
class OrganizerForm(I18nModelForm): class OrganizerForm(I18nModelForm):
@@ -604,6 +604,7 @@ class OrganizerSettingsForm(SettingsForm):
'customer_accounts_require_login_for_order_access', 'customer_accounts_require_login_for_order_access',
'invoice_regenerate_allowed', 'invoice_regenerate_allowed',
'contact_mail', 'contact_mail',
'contact_url',
'imprint_url', 'imprint_url',
'organizer_info_text', 'organizer_info_text',
'event_list_type', 'event_list_type',
@@ -791,7 +792,7 @@ class MailSettingsForm(SettingsForm):
} }
if 'url' in base_parameters: if 'url' in base_parameters:
placeholders['url'] = build_absolute_uri( placeholders['url'] = eventreverse_absolute(
self.organizer, self.organizer,
'presale:organizer.customer.activate' 'presale:organizer.customer.activate'
) + '?token=' + get_random_string(30) ) + '?token=' + get_random_string(30)
+5
View File
@@ -106,6 +106,11 @@ class VoucherForm(I18nModelForm):
pass pass
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['tag'].widget.attrs['data-typeahead-url'] = reverse('control:event.vouchers.tags.typeahead', kwargs={
'event': instance.event.slug,
'organizer': instance.event.organizer.slug,
})
if instance.event.has_subevents: if instance.event.has_subevents:
self.fields['subevent'].queryset = instance.event.subevents.all() self.fields['subevent'].queryset = instance.event.subevents.all()
self.fields['subevent'].widget = Select2( self.fields['subevent'].widget = Select2(
+11 -7
View File
@@ -36,6 +36,7 @@ from urllib.parse import quote, urljoin, urlparse
from django.conf import settings from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME, logout from django.contrib.auth import REDIRECT_FIELD_NAME, logout
from django.contrib.auth.views import redirect_to_login
from django.http import Http404 from django.http import Http404
from django.shortcuts import get_object_or_404, resolve_url from django.shortcuts import get_object_or_404, resolve_url
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
@@ -212,14 +213,17 @@ class AuditLogMiddleware:
if request.path.startswith(get_script_prefix() + 'control') and request.user.is_authenticated: if request.path.startswith(get_script_prefix() + 'control') and request.user.is_authenticated:
if getattr(request.user, "is_hijacked", False): if getattr(request.user, "is_hijacked", False):
hijack_history = request.session.get('hijack_history', False) hijack_history = request.session.get('hijack_history', False)
hijacker = get_object_or_404(User, pk=hijack_history[0]) hijacker = get_object_or_404(User, pk=hijack_history[0]["user"])
ss = hijacker.get_active_staff_session(request.session.get('hijacker_session')) ss = hijacker.get_active_staff_session(request.session.get('hijacker_session'))
if ss: if not ss:
ss.logs.create( # Staff session expired or not found
url=request.path, logout(request)
method=request.method, return redirect_to_login(request.get_full_path())
impersonating=request.user ss.logs.create(
) url=request.path,
method=request.method,
impersonating=request.user
)
else: else:
ss = request.user.get_active_staff_session(request.session.session_key) ss = request.user.get_active_staff_session(request.session.session_key)
if ss: if ss:
+23 -22
View File
@@ -38,6 +38,7 @@ from pretix import __version__
from pretix.base.models import Order, OrderPayment, Transaction from pretix.base.models import Order, OrderPayment, Transaction
from pretix.base.plugins import get_all_plugins from pretix.base.plugins import get_all_plugins
from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.money import money_filter
from pretix.helpers.reportlab import PlainTextParagraph
from pretix.plugins.reports.exporters import ReportlabExportMixin from pretix.plugins.reports.exporters import ReportlabExportMixin
from pretix.settings import DATA_DIR from pretix.settings import DATA_DIR
@@ -79,23 +80,23 @@ class SysReport(ReportlabExportMixin):
style_small.fontSize = 6 style_small.fontSize = 6
story = [ story = [
Paragraph("System report", headlinestyle), PlainTextParagraph("System report", headlinestyle),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
Paragraph("Usage", subheadlinestyle), PlainTextParagraph("Usage", subheadlinestyle),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
self._usage_table(), self._usage_table(),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
Paragraph("Installed versions", subheadlinestyle), PlainTextParagraph("Installed versions", subheadlinestyle),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
self._tech_table(), self._tech_table(),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
Paragraph("Plugins", subheadlinestyle), PlainTextParagraph("Plugins", subheadlinestyle),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
Paragraph(self._get_plugin_versions(), style_small), PlainTextParagraph(self._get_plugin_versions(), style_small),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
Paragraph("Custom templates", subheadlinestyle), PlainTextParagraph("Custom templates", subheadlinestyle),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
Paragraph(self._get_custom_templates(), style_small), PlainTextParagraph(self._get_custom_templates(), style_small),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
] ]
@@ -121,13 +122,13 @@ class SysReport(ReportlabExportMixin):
("RIGHTPADDING", (-1, 0), (-1, -1), 0), ("RIGHTPADDING", (-1, 0), (-1, -1), 0),
] ]
tdata = [ tdata = [
[Paragraph("Site URL:", style), Paragraph(settings.SITE_URL, style)], [PlainTextParagraph("Site URL:", style), Paragraph(settings.SITE_URL, style)],
[Paragraph("pretix version:", style), Paragraph(__version__, style)], [PlainTextParagraph("pretix version:", style), Paragraph(__version__, style)],
[Paragraph("Python version:", style), Paragraph(sys.version, style)], [PlainTextParagraph("Python version:", style), Paragraph(sys.version, style)],
[Paragraph("Platform:", style), Paragraph(platform.platform(), style)], [PlainTextParagraph("Platform:", style), Paragraph(platform.platform(), style)],
[ [
Paragraph("Database engine:", style), PlainTextParagraph("Database engine:", style),
Paragraph(settings.DATABASES["default"]["ENGINE"], style), PlainTextParagraph(settings.DATABASES["default"]["ENGINE"], style),
], ],
] ]
table = Table(tdata, colWidths=colwidths, repeatRows=0) table = Table(tdata, colWidths=colwidths, repeatRows=0)
@@ -206,7 +207,7 @@ class SysReport(ReportlabExportMixin):
year_last = now().year year_last = now().year
tdata = [ tdata = [
[ [
Paragraph(l, style_small_head) PlainTextParagraph(l, style_small_head)
for l in ( for l in (
"Time frame", "Time frame",
"Currency", "Currency",
@@ -257,19 +258,19 @@ class SysReport(ReportlabExportMixin):
tdata.append( tdata.append(
( (
Paragraph( PlainTextParagraph(
date_format(first_day, "M Y") date_format(first_day, "M Y")
+ " " + " "
+ date_format(after_day - timedelta(days=1), "M Y"), + date_format(after_day - timedelta(days=1), "M Y"),
style_small, style_small,
), ),
Paragraph(c, style_small), PlainTextParagraph(c, style_small),
Paragraph(str(orders_count), style_small) if i == 0 else "", PlainTextParagraph(str(orders_count), style_small) if i == 0 else "",
Paragraph(money_filter(revenue_data.get("s_net") or 0, c), style_small), PlainTextParagraph(money_filter(revenue_data.get("s_net") or 0, c), style_small),
Paragraph(str(testmode_count), style_small) if i == 0 else "", PlainTextParagraph(str(testmode_count), style_small) if i == 0 else "",
Paragraph(str(unconfirmed_count), style_small) if i == 0 else "", PlainTextParagraph(str(unconfirmed_count), style_small) if i == 0 else "",
Paragraph(str(revenue_data.get("c") or 0), style_small), PlainTextParagraph(str(revenue_data.get("c") or 0), style_small),
Paragraph(money_filter(revenue_data.get("s_gross") or 0, c), style_small), PlainTextParagraph(money_filter(revenue_data.get("s_gross") or 0, c), style_small),
) )
) )
@@ -66,6 +66,7 @@
<script type="text/javascript" src="{% static "lightbox/js/lightbox.js" %}"></script> <script type="text/javascript" src="{% static "lightbox/js/lightbox.js" %}"></script>
<script type="text/javascript" src="{% static "are-you-sure/jquery.are-you-sure.js" %}"></script> <script type="text/javascript" src="{% static "are-you-sure/jquery.are-you-sure.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/addressform.js" %}"></script> <script type="text/javascript" src="{% static "pretixbase/js/addressform.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/errors.js" %}"></script>
{% endcompress %} {% endcompress %}
{{ html_head|safe }} {{ html_head|safe }}
@@ -1,13 +1,13 @@
{% load i18n %}{% blocktrans with url=url|safe messages=messages|safe %}Hello, {% load i18n %}{% blocktrans with code=code reason=reason instance=instance %}Hello,
{{ reason }} {{ reason }}
{{ code }} {{ code }}
Please do never give this code to another person. Our support team will never ask for this code. Don't share this code with anyone. The {{ instance }} team will never ask you for it.
If this code was not requested by you, please contact us immediately. If you didn't request this code, please contact us immediately.
Best regards, Thanks,
Your {{ instance }} team The {{ instance }} Team
{% endblocktrans %} {% endblocktrans %}
@@ -1,14 +1,15 @@
{% load i18n %}{% blocktrans with code=code instance=instance %}Hello, {% load i18n %}{% blocktrans with code=code address=address instance=instance %}Hello,
someone requested to use {{ address }} as a sender address on {{ instance }}. Someone requested to use {{ address }} as a sender address on {{ instance }}. Once verified, emails sent from {{ instance }} can show this address as the sender.
This will allow them to send emails that are shown to originate from this email address.
If that was you, please enter the following confirmation code:
{{ code }} If this was you, enter the following code in the setup form:
If this was not requested by you, you can safely ignore this email. {{ code }}
Best regards, Don't share this code with anyone unless you want to authorize them to use this address for this purpose. The {{ instance }} team will never ask you for it.
Your {{ instance }} team If you didn't request this, you can safely ignore this email.
{% endblocktrans %}
Thanks,
The {{ instance }} Team
{% endblocktrans %}
@@ -1,9 +1,11 @@
{% load i18n %}{% blocktrans with url=url|safe %}Hello, {% load i18n %}{% blocktrans with url=url|safe %}Hello,
you requested a new password. Please go to the following page to reset your password: We received a request to reset the password for your {{ instance }} account. To choose a new password, follow the link below:
{{ url }} {{ url }}
Best regards, If you didn't request this, you can safely ignore this email — your password won't change.
Your {{ instance }} team
Thanks,
The {{ instance }} Team
{% endblocktrans %} {% endblocktrans %}
@@ -1,17 +1,16 @@
{% load i18n %}{% blocktrans with url=url|safe %}Hello, {% load i18n %}{% blocktrans with url=url|safe %}Hello,
you have been invited to a team on {{ instance }}, a platform to perform event You've been invited to join a team on {{ instance }}, an event ticket sales platform.
ticket sales.
Organizer: {{ organizer }} - Organizer: {{ organizer }}
Team: {{ team }} - Team: {{ team }}
To accept, follow the link below:
If you want to join that team, just click on the following link:
{{ url }} {{ url }}
If you do not want to join, you can safely ignore or delete this email. If you don't want to join, you can safely ignore this email.
Best regards, Thanks,
The {{ instance }} Team
Your {{ instance }} team
{% endblocktrans %} {% endblocktrans %}
@@ -1,13 +1,19 @@
{% load i18n %}{% blocktrans with url=url|safe os=source.os_type agent=source.agent_type %}Hello, {% load i18n %}{% blocktrans %}Hello,
a login to your {{ instance }} account from an unusual or new location was detected. The login was performed using {{ agent }} on {{ os }} from {{ country }}. We noticed a new sign-in to your {{ instance }} account:
{% endblocktrans %}
- {% trans "Time" %}: {{ when }}
- {% trans "Browser" %}: {{ agent }}
- {% trans "Operating system" %}: {{ os }}
{% if device %}- {% trans "Device" %}: {{ device }}
{% endif %}{% if country %}- {% trans "Country" %}: {{ country }}
{% endif %}
{% blocktrans with url=url|safe %}If it was you, no action is needed.
If this was you, you can safely ignore this email. If you don't recognize this sign-in, please change your password immediately:
If this was not you, we recommend that you change your password in your account settings:
{{ url }} {{ url }}
Best regards, Thanks,
Your {{ instance }} team The {{ instance }} Team
{% endblocktrans %} {% endblocktrans %}
@@ -1,16 +1,15 @@
{% load i18n %}{% blocktrans with url=url|safe messages=messages|safe %}Hello, {% load i18n %}{% blocktrans with url=url|safe messages=messages|safe %}Hello,
this is to inform you that the account information of your {{ instance }} account has been The following changes were made to your {{ instance }} account:
changed. In particular, the following changes have been performed:
{{ messages }} {{ messages }}
If this change was not performed by you, please contact us immediately. If you didn't make these changes, please contact the {{ instance }} support team immediately.
You can review and change your account settings here: You can review your account settings here:
{{ url }} {{ url }}
Best regards, Thanks,
Your {{ instance }} team The {{ instance }} Team
{% endblocktrans %} {% endblocktrans %}
@@ -193,6 +193,7 @@
{% endblocktrans %} {% endblocktrans %}
</p> </p>
{% bootstrap_field form.contact_mail layout="control" %} {% bootstrap_field form.contact_mail layout="control" %}
{% bootstrap_field form.contact_url layout="control" %}
{% bootstrap_field form.imprint_url layout="control" %} {% bootstrap_field form.imprint_url layout="control" %}
</div> </div>
</fieldset> </fieldset>
@@ -30,6 +30,7 @@
{% bootstrap_field form.date_admission layout="control" %} {% bootstrap_field form.date_admission layout="control" %}
{% bootstrap_field form.currency layout="control" %} {% bootstrap_field form.currency layout="control" %}
{% bootstrap_field sform.contact_mail layout="control" %} {% bootstrap_field sform.contact_mail layout="control" %}
{% bootstrap_field sform.contact_url layout="control" %}
{% bootstrap_field sform.imprint_url layout="control" %} {% bootstrap_field sform.imprint_url layout="control" %}
{% bootstrap_field form.is_public layout="control" %} {% bootstrap_field form.is_public layout="control" %}
{% bootstrap_field form.all_sales_channels layout="control" %} {% bootstrap_field form.all_sales_channels layout="control" %}
@@ -34,6 +34,7 @@
{% endif %} {% endif %}
{% bootstrap_field sform.imprint_url layout="control" %} {% bootstrap_field sform.imprint_url layout="control" %}
{% bootstrap_field sform.contact_mail layout="control" %} {% bootstrap_field sform.contact_mail layout="control" %}
{% bootstrap_field sform.contact_url layout="control" %}
{% bootstrap_field sform.organizer_info_text layout="control" %} {% bootstrap_field sform.organizer_info_text layout="control" %}
{% bootstrap_field sform.event_team_provisioning layout="control" %} {% bootstrap_field sform.event_team_provisioning layout="control" %}
{% if sform.allowed_restricted_plugins %} {% if sform.allowed_restricted_plugins %}
@@ -19,9 +19,7 @@
{% endif %} {% endif %}
</h1> </h1>
<script type="application/json" id="editor-data"> {{ layout|json_script:"editor-data" }}
{{ layout|safe }}
</script>
<div class="row"> <div class="row">
<div class="col-md-9"> <div class="col-md-9">
<div class="panel panel-default panel-pdf-editor"> <div class="panel panel-default panel-pdf-editor">
+1
View File
@@ -370,6 +370,7 @@ urlpatterns = [
re_path(r'^discounts/add$', discounts.DiscountCreate.as_view(), name='event.items.discounts.add'), re_path(r'^discounts/add$', discounts.DiscountCreate.as_view(), name='event.items.discounts.add'),
re_path(r'^vouchers/$', vouchers.VoucherList.as_view(), name='event.vouchers'), re_path(r'^vouchers/$', vouchers.VoucherList.as_view(), name='event.vouchers'),
re_path(r'^vouchers/tags/$', vouchers.VoucherTags.as_view(), name='event.vouchers.tags'), re_path(r'^vouchers/tags/$', vouchers.VoucherTags.as_view(), name='event.vouchers.tags'),
re_path(r'^vouchers/tags/typeahead$', typeahead.voucher_tag_typeahead, name='event.vouchers.tags.typeahead'),
re_path(r'^vouchers/rng$', vouchers.VoucherRNG.as_view(), name='event.vouchers.rng'), re_path(r'^vouchers/rng$', vouchers.VoucherRNG.as_view(), name='event.vouchers.rng'),
re_path(r'^vouchers/item_select$', typeahead.itemvarquota_select2, name='event.vouchers.itemselect2'), re_path(r'^vouchers/item_select$', typeahead.itemvarquota_select2, name='event.vouchers.itemselect2'),
re_path(r'^vouchers/(?P<voucher>\d+)/$', vouchers.VoucherUpdate.as_view(), name='event.voucher'), re_path(r'^vouchers/(?P<voucher>\d+)/$', vouchers.VoucherUpdate.as_view(), name='event.voucher'),
+11 -7
View File
@@ -41,7 +41,7 @@ from collections import OrderedDict, defaultdict
from decimal import Decimal from decimal import Decimal
from io import BytesIO from io import BytesIO
from itertools import groupby from itertools import groupby
from urllib.parse import urlparse, urlsplit from urllib.parse import urlsplit
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
import bleach import bleach
@@ -64,7 +64,6 @@ from django.shortcuts import get_object_or_404, redirect
from django.urls import NoReverseMatch, reverse from django.urls import NoReverseMatch, reverse
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.html import conditional_escape, format_html from django.utils.html import conditional_escape, format_html
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext, gettext_lazy as _, gettext_noop from django.utils.translation import gettext, gettext_lazy as _, gettext_noop
@@ -97,7 +96,9 @@ from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.views.mailsetup import MailSettingsSetupView from pretix.control.views.mailsetup import MailSettingsSetupView
from pretix.control.views.user import RecentAuthenticationRequiredMixin from pretix.control.views.user import RecentAuthenticationRequiredMixin
from pretix.helpers.database import rolledback_transaction from pretix.helpers.database import rolledback_transaction
from pretix.multidomain.urlreverse import build_absolute_uri, get_event_domain from pretix.multidomain.urlreverse import (
eventreverse_absolute, get_event_domain,
)
from pretix.presale.views.widget import ( from pretix.presale.views.widget import (
version_default as widget_version_default, version_default as widget_version_default,
) )
@@ -1148,8 +1149,11 @@ class EventLive(EventPermissionRequiredMixin, TemplateView):
if request.POST.get("delete") == "yes": if request.POST.get("delete") == "yes":
try: try:
with transaction.atomic(): with transaction.atomic():
for order in request.event.orders.filter(testmode=True): Order.gracefully_delete_bulk(
order.gracefully_delete(user=self.request.user) request.event,
request.event.orders.filter(testmode=True),
user=self.request.user
)
except ProtectedError: except ProtectedError:
messages.error(self.request, _('An order could not be deleted as some constraints (e.g. data ' messages.error(self.request, _('An order could not be deleted as some constraints (e.g. data '
'created by plug-ins) do not allow it.')) 'created by plug-ins) do not allow it.'))
@@ -1734,10 +1738,10 @@ class EventQRCode(EventPermissionRequiredMixin, View):
permission = None permission = None
def get(self, request, *args, filetype, **kwargs): def get(self, request, *args, filetype, **kwargs):
url = build_absolute_uri(request.event, 'presale:event.index') url = eventreverse_absolute(request.event, 'presale:event.index')
if "url" in request.GET: if "url" in request.GET:
if url_has_allowed_host_and_scheme(request.GET["url"], allowed_hosts=[urlparse(url).netloc]): if request.GET["url"].startswith(url):
url = request.GET["url"] url = request.GET["url"]
else: else:
raise PermissionDenied("Untrusted URL") raise PermissionDenied("Untrusted URL")
+5 -4
View File
@@ -28,7 +28,7 @@ from django.core.mail import get_connection
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext, gettext_lazy as _
from django.views.generic import TemplateView from django.views.generic import TemplateView
from pretix.base import email from pretix.base import email
@@ -216,13 +216,14 @@ class MailSettingsSetupView(TemplateView):
messages.error(request, _('The verification code was incorrect, please try again.')) messages.error(request, _('The verification code was incorrect, please try again.'))
else: else:
self.request.session[session_key] = get_random_string(length=6, allowed_chars='1234567890') self.request.session[session_key] = get_random_string(length=6, allowed_chars='1234567890')
sender_address = self.simple_form.cleaned_data.get('mail_from')
mail( mail(
self.simple_form.cleaned_data.get('mail_from'), sender_address,
_('Sender address verification'), gettext('Confirm %(address)s as a sender address') % {'address': sender_address},
'pretixcontrol/email/email_setup.txt', 'pretixcontrol/email/email_setup.txt',
{ {
'code': self.request.session[session_key], 'code': self.request.session[session_key],
'address': self.simple_form.cleaned_data.get('mail_from'), 'address': sender_address,
'instance': settings.PRETIX_INSTANCE_NAME, 'instance': settings.PRETIX_INSTANCE_NAME,
}, },
None, None,
+17 -4
View File
@@ -139,6 +139,7 @@ from pretix.helpers import OF_SELF
from pretix.helpers.compat import CompatDeleteView from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.format import SafeFormatter, format_map from pretix.helpers.format import SafeFormatter, format_map
from pretix.helpers.hierarkey import clean_filename from pretix.helpers.hierarkey import clean_filename
from pretix.helpers.iter import chunked_iterable
from pretix.helpers.json import CustomJSONEncoder from pretix.helpers.json import CustomJSONEncoder
from pretix.helpers.safedownload import check_token from pretix.helpers.safedownload import check_token
from pretix.presale.signals import question_form_fields from pretix.presale.signals import question_form_fields
@@ -240,7 +241,7 @@ class BaseOrderBulkActionView(OrderSearchMixin, EventPermissionRequiredMixin, As
raise NotImplementedError() raise NotImplementedError()
def execute_bulk(self, queryset: QuerySet, form: forms.Form): def execute_bulk(self, queryset: QuerySet, form: forms.Form):
qs = self.allowed_for(self.allowed_for(self.get_queryset())) qs = self.allowed_for(self.get_queryset())
total = qs.count() total = qs.count()
orders_with_successful_action = 0 orders_with_successful_action = 0
for i, o in enumerate(qs): for i, o in enumerate(qs):
@@ -394,9 +395,21 @@ class OrderDeleteBulkActionView(BaseOrderBulkActionView):
testmode=True, testmode=True,
) )
def execute_single(self, instance, form: forms.Form): def execute_bulk(self, queryset: QuerySet, form: forms.Form):
instance.gracefully_delete(user=self.request.user) qs = self.allowed_for(self.get_queryset())
return True total = qs.count()
all_ids = list(qs.values_list("id", flat=True))
orders_with_successful_action = 0
for chunk in chunked_iterable(all_ids, 1000):
Order.gracefully_delete_bulk(
self.request.event,
qs.filter(id__in=chunk),
user=self.request.user,
)
orders_with_successful_action += len(chunk)
self.async_set_progress(orders_with_successful_action / total * 100)
return orders_with_successful_action, total
class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin, ListView): class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin, ListView):
+13 -9
View File
@@ -139,8 +139,8 @@ from pretix.helpers import OF_SELF, GroupConcat
from pretix.helpers.compat import CompatDeleteView from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.dicts import merge_dicts from pretix.helpers.dicts import merge_dicts
from pretix.helpers.format import SafeFormatter, format_map from pretix.helpers.format import SafeFormatter, format_map
from pretix.helpers.urls import build_absolute_uri as build_global_uri from pretix.helpers.urls import mainreverse_absolute
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.presale.forms.customer import TokenGenerator from pretix.presale.forms.customer import TokenGenerator
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -1032,14 +1032,16 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
def _send_invite(self, instance): def _send_invite(self, instance):
mail( mail(
instance.email, instance.email,
_('Account invitation'), gettext('You\'ve been invited to join %(organizer)s') % {
'organizer': self.request.organizer.name,
},
'pretixcontrol/email/invitation.txt', 'pretixcontrol/email/invitation.txt',
{ {
'instance': settings.PRETIX_INSTANCE_NAME, 'instance': settings.PRETIX_INSTANCE_NAME,
'user': self, 'user': self,
'organizer': self.request.organizer.name, 'organizer': self.request.organizer.name,
'team': instance.team.name, 'team': instance.team.name,
'url': build_global_uri('control:auth.invite', kwargs={ 'url': mainreverse_absolute('control:auth.invite', kwargs={
'token': instance.token 'token': instance.token
}) })
}, },
@@ -2851,10 +2853,12 @@ class SSOProviderUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequire
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
ctx['redirect_uri'] = build_absolute_uri(self.request.organizer, 'presale:organizer.customer.login.return', ctx['redirect_uri'] = eventreverse_absolute(
kwargs={ self.request.organizer, 'presale:organizer.customer.login.return',
'provider': self.object.pk kwargs={
}) 'provider': self.object.pk
}
)
return ctx return ctx
def get_form_kwargs(self): def get_form_kwargs(self):
@@ -3085,7 +3089,7 @@ class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
self.customer.log_action('pretix.customer.password.resetrequested', {}, user=self.request.user) self.customer.log_action('pretix.customer.password.resetrequested', {}, user=self.request.user)
ctx = self.customer.get_email_context() ctx = self.customer.get_email_context()
token = TokenGenerator().make_token(self.customer) token = TokenGenerator().make_token(self.customer)
ctx['url'] = build_absolute_uri( ctx['url'] = eventreverse_absolute(
self.request.organizer, self.request.organizer,
'presale:organizer.customer.recoverpw' 'presale:organizer.customer.recoverpw'
) + '?id=' + self.customer.identifier + '&token=' + token ) + '?id=' + self.customer.identifier + '&token=' + token
+1 -1
View File
@@ -284,7 +284,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
ctx['pdf'] = self.get_current_background() ctx['pdf'] = self.get_current_background()
ctx['variables'] = self.get_variables() ctx['variables'] = self.get_variables()
ctx['images'] = self.get_images() ctx['images'] = self.get_images()
ctx['layout'] = json.dumps(self.get_current_layout()) ctx['layout'] = self.get_current_layout()
ctx['title'] = self.title ctx['title'] = self.title
ctx['locales'] = [p for p in settings.LANGUAGES if p[0] in self.request.event.settings.locales] ctx['locales'] = [p for p in settings.LANGUAGES if p[0] in self.request.event.settings.locales]
ctx['maxfilesize'] = self.maxfilesize ctx['maxfilesize'] = self.maxfilesize
+1
View File
@@ -537,6 +537,7 @@ class SubEventDetail(EventPermissionRequiredMixin, DetailView):
pcnt=Subquery( pcnt=Subquery(
OrderPosition.objects.filter( OrderPosition.objects.filter(
subevent=self.object, subevent=self.object,
order_id=OuterRef("id"),
).values("subevent").annotate(c=Count("*")).values("c") ).values("subevent").annotate(c=Count("*")).values("c")
), ),
has_cancellation_request=Exists(CancellationRequest.objects.filter(order=OuterRef("pk"))), has_cancellation_request=Exists(CancellationRequest.objects.filter(order=OuterRef("pk"))),
+15
View File
@@ -975,6 +975,21 @@ def subevent_meta_values(request, organizer, event):
}) })
@event_permission_required('event.vouchers:read')
def voucher_tag_typeahead(request, **kwargs):
q = request.GET.get('q', '')
tags = request.event.vouchers.filter(
tag__isnull=False,
waitinglistentries__isnull=True,
).filter(
tag__icontains=q,
).values_list('tag', flat=True).distinct().order_by('tag')[:10]
return JsonResponse({
'results': [{'name': t} for t in tags]
})
def item_meta_values(request, organizer, event): def item_meta_values(request, organizer, event):
q = request.GET.get('q') q = request.GET.get('q')
propname = request.GET.get('property') propname = request.GET.get('property')
+56 -16
View File
@@ -19,19 +19,23 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
import hmac
import json import json
from contextlib import contextmanager from contextlib import contextmanager
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import ( from django.contrib.auth import (
BACKEND_SESSION_KEY, get_user_model, load_backend, login, BACKEND_SESSION_KEY, HASH_SESSION_KEY, get_user_model, load_backend, login,
logout,
) )
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import redirect_to_login
from django.core.exceptions import PermissionDenied
from django.db import transaction from django.db import transaction
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse from django.urls import reverse
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string, salted_hmac
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views import View from django.views import View
@@ -218,11 +222,13 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
self.object = get_object_or_404(User, pk=self.kwargs.get("id")) self.object = get_object_or_404(User, pk=self.kwargs.get("id"))
staff_session = request.user.get_active_staff_session(request.session.session_key)
self.request.user.log_action('pretix.control.auth.user.impersonated', self.request.user.log_action('pretix.control.auth.user.impersonated',
user=request.user, user=request.user,
data={ data={
'other': self.kwargs.get("id"), 'other': self.kwargs.get("id"),
'other_email': self.object.email 'other_email': self.object.email,
'staff_session': staff_session.pk,
}) })
oldkey = request.session.session_key oldkey = request.session.session_key
@@ -230,7 +236,15 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
hijacked = self.object hijacked = self.object
hijack_history = request.session.get("hijack_history", []) hijack_history = request.session.get("hijack_history", [])
hijack_history.append(request.user._meta.pk.value_to_string(hijacker)) hijack_history.append({
"user": request.user.pk,
# We include the auth_hash, because it is unguessable. So should an attacker gain an attack vector to
# modify hijack_history, they can't just insert or change a user that shouldn't be there. We HMAC it
# again, though, since we also do not want the auth_hash of the admin user to be in the session of an
# unprivileged user to contain the risk if there is some leak of session data.
"auth_hash": salted_hmac(key_salt=b"hijack-history-hash", value=request.session[HASH_SESSION_KEY],
algorithm="sha256", secret=settings.SECRET_KEY).hexdigest(),
})
backend = get_used_backend(request) backend = get_used_backend(request)
backend = f"{backend.__module__}.{backend.__class__.__name__}" backend = f"{backend.__module__}.{backend.__class__.__name__}"
@@ -238,6 +252,12 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
with signals.no_update_last_login(), keep_session_age(request.session): with signals.no_update_last_login(), keep_session_age(request.session):
login(request, hijacked, backend=backend) login(request, hijacked, backend=backend)
request.session.save()
staff_session.logs.create(
method='(NOTE)',
url=f'Begin impersonating user #{hijacked.pk} (request session {oldkey[:8]} -> {request.session.session_key[:8]})',
)
request.session["hijack_history"] = hijack_history request.session["hijack_history"] = hijack_history
signals.hijack_started.send( signals.hijack_started.send(
@@ -254,13 +274,28 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
class UserImpersonateStopView(LoginRequiredMixin, View): class UserImpersonateStopView(LoginRequiredMixin, View):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
impersonated = request.user staff_session_key = request.session['hijacker_session']
prev_session_key = request.session.session_key
hijs = request.session['hijacker_session']
hijack_history = request.session.get("hijack_history", []) hijack_history = request.session.get("hijack_history", [])
hijacked = request.user hijacked = request.user
user_pk = hijack_history.pop() prev_session = hijack_history.pop()
hijacker = get_object_or_404(get_user_model(), pk=user_pk) hijacker = get_object_or_404(get_user_model(), pk=prev_session["user"])
staff_session = hijacker.get_active_staff_session(staff_session_key)
if not staff_session:
raise PermissionDenied
expected_hash = salted_hmac(
key_salt=b"hijack-history-hash",
value=hijacker.get_session_auth_hash(),
algorithm="sha256",
secret=settings.SECRET_KEY
).hexdigest()
if not hmac.compare_digest(expected_hash, prev_session["auth_hash"]):
# Could be an attacker-controlled hijack history, but could also be e.g. a password change of the admin user
# that happened during the hijack session
logout(request)
return redirect_to_login(request.get_full_path())
backend = get_used_backend(request) backend = get_used_backend(request)
backend = f"{backend.__module__}.{backend.__class__.__name__}" backend = f"{backend.__module__}.{backend.__class__.__name__}"
with signals.no_update_last_login(), keep_session_age(request.session): with signals.no_update_last_login(), keep_session_age(request.session):
@@ -275,17 +310,22 @@ class UserImpersonateStopView(LoginRequiredMixin, View):
hijacked=hijacked, hijacked=hijacked,
) )
ss = request.user.get_active_staff_session(hijs) request.session.save()
if ss: staff_session.session_key = request.session.session_key
request.session.save() staff_session.save()
ss.session_key = request.session.session_key
ss.save() staff_session.logs.create(
method='(NOTE)',
url=f'Stop impersonating user #{hijacked.pk} (request session {prev_session_key[:8]}, '
f'staff session {staff_session_key[:8]} -> {request.session.session_key[:8]})',
)
request.user.log_action('pretix.control.auth.user.impersonate_stopped', request.user.log_action('pretix.control.auth.user.impersonate_stopped',
user=request.user, user=request.user,
data={ data={
'other': impersonated.pk, 'other': hijacked.pk,
'other_email': impersonated.email 'other_email': hijacked.email,
'staff_session': staff_session.pk,
}) })
return redirect(reverse('control:index')) return redirect(reverse('control:index'))
+2 -2
View File
@@ -77,7 +77,7 @@ from pretix.control.views import PaginationMixin
from pretix.helpers.compat import CompatDeleteView from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.format import SafeFormatter, format_map from pretix.helpers.format import SafeFormatter, format_map
from pretix.helpers.models import modelcopy from pretix.helpers.models import modelcopy
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import eventreverse_absolute
class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView): class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
@@ -338,7 +338,7 @@ class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
} }
if self.object.subevent_id: if self.object.subevent_id:
url_params['subevent'] = self.object.subevent_id url_params['subevent'] = self.object.subevent_id
ctx['url'] = build_absolute_uri(self.request.event, "presale:event.redeem") + "?" + urlencode(url_params) ctx['url'] = eventreverse_absolute(self.request.event, "presale:event.redeem") + "?" + urlencode(url_params)
return ctx return ctx
+5
View File
@@ -29,3 +29,8 @@ class PretixHelpersConfig(AppConfig):
def ready(self): def ready(self):
from .monkeypatching import monkeypatch_all_at_ready from .monkeypatching import monkeypatch_all_at_ready
monkeypatch_all_at_ready() monkeypatch_all_at_ready()
# Ensure reportlab does not make any calls to the internet or the local disk
from reportlab import rl_config
rl_config.trustedHosts = []
rl_config.trustedSchemes = ['data']
+31 -6
View File
@@ -29,36 +29,61 @@ class EnvOrParserConfig:
self.cp = configparser self.cp = configparser
def _envkey(self, section, option): def _envkey(self, section, option):
section = re.sub('[^a-zA-Z0-9]', '_', section.upper()) section = re.sub("[^a-zA-Z0-9]", "_", section.upper())
option = re.sub('[^a-zA-Z0-9]', '_', option.upper()) option = re.sub("[^a-zA-Z0-9]", "_", option.upper())
return f'PRETIX_{section}_{option}' return f"PRETIX_{section}_{option}"
def _file_envkey(self, section, option):
section = re.sub("[^a-zA-Z0-9]", "_", section.upper())
option = re.sub("[^a-zA-Z0-9]", "_", option.upper())
return f"FILE__PRETIX_{section}_{option}"
def get(self, section, option, *, raw=False, vars=None, fallback=_UNSET): def get(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
if self._file_envkey(section, option) in os.environ:
with open(os.environ[self._file_envkey(section, option)], "r") as f:
return f.read().strip()
if self._envkey(section, option) in os.environ: if self._envkey(section, option) in os.environ:
return os.environ[self._envkey(section, option)] return os.environ[self._envkey(section, option)]
return self.cp.get(section, option, raw=raw, vars=vars, fallback=fallback) return self.cp.get(section, option, raw=raw, vars=vars, fallback=fallback)
def getint(self, section, option, *, raw=False, vars=None, fallback=_UNSET): def getint(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
if self._file_envkey(section, option) in os.environ:
with open(os.environ[self._file_envkey(section, option)], "r") as f:
return int(f.read().strip())
if self._envkey(section, option) in os.environ: if self._envkey(section, option) in os.environ:
return int(os.environ[self._envkey(section, option)]) return int(os.environ[self._envkey(section, option)])
return self.cp.getint(section, option, raw=raw, vars=vars, fallback=fallback) return self.cp.getint(section, option, raw=raw, vars=vars, fallback=fallback)
def getfloat(self, section, option, *, raw=False, vars=None, fallback=_UNSET): def getfloat(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
if self._file_envkey(section, option) in os.environ:
with open(os.environ[self._file_envkey(section, option)], "r") as f:
return float(f.read().strip())
if self._envkey(section, option) in os.environ: if self._envkey(section, option) in os.environ:
return float(os.environ[self._envkey(section, option)]) return float(os.environ[self._envkey(section, option)])
return self.cp.getfloat(section, option, raw=raw, vars=vars, fallback=fallback) return self.cp.getfloat(section, option, raw=raw, vars=vars, fallback=fallback)
def getboolean(self, section, option, *, raw=False, vars=None, fallback=_UNSET): def getboolean(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
if self._file_envkey(section, option) in os.environ:
with open(os.environ[self._file_envkey(section, option)], "r") as f:
return self.cp._convert_to_boolean(f.read().strip())
if self._envkey(section, option) in os.environ: if self._envkey(section, option) in os.environ:
return self.cp._convert_to_boolean(os.environ[self._envkey(section, option)]) return self.cp._convert_to_boolean(
return self.cp.getboolean(section, option, raw=raw, vars=vars, fallback=fallback) os.environ[self._envkey(section, option)]
)
return self.cp.getboolean(
section, option, raw=raw, vars=vars, fallback=fallback
)
def has_section(self, section): def has_section(self, section):
if any(k.startswith(self._envkey(section, '')) for k in os.environ): if any(k.startswith(self._file_envkey(section, "")) for k in os.environ):
return True
if any(k.startswith(self._envkey(section, "")) for k in os.environ):
return True return True
return self.cp.has_section(section) return self.cp.has_section(section)
def has_option(self, section, option): def has_option(self, section, option):
if self._file_envkey(section, option) in os.environ:
return True
if self._envkey(section, option) in os.environ: if self._envkey(section, option) in os.environ:
return True return True
return self.cp.has_option(section, option) return self.cp.has_option(section, option)
+9
View File
@@ -285,3 +285,12 @@ def get_deterministic_ordering(model, ordering):
# on the primary key to provide total ordering. # on the primary key to provide total ordering.
ordering.append("-pk") ordering.append("-pk")
return ordering return ordering
@contextlib.contextmanager
def conditional_atomic(do_atomic, **kwargs):
if do_atomic:
with transaction.atomic(**kwargs):
yield
else:
yield
+11 -1
View File
@@ -20,6 +20,7 @@
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
import logging import logging
import uuid
from django.core.signals import request_finished from django.core.signals import request_finished
from django.dispatch import receiver from django.dispatch import receiver
@@ -46,6 +47,13 @@ class RequestIdFilter(logging.Filter):
return True return True
class SkipNotFoundFilter(logging.Filter):
# Drop the WARNING "Not Found: ..." records django.request emits for 404s
# We have different access logs for that
def filter(self, record):
return getattr(record, 'status_code', None) != 404
class RequestIdMiddleware: class RequestIdMiddleware:
def __init__(self, get_response): def __init__(self, get_response):
self.get_response = get_response self.get_response = get_response
@@ -58,7 +66,9 @@ class RequestIdMiddleware:
import sentry_sdk import sentry_sdk
sentry_sdk.set_tag("request_id", request.request_id) sentry_sdk.set_tag("request_id", request.request_id)
else: else:
local.request_id = request.request_id = None # Web server did not pass a request ID, we still generate one to correlate between django logs and
# celery logs
local.request_id = request.request_id = str(uuid.uuid4())
return self.get_response(request) return self.get_response(request)
+23 -1
View File
@@ -27,6 +27,7 @@ from datetime import datetime
from http import cookies from http import cookies
from django.conf import settings from django.conf import settings
from django.core.exceptions import SuspiciousFileOperation
from PIL import Image from PIL import Image
from requests.adapters import HTTPAdapter from requests.adapters import HTTPAdapter
from urllib3.connection import HTTPConnection, HTTPSConnection from urllib3.connection import HTTPConnection, HTTPSConnection
@@ -40,6 +41,8 @@ from urllib3.util.connection import (
) )
from urllib3.util.timeout import _DEFAULT_TIMEOUT from urllib3.util.timeout import _DEFAULT_TIMEOUT
from pretix.helpers.reportlab import ThumbnailingImageReader
_cgnat_net = ipaddress.ip_network('100.64.0.0/10') _cgnat_net = ipaddress.ip_network('100.64.0.0/10')
@@ -148,13 +151,14 @@ def monkeypatch_urllib3_ssrf_protection():
if not getattr(settings, "ALLOW_HTTP_TO_PRIVATE_NETWORKS", False): if not getattr(settings, "ALLOW_HTTP_TO_PRIVATE_NETWORKS", False):
ip_addr = ipaddress.ip_address(sa[0]) ip_addr = ipaddress.ip_address(sa[0])
check_ip4 = ip_addr.ipv4_mapped if getattr(ip_addr, "ipv4_mapped", None) else ip_addr
if ip_addr.is_multicast: if ip_addr.is_multicast:
raise HTTPError(f"Request to multicast address {sa[0]} blocked") raise HTTPError(f"Request to multicast address {sa[0]} blocked")
if ip_addr.is_loopback or ip_addr.is_link_local: if ip_addr.is_loopback or ip_addr.is_link_local:
raise HTTPError(f"Request to local address {sa[0]} blocked") raise HTTPError(f"Request to local address {sa[0]} blocked")
if ip_addr.is_private: if ip_addr.is_private:
raise HTTPError(f"Request to private address {sa[0]} blocked") raise HTTPError(f"Request to private address {sa[0]} blocked")
if ip_addr in _cgnat_net: if check_ip4 in _cgnat_net:
raise HTTPError(f"Request to RFC 6598 address {sa[0]} blocked") raise HTTPError(f"Request to RFC 6598 address {sa[0]} blocked")
sock = None sock = None
@@ -230,9 +234,27 @@ def monkeypatch_cookie_morsel():
cookies.Morsel._reserved.setdefault("partitioned", "Partitioned") cookies.Morsel._reserved.setdefault("partitioned", "Partitioned")
def monkeypatch_reportlab_imagereader():
from reportlab.lib import utils
old_init = utils.ImageReader.__init__
def new_init(self, fileName, ident=None): # noqa
if not isinstance(fileName, Image.Image) and not hasattr(fileName, 'read') and not hasattr(fileName, 'str'):
if not isinstance(self, ThumbnailingImageReader):
# ThumbnailingImageReader is only used by us explicitly and not by using <img> in html, so it is safe
raise SuspiciousFileOperation("reportlab should not be reading images from disk")
return types.MethodType(old_init, self)(
fileName, ident
)
utils.ImageReader.__init__ = new_init
def monkeypatch_all_at_ready(): def monkeypatch_all_at_ready():
monkeypatch_vobject_performance() monkeypatch_vobject_performance()
monkeypatch_pillow_safer() monkeypatch_pillow_safer()
monkeypatch_requests_timeout() monkeypatch_requests_timeout()
monkeypatch_urllib3_ssrf_protection() monkeypatch_urllib3_ssrf_protection()
monkeypatch_cookie_morsel() monkeypatch_cookie_morsel()
monkeypatch_reportlab_imagereader()
+14 -1
View File
@@ -25,6 +25,19 @@ import text_unidecode
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
EPC_QR_ALLOWED_CHARS = set(
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789/-?:().,'+ "
)
def epc_qr_field(value):
return ''.join(
char for char in text_unidecode.unidecode(str(value or ''))
if char in EPC_QR_ALLOWED_CHARS
)
def dotdecimal(value): def dotdecimal(value):
return str(value).replace(",", ".") return str(value).replace(",", ".")
@@ -77,7 +90,7 @@ def euro_epc_qr(
return { return {
"id": "girocode", "id": "girocode",
"label": "EPC-QR", "label": "EPC-QR",
"qr_data": "\n".join(text_unidecode.unidecode(str(d or '')) for d in [ "qr_data": "\n".join(epc_qr_field(d) for d in [
"BCD", # Service Tag: BCD "BCD", # Service Tag: BCD
"002", # Version: V2 "002", # Version: V2
"2", # Character set: ISO 8859-1 "2", # Character set: ISO 8859-1
+39
View File
@@ -20,14 +20,19 @@
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
import logging import logging
import re
import unicodedata
from arabic_reshaper import ArabicReshaper from arabic_reshaper import ArabicReshaper
from bidi import get_display
from django.conf import settings from django.conf import settings
from django.utils.functional import SimpleLazyObject from django.utils.functional import SimpleLazyObject
from django.utils.html import escape
from PIL import Image from PIL import Image
from reportlab.lib.styles import ParagraphStyle from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.utils import ImageReader from reportlab.lib.utils import ImageReader
from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import Paragraph from reportlab.platypus import Paragraph
from pretix.presale.style import get_fonts from pretix.presale.style import get_fonts
@@ -70,6 +75,20 @@ reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={
})) }))
def normalize_text(text: str) -> str:
# reportlab does not support unicode combination characters
# It's important we do this before we use ArabicReshaper
text = unicodedata.normalize("NFKC", text)
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
# to resolve all ligatures and python-bidi to switch RTL texts.
try:
text = "\n".join(get_display(reshaper.reshape(l)) for l in re.split("\n", text))
except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
return text
class FontFallbackParagraph(Paragraph): class FontFallbackParagraph(Paragraph):
def __init__(self, text, style=None, *args, **kwargs): def __init__(self, text, style=None, *args, **kwargs):
if style is None: if style is None:
@@ -87,6 +106,8 @@ class FontFallbackParagraph(Paragraph):
if not text: if not text:
return True return True
font = pdfmetrics.getFont(font_name) font = pdfmetrics.getFont(font_name)
if not isinstance(font, TTFont):
return True
return all( return all(
ord(c) in font.face.charToGlyph or not c.isprintable() ord(c) in font.face.charToGlyph or not c.isprintable()
for c in text for c in text
@@ -102,6 +123,24 @@ class FontFallbackParagraph(Paragraph):
return family return family
class PlainTextParagraph(FontFallbackParagraph):
def __init__(self, text, style=None, linebreaks=True, *args, **kwargs):
if not isinstance(text, str):
if hasattr(text, '__html__'):
raise ValueError("It is contradictory to pass escaped content to PlainTextParagraph")
text = str(text)
# Normalize unicode and apply reshaping
text = normalize_text(text)
# Escape any HTML in the text
text = escape(text)
if linebreaks:
text = text.strip().replace("\n", "<br />\n")
super().__init__(text, style, *args, **kwargs)
def register_ttf_font_if_new(name, path): def register_ttf_font_if_new(name, path):
from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont from reportlab.pdfbase.ttfonts import TTFont
+10 -5
View File
@@ -27,6 +27,7 @@ from django.conf import settings
from django.contrib.auth import login as auth_login from django.contrib.auth import login as auth_login
from django.contrib.gis import geoip2 from django.contrib.gis import geoip2
from django.core.cache import cache from django.core.cache import cache
from django.utils.formats import date_format
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_countries.fields import Country from django_countries.fields import Country
@@ -35,7 +36,7 @@ from geoip2.errors import AddressNotFoundError
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.services.mail import mail from pretix.base.services.mail import mail
from pretix.helpers.http import get_client_ip from pretix.helpers.http import get_client_ip
from pretix.helpers.urls import build_absolute_uri from pretix.helpers.urls import mainreverse_absolute
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -169,13 +170,17 @@ def handle_login_source(user, request):
with language(user.locale): with language(user.locale):
mail( mail(
user.email, user.email,
_('Login from new source detected'), _('New sign-in to your account'),
'pretixcontrol/email/login_notice.txt', 'pretixcontrol/email/login_notice.txt',
{ {
'source': src, 'when': date_format(src.last_seen, 'DATETIME_FORMAT'),
'country': Country(str(country)).name if country else _('Unknown country'), 'agent': src.agent_type,
'os': src.os_type,
# ua-parser returns "Other" for unidentified desktop devices.
'device': src.device_type if src.device_type and src.device_type != 'Other' else None,
'country': Country(str(country)).name if country else None,
'instance': settings.PRETIX_INSTANCE_NAME, 'instance': settings.PRETIX_INSTANCE_NAME,
'url': build_absolute_uri('control:user.settings') 'url': mainreverse_absolute('control:user.settings')
}, },
event=None, event=None,
user=user, user=user,
@@ -0,0 +1,33 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import logging
from django import template
from django.utils.html import format_html
register = template.Library()
logger = logging.getLogger(__name__)
@register.filter
def wrap_in(content, tag_name):
return format_html(f'<{tag_name}>{{}}</{tag_name}>', content)
+10
View File
@@ -19,6 +19,7 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
import warnings
from urllib.parse import urljoin from urllib.parse import urljoin
from django.conf import settings from django.conf import settings
@@ -26,6 +27,15 @@ from django.urls import reverse
def build_absolute_uri(urlname, args=None, kwargs=None): def build_absolute_uri(urlname, args=None, kwargs=None):
warnings.warn(
'Usage of build_absolute_uri is confusing since there are many functions with that name. '
'Replace this usage with ',
DeprecationWarning
)
return mainreverse_absolute(urlname, args, kwargs)
def mainreverse_absolute(urlname, args=None, kwargs=None):
from pretix.multidomain import maindomain_urlconf from pretix.multidomain import maindomain_urlconf
return urljoin(settings.SITE_URL, reverse(urlname, args=args, kwargs=kwargs, urlconf=maindomain_urlconf)) return urljoin(settings.SITE_URL, reverse(urlname, args=args, kwargs=kwargs, urlconf=maindomain_urlconf))
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

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