Compare commits

...

207 Commits

Author SHA1 Message Date
Mira Weller 3ceea9d0c9 Escape HTML in placeholder samples in mail preview 2024-08-22 14:25:28 +02:00
Raphael Michel a6f93b6cf0 Seats API: Add is_available filter (Z#23163419) (#4409)
* Seats API: Add is_available filter (Z#23163419)

* docs
2024-08-21 17:43:13 +02:00
Raphael Michel b96374fcf6 Do not create duplicate memberships on order changes (Z#23163336) (#4408) 2024-08-21 17:30:42 +02:00
Raphael Michel eb2ad48089 Docs: Android 4 support dropped for pretixPRINT 2024-08-19 17:26:02 +02:00
dependabot[bot] 64dac504ca Bump markdown from 3.6 to 3.7 (#4402)
Bumps [markdown](https://github.com/Python-Markdown/markdown) from 3.6 to 3.7.
- [Release notes](https://github.com/Python-Markdown/markdown/releases)
- [Changelog](https://github.com/Python-Markdown/markdown/blob/master/docs/changelog.md)
- [Commits](https://github.com/Python-Markdown/markdown/compare/3.6...3.7)

---
updated-dependencies:
- dependency-name: markdown
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-19 14:24:58 +02:00
Raphael Michel cf15a08712 Link between waiting list settings and maangement (Z#23143161) (#4394) 2024-08-19 13:47:54 +02:00
Raphael Michel 9197274528 Add documentation on CSP for the widget (#4398) 2024-08-19 13:47:47 +02:00
Tinna Sandström d19176ab41 Translations: Update Swedish
Currently translated at 99.3% (5685 of 5722 strings)

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

powered by weblate
2024-08-16 15:08:26 +02:00
Michael Stapelberg 8d8abbd941 Also enable DEBUG for runserver_plus, not just runserver (#4385)
When using runserver_plus from the django-extensions package (for serving a development instance with a TLS certificate), I noticed the DEBUG setting was not set correctly, which resulted in static files not being served correctly.
2024-08-15 14:25:21 +02:00
dependabot[bot] 5142c62e6e Update sentry-sdk requirement from ==2.12.* to ==2.13.* (#4392)
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.12.0...2.13.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-15 14:25:07 +02:00
Tinna Sandström 7f7223fcdc Translations: Update Swedish
Currently translated at 99.3% (5685 of 5722 strings)

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

powered by weblate
2024-08-15 14:24:57 +02:00
George Hickman cdde688964 Remove git dir and work tree config from pre-commit hook (#4397)
These variables effectively hardcode the location from which the script
is run.  We shouldn't need these since git should know that it is inside
a repo when run.
2024-08-15 12:42:57 +02:00
George Hickman 233bcaf00e Add a .node-version file (#4396) 2024-08-15 12:42:02 +02:00
Raphael Michel 0a5f3e6dd5 Fix availability of payment methods in time machine (Z#23162163) (#4390) 2024-08-13 12:52:14 +02:00
Raphael Michel 446d24553e Tests: Ignore a deprecation warning 2024-08-13 11:46:11 +02:00
Tinna Sandström 45c32bcb05 Translations: Update Swedish
Currently translated at 99.3% (5684 of 5722 strings)

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

powered by weblate
2024-08-09 09:59:11 +02:00
Raphael Michel 5a5090604a Device list: Filter by software brand 2024-08-09 09:55:16 +02:00
Raphael Michel 2b370bde6d PDF layout schema: Add textcontainer 2024-08-08 13:49:23 +02:00
Raphael Michel 024a223ec7 PDF editor: Fix bug in previous change (Z#23162122) 2024-08-08 13:36:50 +02:00
Raphael Michel 022f44ad00 PDF editor: New text element implementation (#4246)
* draft

* almost working

* Widgth adjustment

* Fix crash on empty text

* Change default layouts

* Fix editor bugs

* Update src/pretix/control/templates/pretixcontrol/pdf/index.html

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

* Show deprecated text on old text

* lockScalingFlip

* Regroup editor controls

* Update src/pretix/static/pretixcontrol/js/ui/main.js

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

* Update src/pretix/static/pretixcontrol/js/ui/main.js

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

* Update src/pretix/static/pretixcontrol/js/ui/main.js

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

* Update src/pretix/static/pretixcontrol/js/ui/editor.js

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

* Increase default height even further

* Add a small version warning

* Update src/pretix/control/templates/pretixcontrol/pdf/index.html

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

* Update src/pretix/control/templates/pretixcontrol/pdf/index.html

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

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-08-07 11:26:47 +02:00
Raphael Michel a682eab18e Force Django security upgrade 2024-08-07 09:52:51 +02:00
dependabot[bot] 6721762a3f Update kombu requirement from ==5.3.* to ==5.4.* (#4380)
Updates the requirements on [kombu](https://github.com/celery/kombu) to permit the latest version.
- [Release notes](https://github.com/celery/kombu/releases)
- [Changelog](https://github.com/celery/kombu/blob/main/Changelog.rst)
- [Commits](https://github.com/celery/kombu/compare/v5.3.0a1...v5.4.0)

---
updated-dependencies:
- dependency-name: kombu
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-07 09:40:38 +02:00
Raphael Michel ad443d0eb6 Improve a help text 2024-08-05 12:34:33 +02:00
Raphael Michel ececd3e572 Order search: Provide typing for answers to questions (Z#23160848) (#4350)
* Order search: Provide typing for answers to questions (Z#23160848)

* Update src/pretix/control/forms/filter.py

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

* Update src/pretix/control/forms/filter.py

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

* Apply suggestions from code review

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

---------

Co-authored-by: Mira <weller@rami.io>
2024-08-05 12:34:15 +02:00
baris gormez ffc4a76b11 Translations: Update Turkish
Currently translated at 43.7% (2504 of 5722 strings)

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

powered by weblate
2024-08-05 09:05:19 +02:00
dependabot[bot] 4beb0c2e30 Bump django-filter from 24.2 to 24.3 (#4374)
Bumps [django-filter](https://github.com/carltongibson/django-filter) from 24.2 to 24.3.
- [Release notes](https://github.com/carltongibson/django-filter/releases)
- [Changelog](https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst)
- [Commits](https://github.com/carltongibson/django-filter/compare/24.2...24.3)

---
updated-dependencies:
- dependency-name: django-filter
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 09:05:11 +02:00
dependabot[bot] 48e161d2d4 Bump @babel/core from 7.24.7 to 7.25.2 in /src/pretix/static/npm_dir (#4369)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.24.7 to 7.25.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.25.2/packages/babel-core)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-02 09:24:52 +02:00
Mira dc1973f4ff Add API endpoint /seats to event (Z#23159536) (#4321)
* add API endpoint /seats to event

* fix logging

* add Seat annotations

* add seats endpoint for subevents

* return ids of occupying objects instead of boolean flags

* wip

* include orderposition instead of order in seat info

* add API documentation

* Apply suggestions from code review

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

* Apply suggestions from code review

* Clarify API docs

* add api examples

* add test cases

* require can_view_orders permission for retrieving seats

* improve permission handling

* Revert "improve permission handling"

This reverts commit f32b532cc68760a8a4af03208bd17e75e8c5723d.

* improve permission handling (minimal version)

* formatting

* add permission tests

* fix bug

* update permission checks

* Apply suggestions from code review

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

* add tests for permission checks

* add tests for expand=voucher and expand=cartposition

* remove unused parameter

* test query count

* codestyle

---------

Co-authored-by: Raphael Michel <michel@rami.io>
2024-08-02 09:17:46 +02:00
Mira a0b046d204 Fix #4201, Fix #4271 -- Time machine issues (#4371)
* Fix issue #4201

* Fix issue #4271

* Use time_machine_now() for subevent calendar display
2024-08-02 09:11:07 +02:00
dependabot[bot] 0032f83d93 Update pyjwt requirement from ==2.8.* to ==2.9.* (#4370)
Updates the requirements on [pyjwt](https://github.com/jpadilla/pyjwt) to permit the latest version.
- [Release notes](https://github.com/jpadilla/pyjwt/releases)
- [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jpadilla/pyjwt/compare/2.8.0...2.9.0)

---
updated-dependencies:
- dependency-name: pyjwt
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-02 09:10:25 +02:00
dependabot[bot] f312200881 Bump @babel/preset-env from 7.24.7 to 7.25.3 in /src/pretix/static/npm_dir (#4368)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.24.7 to 7.25.3.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.25.3/packages/babel-preset-env)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-02 09:10:08 +02:00
Raphael Michel 9946da57c2 Stripe: Add Revolut Pay (#4366)
* Stripe: Add Revolut Pay

* Remove is_enabled flag
2024-08-01 17:20:42 +02:00
Raphael Michel 11e04ea3f2 ListExporter: Allow to override CSV encoding in subclass (Z#23160604) (#4367) 2024-08-01 16:20:00 +02:00
Martin Weinelt 9cef63d641 Prevent race condition in directory creation (#4362)
Checking whether a path does not exist before trying to create it does
not follow the Python paradigm of asking for forgiveness, rather than
permission, and opens up a time-of-check to time-of-use race.
2024-08-01 13:12:00 +02:00
pretix translation bot cb833cc6da Update translations (#4361)
Currently translated at 100.0% (5722 of 5722 strings)

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

powered by weblate

Co-authored-by: pajowu <pajowu@pajowu.de>
2024-08-01 09:32:16 +02:00
dependabot[bot] 5320a69c27 Update sentry-sdk requirement from ==2.11.* to ==2.12.* (#4360)
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.11.0...2.12.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-01 09:31:47 +02:00
dependabot[bot] 510ca67107 Update aiohttp requirement from ==3.9.* to ==3.10.* (#4358)
Updates the requirements on [aiohttp](https://github.com/aio-libs/aiohttp) to permit the latest version.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.9.0b0...v3.10.0)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-01 09:31:24 +02:00
Raphael Michel 13720e731e Easier PCI DSS compliance for payment pages (#4273)
* Assign names to compressed scripts

* Make PCI-relevant pages detectable

* Make payment summary markup more consistant to easy work in tracking plugin

* Add docs note
2024-07-31 13:11:38 +02:00
dependabot[bot] 78cfbd6460 Update python-bidi requirement from ==0.5.* to ==0.6.* (#4357)
Updates the requirements on [python-bidi](https://github.com/MeirKriheli/python-bidi) to permit the latest version.
- [Release notes](https://github.com/MeirKriheli/python-bidi/releases)
- [Changelog](https://github.com/MeirKriheli/python-bidi/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/MeirKriheli/python-bidi/compare/v0.5.0...v0.6.0)

---
updated-dependencies:
- dependency-name: python-bidi
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-31 12:21:48 +02:00
Raphael Michel a65f94fa85 Autocheckin: Fix handling of mixed orders 2024-07-31 10:05:12 +02:00
Raphael Michel 288f73b735 Bulk voucher creation: Use event default language for default text (#4349)
* Bulk voucher creation: Use event default language for default text

* Update src/pretix/control/forms/vouchers.py

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

* Apply suggestions from code review

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

* Style

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-07-30 16:48:30 +02:00
Raphael Michel ad33785f4c API: Allow to set seating_allow_blocked_seats_for_channel (Z#23159519) (#4333) 2024-07-30 16:28:08 +02:00
Ismael Menéndez Fernández bbc175d3d6 Translations: Update Galician
Currently translated at 9.9% (571 of 5722 strings)

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

powered by weblate
2024-07-30 12:46:46 +02:00
dependabot[bot] 2876ff5549 Update sentry-sdk requirement from ==2.10.* to ==2.11.* (#4334)
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.10.0...2.11.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-30 09:26:19 +02:00
Richard Schreiber ed9caa04fc Fix nup-badges for PDFs with cropbox (Z#23160479) (#4354) 2024-07-30 09:20:55 +02:00
Richard Schreiber 83a8fcaa47 Fix serving media-URLs for development (#4355) 2024-07-30 09:17:27 +02:00
Mira 858a448db5 Fix voucher redemption with time machine [Z#23159226] (#4352)
Redeeming a voucher failed if current time is outside the booking period and the shop was accessed via time machine.
2024-07-30 09:14:36 +02:00
Richard Schreiber 58b803539b Fix auto-linking error-inputs self-referencing (Z#23159088) (#4351) 2024-07-30 09:14:06 +02:00
Raphael Michel 6c92c5bacf Translations: Fix typo 2024-07-29 13:27:27 +02:00
Raphael Michel f0089f20fb Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5722 of 5722 strings)

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

powered by weblate
2024-07-29 13:27:03 +02:00
Raphael Michel cb2d056afd Translations: Update German
Currently translated at 100.0% (5722 of 5722 strings)

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

powered by weblate
2024-07-29 13:27:03 +02:00
Raphael Michel afb115c9a2 Remove static3 and dj-static (#4346) 2024-07-29 13:17:50 +02:00
Raphael Michel bb92ffe4eb Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2024-07-29 12:52:25 +02:00
Ismael Menéndez Fernández 8da8e2f43d Translations: Update Galician
Currently translated at 10.0% (570 of 5700 strings)

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

powered by weblate
2024-07-29 12:52:00 +02:00
Raphael Michel cab360bdb6 Move auto check-in to plugin with more functionality (#4331)
* Move auto check-in to plugin with more functionality

* Rename field

* Add to MANIFEST.in
2024-07-29 09:46:53 +02:00
Raphael Michel c6a2ae3783 Docs: Remove pygments-markdown-lexer (unmaintained, and now in pygments core) 2024-07-29 09:45:53 +02:00
Raphael Michel 26ec9dcf6c Downgrade setuptools 2024-07-29 09:41:05 +02:00
Raphael Michel c0832098ef Bump version to 2024.8.0.dev0 2024-07-26 17:04:23 +02:00
Raphael Michel fa3ac69b8e API: Allow to filter enabled webhooks (Z#23160605) (#4336)
* API: Allow to filter enabled webhooks (Z#23160605)

* Fix naming

* Fix isort
2024-07-26 17:04:12 +02:00
Raphael Michel 17f1d571b0 API: Allow querying invoices with multiple order codes (Z#23158921) (#4332) 2024-07-26 16:32:29 +02:00
Raphael Michel a692940397 Bump version to 2024.7.0 2024-07-26 14:36:20 +02:00
Raphael Michel 7f2ec51c64 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5700 of 5700 strings)

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

powered by weblate
2024-07-26 14:34:02 +02:00
Raphael Michel aba59a391c Translations: Update German
Currently translated at 100.0% (5700 of 5700 strings)

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

powered by weblate
2024-07-26 14:34:02 +02:00
Ismael Menéndez Fernández a819b8bb71 Translations: Update Galician
Currently translated at 9.9% (568 of 5700 strings)

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

powered by weblate
2024-07-26 14:34:02 +02:00
Raphael Michel 8a3b18fbd2 Translations: Update German
Currently translated at 100.0% (5700 of 5700 strings)

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

powered by weblate
2024-07-26 14:34:02 +02:00
Raphael Michel dd444299f0 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5700 of 5700 strings)

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

powered by weblate
2024-07-26 14:34:02 +02:00
Raphael Michel 3ee5e9cfbc Docs: Add migration note 2024-07-26 13:54:23 +02:00
Raphael Michel f660f35766 Fix an English word usage 2024-07-26 13:54:16 +02:00
Raphael Michel 42e26738e5 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2024-07-26 09:19:14 +02:00
Raphael Michel 7c43f115b2 Docs: Update Android version policy 2024-07-25 13:18:37 +02:00
dependabot[bot] f055a598ce Bump django-compressor from 4.5 to 4.5.1 (#4330)
Bumps [django-compressor](https://github.com/django-compressor/django-compressor) from 4.5 to 4.5.1.
- [Changelog](https://github.com/django-compressor/django-compressor/blob/develop/docs/changelog.txt)
- [Commits](https://github.com/django-compressor/django-compressor/compare/4.5...4.5.1)

---
updated-dependencies:
- dependency-name: django-compressor
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-24 14:38:54 +02:00
Raphael Michel 9138464896 Translations: Delete Abron 2024-07-24 14:38:38 +02:00
Martin Gross 479f51a84c Apply suggestions from code review
Co-authored-by: robbi5 <richt@rami.io>
Co-authored-by: Felix Rindt <felix@rindt.me>
2024-07-24 14:23:12 +02:00
Martin Gross a3ac54d419 Docs: Add documentation on GetYourGuide 2024-07-24 14:23:12 +02:00
Raphael Michel b2841e5c61 SSO Providers: Use redacted field for secret key 2024-07-23 16:26:37 +02:00
dependabot[bot] 3009f50d51 Update pytest requirement from ==8.2.* to ==8.3.* (#4324)
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/8.2.0.dev0...8.3.1)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-23 15:54:51 +02:00
Gero Nagel ff3a49ab2a Docs: Remove redundant package dependency (#4327)
* remove naming python3-dev twice

* Revert "ListExporter: Do not create excel sheets with more than 30 characters"

This reverts commit ca3802da90.

* Revert "remove naming python3-dev twice"

This reverts commit 7adf2d65e89e4afd82a198e5c0a7416ed8d44b3b.

* Revert "Revert "ListExporter: Do not create excel sheets with more than 30 characters""

This reverts commit d289dc0d1dbf5831452799995a2264642c284c5d.

* delete naming pyhton3-dev twice
2024-07-23 15:54:37 +02:00
Raphael Michel 19f3fbc7e8 Order data export: Include ID of parent position of add-ons 2024-07-23 15:52:55 +02:00
dependabot[bot] bb9b9ac9aa Update pypdf requirement from ==4.2.* to ==4.3.* (#4307)
Updates the requirements on [pypdf](https://github.com/py-pdf/pypdf) to permit the latest version.
- [Release notes](https://github.com/py-pdf/pypdf/releases)
- [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/py-pdf/pypdf/compare/4.2.0...4.3.0)

---
updated-dependencies:
- dependency-name: pypdf
  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@rami.io>
2024-07-23 13:48:11 +02:00
dependabot[bot] d7f6befb5b Bump @babel/preset-env from 7.24.6 to 7.24.7 in /src/pretix/static/npm_dir (#4281)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.24.6 to 7.24.7.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.24.7/packages/babel-preset-env)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-23 13:30:58 +02:00
dependabot[bot] 2287be2009 Update sphinx requirement from ==7.3.* to ==7.4.* (#4306)
Updates the requirements on [sphinx](https://github.com/sphinx-doc/sphinx) to permit the latest version.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES.rst)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v7.3.0...v7.4.4)

---
updated-dependencies:
- dependency-name: sphinx
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-23 13:29:18 +02:00
dependabot[bot] 0480b6873d Update sentry-sdk requirement from ==2.5.* to ==2.10.* (#4305)
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.5.0...2.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-23 13:29:11 +02:00
dependabot[bot] 711f08c9e8 Update python-bidi requirement from ==0.4.* to ==0.5.* (#4325)
* Update python-bidi requirement from ==0.4.* to ==0.5.*

Updates the requirements on [python-bidi](https://github.com/MeirKriheli/python-bidi) to permit the latest version.
- [Release notes](https://github.com/MeirKriheli/python-bidi/releases)
- [Changelog](https://github.com/MeirKriheli/python-bidi/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/MeirKriheli/python-bidi/compare/v0.4.0...v0.5.0)

---
updated-dependencies:
- dependency-name: python-bidi
  dependency-type: direct:production
...

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

* Update import

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Raphael Michel <michel@rami.io>
2024-07-23 13:27:15 +02:00
Alberto Ortega d18914fcca Translations: Update Spanish
Currently translated at 88.5% (5041 of 5690 strings)

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

powered by weblate
2024-07-23 13:27:07 +02:00
Raphael Michel 2411144262 Ignore a deprecation warning in pypdf 2024-07-23 13:06:00 +02:00
test\"img src=x onerror=prompt(document.cookie) 2f02d35a52 Translations: Add Abron 2024-07-23 11:56:05 +02:00
Richard Schreiber 71e82fda81 Auto-fill questions from invoice address (Z#23143497) (#4303)
* Match invoice address to questions by id, placeholder and label

* make label-text extraction more robust

* match actual label as well
2024-07-23 11:54:10 +02:00
Raphael Michel ca3802da90 ListExporter: Do not create excel sheets with more than 30 characters 2024-07-23 09:35:34 +02:00
JnnisCanis 2c68b9e895 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5690 of 5690 strings)

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

powered by weblate
2024-07-22 10:07:01 +02:00
bokor-rami-io 01092498f4 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5690 of 5690 strings)

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

powered by weblate
2024-07-22 10:07:01 +02:00
Raphael Michel fd841ed66d Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5690 of 5690 strings)

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

powered by weblate
2024-07-22 10:07:01 +02:00
Raphael Michel 04cbccb536 Translations: Update German
Currently translated at 100.0% (5690 of 5690 strings)

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

powered by weblate
2024-07-22 10:07:01 +02:00
Raphael Michel b8ea93de1e Fix ticket_download_require_validated_email after sales channel change 2024-07-22 09:38:35 +02:00
Richard Schreiber c49f42301c Fix file-type check for product image 2024-07-19 12:44:49 +02:00
Reece Needham 2ae0a16e67 Translations: Update Spanish
Currently translated at 100.0% (231 of 231 strings)

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

powered by weblate
2024-07-19 11:52:27 +02:00
Reece Needham 6b06fdf822 Translations: Update Spanish
Currently translated at 88.6% (5045 of 5690 strings)

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

powered by weblate
2024-07-19 11:52:27 +02:00
Reece Needham ea3f4e5f62 Translations: Update Spanish
Currently translated at 100.0% (231 of 231 strings)

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

powered by weblate
2024-07-19 11:52:27 +02:00
Reece Needham d71c23f7e0 Translations: Update Spanish
Currently translated at 87.5% (4983 of 5690 strings)

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

powered by weblate
2024-07-19 11:52:27 +02:00
Nikolai 5ed7b0032b Translations: Update Danish
Currently translated at 49.4% (2815 of 5690 strings)

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

powered by weblate
2024-07-19 11:52:27 +02:00
Martin Gross a77f2d01a7 CartManager: Allow to explicitly set an order expiration 2024-07-19 11:38:36 +02:00
Raphael Michel ca4f511cde Voucher import: Fix subevent column 2024-07-19 10:56:17 +02:00
Raphael Michel 83b1c2ea7e Do not take "optional" as part of error message label (#4309) 2024-07-18 14:57:25 +02:00
Richard Schreiber c91eb2e20d Set cursor to not-allowed on labels for disabled checkboxes 2024-07-18 11:54:43 +02:00
Richard Schreiber bfb480a288 UI: in plugin-list only show border-top when necessary (#4314) 2024-07-18 11:43:24 +02:00
Richard Schreiber 22e2143623 API: add api_meta to order 2024-07-18 10:01:03 +02:00
Raphael Michel 9e61f7f978 API: Fix admin permission issue in subevent endpoint 2024-07-18 09:44:30 +02:00
Raphael Michel 092de9e3c4 Add webhook for pretix.event.order.deleted (Z#23159520) (#4310) 2024-07-17 17:15:35 +02:00
Richard Schreiber f0822d3c27 Docs: add help for HTTP-COOP: same-origin, widget and Paypal (#4311)
* Docs: add help for HTTP-COOP, widget and Paypal

* fix typo Paypal => PayPal

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

---------

Co-authored-by: Raphael Michel <michel@rami.io>
2024-07-17 16:12:17 +02:00
dependabot[bot] 6fc47ca3b6 Update django-hijack requirement from ==3.5.* to ==3.6.* (#4290)
Updates the requirements on [django-hijack](https://github.com/django-hijack/django-hijack) to permit the latest version.
- [Release notes](https://github.com/django-hijack/django-hijack/releases)
- [Changelog](https://github.com/django-hijack/django-hijack/blob/master/docs/release-button.png)
- [Commits](https://github.com/django-hijack/django-hijack/compare/3.5.0...3.6.0)

---
updated-dependencies:
- dependency-name: django-hijack
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-17 14:27:42 +02:00
Raphael Michel 3716a686f5 Translations: Add Basque 2024-07-17 14:27:30 +02:00
Erik Löfman 9c0c77958e Translations: Update Swedish
Currently translated at 99.9% (5685 of 5690 strings)

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

powered by weblate
2024-07-17 14:27:30 +02:00
Nikolai 6154a44274 Translations: Update Danish
Currently translated at 47.9% (2727 of 5690 strings)

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

powered by weblate
2024-07-17 14:27:30 +02:00
Erik Löfman d5aff10297 Translations: Update Swedish
Currently translated at 98.5% (5610 of 5690 strings)

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

powered by weblate
2024-07-17 14:27:30 +02:00
Erik Löfman 846e39a652 Translations: Update Swedish
Currently translated at 94.4% (5374 of 5690 strings)

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

powered by weblate
2024-07-17 14:27:30 +02:00
Mira 75d37b2a37 Don't exclude localization PRs from test CI action (#4276)
* Don't exclude localization PRs from test CI action

* Update tests.yml

---------

Co-authored-by: Raphael Michel <michel@rami.io>
2024-07-17 14:23:39 +02:00
Raphael Michel 2a0c3da8c4 Fix N+1 query found by sentry (PRETIXEU-AC2) 2024-07-16 11:25:48 +02:00
Martin Gross fb7f4d1160 Control/Waitlist: Add help_text to waiting_list_limit_per_user (Z#23158537) 2024-07-15 13:39:31 +02:00
Erik Löfman 5c8817f0c3 Translations: Update Swedish
Currently translated at 94.2% (5364 of 5690 strings)

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

powered by weblate
2024-07-15 09:46:49 +02:00
Erik Löfman 7663bf7994 Translations: Update Swedish
Currently translated at 87.3% (4972 of 5690 strings)

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

powered by weblate
2024-07-15 09:46:49 +02:00
Erik Löfman ea8f74f8aa Translations: Update Swedish
Currently translated at 87.3% (4970 of 5690 strings)

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

powered by weblate
2024-07-15 09:46:49 +02:00
Nikolai 7b34701449 Translations: Update Danish
Currently translated at 46.1% (2625 of 5690 strings)

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

powered by weblate
2024-07-15 09:46:49 +02:00
Dirk-jan mollema 06f4bfea24 Translations: Update Dutch
Currently translated at 86.5% (4925 of 5690 strings)

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

powered by weblate
2024-07-15 09:46:49 +02:00
Nikolai e2862a98a0 Translations: Update Danish
Currently translated at 45.2% (2572 of 5690 strings)

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

powered by weblate
2024-07-15 09:46:49 +02:00
Erik Löfman 82f4feadc3 Translations: Update Swedish
Currently translated at 81.8% (4657 of 5690 strings)

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

powered by weblate
2024-07-15 09:46:49 +02:00
Nikolai 906222c7d3 Translations: Update Danish
Currently translated at 66.2% (153 of 231 strings)

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

powered by weblate
2024-07-15 09:46:49 +02:00
Nikolai 7ce2089ca8 Translations: Update Danish
Currently translated at 44.4% (2532 of 5690 strings)

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

powered by weblate
2024-07-15 09:46:49 +02:00
Erik Löfman 2133584ed2 Translations: Update Swedish
Currently translated at 75.2% (4280 of 5690 strings)

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

powered by weblate
2024-07-15 09:46:49 +02:00
Nikolai c3c50d7205 Translations: Update Danish
Currently translated at 41.6% (2369 of 5690 strings)

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

powered by weblate
2024-07-15 09:46:49 +02:00
Erik Löfman 4f7a41a4b2 Translations: Update Swedish
Currently translated at 71.2% (4055 of 5690 strings)

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

powered by weblate
2024-07-15 09:46:49 +02:00
Nikolai 93fd79ab33 Translations: Update Danish
Currently translated at 65.8% (152 of 231 strings)

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

powered by weblate
2024-07-15 09:46:49 +02:00
Nikolai 59595c2f10 Translations: Update Danish
Currently translated at 40.9% (2332 of 5690 strings)

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

powered by weblate
2024-07-15 09:46:49 +02:00
Erik Löfman 91c6d09f0b Translations: Update Swedish
Currently translated at 64.8% (3688 of 5690 strings)

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

powered by weblate
2024-07-15 09:46:49 +02:00
Nikolai 570a818129 Translations: Update Danish
Currently translated at 35.5% (2025 of 5690 strings)

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

powered by weblate
2024-07-15 09:46:49 +02:00
Martin Gross d63e2ebe2d Chore/flake8: Remove extra blank line 2024-07-12 12:34:39 +02:00
Martin Gross 884c97d62a Stripe/SOFORT: Move SOFORT from iFrame to proper redirect (Z#23158598) 2024-07-12 12:30:37 +02:00
Mira 032e958a00 Fix test cases for checkinlist export (#4301) 2024-07-12 11:58:10 +02:00
Mira 7b16dfefbc New column and sort options for check-in-list export (#4300)
Allow check-in-list export to by sorted by order datetime (Z#23158159)
Add check-in text to export
2024-07-12 10:37:22 +02:00
Richard Schreiber 8a5b13dee9 Waitinglist: show time for subevent in overview 2024-07-09 15:37:51 +02:00
Richard Schreiber d7dde8c23e Fix CSS-color in alert-danger icon 2024-07-09 10:17:20 +02:00
Richard Schreiber 1e2f93fbc5 Waitinglist: add time to subevent dropdown when sending vouchers (#4296) 2024-07-09 09:28:19 +02:00
Richard Schreiber 493fc03686 Fix PayPal CSP img-src 2024-07-09 09:26:46 +02:00
Raphael Michel 8d6d885f6e API: Add various filtering options
* API: Add various filtering options (#23158339)

* fix query-param description

* simplify payment provider qs-filter

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-07-08 21:31:54 +02:00
Martin Gross 2aa989c293 PPv2: Do not fail hard on refunds that are based on manually confirmed payments (Fixes: #PRETIXEU-AC1) 2024-07-04 15:12:53 +02:00
dependabot[bot] 06b226f40f Update pillow requirement from ==10.3.* to ==10.4.* (#4280)
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/10.3.0...10.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-04 14:46:46 +02:00
Raphael Michel 73038b0d97 Fix enforcement of restricted plugins (#4286) 2024-07-03 17:14:03 +02:00
Erik Löfman 4513e31f0d Translations: Update Swedish
Currently translated at 63.0% (3585 of 5690 strings)

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

powered by weblate
2024-07-03 17:05:45 +02:00
Raphael Michel d34175114b Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5690 of 5690 strings)

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

powered by weblate
2024-07-03 17:05:45 +02:00
Raphael Michel b2c71b47ce Translations: Update German
Currently translated at 100.0% (5690 of 5690 strings)

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

powered by weblate
2024-07-03 17:05:45 +02:00
Raphael Michel a889abc52b Unify terminology choice Ticketshop (German) 2024-07-03 13:41:09 +02:00
Raphael Michel 8c01b2a469 API: Fix crash in creating variations 2024-07-03 12:37:41 +02:00
Raphael Michel 720c7fd7bb Fix crash in event cloning (PRETIXEU-ABX) 2024-07-03 11:52:57 +02:00
Raphael Michel 6ae6eba4de API: Add details of seats (#4282) 2024-07-03 09:48:59 +02:00
Raphael Michel a173e347ea Optimize availability queries 2024-07-02 18:29:44 +02:00
Raphael Michel 94d13e4cdd API: Raise the right validation error (PRETIXEU-ABV) 2024-07-02 17:49:09 +02:00
Raphael Michel e618441231 API: Fix crash expanding variations 2024-07-02 14:44:35 +02:00
Raphael Michel cd57f1f024 API: Fix creation of embedded variations with explicit sales channels 2024-07-02 09:25:19 +02:00
Raphael Michel 075b9c187f Item list: Fix exclusive tax rules 2024-07-02 09:03:57 +02:00
Erik Löfman d9f46cb817 Translations: Update Swedish
Currently translated at 59.2% (3374 of 5690 strings)

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

powered by weblate
2024-07-01 18:24:27 +02:00
Anarion Dunedain 2892d16861 Translations: Update Polish
Currently translated at 100.0% (5690 of 5690 strings)

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

powered by weblate
2024-07-01 18:24:27 +02:00
Nikolai 9128624d68 Translations: Update Danish
Currently translated at 34.8% (1985 of 5690 strings)

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

powered by weblate
2024-07-01 18:24:27 +02:00
Raphael Michel d2cf8f801d Sales channels: Fix update view 2024-07-01 17:50:27 +02:00
Raphael Michel 682d0f886d Fix order change edge case (PRETIXEU-ABH) 2024-07-01 14:39:34 +02:00
Raphael Michel d2cbd41a19 Fix ticket preview (PRETIXEU-ABF) 2024-07-01 14:38:25 +02:00
Raphael Michel 828f4e3168 Fix isort and docs test 2024-07-01 11:46:46 +02:00
Raphael Michel e691afdd34 Add auto-generated migration 2024-07-01 11:26:55 +02:00
Raphael Michel add90b08ec Export: Fix sales channels in JSON export 2024-07-01 11:22:18 +02:00
Raphael Michel 4539c6523b Fix failing tests 2024-07-01 09:24:42 +02:00
Raphael Michel 3453818c16 Fix email layout preview (PRETIXEU-AB9) 2024-07-01 08:56:36 +02:00
Raphael Michel e5725d6d33 Fix statistics view (PRETIXEU-AB5) 2024-07-01 08:51:23 +02:00
Raphael Michel 0cda1aeaaf Remove nl_BE again 2024-06-30 23:44:55 +02:00
Raphael Michel 769c451bcb Docs: Fix incorrect argument name 2024-06-30 23:34:17 +02:00
Raphael Michel 5b60928205 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (231 of 231 strings)

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

powered by weblate
2024-06-30 23:13:36 +02:00
Raphael Michel b05f3a449d Translations: Update German
Currently translated at 100.0% (231 of 231 strings)

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

powered by weblate
2024-06-30 23:13:36 +02:00
Raphael Michel 3262f2ba84 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5690 of 5690 strings)

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

powered by weblate
2024-06-30 23:13:36 +02:00
Raphael Michel fe9d8e58a1 Translations: Update German
Currently translated at 100.0% (5690 of 5690 strings)

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

powered by weblate
2024-06-30 23:13:36 +02:00
Raphael Michel 6706dbb4db Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2024-06-30 20:56:24 +02:00
Raphael Michel 87632ff8c5 Merge pull request #4274 from pretix-translations/weblate-pretix-pretix 2024-06-30 20:55:22 +02:00
Weblate 2278dbdc4a Merge remote-tracking branch 'origin/master' 2024-06-30 20:54:15 +02:00
Raphael Michel e4e6cc0fcc Revert "Update po files"
This reverts commit ea73977c37.
2024-06-30 20:53:13 +02:00
Raphael Michel ea73977c37 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2024-06-30 20:44:34 +02:00
Raphael Michel 4fb5c6bef0 New implementation of sales channels (#4111)
Co-authored-by: Martin Gross <gross@rami.io>
2024-06-30 19:24:30 +02:00
Raphael Michel 95511b0330 Remove X-XSS-Protection, no longer supported by any browser 2024-06-29 19:25:34 +02:00
Raphael Michel 3340599aec Phase out giropay support (#4266) 2024-06-28 16:17:45 +02:00
pretix translation bot e9a52d07d1 Update translations (#4265)
Co-authored-by: Nikolai <nikolai@lengefeldt.de>
Co-authored-by: Anarion Dunedain <anarion80@gmail.com>
Co-authored-by: Erik Löfman <erik@disruptiveventures.se>
Co-authored-by: Kristian Feldsam <feldsam@gmail.com>
2024-06-28 12:54:45 +02:00
Erik Löfman 0a00a35ab1 Translations: Update Swedish
Currently translated at 57.1% (3237 of 5664 strings)

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

powered by weblate
2024-06-28 12:45:58 +02:00
Nikolai 24d8dc6c76 Translations: Update Danish
Currently translated at 31.3% (1777 of 5664 strings)

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

powered by weblate
2024-06-28 12:45:58 +02:00
Erik Löfman a9d48eaafe Translations: Update Swedish
Currently translated at 87.0% (201 of 231 strings)

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

powered by weblate
2024-06-28 12:45:58 +02:00
Erik Löfman bb959fa494 Translations: Update Swedish
Currently translated at 56.6% (3210 of 5664 strings)

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

powered by weblate
2024-06-28 12:45:58 +02:00
Anarion Dunedain 6126309429 Translations: Update Polish
Currently translated at 100.0% (231 of 231 strings)

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

powered by weblate
2024-06-28 12:45:58 +02:00
Anarion Dunedain 29b49ca82f Translations: Update Polish
Currently translated at 100.0% (5664 of 5664 strings)

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

powered by weblate
2024-06-28 12:45:58 +02:00
Nikolai 7b7b83cb3e Translations: Update Danish
Currently translated at 31.3% (1774 of 5664 strings)

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

powered by weblate
2024-06-28 12:45:58 +02:00
Erik Löfman 6bb2b3425d Translations: Update Swedish
Currently translated at 52.8% (2991 of 5664 strings)

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

powered by weblate
2024-06-28 12:45:58 +02:00
Anarion Dunedain 182d30ffb7 Translations: Update Polish
Currently translated at 100.0% (5664 of 5664 strings)

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

powered by weblate
2024-06-28 12:45:58 +02:00
Anarion Dunedain 844c291575 Translations: Update Polish
Currently translated at 97.9% (5550 of 5664 strings)

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

powered by weblate
2024-06-28 12:45:58 +02:00
Kristian Feldsam ecdb1a8e09 Translations: Update Slovak
Currently translated at 2.5% (6 of 231 strings)

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

powered by weblate
2024-06-28 12:45:58 +02:00
Kristian Feldsam 6c6e5d5af6 Translations: Update Slovak
Currently translated at 93.1% (5276 of 5664 strings)

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

powered by weblate
2024-06-28 12:45:58 +02:00
Anarion Dunedain c33853173b Translations: Update Polish
Currently translated at 91.5% (5186 of 5664 strings)

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

powered by weblate
2024-06-28 12:45:58 +02:00
Erik Löfman 5f90bf80b8 Translations: Update Swedish
Currently translated at 52.7% (2985 of 5664 strings)

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

powered by weblate
2024-06-28 12:45:58 +02:00
Anarion Dunedain 0ba246846b Translations: Update Polish
Currently translated at 85.7% (4859 of 5664 strings)

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

powered by weblate
2024-06-28 12:45:57 +02:00
Erik Löfman 88ea04551e Translations: Update Swedish
Currently translated at 45.9% (2602 of 5664 strings)

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

powered by weblate
2024-06-28 12:45:57 +02:00
Anarion Dunedain 2bfd8e17b0 Translations: Update Polish
Currently translated at 78.9% (4469 of 5664 strings)

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

powered by weblate
2024-06-28 12:45:57 +02:00
Nikolai 1379e5f723 Translations: Update Danish
Currently translated at 31.1% (1766 of 5664 strings)

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

powered by weblate
2024-06-28 12:45:57 +02:00
Martin Gross 9fcaa3d730 Languages: On second thought, make Swedish a regular community-language. 2024-06-28 12:45:52 +02:00
Martin Gross 22d92025c9 Languages: Adding Swedish to incubating languages 2024-06-28 12:33:32 +02:00
Richard Schreiber 2b84c8d7a7 Fix CSS vars naming prefix 2024-06-28 09:36:26 +02:00
Richard Schreiber 25ba2f1145 Add signal seatingframe_html_head (#4270) 2024-06-27 10:46:26 +02:00
Richard Schreiber ae8ff60964 Fix missing CSS in seatingframe (#4269) 2024-06-27 10:20:09 +02:00
373 changed files with 199261 additions and 141618 deletions
-1
View File
@@ -5,7 +5,6 @@ on:
branches: [ master ]
paths-ignore:
- 'doc/**'
- 'src/pretix/locale/**'
pull_request:
branches: [ master ]
paths-ignore:
+1
View File
@@ -0,0 +1 @@
17
+2
View File
@@ -10,6 +10,8 @@ recursive-include src/pretix/helpers/locale *
recursive-include src/pretix/base/templates *
recursive-include src/pretix/control/templates *
recursive-include src/pretix/presale/templates *
recursive-include src/pretix/plugins/autocheckin/templates *
recursive-include src/pretix/plugins/autocheckin/static *
recursive-include src/pretix/plugins/banktransfer/templates *
recursive-include src/pretix/plugins/banktransfer/static *
recursive-include src/pretix/plugins/manualpayment/templates *
+1 -1
View File
@@ -65,7 +65,7 @@ Package dependencies
To build and run pretix, you will need the following debian packages::
# apt-get install git build-essential python3-dev python3-venv python3 python3-pip \
python3-dev libxml2-dev libxslt1-dev libffi-dev zlib1g-dev libssl-dev \
libxml2-dev libxslt1-dev libffi-dev zlib1g-dev libssl-dev \
gettext libpq-dev libjpeg-dev libopenjp2-7-dev
Config file
+7
View File
@@ -73,4 +73,11 @@ This release includes a migration that changes retroactively fills an `organizer
`pretixbase_logentry`. If you have a large database, the migration step of the upgrade might take significantly
longer than usual, so plan the update accordingly.
Upgrade to 2024.7.0 or newer
"""""""""""""""""""""""""""""
This release includes a migration that changes how sales channels are referred on orders.
If you have a large database, the migration step of the upgrade might take significantly longer than usual, so plan
the update accordingly.
.. _blog: https://pretix.eu/about/en/blog/
+259
View File
@@ -0,0 +1,259 @@
.. _rest-autocheckinrules:
Auto check-in rules
===================
This feature requires the bundled ``pretix.plugins.autocheckin`` plugin to be active for the event in order to work properly.
Resource description
--------------------
Auto check-in rules specify that tickets should under specific conditions automatically be considered checked in after
they have been purchased.
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the rule
list integer ID of the check-in list to check the ticket in on. If
``None``, the system will select all matching check-in lists.
mode string ``"placed"`` if the rule should be evaluated right after
an order has been created, ``"paid"`` if the rule should
be evaluated after the order has been fully paid.
all_sales_channels boolean If ``true`` (default), the rule applies to tickets sold on all sales channels.
limit_sales_channels list of strings List of sales channel identifiers the rule should apply to
if ``all_sales_channels`` is ``false``.
all_products boolean If ``true`` (default), the rule affects all products and variations.
limit_products list of integers List of item IDs, if ``all_products`` is not set. If the
product listed here has variations, all variations will be matched.
limit_variations list of integers List of product variation IDs, if ``all_products`` is not set.
The parent product does not need to be part of ``limit_products``.
all_payment_methods boolean If ``true`` (default), the rule applies to tickets paid with all payment methods.
limit_payment_methods list of strings List of payment method identifiers the rule should apply to
if ``all_payment_methods`` is ``false``.
===================================== ========================== =======================================================
.. versionadded:: 2024.7
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/auto_checkin_rules/
Returns a list of all rules configured for an event.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/auto_checkin_rules/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"list": 12345,
"mode": "placed",
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"all_products": False,
"limit_products": [2, 3],
"limit_variations": [456],
"all_payment_methods": true,
"limit_payment_methods": []
}
]
}
:query page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/auto_checkin_rules/(id)/
Returns information on one rule, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/auto_checkin_rules/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"list": 12345,
"mode": "placed",
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"all_products": False,
"limit_products": [2, 3],
"limit_variations": [456],
"all_payment_methods": true,
"limit_payment_methods": []
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param id: The ``id`` field of the rule to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to view it.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/auto_checkin_rules/
Create a new rule.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/auto_checkin_rules/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 166
{
"list": 12345,
"mode": "placed",
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"all_products": False,
"limit_products": [2, 3],
"limit_variations": [456],
"all_payment_methods": true,
"limit_payment_methods": []
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 1,
"list": 12345,
"mode": "placed",
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"all_products": False,
"limit_products": [2, 3],
"limit_variations": [456],
"all_payment_methods": true,
"limit_payment_methods": []
}
:param organizer: The ``slug`` field of the organizer to create a rule for
:param event: The ``slug`` field of the event to create a rule for
:statuscode 201: no error
:statuscode 400: The rule could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create rules.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/auto_checkin_rules/(id)/
Update a rule. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
want to change.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/auto_checkin_rules/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 34
{
"mode": "paid",
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"id": 1,
"list": 12345,
"mode": "placed",
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"all_products": False,
"limit_products": [2, 3],
"limit_variations": [456],
"all_payment_methods": true,
"limit_payment_methods": []
}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the rule to modify
:statuscode 200: no error
:statuscode 400: The rule could not be modified due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/auto_checkin_rules/(id)/
Delete a rule.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/auto_checkin_rules/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the rule to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it **or** this rule cannot be deleted since it is currently in use.
+5
View File
@@ -40,6 +40,11 @@ answers list of objects Answers to user
seat objects The assigned seat (or ``null``)
├ id integer Internal ID of the seat instance
├ name string Human-readable seat name
├ zone_name string Name of the zone the seat is in
├ row_name string Name/number of the row the seat is in
├ row_label string Additional label of the row (or ``null``)
├ seat_number string Number of the seat within the row
├ seat_label string Additional label of the seat (or ``null``)
└ seat_guid string Identifier of the seat within the seating plan
===================================== ========================== =======================================================
+1
View File
@@ -32,6 +32,7 @@ position_count integer Number of ticke
checkin_count integer Number of check-ins performed on this list (read-only).
include_pending boolean If ``true``, the check-in list also contains tickets from orders in pending state.
auto_checkin_sales_channels list of strings All items on the check-in list will be automatically marked as checked-in when purchased through any of the listed sales channels.
**Deprecated, will be removed in pretix 2024.10.** Use :ref:`rest-autocheckinrules`: instead.
allow_multiple_entries boolean If ``true``, subsequent scans of a ticket on this list should not show a warning but instead be stored as an additional check-in.
allow_entry_after_exit boolean If ``true``, subsequent scans of a ticket on this list are valid if the last scan of the ticket was an exit scan.
rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged.
+16 -2
View File
@@ -20,8 +20,12 @@ id integer Internal ID
active boolean The discount will be ignored if this is ``false``
internal_name string A name for the rule used in the backend
position integer An integer, used for sorting the rules which are applied in order
sales_channels list of strings Sales channels this discount is available on, such as
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
all_sales_channels boolean If ``true`` (default), the discount is available on all sales channels
that support discounts.
limit_sales_channels list of strings List of sales channel identifiers the discount is available on
if ``all_sales_channels`` is ``false``.
sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
and ``limit_sales_channels`` instead.
available_from datetime The first date time at which this discount can be applied
(or ``null``).
available_until datetime The last date time at which this discount can be applied
@@ -95,6 +99,8 @@ Endpoints
"active": true,
"internal_name": "3 for 2",
"position": 1,
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
@@ -151,6 +157,8 @@ Endpoints
"active": true,
"internal_name": "3 for 2",
"position": 1,
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
@@ -193,6 +201,8 @@ Endpoints
"active": true,
"internal_name": "3 for 2",
"position": 1,
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
@@ -224,6 +234,8 @@ Endpoints
"active": true,
"internal_name": "3 for 2",
"position": 1,
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
@@ -284,6 +296,8 @@ Endpoints
"active": false,
"internal_name": "3 for 2",
"position": 1,
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"sales_channels": ["web"],
"available_from": null,
"available_until": null,
+20 -13
View File
@@ -49,8 +49,11 @@ item_meta_properties object Item-specific m
valid_keys object Cryptographic keys for non-default signature schemes.
For performance reason, value is omitted in lists and
only contained in detail views. Value can be cached.
sales_channels list A list of sales channels this event is available for
sale on.
all_sales_channels boolean If ``true`` (default), the event is available on all sales channels.
limit_sales_channels list of strings List of sales channel identifiers the event is available on
if ``all_sales_channels`` is ``false``.
sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
and ``limit_sales_channels`` instead.
public_url string The public, customer-facing URL of the event (read-only).
===================================== ========================== =======================================================
@@ -131,11 +134,13 @@ Endpoints
"pretix.plugins.paypal",
"pretix.plugins.ticketoutputpdf"
],
"sales_channels": [
"all_sales_channels": false,
"limit_sales_channels": [
"web",
"pretixpos",
"resellers"
],
"sales_channels": [],
"public_url": "https://pretix.eu/bigevents/sampleconf/"
}
]
@@ -225,6 +230,8 @@ Endpoints
"LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUNvd0JRWURLMlZ3QXlFQTdBRDcvdkZBMzNFc1k0ejJQSHI3aVpQc1o4bjVkaDBhalA4Z3l6Tm1tSXM9Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo="
]
},
"all_sales_channels": true,
"limit_sales_channels": [],
"sales_channels": [
"web",
"pretixpos",
@@ -282,11 +289,8 @@ Endpoints
"pretix.plugins.stripe",
"pretix.plugins.paypal"
],
"sales_channels": [
"web",
"pretixpos",
"resellers"
]
"all_sales_channels": true,
"limit_sales_channels": []
}
**Example response**:
@@ -322,6 +326,8 @@ Endpoints
"pretix.plugins.stripe",
"pretix.plugins.paypal"
],
"all_sales_channels": true,
"limit_sales_channels": [],
"sales_channels": [
"web",
"pretixpos",
@@ -387,11 +393,8 @@ Endpoints
"pretix.plugins.stripe",
"pretix.plugins.paypal"
],
"sales_channels": [
"web",
"pretixpos",
"resellers"
]
"all_sales_channels": true,
"limit_sales_channels": []
}
**Example response**:
@@ -427,6 +430,8 @@ Endpoints
"pretix.plugins.stripe",
"pretix.plugins.paypal"
],
"all_sales_channels": true,
"limit_sales_channels": [],
"sales_channels": [
"web",
"pretixpos",
@@ -502,6 +507,8 @@ Endpoints
"pretix.plugins.paypal",
"pretix.plugins.pretixdroid"
],
"all_sales_channels": true,
"limit_sales_channels": [],
"sales_channels": [
"web",
"pretixpos",
+2
View File
@@ -96,6 +96,8 @@ Endpoints
:query integer page: The page number in case of a multi-page result set, default is 1
:query string secret: Only show gift cards with the given secret.
:query string value: Only show gift cards with the given value.
:query boolean expired: Filter for gift cards that are (not) expired.
:query boolean testmode: Filter for gift cards that are (not) in test mode.
:query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer.
:query string expand: If you pass ``"owner_ticket"``, the respective field will be shown as a nested value instead of just an ID.
+4 -1
View File
@@ -30,6 +30,7 @@ at :ref:`plugin-docs`.
checkinlists
waitinglist
customers
saleschannels
membershiptypes
memberships
giftcards
@@ -43,5 +44,7 @@ at :ref:`plugin-docs`.
scheduled_exports
shredders
sendmail_rules
auto_checkin_rules
billing_invoices
billing_var
billing_var
seats
+3
View File
@@ -217,6 +217,9 @@ List of all invoices
:query boolean is_cancellation: If set to ``true`` or ``false``, only invoices with this value for the field
``is_cancellation`` will be returned.
:query string order: If set, only invoices belonging to the order with the given order code will be returned.
This parameter may be given multiple times. In this case, all invoices matching one of the inputs will be returned.
:query string number: If set, only invoices with the given invoice number will be returned.
This parameter may be given multiple times. In this case, all invoices matching one of the inputs will be returned.
:query string refers: If set, only invoices referring to the given invoice will be returned.
:query string locale: If set, only invoices with the given locale will be returned.
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``date`` and
+20 -5
View File
@@ -38,11 +38,14 @@ require_membership boolean If ``true``, bo
require_membership_hidden boolean If ``true`` and ``require_membership`` is set, this variation will
be hidden from users without a valid membership.
require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
sales_channels list of strings Sales channels this variation is available on, such as
``"web"`` or ``"resellers"``. Defaults to all existing sales channels.
all_sales_channels boolean If ``true`` (default), the variation is available on all sales channels.
limit_sales_channels list of strings List of sales channel identifiers the variation is available on
if ``all_sales_channels`` is ``false``.
The item-level list takes precedence, i.e. a sales
channel needs to be on both lists for the item to be
available.
channel needs to be on both lists for the variation to be
available (unless ``all_sales_channels`` is used).
sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
and ``limit_sales_channels`` instead.
available_from datetime The first date time at which this variation can be bought
(or ``null``).
available_from_mode string If ``hide`` (the default), this variation is hidden in the shop
@@ -111,6 +114,8 @@ Endpoints
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
@@ -139,6 +144,8 @@ Endpoints
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
@@ -157,6 +164,7 @@ Endpoints
}
:query integer page: The page number in case of a multi-page result set, default is 1
:query string search: Filter the list by the value of the variation (substring search).
:query boolean active: If set to ``true`` or ``false``, only items with this value for the field ``active`` will be
returned.
:param organizer: The ``slug`` field of the organizer to fetch
@@ -202,6 +210,8 @@ Endpoints
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
@@ -244,7 +254,8 @@ Endpoints
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
"sales_channels": ["web"],
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
@@ -277,6 +288,8 @@ Endpoints
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
@@ -341,6 +354,8 @@ Endpoints
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
+43 -9
View File
@@ -46,8 +46,11 @@ personalized boolean ``true`` for
position integer An integer, used for sorting
picture file A product picture to be displayed in the shop
(can be ``null``).
sales_channels list of strings Sales channels this product is available on, such as
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
all_sales_channels boolean If ``true`` (default), the item is available on all sales channels.
limit_sales_channels list of strings List of sales channel identifiers the item is available on
if ``all_sales_channels`` is ``false``.
sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
and ``limit_sales_channels`` instead.
available_from datetime The first date time at which this item can be bought
(or ``null``).
available_from_mode string If ``hide`` (the default), this item is hidden in the shop
@@ -157,11 +160,14 @@ variations list of objects A list with o
be hidden from users without a valid membership.
├ require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
Markdown syntax or can be ``null``.
├ sales_channels list of strings Sales channels this variation is available on, such as
``"web"`` or ``"resellers"``. Defaults to all existing sales channels.
all_sales_channels boolean If ``true`` (default), the variation is available on all sales channels.
├ limit_sales_channels list of strings List of sales channel identifiers the variation is available on
if ``all_sales_channels`` is ``false``.
The item-level list takes precedence, i.e. a sales
channel needs to be on both lists for the item to be
available.
channel needs to be on both lists for the variation to be
available (unless ``all_sales_channels`` is used).
├ sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
and ``limit_sales_channels`` instead.
├ available_from datetime The first date time at which this variation can be bought
(or ``null``).
├ available_from_mode string If ``hide`` (the default), this variation is hidden in the shop
@@ -276,6 +282,8 @@ Endpoints
"id": 1,
"name": {"en": "Standard ticket"},
"internal_name": "",
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"sales_channels": ["web"],
"default_price": "23.00",
"original_price": null,
@@ -340,6 +348,8 @@ Endpoints
"require_approval": false,
"require_membership": false,
"require_membership_types": [],
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
@@ -362,6 +372,8 @@ Endpoints
"require_approval": false,
"require_membership": false,
"require_membership_types": [],
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
@@ -380,6 +392,7 @@ Endpoints
}
:query integer page: The page number in case of a multi-page result set, default is 1
:query string search: Filter the list by internal name or name of the item (substring search).
:query boolean active: If set to ``true`` or ``false``, only items with this value for the field ``active`` will be
returned.
:query integer category: If set to the ID of a category, only items within that category will be returned.
@@ -420,6 +433,8 @@ Endpoints
"id": 1,
"name": {"en": "Standard ticket"},
"internal_name": "",
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"sales_channels": ["web"],
"default_price": "23.00",
"original_price": null,
@@ -485,6 +500,8 @@ Endpoints
"require_membership": false,
"require_membership_types": [],
"description": null,
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
@@ -506,6 +523,8 @@ Endpoints
"require_approval": false,
"require_membership": false,
"require_membership_types": [],
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
@@ -545,7 +564,8 @@ Endpoints
"id": 1,
"name": {"en": "Standard ticket"},
"internal_name": "",
"sales_channels": ["web"],
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"default_price": "23.00",
"original_price": null,
"category": null,
@@ -608,7 +628,8 @@ Endpoints
"require_approval": false,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
@@ -630,7 +651,8 @@ Endpoints
"require_approval": false,
"require_membership": false,
"require_membership_types": [],
"sales_channels": ["web"],
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
@@ -657,6 +679,8 @@ Endpoints
"id": 1,
"name": {"en": "Standard ticket"},
"internal_name": "",
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"sales_channels": ["web"],
"default_price": "23.00",
"original_price": null,
@@ -721,6 +745,8 @@ Endpoints
"require_approval": false,
"require_membership": false,
"require_membership_types": [],
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
@@ -743,6 +769,8 @@ Endpoints
"require_approval": false,
"require_membership": false,
"require_membership_types": [],
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
@@ -801,6 +829,8 @@ Endpoints
"id": 1,
"name": {"en": "Ticket"},
"internal_name": "",
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"sales_channels": ["web"],
"default_price": "25.00",
"original_price": null,
@@ -865,6 +895,8 @@ Endpoints
"require_approval": false,
"require_membership": false,
"require_membership_types": [],
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
@@ -887,6 +919,8 @@ Endpoints
"require_approval": false,
"require_membership": false,
"require_membership_types": [],
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
+14 -1
View File
@@ -42,6 +42,8 @@ payment_date date **DEPRECATED AN
payment_provider string **DEPRECATED AND INACCURATE** Payment provider used for this order
total money (string) Total value of this order
comment string Internal comment on this order
api_meta object Meta data for that order. Only available through API, no guarantees
on the content structure. You can use this to save references to your system.
custom_followup_at date Internal date for a custom follow-up action
checkin_attention boolean If ``true``, the check-in app should show a warning
that this ticket requires special attention if a ticket
@@ -215,6 +217,11 @@ answers list of objects Answers to user
seat objects The assigned seat. Can be ``null``.
├ id integer Internal ID of the seat instance
├ name string Human-readable seat name
├ zone_name string Name of the zone the seat is in
├ row_name string Name/number of the row the seat is in
├ row_label string Additional label of the row (or ``null``)
├ seat_number string Number of the seat within the row
├ seat_label string Additional label of the seat (or ``null``)
└ seat_guid string Identifier of the seat within the seating plan
pdf_data object Data object required for ticket PDF generation. By default,
this field is missing. It will be added only if you add the
@@ -455,10 +462,13 @@ List of all orders
:query datetime modified_since: Only return orders that have changed since the given date. Be careful: We only
recommend using this in combination with ``testmode=false``, since test mode orders can vanish at any time and
you will not notice it using this method.
:query datetime created_since: Only return orders that have been created since the given date.
:query datetime created_since: Only return orders that have been created since the given date (inclusive).
:query datetime created_before: Only return orders that have been created before the given date (exclusive).
:query integer subevent: Only return orders with a position that contains this subevent ID. *Warning:* Result will also include orders if they contain mixed subevents, and it will even return orders where the subevent is only contained in a canceled position.
:query datetime subevent_after: Only return orders that contain a ticket for a subevent taking place after the given date. This is an exclusive after, and it considers the **end** of the subevent (or its start, if the end is not set).
:query datetime subevent_before: Only return orders that contain a ticket for a subevent taking place after the given date. This is an exclusive before, and it considers the **start** of the subevent.
:query string sales_channel: Only return orders with the given sales channel identifier (e.g. ``"web"``).
:query string payment_provider: Only return orders that contain a payment using the given payment provider. Note that this also searches for partial incomplete, or failed payments within the order and is not useful to get a sum of payment amounts without further processing.
:query string exclude: Exclude a field from the output, e.g. ``fees`` or ``positions.downloads``. Can be used as a performance optimization. Can be passed multiple times.
:query string include: Include only the given field in the output, e.g. ``fees`` or ``positions.downloads``. Can be used as a performance optimization. Can be passed multiple times. ``include`` is applied before ``exclude``, so ``exclude`` takes precedence.
:param organizer: The ``slug`` field of the organizer to fetch
@@ -554,6 +564,7 @@ Fetching individual orders
"fees": [],
"total": "23.00",
"comment": "",
"api_meta": {},
"custom_followup_at": null,
"checkin_attention": false,
"checkin_text": null,
@@ -734,6 +745,8 @@ Updating order fields
* ``comment``
* ``api_meta``
* ``custom_followup_at``
* ``invoice_address`` (you always need to supply the full object, or ``null`` to delete the current address)
+219
View File
@@ -0,0 +1,219 @@
Sales channels
==============
Resource description
--------------------
The sales channel resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
identifier string Internal ID of the sales channel. For sales channel types
that allow only one instance, this is the same as ``type``.
For sales channel types that allow multiple instances, this
is always prefixed with ``type.``.
label multi-lingual string Human-readable name of the sales channel
type string Type of the sales channel. Only channels with type ``api``
can currently be created through the API.
position integer Position for sorting lists of sales channels
===================================== ========================== =======================================================
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/saleschannels/
Returns a list of all sales channels within a given organizer.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/saleschannels/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"identifier": "web",
"name": {
"en": "Online shop"
},
"type": "web",
"position": 0
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/saleschannels/(identifier)/
Returns information on one sales channel, identified by its identifier.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/saleschannels/web/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"identifier": "web",
"name": {
"en": "Online shop"
},
"type": "web",
"position": 0
}
:param organizer: The ``slug`` field of the organizer to fetch
:param identifier: The ``identifier`` field of the sales channel to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/saleschannels/
Creates a sales channel
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/saleschannels/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"identifier": "api.custom",
"name": {
"en": "Custom integration"
},
"type": "api",
"position": 2
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"identifier": "api.custom",
"name": {
"en": "Custom integration"
},
"type": "api",
"position": 2
}
:param organizer: The ``slug`` field of the organizer to create a sales channel for
:statuscode 201: no error
:statuscode 400: The sales channel could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/saleschannels/(identifier)/
Update a sales channel. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
want to change.
You can change all fields of the resource except the ``identifier`` and ``type`` fields.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/saleschannels/web/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"position": 5
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"identifier": "web",
"name": {
"en": "Online shop"
},
"type": "web",
"position": 5
}
:param organizer: The ``slug`` field of the organizer to modify
:param identifier: The ``identifier`` field of the sales channel to modify
:statuscode 200: no error
:statuscode 400: The sales channel could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
.. http:delete:: /api/v1/organizers/(organizer)/saleschannels/(identifier)/
Delete a sales channel. You can not delete sales channels which have already been used or which are integral parts
of the system.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/saleschannels/api.custom/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer to modify
:param identifier: The ``identifier`` field of the sales channel to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource **or** the sales channel is currently in use.
+262
View File
@@ -0,0 +1,262 @@
.. _`rest-reusablemedia`:
Seats
=====
The seat resource represents the seats in a seating plan in a specific event or subevent.
Resource description
--------------------
The seat resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of this seat
subevent integer Internal ID of the subevent this seat belongs to
zone_name string Name of the zone the seat is in
row_name string Name/number of the row the seat is in
row_label string Additional label of the row (or ``null``)
seat_number string Number of the seat within the row
seat_label string Additional label of the seat (or ``null``)
seat_guid string Identifier of the seat within the seating plan
product integer Internal ID of the product that is mapped to this seat
blocked boolean Whether this seat is blocked manually.
orderposition integer / object Internal ID of an order position reserving this seat.
cartposition integer / object Internal ID of a cart position reserving this seat.
voucher integer / object Internal ID of a voucher reserving this seat.
===================================== ========================== =======================================================
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/seats/
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/(subevent_id)/seats/
Returns a list of all seats in the specified event or subevent. Depending on whether the event has subevents, the
according endpoint has to be used.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/seats/ HTTP/1.1
Host: pretix.eu
Accept: application/json
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 500,
"next": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/seats/?page=2",
"previous": null,
"results": [
{
"id": 1633,
"subevent": null,
"zone_name": "Ground floor",
"row_name": "1",
"row_label": null,
"seat_number": "1",
"seat_label": null,
"seat_guid": "b9746230-6f31-4f41-bbc9-d6b60bdb3342",
"product": 104,
"blocked": false,
"orderposition": null,
"cartposition": null,
"voucher": 51
},
{
"id": 1634,
"subevent": null,
"zone_name": "Ground floor",
"row_name": "1",
"row_label": null,
"seat_number": "2",
"seat_label": null,
"seat_guid": "1d29fe20-8e1e-4984-b0ee-2773b0d07e07",
"product": 104,
"blocked": true,
"orderposition": 4321,
"cartposition": null,
"voucher": null
},
// ...
]
}
:query integer page: The page number in case of a multi-page result set, default is 1.
:query string zone_name: Only show seats with the given zone_name.
:query string row_name: Only show seats with the given row_name.
:query string row_label: Only show seats with the given row_label.
:query string seat_number: Only show seats with the given seat_number.
:query string seat_label: Only show seats with the given seat_label.
:query string seat_guid: Only show seats with the given seat_guid.
:query boolean blocked: Only show seats with the given blocked status.
:query boolean is_available: Only show seats that are (not) currently available.
:query string expand: If you pass ``"orderposition"``, ``"cartposition"``, or ``"voucher"``, the respective field will be
shown as a nested value instead of just an ID. This requires permission to access that object.
The nested objects are identical to the respective resources, except that order positions
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
matching easier, and won't include the `seat` attribute, as that would be redundant.
The parameter can be given multiple times.
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param subevent_id: The ``id`` field of the subevent to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
:statuscode 404: Endpoint without subevent id was used for event with subevents, or vice versa.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/seats/(id)/
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/(subevent_id)/seats/(id)/
Returns information on one seat, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/seats/1634/?expand=orderposition HTTP/1.1
Host: pretix.eu
Accept: application/json
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1634,
"subevent": null,
"zone_name": "Ground floor",
"row_name": "1",
"row_label": null,
"seat_number": "2",
"seat_label": null,
"seat_guid": "1d29fe20-8e1e-4984-b0ee-2773b0d07e07",
"product": 104,
"blocked": true,
"orderposition": {
"id": 134,
"order": {
"code": "U0HW7",
"event": "sampleconf"
},
"positionid": 1,
"item": 104,
"variation": 59,
"price": "60.00",
"attendee_name": "",
"attendee_name_parts": {
"_scheme": "given_family"
},
"company": null,
"street": null,
"zipcode": null,
"city": null,
"country": null,
"state": null,
"discount": null,
"attendee_email": null,
"voucher": null,
"tax_rate": "0.00",
"tax_value": "0.00",
"secret": "4rfgp263jduratnsvwvy6cc6r6wnptbj",
"addon_to": null,
"subevent": null,
"checkins": [],
"downloads": [],
"answers": [],
"tax_rule": null,
"pseudonymization_id": "ZSNYSG3URZ",
"canceled": false,
"valid_from": null,
"valid_until": null,
"blocked": null,
"voucher_budget_use": null
},
"cartposition": null,
"voucher": null
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param subevent_id: The ``id`` field of the subevent to fetch
:param id: The ``id`` field of the seat to fetch
:query string expand: If you pass ``"orderposition"``, ``"cartposition"``, or ``"voucher"``, the respective field will be
shown as a nested value instead of just an ID. This requires permission to access that object.
The nested objects are identical to the respective resources, except that order positions
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
matching easier, and won't include the `seat` attribute, as that would be redundant.
The parameter can be given multiple times.
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
:statuscode 404: Seat does not exist; or the endpoint without subevent id was used for event with subevents, or vice versa.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/seats/(id)/
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/seats/(id)/
Update a seat.
You can only change the ``blocked`` field.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/seats/1636/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"blocked": true
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1636,
"subevent": null,
"zone_name": "Ground floor",
"row_name": "1",
"row_label": null,
"seat_number": "4",
"seat_label": null,
"seat_guid": "6c0e29e5-05d6-421f-99f3-afd01478ecad",
"product": 104,
"blocked": true,
"orderposition": null,
"cartposition": null,
"voucher": null
},
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param subevent_id: The ``id`` field of the subevent to modify
:param id: The ``id`` field of the seat to modify
:statuscode 200: no error
:statuscode 400: The seat could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to change this resource.
:statuscode 404: Seat does not exist; or the endpoint without subevent id was used for event with subevents, or vice versa.
+3
View File
@@ -1,6 +1,8 @@
Scheduled email rules
=====================
This feature requires the bundled ``pretix.plugins.sendmail`` plugin to be active for the event in order to work properly.
Resource description
--------------------
@@ -48,6 +50,7 @@ send_to string Can be ``"order
or ``"both"``.
date. Otherwise it is relative to the event start date.
===================================== ========================== =======================================================
.. versionchanged:: 2023.7
The ``include_pending`` field has been deprecated.
+2
View File
@@ -41,6 +41,7 @@ The following values for ``action_types`` are valid with pretix core:
* ``pretix.event.order.modified``
* ``pretix.event.order.contact.changed``
* ``pretix.event.order.changed.*``
* ``pretix.event.order.deleted`` (can only occur for test mode orders)
* ``pretix.event.order.refund.created``
* ``pretix.event.order.refund.created.externally``
* ``pretix.event.order.refund.requested``
@@ -115,6 +116,7 @@ Endpoints
}
:query integer page: The page number in case of a multi-page result set, default is 1
:query boolean enabled: Only show webhooks that are or are not enabled
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
+3 -3
View File
@@ -12,7 +12,7 @@ Core
.. automodule:: pretix.base.signals
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification,
item_copy_data, register_sales_channels, register_global_settings, quota_availability, global_email_filter,
item_copy_data, register_sales_channel_types, register_global_settings, quota_availability, global_email_filter,
register_ticket_secret_generators, gift_card_transaction_display,
register_text_placeholders, register_mail_placeholders
@@ -35,11 +35,11 @@ Frontend
--------
.. automodule:: pretix.presale.signals
:members: html_head, html_footer, footer_link, global_footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header
:members: html_head, html_footer, footer_link, global_footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header, seatingframe_html_head
.. automodule:: pretix.presale.signals
:members: order_info, order_info_top, order_meta_from_request
:members: order_info, order_info_top, order_meta_from_request, order_api_meta_from_request
Request flow
""""""""""""
+1 -1
View File
@@ -17,7 +17,7 @@ The project pretix is split into several components. The main components are:
create and manage their events, items, orders and tickets.
**presale**
This is the ticket-shop itself, containing all of the parts visible to the
This is the ticket shop itself, containing all of the parts visible to the
end user. Also called "frontend" in parts of this documentation.
**api**
+1 -3
View File
@@ -136,9 +136,7 @@ It is a good idea to put this command into your git hook ``.git/hooks/pre-commit
for example, to check for any errors in any staged files when committing::
#!/bin/bash
cd $GIT_DIR/../src
export GIT_WORK_TREE=../
export GIT_DIR=../.git
source ../env/bin/activate # Adjust to however you activate your virtual environment
for file in $(git diff --cached --name-only | grep -E '\.py$' | grep -Ev "migrations|mt940\.py|pretix/settings\.py|make_testdata\.py|testutils/settings\.py|tests/settings\.py|pretix/base/models/__init__\.py|.*_pb2\.py")
do
+105
View File
@@ -0,0 +1,105 @@
GetYourGuide
============
.. note::
The GetYourGuide integration is currently in Beta. Please contact support@pretix.eu to enable the integration
for your pretix.eu organizer account.
Introduction
------------
Using third party aggregators, such als GetYourGuide, event organizers can sell tickets to their events not only on
their own ticket-shop but also on the aggregator's portal. While this service is not for free, it allows event
organizers to reacher a larger audience that would otherwise not have found their way into the organizers webshop.
Using pretix' integration with GetYourGuide, event organizers can profit from an additional sales and revenue channel,
while keeping the effort for setting up and maintaining multiple ticket shops to a minimum.
Preparing your organizer account
--------------------------------
The first step in enabling the GetYourGuide integration, is to setup a corresponding Sales Channel, which will be used
to properly attribute the sales generated. This needs to be done only once per organizer account.
To do so, log into the pretix backend, select ``Organizers`` from the navigation and then the organizer in question.
Extending the ``Settings``-menu, find the ``Sales channels`` configuration and click the ``Add a new channel`` button.
On the following page, you will be able to select ``GetYourGuide`` as the sales channel type and give it a custom name.
Preparing your event
--------------------
In order to now sell your events on GetYourGuide, you will need to configure each event in question.
1. Enabling the plugin
Within your event, extend the ``Settings`` menu and navigate to ``Plugins``. Activate the plugin in the
``Integrations`` tab.
2. Sell the event on the sales channel
Pick the sales channel or channels, on which you would like to sell your event by navigating to the event's general
settings page using the ``Sell on all sales channels`` or ``Restrict to specific sales channels`` checkboxes.
3. Configure one or more products to be sold on GetYourGuide
Either create a new or edit an existing product, that you would like to sell on GetYourGuide. To do so, you will
need to have checked the ``Sell on all sales channels`` or appropriate ``Restrict to specific sales channels``
checkbox of the product within it's ``Availability`` tab.
In addition, you will also need to set the GetYourGuide equivalent ticket category in the product's accordingly
named settings tab. Within your event, there can be only one product per ticket category. Depending on your further
configuration, you must at least select one product to be in the ``Adult`` or ``Group`` category.
4. Configuring the GetYourGuide-plugin
Once you have configured one or more products to be eligible to be sold on GetYourGuide, you'll need to configure a
few basic settings within the event (``Settings`` --> ``GetYourGuide``). The most important settings can be found
the in the ``Configuration`` tab, such as the location of the event on sale.
Ticket Categories
-----------------
While pretix only uses the ticket category term loosely to group together multiple products for nicer display,
GetYourGuide is relying on the ticket categories to price the tickets.
First of all, you need to make the decision on how you are planning on selling your tickets on GetYourGuide - in most
cases, this will reflect your current sales strategy within your pretix shop.
- Individual tickets
Every single person attending will need to purchase their own ticket. A family of two adults and two
children will have to purchase and pay for a total of 4 tickets.
In this case, you will need to offer *at least* a ticket of the ``Adult`` type, but may offer any other ticket
category type (Child, Youth, Senior, ...) in addition. But you cannot offer a ``Group`` ticket.
- Group tickets
Two groups, consisting of 10 and 20 participants respectively, won't need to purchase a total of 30 tickets, but
rather two group tickets. It is up to you to configure the group size limits within the GetYourGuide-settings of your
product.
Choosing this option, you cannot offer any other ticket categories besides ``Group``.
Setting up event dates and quotas
---------------------------------
Of course, in addition to creating products, you will also need to add them to a quota for them to be available for
sale. The process for doing this is the very same as for any regular event or event series.
.. note::
When selling individual tickets through GetYourGuide, you will not be able to offer differing quantities for
individual ticket categories.
For this reason, we recommend to place all GetYourGuide-eligible products into the same quota. Should you however opt
to create multiple quotas which create an imbalance, pretix will report only the available number of tickets for the
lowest relevant quota.
Connecting your event to GetYourGuide
-------------------------------------
Once you have set up your event and products and performed all necessary configuration, you may want to use the
Analyzer-feature of our GetYourGuide-plugin (``Settings`` -> ``GetYourGuide`` -> tab ``Analyzer``).
The Analyzer should not display any blocking error messages and at least one event date that is ready for publishing on
the GetYourGuide platform.
At this point, you will need to setup your event (called ``product`` in the GetYourGuide universe) on their
`Supplier Portal`_ and connect it with your pretix shop. To do so, please follow the
`Connecting a new product to your Reservation System`_ on the GetYourGuide Supply Partner Help Center.
Select ``pretix.eu`` as your reservation system; the required ``product ID`` can be found in the ``Configuration`` tab
of the GetYourGuide plugin settings page.
From this point on, GetYourGuide will automatically import the availabilities and products and offer them for sale.
.. _Supplier Portal: https://suppliers.getyourguide.com/
.. _Connecting a new product to your Reservation System: https://supply.getyourguide.support/hc/en-us/articles/18008029689373-Connecting-a-new-product-to-your-Reservation-system
+1
View File
@@ -25,3 +25,4 @@ If you want to **create** a plugin, please go to the
webinar
presale-saml
kulturpass
getyourguide
+1 -2
View File
@@ -1,4 +1,4 @@
sphinx==7.3.*
sphinx==7.4.*
jinja2==3.1.*
sphinx-rtd-theme
sphinxcontrib-httpdomain
@@ -6,5 +6,4 @@ sphinxcontrib-images
sphinxcontrib-jquery
sphinxcontrib-spelling==8.*
sphinxemoji
pygments-markdown-lexer
pyenchant==3.2.*
+1 -2
View File
@@ -1,5 +1,5 @@
-e ../
sphinx==7.3.*
sphinx==7.4.*
jinja2==3.1.*
sphinx-rtd-theme
sphinxcontrib-httpdomain
@@ -7,5 +7,4 @@ sphinxcontrib-images
sphinxcontrib-jquery
sphinxcontrib-spelling==8.*
sphinxemoji
pygments-markdown-lexer
pyenchant==3.2.*
+3 -6
View File
@@ -31,8 +31,7 @@ Android 9 Support planned until at least 12/2025.
Android 8 Support planned until at least 12/2025.
Android 7 Support planned until at least 06/2025.
Android 6 Support planned until at least 06/2025.
Android 5 | Support planned until at least 06/2025.
| No support for COVID certificate verification.
Android 5 Support planned until at least 06/2025.
Android 4 Support dropped.
=========================== ==========================================================
@@ -57,16 +56,17 @@ Android 8 | Support planned until at least 12/2025.
Android 7 | Support planned until at least 12/2024.
| Support for Stripe Terminal to be dropped 05/2024.
| No support for Cryptovision TSE.
| No support for SumUp.
Android 6 | Support planned until at least 12/2024.
| No support for Cryptovision TSE.
| No support for Fiskal Cloud.
| No support for Stripe Terminal.
| No support for SumUp.
Android 5 | Support planned until at least 12/2024.
| No support for Cryptovision TSE.
| No support for Fiskal Cloud.
| No support for Stripe Terminal.
| No support for SumUp.
| No support for COVID certificate verification.
Android 4 Support dropped.
=========================== ==========================================================
@@ -87,9 +87,6 @@ Android 7 Support planned until at least 06/2025.
Android 6 Support planned until at least 06/2025.
Android 5 | Support planned until at least 06/2025.
| No support for Evolis printers on some devices.
Android 4.4 | Support planned until at least 06/2024.
| No support for USB printers.
| No support for Evolis printers.
Android 4 Support dropped.
=========================== ==========================================================
+23
View File
@@ -449,6 +449,29 @@ Further reading:
* `Stripe Payment Method Domain registration`_
Content Security Policy
-----------------------
When using a Content Security Policy (CSP) on your website, you may need to make some adjustments. If your pretix
shop is running under a custom domain, you need to add the following rules:
* ``script-src``: ``'unsafe-eval' https://pretix.eu`` (adjust to your domain for self-hosted pretix)
* ``style-src``: ``https://pretix.eu`` (adjust to your domain for self-hosted pretix **and** for custom domain on pretix Hosted)
* ``connect-src``: ``https://pretix.eu`` (adjust to your domain for self-hosted pretix **and** for custom domain on pretix Hosted)
* ``frame-src``: ``https://pretix.eu`` (adjust to your domain for self-hosted pretix **and** for custom domain on pretix Hosted)
* ``img-src``: ``https://pretix.eu`` (adjust to your domain for self-hosted pretix **and** for custom domain on pretix Hosted) and for pretix Hosted additionally add ``https://cdn.pretix.space``
External payment providers and Cross-Origin-Opener-Policy
---------------------------------------------------------
If you use a payment provider that opens a new window during checkout (such as PayPal), be aware that setting
``Cross-Origin-Opener-Policy: same-origin`` results in an empty popup-window being opened in the foreground. This is
due to JavaScript not having access to the opened window. To mitigate this, you either need to always open the widgets
checkout in a new tab (see :ref:`Always open a new tab`) or set ``Cross-Origin-Opener-Policy: same-origin-allow-popups``
Working with Cross-Origin-Embedder-Policy
-----------------------------------------
+14 -16
View File
@@ -22,7 +22,7 @@ classifiers = [
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Framework :: Django :: 4.1",
"Framework :: Django :: 4.2",
]
dependencies = [
@@ -35,16 +35,15 @@ dependencies = [
"cryptography>=3.4.2",
"css-inline==0.14.*",
"defusedcsv>=1.1.0",
"dj-static",
"Django[argon2]==4.2.*",
"Django[argon2]==4.2.*,>=4.2.15",
"django-bootstrap3==24.2",
"django-compressor==4.5",
"django-compressor==4.5.1",
"django-countries==7.6.*",
"django-filter==24.2",
"django-filter==24.3",
"django-formset-js-improved==0.5.0.3",
"django-formtools==2.5.1",
"django-hierarkey==1.2.*",
"django-hijack==3.5.*",
"django-hijack==3.6.*",
"django-i18nfield==1.9.*,>=1.9.4",
"django-libsass==0.9",
"django-localflavor==4.0",
@@ -62,10 +61,10 @@ dependencies = [
"importlib_metadata==8.*", # Polyfill, we can probably drop this once we require Python 3.10+
"isoweek",
"jsonschema",
"kombu==5.3.*",
"kombu==5.4.*",
"libsass==0.23.*",
"lxml",
"markdown==3.6", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
"markdown==3.7", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
"mt-940==4.30.*",
"oauthlib==3.2.*",
@@ -73,17 +72,17 @@ dependencies = [
"packaging",
"paypalrestsdk==1.13.*",
"paypal-checkout-serversdk==1.0.*",
"PyJWT==2.8.*",
"PyJWT==2.9.*",
"phonenumberslite==8.13.*",
"Pillow==10.3.*",
"Pillow==10.4.*",
"pretix-plugin-build",
"protobuf==5.27.*",
"psycopg2-binary",
"pycountry",
"pycparser==2.22",
"pycryptodome==3.20.*",
"pypdf==4.2.*",
"python-bidi==0.4.*", # Support for Arabic in reportlab
"pypdf==4.3.*",
"python-bidi==0.6.*", # Support for Arabic in reportlab
"python-dateutil==2.9.*",
"pytz",
"pytz-deprecation-shim==0.1.*",
@@ -92,10 +91,9 @@ dependencies = [
"redis==5.0.*",
"reportlab==4.2.*",
"requests==2.31.*",
"sentry-sdk==2.5.*",
"sentry-sdk==2.13.*",
"sepaxml==2.6.*",
"slimit",
"static3==0.7.*",
"stripe==7.9.*",
"text-unidecode==1.*",
"tlds>=2020041600",
@@ -110,7 +108,7 @@ dependencies = [
[project.optional-dependencies]
memcached = ["pylibmc"]
dev = [
"aiohttp==3.9.*",
"aiohttp==3.10.*",
"coverage",
"coveralls",
"fakeredis==2.23.*",
@@ -127,7 +125,7 @@ dev = [
"pytest-rerunfailures==14.*",
"pytest-sugar",
"pytest-xdist==3.6.*",
"pytest==8.2.*",
"pytest==8.3.*",
"responses",
]
+1 -1
View File
@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "2024.7.0.dev0"
__version__ = "2024.8.0.dev0"
+2
View File
@@ -62,6 +62,7 @@ INSTALLED_APPS = [
'pretix.plugins.badges',
'pretix.plugins.manualpayment',
'pretix.plugins.returnurl',
'pretix.plugins.autocheckin',
'pretix.plugins.webcheckin',
'django_countries',
'oauth2_provider',
@@ -100,6 +101,7 @@ ALL_LANGUAGES = [
('ro', _('Romanian')),
('ru', _('Russian')),
('sk', _('Slovak')),
('sv', _('Swedish')),
('es', _('Spanish')),
('tr', _('Turkish')),
('uk', _('Ukrainian')),
+82
View File
@@ -0,0 +1,82 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.db.models.constants import LOOKUP_SEP
from django.forms import MultipleChoiceField
from django_filters import Filter
from django_filters.conf import settings
class MultipleCharField(forms.CharField):
widget = forms.MultipleHiddenInput
def to_python(self, value):
if not value:
return []
elif not isinstance(value, (list, tuple)):
raise ValidationError(
MultipleChoiceField.default_error_messages["invalid_list"], code="invalid_list"
)
return [str(val) for val in value]
class MultipleCharFilter(Filter):
"""
This filter performs OR(by default) or AND(using conjoined=True) query
on the selected inputs.
"""
field_class = MultipleCharField
def __init__(self, *args, **kwargs):
self.conjoined = kwargs.pop("conjoined", False)
super().__init__(*args, **kwargs)
def filter(self, qs, value):
if not value:
# Even though not a noop, no point filtering if empty.
return qs
if not self.conjoined:
q = Q()
for v in set(value):
predicate = self.get_filter_predicate(v)
if self.conjoined:
qs = self.get_method(qs)(**predicate)
else:
q |= Q(**predicate)
if not self.conjoined:
qs = self.get_method(qs)(q)
return qs.distinct() if self.distinct else qs
def get_filter_predicate(self, v):
name = self.field_name
if name and self.lookup_expr != settings.DEFAULT_LOOKUP_EXPR:
name = LOOKUP_SEP.join([name, self.lookup_expr])
try:
return {name: getattr(v, self.field.to_field_name)}
except (AttributeError, TypeError):
return {name: v}
+58
View File
@@ -21,7 +21,9 @@
#
import json
from django.db.models import prefetch_related_objects
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
class AsymmetricField(serializers.Field):
@@ -61,3 +63,59 @@ class CompatibleJSONField(serializers.JSONField):
if value:
return json.loads(value)
return value
class SalesChannelMigrationMixin:
"""
Translates between the old field "sales_channels" and the new field combo "all_sales_channels"/"limit_sales_channels".
"""
@property
def organizer(self):
if "organizer" in self.context:
return self.context["organizer"]
elif "event" in self.context:
return self.context["event"].organizer
else:
raise ValueError("organizer not in context")
def to_internal_value(self, data):
if "sales_channels" in data:
prefetch_related_objects([self.organizer], "sales_channels")
all_channels = {
s.identifier for s in
self.organizer.sales_channels.all()
}
if data.get("all_sales_channels") and set(data["sales_channels"]) != all_channels:
raise ValidationError(
"If 'all_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
"the list of all sales channels."
)
if data.get("limit_sales_channels") and set(data["sales_channels"]) != set(data["limit_sales_channels"]):
raise ValidationError(
"If 'limit_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
"the same list."
)
if data["sales_channels"] == all_channels:
data["all_sales_channels"] = True
data["limit_sales_channels"] = []
else:
data["all_sales_channels"] = False
data["limit_sales_channels"] = data["sales_channels"]
del data["sales_channels"]
return super().to_internal_value(data)
def to_representation(self, value):
value = super().to_representation(value)
if value.get("all_sales_channels"):
prefetch_related_objects([self.organizer], "sales_channels")
value["sales_channels"] = sorted([
s.identifier for s in
self.organizer.sales_channels.all()
])
else:
value["sales_channels"] = value["limit_sales_channels"]
return value
+10 -2
View File
@@ -33,7 +33,7 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.order import (
AnswerCreateSerializer, AnswerSerializer, InlineSeatSerializer,
)
from pretix.base.models import Seat, Voucher
from pretix.base.models import SalesChannel, Seat, Voucher
from pretix.base.models.orders import CartPosition
@@ -212,7 +212,11 @@ class CartPositionCreateSerializer(BaseCartPositionCreateSerializer):
addons = BaseCartPositionCreateSerializer(many=True, required=False)
bundled = BaseCartPositionCreateSerializer(many=True, required=False)
seat = serializers.CharField(required=False, allow_null=True)
sales_channel = serializers.CharField(required=False, default='sales_channel')
sales_channel = serializers.SlugRelatedField(
slug_field='identifier',
queryset=SalesChannel.objects.none(),
required=False,
)
voucher = serializers.CharField(required=False, allow_null=True)
class Meta:
@@ -221,6 +225,10 @@ class CartPositionCreateSerializer(BaseCartPositionCreateSerializer):
'cart_id', 'expires', 'addons', 'bundled', 'seat', 'sales_channel', 'voucher'
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
def validate_cart_id(self, cid):
if cid and not cid.endswith('@api'):
raise ValidationError('Cart ID should end in @api or be empty.')
+10 -6
View File
@@ -25,14 +25,20 @@ from rest_framework.exceptions import ValidationError
from pretix.api.serializers.event import SubEventSerializer
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.channels import get_all_sales_channels
from pretix.base.media import MEDIA_TYPES
from pretix.base.models import Checkin, CheckinList
from pretix.base.models import Checkin, CheckinList, SalesChannel
class CheckinListSerializer(I18nAwareModelSerializer):
checkin_count = serializers.IntegerField(read_only=True)
position_count = serializers.IntegerField(read_only=True)
auto_checkin_sales_channels = serializers.SlugRelatedField(
slug_field="identifier",
queryset=SalesChannel.objects.none(),
required=False,
allow_empty=True,
many=True,
)
class Meta:
model = CheckinList
@@ -43,6 +49,8 @@ class CheckinListSerializer(I18nAwareModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['auto_checkin_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
if 'subevent' in self.context['request'].query_params.getlist('expand'):
self.fields['subevent'] = SubEventSerializer(read_only=True)
@@ -72,10 +80,6 @@ class CheckinListSerializer(I18nAwareModelSerializer):
if full_data.get('subevent'):
raise ValidationError(_('The subevent does not belong to this event.'))
for channel in full_data.get('auto_checkin_sales_channels') or []:
if channel not in get_all_sales_channels():
raise ValidationError(_('Unknown sales channel.'))
CheckinList.validate_rules(data.get('rules'))
return data
+15 -5
View File
@@ -19,18 +19,27 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from rest_framework import serializers
from pretix.api.serializers import SalesChannelMigrationMixin
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import Discount
from pretix.base.models import Discount, SalesChannel
class DiscountSerializer(I18nAwareModelSerializer):
class DiscountSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
limit_sales_channels = serializers.SlugRelatedField(
slug_field="identifier",
queryset=SalesChannel.objects.none(),
required=False,
allow_empty=True,
many=True,
)
class Meta:
model = Discount
fields = ('id', 'active', 'internal_name', 'position', 'sales_channels', 'available_from',
'available_until', 'subevent_mode', 'condition_all_products', 'condition_limit_products',
'condition_apply_to_addons', 'condition_min_count', 'condition_min_value',
fields = ('id', 'active', 'internal_name', 'position', 'all_sales_channels', 'limit_sales_channels',
'available_from', 'available_until', 'subevent_mode', 'condition_all_products',
'condition_limit_products', 'condition_apply_to_addons', 'condition_min_count', 'condition_min_value',
'benefit_discount_matching_percent', 'benefit_only_apply_to_cheapest_n_matches',
'benefit_same_products', 'benefit_limit_products', 'benefit_apply_to_addons',
'benefit_ignore_voucher_discounted', 'condition_ignore_voucher_discounted')
@@ -39,6 +48,7 @@ class DiscountSerializer(I18nAwareModelSerializer):
super().__init__(*args, **kwargs)
self.fields['condition_limit_products'].queryset = self.context['event'].items.all()
self.fields['benefit_limit_products'].queryset = self.context['event'].items.all()
self.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
def validate(self, data):
data = super().validate(data)
+98 -6
View File
@@ -35,7 +35,7 @@
import logging
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.exceptions import PermissionDenied, ValidationError
from django.db import transaction
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
@@ -46,10 +46,15 @@ from rest_framework import serializers
from rest_framework.fields import ChoiceField, Field
from rest_framework.relations import SlugRelatedField
from pretix.api.serializers import CompatibleJSONField
from pretix.api.serializers import (
CompatibleJSONField, SalesChannelMigrationMixin,
)
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.settings import SettingsSerializer
from pretix.base.models import Device, Event, TaxRule, TeamAPIToken
from pretix.base.models import (
CartPosition, Device, Event, OrderPosition, SalesChannel, Seat, TaxRule,
TeamAPIToken, Voucher,
)
from pretix.base.models.event import SubEvent
from pretix.base.models.items import (
ItemMetaProperty, SubEventItem, SubEventItemVariation,
@@ -161,7 +166,7 @@ class ValidKeysField(Field):
}
class EventSerializer(I18nAwareModelSerializer):
class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
meta_data = MetaDataField(required=False, source='*')
item_meta_properties = MetaPropertyField(required=False, source='*')
plugins = PluginsField(required=False, source='*')
@@ -170,6 +175,13 @@ class EventSerializer(I18nAwareModelSerializer):
valid_keys = ValidKeysField(source='*', read_only=True)
best_availability_state = serializers.IntegerField(allow_null=True, read_only=True)
public_url = serializers.SerializerMethodField('get_event_url', read_only=True)
limit_sales_channels = serializers.SlugRelatedField(
slug_field="identifier",
queryset=SalesChannel.objects.none(),
required=False,
allow_empty=True,
many=True,
)
def get_event_url(self, event):
return build_absolute_uri(event, 'presale:event.index')
@@ -180,7 +192,7 @@ class EventSerializer(I18nAwareModelSerializer):
'date_to', 'date_admission', 'is_public', 'presale_start',
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
'plugins', 'seat_category_mapping', 'timezone', 'item_meta_properties', 'valid_keys',
'sales_channels', 'best_availability_state', 'public_url')
'all_sales_channels', 'limit_sales_channels', 'best_availability_state', 'public_url')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -188,6 +200,7 @@ class EventSerializer(I18nAwareModelSerializer):
self.fields.pop('valid_keys')
if not self.context.get('request') or 'with_availability_for' not in self.context['request'].GET:
self.fields.pop('best_availability_state')
self.fields['limit_sales_channels'].child_relation.queryset = self.context['organizer'].sales_channels.all()
def validate(self, data):
data = super().validate(data)
@@ -269,13 +282,17 @@ class EventSerializer(I18nAwareModelSerializer):
from pretix.base.plugins import get_all_plugins
plugins_available = {
p.module for p in get_all_plugins(self.instance)
p.module: p for p in get_all_plugins(self.instance)
if not p.name.startswith('.') and getattr(p, 'visible', True)
}
settings_holder = self.instance if self.instance and self.instance.pk else self.context['organizer']
for plugin in value.get('plugins'):
if plugin not in plugins_available:
raise ValidationError(_('Unknown plugin: \'{name}\'.').format(name=plugin))
if getattr(plugins_available[plugin], 'restricted', False):
if plugin not in settings_holder.settings.allowed_restricted_plugins:
raise ValidationError(_('Restricted plugin: \'{name}\'.').format(name=plugin))
return value
@@ -828,6 +845,7 @@ class EventSettingsSerializer(SettingsSerializer):
'reusable_media_type_nfc_mf0aes_autocreate_giftcard',
'reusable_media_type_nfc_mf0aes_autocreate_giftcard_currency',
'reusable_media_type_nfc_mf0aes_random_uid',
'seating_allow_blocked_seats_for_channel',
]
readonly_fields = [
# These are read-only since they are currently only settable on organizers, not events
@@ -953,3 +971,77 @@ class ItemMetaPropertiesSerializer(I18nAwareModelSerializer):
class Meta:
model = ItemMetaProperty
fields = ('id', 'name', 'default', 'required', 'allowed_values')
def prefetch_by_id(items, qs, id_attr, target_attr):
"""
Prefetches a related object on each item in the given list of items by searching by id or another
unique field. The id value is read from the attribute on item specified in `id_attr`, searched on queryset `qs` by
the primary key, and the resulting prefetched model object is stored into `target_attr` on the item.
"""
ids = [getattr(item, id_attr) for item in items if getattr(item, id_attr)]
if ids:
result = qs.in_bulk(id_list=ids)
for item in items:
setattr(item, target_attr, result.get(getattr(item, id_attr)))
class SeatSerializer(I18nAwareModelSerializer):
orderposition = serializers.IntegerField(source='orderposition_id')
cartposition = serializers.IntegerField(source='cartposition_id')
voucher = serializers.IntegerField(source='voucher_id')
class Meta:
model = Seat
read_only_fields = (
'id', 'subevent', 'zone_name', 'row_name', 'row_label',
'seat_number', 'seat_label', 'seat_guid', 'product',
'orderposition', 'cartposition', 'voucher',
)
fields = (
'id', 'subevent', 'zone_name', 'row_name', 'row_label',
'seat_number', 'seat_label', 'seat_guid', 'product', 'blocked',
'orderposition', 'cartposition', 'voucher',
)
def prefetch_expanded_data(self, items, request, expand_fields):
if 'orderposition' in expand_fields:
if 'can_view_orders' not in request.eventpermset:
raise PermissionDenied('can_view_orders permission required for expand=orderposition')
prefetch_by_id(items, OrderPosition.objects.prefetch_related('order'), 'orderposition_id', 'orderposition')
if 'cartposition' in expand_fields:
if 'can_view_orders' not in request.eventpermset:
raise PermissionDenied('can_view_orders permission required for expand=cartposition')
prefetch_by_id(items, CartPosition.objects, 'cartposition_id', 'cartposition')
if 'voucher' in expand_fields:
if 'can_view_vouchers' not in request.eventpermset:
raise PermissionDenied('can_view_vouchers permission required for expand=voucher')
prefetch_by_id(items, Voucher.objects, 'voucher_id', 'voucher')
def __init__(self, instance, *args, **kwargs):
if not kwargs.get('data'):
self.prefetch_expanded_data(instance if hasattr(instance, '__iter__') else [instance],
kwargs['context']['request'],
kwargs['context']['expand_fields'])
super().__init__(instance, *args, **kwargs)
if 'orderposition' in self.context['expand_fields']:
from pretix.api.serializers.media import (
NestedOrderPositionSerializer,
)
self.fields['orderposition'] = NestedOrderPositionSerializer(read_only=True, context=self.context['order_context'])
try:
del self.fields['orderposition'].fields['seat']
except KeyError:
pass
if 'cartposition' in self.context['expand_fields']:
from pretix.api.serializers.cart import CartPositionSerializer
self.fields['cartposition'] = CartPositionSerializer(read_only=True)
del self.fields['cartposition'].fields['seat']
if 'voucher' in self.context['expand_fields']:
from pretix.api.serializers.voucher import VoucherSerializer
self.fields['voucher'] = VoucherSerializer(read_only=True)
del self.fields['voucher'].fields['seat']
+46 -8
View File
@@ -42,19 +42,27 @@ from django.utils.functional import cached_property, lazy
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from pretix.api.serializers import SalesChannelMigrationMixin
from pretix.api.serializers.event import MetaDataField
from pretix.api.serializers.fields import UploadedFileField
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import (
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation,
ItemVariationMetaValue, Question, QuestionOption, Quota,
ItemVariationMetaValue, Question, QuestionOption, Quota, SalesChannel,
)
class InlineItemVariationSerializer(I18nAwareModelSerializer):
class InlineItemVariationSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=13,
coerce_to_string=True)
meta_data = MetaDataField(required=False, source='*')
limit_sales_channels = serializers.SlugRelatedField(
slug_field="identifier",
queryset=SalesChannel.objects.none(),
required=False,
allow_empty=True,
many=True,
)
class Meta:
model = ItemVariation
@@ -63,11 +71,14 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
'require_membership', 'require_membership_types', 'require_membership_hidden',
'checkin_attention', 'checkin_text',
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
'sales_channels', 'hide_without_voucher', 'meta_data')
'all_sales_channels', 'limit_sales_channels', 'hide_without_voucher', 'meta_data')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['require_membership_types'].queryset = lazy(lambda: self.context['event'].organizer.membership_types.all(), QuerySet)
self.fields['limit_sales_channels'].child_relation.queryset = (
self.context['event'].organizer.sales_channels.all() if 'event' in self.context else SalesChannel.objects.none()
)
def validate_meta_data(self, value):
for key in value['meta_data'].keys():
@@ -76,10 +87,17 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
return value
class ItemVariationSerializer(I18nAwareModelSerializer):
class ItemVariationSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=13,
coerce_to_string=True)
meta_data = MetaDataField(required=False, source='*')
limit_sales_channels = serializers.SlugRelatedField(
slug_field="identifier",
queryset=SalesChannel.objects.none(),
required=False,
allow_empty=True,
many=True,
)
class Meta:
model = ItemVariation
@@ -88,21 +106,26 @@ class ItemVariationSerializer(I18nAwareModelSerializer):
'require_membership', 'require_membership_types', 'require_membership_hidden',
'checkin_attention', 'checkin_text',
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
'sales_channels', 'hide_without_voucher', 'meta_data')
'all_sales_channels', 'limit_sales_channels', 'hide_without_voucher', 'meta_data')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
self.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
@transaction.atomic
def create(self, validated_data):
meta_data = validated_data.pop('meta_data', None)
require_membership_types = validated_data.pop('require_membership_types', [])
limit_sales_channels = validated_data.pop('limit_sales_channels', [])
variation = ItemVariation.objects.create(**validated_data)
if require_membership_types:
variation.require_membership_types.add(*require_membership_types)
if limit_sales_channels:
variation.limit_sales_channels.add(*limit_sales_channels)
# Meta data
if meta_data is not None:
for key, value in meta_data.items():
@@ -223,7 +246,7 @@ class ItemTaxRateField(serializers.Field):
return str(Decimal('0.00'))
class ItemSerializer(I18nAwareModelSerializer):
class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
addons = InlineItemAddOnSerializer(many=True, required=False)
bundles = InlineItemBundleSerializer(many=True, required=False)
variations = InlineItemVariationSerializer(many=True, required=False)
@@ -232,11 +255,18 @@ class ItemSerializer(I18nAwareModelSerializer):
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
'image/png', 'image/jpeg', 'image/gif'
), max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE)
limit_sales_channels = serializers.SlugRelatedField(
slug_field="identifier",
queryset=SalesChannel.objects.none(),
required=False,
allow_empty=True,
many=True,
)
class Meta:
model = Item
fields = ('id', 'category', 'name', 'internal_name', 'active', 'sales_channels', 'description',
'default_price', 'free_price', 'free_price_suggestion', 'tax_rate', 'tax_rule', 'admission',
fields = ('id', 'category', 'name', 'internal_name', 'active', 'all_sales_channels', 'limit_sales_channels',
'description', 'default_price', 'free_price', 'free_price_suggestion', 'tax_rate', 'tax_rule', 'admission',
'personalized', 'position', 'picture',
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
@@ -259,6 +289,8 @@ class ItemSerializer(I18nAwareModelSerializer):
if not self.read_only:
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
self.fields['grant_membership_type'].queryset = self.context['event'].organizer.membership_types.all()
self.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
self.fields['variations'].child.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
def validate(self, data):
data = super().validate(data)
@@ -335,7 +367,10 @@ class ItemSerializer(I18nAwareModelSerializer):
meta_data = validated_data.pop('meta_data', None)
picture = validated_data.pop('picture', None)
require_membership_types = validated_data.pop('require_membership_types', [])
limit_sales_channels = validated_data.pop('limit_sales_channels', [])
item = Item.objects.create(**validated_data)
if limit_sales_channels:
item.limit_sales_channels.add(*limit_sales_channels)
if picture:
item.picture.save(os.path.basename(picture.name), picture)
if require_membership_types:
@@ -343,10 +378,13 @@ class ItemSerializer(I18nAwareModelSerializer):
for variation_data in variations_data:
require_membership_types = variation_data.pop('require_membership_types', [])
limit_sales_channels = variation_data.pop('limit_sales_channels', [])
var_meta_data = variation_data.pop('meta_data', {})
v = ItemVariation.objects.create(item=item, **variation_data)
if require_membership_types:
v.require_membership_types.add(*require_membership_types)
if limit_sales_channels:
v.limit_sales_channels.add(*limit_sales_channels)
if var_meta_data is not None:
for key, value in var_meta_data.items():
+32 -28
View File
@@ -46,13 +46,12 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.item import (
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
)
from pretix.base.channels import get_all_sales_channels
from pretix.base.decimal import round_decimal
from pretix.base.i18n import language
from pretix.base.models import (
CachedFile, Checkin, Customer, Invoice, InvoiceAddress, InvoiceLine, Item,
ItemVariation, Order, OrderPosition, Question, QuestionAnswer,
ReusableMedium, Seat, SubEvent, TaxRule, Voucher,
ReusableMedium, SalesChannel, Seat, SubEvent, TaxRule, Voucher,
)
from pretix.base.models.orders import (
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
@@ -166,7 +165,7 @@ class InlineSeatSerializer(I18nAwareModelSerializer):
class Meta:
model = Seat
fields = ('id', 'name', 'seat_guid')
fields = ('id', 'name', 'seat_guid', 'zone_name', 'row_name', 'row_label', 'seat_label', 'seat_number')
class AnswerSerializer(I18nAwareModelSerializer):
@@ -586,7 +585,7 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
self.fields['item'] = ItemSerializer(read_only=True, context=self.context)
if 'variation' in self.context['expand']:
self.fields['variation'] = InlineItemVariationSerializer(read_only=True)
self.fields['variation'] = InlineItemVariationSerializer(read_only=True, context=self.context)
if 'answers.question' in self.context['expand']:
self.fields['answers'].child.fields['question'] = QuestionSerializer(read_only=True)
@@ -714,6 +713,11 @@ class OrderSerializer(I18nAwareModelSerializer):
payment_provider = OrderPaymentTypeField(source='*', read_only=True)
url = OrderURLField(source='*', read_only=True)
customer = serializers.SlugRelatedField(slug_field='identifier', read_only=True)
sales_channel = serializers.SlugRelatedField(
slug_field='identifier',
queryset=SalesChannel.objects.none(),
required=False,
)
class Meta:
model = Order
@@ -722,7 +726,7 @@ class OrderSerializer(I18nAwareModelSerializer):
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
'url', 'customer', 'valid_if_pending'
'url', 'customer', 'valid_if_pending', 'api_meta'
)
read_only_fields = (
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
@@ -732,6 +736,10 @@ class OrderSerializer(I18nAwareModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if "organizer" in self.context:
self.fields["sales_channel"].queryset = self.context["organizer"].sales_channels.all()
else:
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
if not self.context['pdf_data']:
self.fields['positions'].child.fields.pop('pdf_data', None)
@@ -778,7 +786,7 @@ class OrderSerializer(I18nAwareModelSerializer):
# Even though all fields that shouldn't be edited are marked as read_only in the serializer
# (hopefully), we'll be extra careful here and be explicit about the model fields we update.
update_fields = ['comment', 'custom_followup_at', 'checkin_attention', 'checkin_text', 'email', 'locale',
'phone', 'valid_if_pending']
'phone', 'valid_if_pending', 'api_meta']
if 'invoice_address' in validated_data:
iadata = validated_data.pop('invoice_address')
@@ -1033,19 +1041,25 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
require_approval = serializers.BooleanField(default=False, required=False)
simulate = serializers.BooleanField(default=False, required=False)
customer = serializers.SlugRelatedField(slug_field='identifier', queryset=Customer.objects.none(), required=False)
sales_channel = serializers.SlugRelatedField(
slug_field='identifier',
queryset=SalesChannel.objects.none(),
required=False,
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['positions'].child.fields['voucher'].queryset = self.context['event'].vouchers.all()
self.fields['customer'].queryset = self.context['event'].organizer.customers.all()
self.fields['expires'].required = False
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
class Meta:
model = Order
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date',
'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at',
'require_approval', 'valid_if_pending', 'expires')
'require_approval', 'valid_if_pending', 'expires', 'api_meta')
def validate_payment_provider(self, pp):
if pp is None:
@@ -1059,11 +1073,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
raise ValidationError('Expiration date must be in the future.')
return expires
def validate_sales_channel(self, channel):
if channel not in get_all_sales_channels():
raise ValidationError('Unknown sales channel.')
return channel
def validate_code(self, code):
if code and Order.objects.filter(event__organizer=self.context['event'].organizer, code=code).exists():
raise ValidationError(
@@ -1125,20 +1134,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
raise ValidationError(errs)
return data
def validate_testmode(self, testmode):
if 'sales_channel' in self.initial_data:
try:
sales_channel = get_all_sales_channels()[self.initial_data['sales_channel']]
if testmode and not sales_channel.testmode_supported:
raise ValidationError('This sales channel does not provide support for test mode.')
except KeyError:
# We do not need to raise a ValidationError here, since there is another check to validate the
# sales_channel
pass
return testmode
def create(self, validated_data):
fees_data = validated_data.pop('fees') if 'fees' in validated_data else []
positions_data = validated_data.pop('positions') if 'positions' in validated_data else []
@@ -1147,9 +1142,16 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
payment_date = validated_data.pop('payment_date', now())
force = validated_data.pop('force', False)
simulate = validated_data.pop('simulate', False)
if not validated_data.get("sales_channel"):
validated_data["sales_channel"] = self.context['event'].organizer.sales_channels.get(identifier="web")
if validated_data.get("testmode") and not validated_data["sales_channel"].type_instance.testmode_supported:
raise ValidationError({"testmode": ["This sales channel does not provide support for test mode."]})
self._send_mail = validated_data.pop('send_email', False)
if self._send_mail is None:
self._send_mail = validated_data.get('sales_channel') in self.context['event'].settings.mail_sales_channel_placed_paid
self._send_mail = validated_data["sales_channel"].identifier in self.context['event'].settings.mail_sales_channel_placed_paid
if 'invoice_address' in validated_data:
iadata = validated_data.pop('invoice_address')
@@ -1309,7 +1311,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
errs[i]['seat'] = ['The specified seat does not exist.']
else:
seat_usage[seat] += 1
if (seat_usage[seat] > 0 and not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web'))) or seat_usage[seat] > 1:
sales_channel_id = validated_data['sales_channel'].identifier
if (seat_usage[seat] > 0 and not seat.is_available(sales_channel=sales_channel_id)) or seat_usage[seat] > 1:
errs[i]['seat'] = [gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
elif seated:
errs[i]['seat'] = ['The specified product requires to choose a seat.']
@@ -1368,6 +1371,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
if validated_data.get('locale', None) is None:
validated_data['locale'] = self.context['event'].settings.locale
order = Order(event=self.context['event'], **validated_data)
if not validated_data.get('expires'):
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
+31 -1
View File
@@ -38,7 +38,7 @@ from pretix.base.i18n import get_language_without_region
from pretix.base.models import (
Customer, Device, GiftCard, GiftCardAcceptance, GiftCardTransaction,
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium,
SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
)
from pretix.base.models.seating import SeatingPlanLayoutValidator
from pretix.base.services.mail import SendMailException, mail
@@ -165,6 +165,36 @@ class FlexibleTicketRelatedField(serializers.PrimaryKeyRelatedField):
self.fail('incorrect_type', data_type=type(data).__name__)
class SalesChannelSerializer(I18nAwareModelSerializer):
type = serializers.CharField(default="api")
class Meta:
model = SalesChannel
fields = ('identifier', 'type', 'label', 'position')
def validate_type(self, value):
if (not self.instance or not self.instance.pk) and value != "api":
raise ValidationError(
"You can currently only create channels of type 'api' through the API."
)
if value and self.instance and self.instance.pk and self.instance.type != value:
raise ValidationError(
"You cannot change the type of a sales channel."
)
return value
def validate_identifier(self, value):
if (not self.instance or not self.instance.pk) and not value.startswith("api."):
raise ValidationError(
"Your identifier needs to start with 'api.'."
)
if value and self.instance and self.instance.pk and self.instance.identifier != value:
raise ValidationError(
"You cannot change the identifier of a sales channel."
)
return value
class GiftCardSerializer(I18nAwareModelSerializer):
value = serializers.DecimalField(max_digits=13, decimal_places=2, min_value=Decimal('0.00'))
owner_ticket = FlexibleTicketRelatedField(required=False, allow_null=True, queryset=OrderPosition.all.none())
+6
View File
@@ -56,6 +56,7 @@ orga_router.register(r'webhooks', webhooks.WebHookViewSet)
orga_router.register(r'seatingplans', organizer.SeatingPlanViewSet)
orga_router.register(r'giftcards', organizer.GiftCardViewSet)
orga_router.register(r'customers', organizer.CustomerViewSet)
orga_router.register(r'saleschannels', organizer.SalesChannelViewSet)
orga_router.register(r'memberships', organizer.MembershipViewSet)
orga_router.register(r'membershiptypes', organizer.MembershipTypeViewSet)
orga_router.register(r'reusablemedia', media.ReusableMediaViewSet)
@@ -86,6 +87,7 @@ event_router.register(r'invoices', order.InvoiceViewSet)
event_router.register(r'revokedsecrets', order.RevokedSecretViewSet, basename='revokedsecrets')
event_router.register(r'blockedsecrets', order.BlockedSecretViewSet, basename='blockedsecrets')
event_router.register(r'taxrules', event.TaxRuleViewSet)
event_router.register(r'seats', event.SeatViewSet)
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
event_router.register(r'checkinlists', checkin.CheckinListViewSet)
event_router.register(r'cartpositions', cart.CartPositionViewSet)
@@ -94,6 +96,9 @@ event_router.register(r'exporters', exporters.EventExportersViewSet, basename='e
event_router.register(r'shredders', shredders.EventShreddersViewSet, basename='shredders')
event_router.register(r'item_meta_properties', event.ItemMetaPropertiesViewSet)
subevent_router = routers.DefaultRouter()
subevent_router.register(r'seats', event.SeatViewSet)
checkinlist_router = routers.DefaultRouter()
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet, basename='checkinlistpos')
@@ -131,6 +136,7 @@ urlpatterns = [
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/settings/$', event.EventSettingsView.as_view(),
name="event.settings"),
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/', include(event_router.urls)),
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/subevents/(?P<subevent>\d+)/', include(subevent_router.urls)),
re_path(r'^organizers/(?P<organizer>[^/]+)/teams/(?P<team>[^/]+)/', include(team_router.urls)),
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/items/(?P<item>[^/]+)/', include(item_router.urls)),
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/questions/(?P<question>[^/]+)/',
+5 -1
View File
@@ -211,8 +211,12 @@ class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
if validated_data.get('seat'):
# Assumption: Add-ons currently can't have seats, thus we only need to check the main product
if validated_data.get('sales_channel'):
sales_channel_id = validated_data.get('sales_channel').identifier
else:
sales_channel_id = "web"
if not validated_data['seat'].is_available(
sales_channel=validated_data.get('sales_channel', 'web'),
sales_channel=sales_channel_id,
distance_ignore_cart_id=validated_data['cart_id'],
ignore_voucher_id=validated_data['voucher'].pk if validated_data.get('voucher') else None,
):
+2 -2
View File
@@ -115,7 +115,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
if 'subevent' in self.request.query_params.getlist('expand'):
qs = qs.prefetch_related(
'subevent', 'subevent__event', 'subevent__subeventitem_set', 'subevent__subeventitemvariation_set',
'subevent__seat_category_mappings', 'subevent__meta_values'
'subevent__seat_category_mappings', 'subevent__meta_values', 'auto_checkin_sales_channels'
)
return qs
@@ -406,7 +406,7 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
'item__variations').select_related('item__tax_rule')
if expand and 'variation' in expand:
qs = qs.prefetch_related('variation')
qs = qs.prefetch_related('variation', 'variation__meta_values')
return qs
+3 -1
View File
@@ -60,7 +60,9 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
write_permission = 'can_change_items'
def get_queryset(self):
return self.request.event.discounts.all()
return self.request.event.discounts.prefetch_related(
'limit_sales_channels',
)
def perform_create(self, serializer):
serializer.save(event=self.request.event)
+115 -11
View File
@@ -40,19 +40,22 @@ from django.utils.timezone import now
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import serializers, views, viewsets
from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.exceptions import (
NotFound, PermissionDenied, ValidationError,
)
from rest_framework.generics import get_object_or_404
from rest_framework.response import Response
from pretix.api.auth.permission import EventCRUDPermission
from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.event import (
CloneEventSerializer, DeviceEventSettingsSerializer, EventSerializer,
EventSettingsSerializer, ItemMetaPropertiesSerializer, SubEventSerializer,
TaxRuleSerializer,
EventSettingsSerializer, ItemMetaPropertiesSerializer, SeatSerializer,
SubEventSerializer, TaxRuleSerializer,
)
from pretix.api.views import ConditionalListView
from pretix.base.models import (
CartPosition, Device, Event, ItemMetaProperty, SeatCategoryMapping,
CartPosition, Device, Event, ItemMetaProperty, Seat, SeatCategoryMapping,
TaxRule, TeamAPIToken,
)
from pretix.base.models.event import SubEvent
@@ -113,7 +116,10 @@ with scopes_disabled():
return queryset.exclude(expr)
def sales_channel_qs(self, queryset, name, value):
return queryset.filter(sales_channels__contains=value)
return queryset.filter(
Q(all_sales_channels=True) |
Q(limit_sales_channels__identifier=value)
)
def search_qs(self, queryset, name, value):
return queryset.filter(
@@ -135,6 +141,12 @@ class EventViewSet(viewsets.ModelViewSet):
ordering_fields = ('date_from', 'slug')
filterset_class = EventFilter
def get_serializer_context(self):
return {
**super().get_serializer_context(),
"organizer": self.request.organizer,
}
def get_copy_from_queryset(self):
if isinstance(self.request.auth, (TeamAPIToken, Device)):
return self.request.auth.get_events_with_any_permission()
@@ -153,13 +165,20 @@ class EventViewSet(viewsets.ModelViewSet):
qs = filter_qs_by_attr(qs, self.request)
if 'with_availability_for' in self.request.GET:
qs = Event.annotated(qs, channel=self.request.GET.get('with_availability_for'))
qs = Event.annotated(
qs,
channel=get_object_or_404(
self.request.organizer.sales_channels,
identifier=self.request.GET.get('with_availability_for')
)
)
return qs.prefetch_related(
'organizer',
'meta_values',
'meta_values__property',
'item_meta_properties',
'limit_sales_channels',
Prefetch(
'seat_category_mappings',
to_attr='_seat_category_mappings',
@@ -268,8 +287,6 @@ class EventViewSet(viewsets.ModelViewSet):
new_event.is_public = serializer.validated_data['is_public']
if 'testmode' in serializer.validated_data:
new_event.testmode = serializer.validated_data['testmode']
if 'sales_channels' in serializer.validated_data:
new_event.sales_channels = serializer.validated_data['sales_channels']
if 'has_subevents' in serializer.validated_data:
new_event.has_subevents = serializer.validated_data['has_subevents']
if 'date_admission' in serializer.validated_data:
@@ -277,6 +294,10 @@ class EventViewSet(viewsets.ModelViewSet):
new_event.save()
if 'timezone' in serializer.validated_data:
new_event.settings.timezone = serializer.validated_data['timezone']
if 'all_sales_channels' in serializer.validated_data and 'sales_channels' in serializer.validated_data:
new_event.all_sales_channels = serializer.validated_data['all_sales_channels']
new_event.limit_sales_channels.set(serializer.validated_data['limit_sales_channels'])
else:
serializer.instance.set_defaults()
@@ -379,7 +400,10 @@ with scopes_disabled():
return queryset.exclude(expr)
def sales_channel_qs(self, queryset, name, value):
return queryset.filter(event__sales_channels__contains=value)
return queryset.filter(
Q(event__all_sales_channels=True) |
Q(event__limit_sales_channels__identifier=value)
)
def search_qs(self, queryset, name, value):
return queryset.filter(
@@ -421,13 +445,19 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
elif self.request.user.is_authenticated:
qs = SubEvent.objects.filter(
event__organizer=self.request.organizer,
event__in=self.request.user.get_events_with_any_permission()
event__in=self.request.user.get_events_with_any_permission(request=self.request)
)
qs = filter_qs_by_attr(qs, self.request)
if 'with_availability_for' in self.request.GET:
qs = SubEvent.annotated(qs, channel=self.request.GET.get('with_availability_for'))
qs = SubEvent.annotated(
qs,
channel=get_object_or_404(
self.request.organizer.sales_channels,
identifier=self.request.GET.get('with_availability_for')
)
)
return qs.prefetch_related(
'event',
@@ -639,3 +669,77 @@ class EventSettingsView(views.APIView):
'request': request
})
return Response(s.data)
class SeatFilter(FilterSet):
is_available = django_filters.BooleanFilter(method="is_available_qs")
def is_available_qs(self, queryset, name, value):
expr = (
Q(orderposition_id__isnull=True, cartposition_id__isnull=True, voucher_id__isnull=True)
)
if self.request.event.settings.seating_minimal_distance:
expr = expr & Q(has_closeby_taken=False)
if value:
return queryset.filter(expr)
else:
return queryset.exclude(expr)
class Meta:
model = Seat
fields = ('zone_name', 'row_name', 'row_label', 'seat_number', 'seat_label', 'seat_guid', 'blocked',)
class SeatViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer_class = SeatSerializer
queryset = Seat.objects.none()
write_permission = 'can_change_event_settings'
filter_backends = (DjangoFilterBackend, )
filterset_class = SeatFilter
def get_queryset(self):
if self.request.event.has_subevents and 'subevent' in self.request.resolver_match.kwargs:
try:
subevent = self.request.event.subevents.get(pk=self.request.resolver_match.kwargs['subevent'])
except SubEvent.DoesNotExist:
raise NotFound('Subevent not found')
qs = Seat.annotated(
event_id=self.request.event.id,
subevent=subevent,
qs=subevent.seats.all(),
annotate_ids=True,
minimal_distance=self.request.event.settings.seating_minimal_distance,
distance_only_within_row=self.request.event.settings.seating_distance_only_within_row,
)
elif not self.request.event.has_subevents and 'subevent' not in self.request.resolver_match.kwargs:
qs = Seat.annotated(
event_id=self.request.event.id,
subevent=None,
qs=self.request.event.seats.all(),
annotate_ids=True,
minimal_distance=self.request.event.settings.seating_minimal_distance,
distance_only_within_row=self.request.event.settings.seating_distance_only_within_row,
)
else:
raise NotFound('Please use the subevent-specific endpoint' if self.request.event.has_subevents
else 'This event has no subevents')
return qs
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['expand_fields'] = self.request.query_params.getlist('expand')
ctx['order_context'] = {
'event': self.request.event,
'pdf_data': None,
}
return ctx
def perform_update(self, serializer):
super().perform_update(serializer)
serializer.instance.event.log_action(
"pretix.event.seats.blocks.changed",
user=self.request.user,
auth=self.request.auth,
data={"seats": [serializer.instance.pk]},
)
+23 -1
View File
@@ -56,10 +56,17 @@ from pretix.base.models import (
)
from pretix.base.services.quotas import QuotaAvailability
from pretix.helpers.dicts import merge_dicts
from pretix.helpers.i18n import i18ncomp
with scopes_disabled():
class ItemFilter(FilterSet):
tax_rate = django_filters.CharFilter(method='tax_rate_qs')
search = django_filters.CharFilter(method='search_qs')
def search_qs(self, queryset, name, value):
return queryset.filter(
Q(internal_name__icontains=value) | Q(name__icontains=i18ncomp(value))
)
def tax_rate_qs(self, queryset, name, value):
if value in ("0", "None", "0.00"):
@@ -71,6 +78,18 @@ with scopes_disabled():
model = Item
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
class ItemVariationFilter(FilterSet):
search = django_filters.CharFilter(method='search_qs')
def search_qs(self, queryset, name, value):
return queryset.filter(
Q(value__icontains=i18ncomp(value))
)
class Meta:
model = ItemVariation
fields = ['active']
class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer_class = ItemSerializer
@@ -87,6 +106,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
'variations', 'addons', 'bundles', 'meta_values', 'meta_values__property',
'variations__meta_values', 'variations__meta_values__property',
'require_membership_types', 'variations__require_membership_types',
'limit_sales_channels', 'variations__limit_sales_channels',
).all()
def perform_create(self, serializer):
@@ -139,6 +159,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
serializer_class = ItemVariationSerializer
queryset = ItemVariation.objects.none()
filter_backends = (DjangoFilterBackend, TotalOrderingFilter,)
filterset_class = ItemVariationFilter
ordering_fields = ('id', 'position')
ordering = ('id',)
permission = None
@@ -152,7 +173,8 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
return self.item.variations.all().prefetch_related(
'meta_values',
'meta_values__property',
'require_membership_types'
'require_membership_types',
'limit_sales_channels',
)
def get_serializer_context(self):
+17 -6
View File
@@ -49,6 +49,7 @@ from rest_framework.mixins import CreateModelMixin
from rest_framework.permissions import SAFE_METHODS
from rest_framework.response import Response
from pretix.api.filters import MultipleCharFilter
from pretix.api.models import OAuthAccessToken
from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.order import (
@@ -108,6 +109,7 @@ with scopes_disabled():
status = django_filters.CharFilter(field_name='status', lookup_expr='iexact')
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
created_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte')
created_before = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='lt')
subevent_after = django_filters.IsoDateTimeFilter(method='subevent_after_qs')
subevent_before = django_filters.IsoDateTimeFilter(method='subevent_before_qs')
search = django_filters.CharFilter(method='search_qs')
@@ -115,6 +117,8 @@ with scopes_disabled():
variation = django_filters.CharFilter(field_name='all_positions', lookup_expr='variation_id', distinct=True)
subevent = django_filters.CharFilter(field_name='all_positions', lookup_expr='subevent_id', distinct=True)
customer = django_filters.CharFilter(field_name='customer__identifier')
sales_channel = django_filters.CharFilter(field_name='sales_channel__identifier')
payment_provider = django_filters.CharFilter(method='provider_qs')
class Meta:
model = Order
@@ -138,6 +142,11 @@ with scopes_disabled():
)
return qs
def provider_qs(self, qs, name, value):
return qs.filter(Exists(
OrderPayment.objects.filter(order=OuterRef('pk'), provider=value)
))
def subevent_before_qs(self, qs, name, value):
if getattr(self.request, 'event', None):
subevents = self.request.event.subevents
@@ -229,7 +238,7 @@ class OrderViewSetMixin:
if 'customer' not in self.request.GET.getlist('exclude'):
qs = qs.select_related('customer')
qs = qs.prefetch_related(self._positions_prefetch(self.request))
qs = qs.select_related('sales_channel').prefetch_related(self._positions_prefetch(self.request))
return qs
def _positions_prefetch(self, request):
@@ -316,6 +325,11 @@ class OrganizerOrderViewSet(OrderViewSetMixin, viewsets.ReadOnlyModelViewSet):
else:
raise PermissionDenied()
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['organizer'] = self.request.organizer
return ctx
class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
permission = 'can_view_orders'
@@ -1812,17 +1826,14 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
with scopes_disabled():
class InvoiceFilter(FilterSet):
refers = django_filters.CharFilter(method='refers_qs')
number = django_filters.CharFilter(method='nr_qs')
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
number = MultipleCharFilter(field_name='nr', lookup_expr='iexact')
order = MultipleCharFilter(field_name='order', lookup_expr='code__iexact')
def refers_qs(self, queryset, name, value):
return queryset.annotate(
refers_nr=Concat('refers__prefix', 'refers__invoice_no')
).filter(refers_nr__iexact=value)
def nr_qs(self, queryset, name, value):
return queryset.filter(nr__iexact=value)
class Meta:
model = Invoice
fields = ['order', 'number', 'is_cancellation', 'refers', 'locale']
+79 -5
View File
@@ -24,10 +24,11 @@ from decimal import Decimal
import django_filters
from django.contrib.auth.hashers import make_password
from django.db import transaction
from django.db.models import OuterRef, Subquery, Sum
from django.db.models import OuterRef, Q, Subquery, Sum
from django.db.models.functions import Coalesce
from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property
from django.utils.timezone import now
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import mixins, serializers, status, views, viewsets
@@ -43,13 +44,13 @@ from pretix.api.serializers.organizer import (
CustomerCreateSerializer, CustomerSerializer, DeviceSerializer,
GiftCardSerializer, GiftCardTransactionSerializer, MembershipSerializer,
MembershipTypeSerializer, OrganizerSerializer, OrganizerSettingsSerializer,
SeatingPlanSerializer, TeamAPITokenSerializer, TeamInviteSerializer,
TeamMemberSerializer, TeamSerializer,
SalesChannelSerializer, SeatingPlanSerializer, TeamAPITokenSerializer,
TeamInviteSerializer, TeamMemberSerializer, TeamSerializer,
)
from pretix.base.models import (
Customer, Device, GiftCard, GiftCardTransaction, Membership,
MembershipType, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite,
User,
MembershipType, Organizer, SalesChannel, SeatingPlan, Team, TeamAPIToken,
TeamInvite, User,
)
from pretix.helpers import OF_SELF
from pretix.helpers.dicts import merge_dicts
@@ -136,11 +137,19 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
with scopes_disabled():
class GiftCardFilter(FilterSet):
secret = django_filters.CharFilter(field_name='secret', lookup_expr='iexact')
expired = django_filters.BooleanFilter(method='expired_qs')
value = django_filters.NumberFilter(field_name='cached_value')
class Meta:
model = GiftCard
fields = ['secret', 'testmode']
def expired_qs(self, qs, name, value):
if value:
return qs.filter(expires__isnull=False, expires__lt=now())
else:
return qs.filter(Q(expires__isnull=True) | Q(expires__gte=now()))
class GiftCardViewSet(viewsets.ModelViewSet):
serializer_class = GiftCardSerializer
@@ -675,3 +684,68 @@ class MembershipViewSet(viewsets.ModelViewSet):
data=self.request.data,
)
return inst
with scopes_disabled():
class SalesChannelFilter(FilterSet):
class Meta:
model = SalesChannel
fields = ['type', 'identifier']
class SalesChannelViewSet(viewsets.ModelViewSet):
serializer_class = SalesChannelSerializer
queryset = SalesChannel.objects.none()
permission = 'can_change_organizer_settings'
write_permission = 'can_change_organizer_settings'
filter_backends = (DjangoFilterBackend,)
filterset_class = SalesChannelFilter
lookup_field = 'identifier'
lookup_url_kwarg = 'identifier'
lookup_value_regex = r"[a-zA-Z0-9.\-_]+"
def get_queryset(self):
return self.request.organizer.sales_channels.all()
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['organizer'] = self.request.organizer
return ctx
@transaction.atomic()
def perform_create(self, serializer):
inst = serializer.save(
organizer=self.request.organizer,
type="api"
)
inst.log_action(
'pretix.saleschannel.created',
user=self.request.user,
auth=self.request.auth,
data=merge_dicts(self.request.data, {'id': inst.pk})
)
@transaction.atomic()
def perform_update(self, serializer):
inst = serializer.save(
type=serializer.instance.type,
identifier=serializer.instance.identifier,
)
inst.log_action(
'pretix.sales_channel.changed',
user=self.request.user,
auth=self.request.auth,
data=self.request.data,
)
return inst
def perform_destroy(self, instance):
if not instance.allow_delete():
raise PermissionDenied("Can only be deleted if unused.")
instance.log_action(
'pretix.saleschannel.deleted',
user=self.request.user,
auth=self.request.auth,
data={'id': instance.pk}
)
instance.delete()
+8
View File
@@ -19,6 +19,8 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import django_filters
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from rest_framework import viewsets
from pretix.api.models import WebHook
@@ -26,11 +28,17 @@ from pretix.api.serializers.webhooks import WebHookSerializer
from pretix.helpers.dicts import merge_dicts
class WebhookFilter(FilterSet):
enabled = django_filters.rest_framework.BooleanFilter()
class WebHookViewSet(viewsets.ModelViewSet):
serializer_class = WebHookSerializer
queryset = WebHook.objects.none()
permission = 'can_change_organizer_settings'
write_permission = 'can_change_organizer_settings'
filter_backends = (DjangoFilterBackend,)
filterset_class = WebhookFilter
def get_queryset(self):
return self.request.organizer.webhooks.prefetch_related('listeners')
+15
View File
@@ -126,6 +126,17 @@ class ParametrizedOrderWebhookEvent(ParametrizedWebhookEvent):
}
class DeletedOrderWebhookEvent(ParametrizedWebhookEvent):
def build_payload(self, logentry: LogEntry):
return {
'notification_id': logentry.pk,
'organizer': logentry.organizer.slug,
'event': logentry.event.slug,
'code': logentry.parsed_data.get("code"),
'action': logentry.action_type,
}
class ParametrizedEventWebhookEvent(ParametrizedWebhookEvent):
def build_payload(self, logentry: LogEntry):
@@ -297,6 +308,10 @@ def register_default_webhook_events(sender, **kwargs):
'pretix.event.order.denied',
_('Order denied'),
),
DeletedOrderWebhookEvent(
'pretix.event.order.deleted',
_('Order deleted'),
),
ParametrizedOrderPositionCheckinWebhookEvent(
'pretix.event.checkin',
_('Ticket checked in'),
+85 -25
View File
@@ -20,56 +20,83 @@
# <https://www.gnu.org/licenses/>.
#
import logging
import warnings
from collections import OrderedDict
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from pretix.base.signals import register_sales_channels
from pretix.base.signals import (
register_sales_channel_types, register_sales_channels,
)
logger = logging.getLogger(__name__)
_ALL_CHANNELS = None
_ALL_CHANNEL_TYPES = None
class SalesChannel:
class SalesChannelType:
def __repr__(self):
return '<SalesChannel: {}>'.format(self.identifier)
return '<SalesChannelType: {}>'.format(self.identifier)
@property
def identifier(self) -> str:
"""
The internal identifier of this sales channel.
The internal identifier of this sales channel type.
"""
raise NotImplementedError() # NOQA
@property
def verbose_name(self) -> str:
"""
A human-readable name of this sales channel.
A human-readable name of this sales channel type.
"""
raise NotImplementedError() # NOQA
@property
def description(self) -> str:
"""
A human-readable description of this sales channel type.
"""
return ""
@property
def icon(self) -> str:
"""
The name of a Font Awesome icon to represent this channel
This can be:
- The name of a Font Awesome icon to represent this channel type.
- The name of a SVG icon file that is resolvable through the static file system. We recommend to design for a size of 18x14 pixels.
"""
return "circle"
@property
def default_created(self) -> bool:
"""
Indication, if a sales channel of this type should automatically be created for every organizer
"""
return True
@property
def multiple_allowed(self) -> bool:
"""
Indication, if multiple sales channels of this type may exist in the same organizer
"""
return False
@property
def testmode_supported(self) -> bool:
"""
Indication, if a saleschannels supports test mode orders
Indication, if a sales channel of this type supports test mode orders
"""
return True
@property
def payment_restrictions_supported(self) -> bool:
"""
If this property is ``True``, organizers can restrict the usage of payment providers to this sales channel.
If this property is ``True``, organizers can restrict the usage of payment providers to this sales channel type.
Example: pretixPOS provides its own sales channel, ignores the configured payment providers completely and
handles payments locally. Therefor, this property should be set to ``False`` for the pretixPOS sales channel as
Example: pretixPOS provides its own sales channel type, ignores the configured payment providers completely and
handles payments locally. Therefore, this property should be set to ``False`` for the pretixPOS sales channel as
the event organizer cannot restrict the usage of any payment provider through the backend.
"""
return True
@@ -77,8 +104,8 @@ class SalesChannel:
@property
def unlimited_items_per_order(self) -> bool:
"""
If this property is ``True``, purchases made using this sales channel are not limited to the maximum amount of
items defined in the event settings.
If this property is ``True``, purchases made using sales channels of this type are not limited to the maximum
amount of items defined in the event settings.
"""
return False
@@ -96,34 +123,67 @@ class SalesChannel:
"""
return True
@property
def required_event_plugin(self) -> str:
"""
Name of an event plugin that is required for this sales channel to be useful. Defaults to ``None``.
"""
return
def get_all_sales_channels():
global _ALL_CHANNELS
if _ALL_CHANNELS:
return _ALL_CHANNELS
def get_all_sales_channel_types():
from pretix.base.signals import register_sales_channel_types
global _ALL_CHANNEL_TYPES
if _ALL_CHANNEL_TYPES:
return _ALL_CHANNEL_TYPES
channels = []
for recv, ret in register_sales_channels.send(None):
for recv, ret in register_sales_channel_types.send(None):
if isinstance(ret, (list, tuple)):
channels += ret
else:
channels.append(ret)
for recv, ret in register_sales_channels.send(None): # todo: remove me
if isinstance(ret, (list, tuple)):
channels += ret
else:
channels.append(ret)
channels.sort(key=lambda c: c.identifier)
_ALL_CHANNELS = OrderedDict([(c.identifier, c) for c in channels])
if 'web' in _ALL_CHANNELS:
_ALL_CHANNELS.move_to_end('web', last=False)
return _ALL_CHANNELS
_ALL_CHANNEL_TYPES = OrderedDict([(c.identifier, c) for c in channels])
if 'web' in _ALL_CHANNEL_TYPES:
_ALL_CHANNEL_TYPES.move_to_end('web', last=False)
return _ALL_CHANNEL_TYPES
class WebshopSalesChannel(SalesChannel):
def get_all_sales_channels():
# TODO: remove me
warnings.warn('Using get_all_sales_channels() is no longer appropriate, use get_al_sales_channel_types() instead.',
DeprecationWarning, stacklevel=2)
return get_all_sales_channel_types()
class WebshopSalesChannelType(SalesChannelType):
identifier = "web"
verbose_name = _('Online shop')
icon = "globe"
@receiver(register_sales_channels, dispatch_uid="base_register_default_sales_channels")
class ApiSalesChannelType(SalesChannelType):
identifier = "api"
verbose_name = _('API')
description = _('API sales channels come with no built-in functionality, but may be used for custom integrations.')
icon = "exchange"
default_created = False
multiple_allowed = True
SalesChannel = SalesChannelType # TODO: remove me
@receiver(register_sales_channel_types, dispatch_uid="base_register_default_sales_channel_types")
def base_sales_channels(sender, **kwargs):
return (
WebshopSalesChannel(),
WebshopSalesChannelType(),
ApiSalesChannelType(),
)
+7 -4
View File
@@ -207,10 +207,13 @@ class ListExporter(BaseExporter):
def get_filename(self):
return 'export'
def get_csv_encoding(self):
return 'utf-8'
def _render_csv(self, form_data, output_file=None, **kwargs):
if output_file:
if 'b' in output_file.mode:
output_file = io.TextIOWrapper(output_file, encoding='utf-8', newline='')
output_file = io.TextIOWrapper(output_file, encoding=self.get_csv_encoding(), errors='replace', newline='')
writer = csv.writer(output_file, **kwargs)
total = 0
counter = 0
@@ -246,7 +249,7 @@ class ListExporter(BaseExporter):
if counter % max(10, total // 100) == 0:
self.progress_callback(counter / total * 100)
writer.writerow(line)
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode(self.get_csv_encoding(), errors='replace')
def prepare_xlsx_sheet(self, ws):
pass
@@ -256,7 +259,7 @@ class ListExporter(BaseExporter):
ws = wb.create_sheet()
self.prepare_xlsx_sheet(ws)
try:
ws.title = str(self.verbose_name)
ws.title = str(self.verbose_name)[:30]
except:
pass
total = 0
@@ -374,7 +377,7 @@ class MultiSheetListExporter(ListExporter):
wb = SafeWorkbook(write_only=True)
n_sheets = len(self.sheets)
for i_sheet, (s, l) in enumerate(self.sheets):
ws = wb.create_sheet(str(l))
ws = wb.create_sheet(str(l)[:30])
if hasattr(self, 'prepare_xlsx_sheet_' + s):
getattr(self, 'prepare_xlsx_sheet_' + s)(ws)
+12 -4
View File
@@ -27,7 +27,6 @@ from openpyxl.styles import Alignment
from openpyxl.utils import get_column_letter
from ...helpers.safe_openpyxl import SafeCell
from ..channels import get_all_sales_channels
from ..exporter import ListExporter
from ..models import ItemMetaValue, ItemVariation, ItemVariationMetaValue
from ..signals import register_data_exporters
@@ -53,7 +52,7 @@ class ItemDataExporter(ListExporter):
def iterate_list(self, form_data):
locales = self.event.settings.locales
scs = get_all_sales_channels()
scs = self.organizer.sales_channels.all()
header = [
_("Product ID"),
_("Variation ID"),
@@ -141,9 +140,15 @@ class ItemDataExporter(ListExporter):
row.append(i.name.localize(l))
for l in locales:
row.append(v.value.localize(l))
sales_channels = list(scs)
if not i.all_sales_channels:
sales_channels = [s for s in sales_channels if s.identifier in (p.identifier for p in i.limit_sales_channels.all())]
if not v.all_sales_channels:
sales_channels = [s for s in sales_channels if s.identifier in (p.identifier for p in v.limit_sales_channels.all())]
row += [
_("Yes") if i.active and v.active else "",
", ".join([str(sn.verbose_name) for s, sn in scs.items() if s in i.sales_channels and s in v.sales_channels]),
", ".join([str(sn.label) for sn in sales_channels]),
v.default_price or i.default_price,
_("Yes") if i.free_price else "",
str(i.tax_rule) if i.tax_rule else "",
@@ -186,9 +191,12 @@ class ItemDataExporter(ListExporter):
row.append(i.name.localize(l))
for l in locales:
row.append("")
sales_channels = list(scs)
if not i.all_sales_channels:
sales_channels = [s for s in sales_channels if s.identifier in (p.identifier for p in i.limit_sales_channels.all())]
row += [
_("Yes") if i.active else "",
", ".join([str(sn.verbose_name) for s, sn in scs.items() if s in i.sales_channels]),
", ".join([str(sn.label) for sn in sales_channels]),
i.default_price,
_("Yes") if i.free_price else "",
str(i.tax_rule) if i.tax_rule else "",
+8 -3
View File
@@ -54,6 +54,7 @@ class JSONExporter(BaseExporter):
'import in third-party systems.')
def render(self, form_data):
all_sales_channels = self.event.organizer.sales_channels.all()
jo = {
'event': {
'name': str(self.event.name),
@@ -85,7 +86,7 @@ class JSONExporter(BaseExporter):
'admission': item.admission,
'personalized': item.personalized,
'active': item.active,
'sales_channels': item.sales_channels,
'sales_channels': [c.identifier for c in (all_sales_channels if item.all_sales_channels else item.limit_sales_channels.all())],
'description': str(item.description),
'available_from': item.available_from,
'available_until': item.available_until,
@@ -114,7 +115,9 @@ class JSONExporter(BaseExporter):
'checkin_text': variation.checkin_text,
'require_approval': variation.require_approval,
'require_membership': variation.require_membership,
'sales_channels': variation.sales_channels,
'sales_channels': [
c.identifier for c in (all_sales_channels if variation.all_sales_channels else variation.limit_sales_channels.all())
],
'available_from': variation.available_from,
'available_until': variation.available_until,
'hide_without_voucher': variation.hide_without_voucher,
@@ -122,6 +125,7 @@ class JSONExporter(BaseExporter):
} for variation in item.variations.all()
]
} for item in self.event.items.select_related('tax_rule').prefetch_related(
'limit_sales_channels',
Prefetch(
'meta_values',
ItemMetaValue.objects.select_related('property'),
@@ -130,6 +134,7 @@ class JSONExporter(BaseExporter):
Prefetch(
'variations',
queryset=ItemVariation.objects.prefetch_related(
'limit_sales_channels',
Prefetch(
'meta_values',
ItemVariationMetaValue.objects.select_related('property'),
@@ -167,7 +172,7 @@ class JSONExporter(BaseExporter):
'require_approval': order.require_approval,
'checkin_attention': order.checkin_attention,
'checkin_text': order.checkin_text,
'sales_channel': order.sales_channel,
'sales_channel': order.sales_channel.identifier,
'expires': order.expires,
'datetime': order.datetime,
'fees': [
+5 -2
View File
@@ -560,7 +560,7 @@ class OrderListExporter(MultiSheetListExporter):
),
).select_related(
'order', 'order__invoice_address', 'order__customer', 'item', 'variation',
'voucher', 'tax_rule'
'voucher', 'tax_rule', 'addon_to',
).prefetch_related(
'subevent', 'subevent__meta_values',
'answers', 'answers__question', 'answers__options'
@@ -619,6 +619,7 @@ class OrderListExporter(MultiSheetListExporter):
_('Valid until'),
_('Order comment'),
_('Follow-up date'),
_('Add-on to position ID'),
]
questions = list(Question.objects.filter(event__in=self.events))
@@ -652,7 +653,8 @@ class OrderListExporter(MultiSheetListExporter):
_('VAT ID'),
]
headers += [
_('Sales channel'), _('Order locale'),
_('Sales channel'),
_('Order locale'),
_('E-mail address verified'),
_('External customer ID'),
_('Check-in lists'),
@@ -743,6 +745,7 @@ class OrderListExporter(MultiSheetListExporter):
]
row.append(order.comment)
row.append(order.custom_followup_at.strftime("%Y-%m-%d") if order.custom_followup_at else "")
row.append(op.addon_to.positionid if op.addon_to_id else "")
acache = {}
for a in op.answers.all():
# We do not want to localize Date, Time and Datetime question answers, as those can lead
+2 -1
View File
@@ -38,6 +38,7 @@ from datetime import datetime
from django import forms
from django.utils.formats import get_format
from django.utils.functional import lazy
from django.utils.html import escape
from django.utils.timezone import get_current_timezone, now
from django.utils.translation import gettext_lazy as _
@@ -64,7 +65,7 @@ def format_placeholders_help_text(placeholders, event=None):
placeholders = [(k, v.render_sample(event) if event else v) for k, v in placeholders.items()]
placeholders.sort(key=lambda x: x[0])
phs = [
'<button type="button" class="content-placeholder" title="%s">{%s}</button>' % (_("Sample: %s") % v if v else "", k)
'<button type="button" class="content-placeholder" title="%s">{%s}</button>' % (escape(_("Sample: %s") % v) if v else "", escape(k))
for k, v in placeholders
]
return _('Available placeholders: {list}').format(
+1 -1
View File
@@ -30,7 +30,7 @@ from typing import Tuple
import bleach
import vat_moss.exchange_rates
from bidi.algorithm import get_display
from bidi import get_display
from django.contrib.staticfiles import finders
from django.db.models import Sum
from django.dispatch import receiver
-2
View File
@@ -256,8 +256,6 @@ class SecurityMiddleware(MiddlewareMixin):
# pages
return resp
resp['X-XSS-Protection'] = '1'
# We just need to have a P3P, not matter whats in there
# https://blogs.msdn.microsoft.com/ieinternals/2013/09/17/a-quick-look-at-p3p/
# https://github.com/pretix/pretix/issues/765
@@ -2,7 +2,7 @@
from django.db import migrations
from pretix.base.channels import get_all_sales_channels
from pretix.base.channels import get_all_sales_channel_types
def set_sales_channels(apps, schema_editor):
@@ -11,7 +11,7 @@ def set_sales_channels(apps, schema_editor):
# Therefore, for existing events, we enable all sales channels
Event_SettingsStore = apps.get_model('pretixbase', 'Event_SettingsStore')
Event = apps.get_model('pretixbase', 'Event')
all_sales_channels = "[" + ", ".join('"' + sc + '"' for sc in get_all_sales_channels()) + "]"
all_sales_channels = "[" + ", ".join('"' + sc + '"' for sc in get_all_sales_channel_types()) + "]"
batch_size = 1000
Event_SettingsStore.objects.bulk_create([
Event_SettingsStore(
@@ -3,7 +3,7 @@
from django.db import migrations
import pretix.base.models.fields
from pretix.base.channels import get_all_sales_channels
from pretix.base.channels import get_all_sales_channel_types
class Migration(migrations.Migration):
@@ -15,6 +15,6 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='event',
name='sales_channels',
field=pretix.base.models.fields.MultiStringField(default=list(get_all_sales_channels().keys())),
field=pretix.base.models.fields.MultiStringField(default=list(get_all_sales_channel_types().keys())),
),
]
@@ -0,0 +1,110 @@
# Generated by Django 4.2.8 on 2024-03-24 17:43
import django.db.models.deletion
import i18nfield.fields
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0264_order_internal_secret"),
]
operations = [
migrations.CreateModel(
name="SalesChannel",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
("label", i18nfield.fields.I18nCharField(max_length=200)),
("identifier", models.CharField(max_length=200)),
("type", models.CharField(max_length=200)),
("position", models.PositiveIntegerField(default=0)),
("configuration", models.JSONField(default=dict)),
],
),
migrations.RenameField(
model_name="checkinlist",
old_name="auto_checkin_sales_channels",
new_name="auto_checkin_sales_channel_types",
),
migrations.AddField(
model_name="discount",
name="all_sales_channels",
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name="event",
name="all_sales_channels",
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name="item",
name="all_sales_channels",
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name="itemvariation",
name="all_sales_channels",
field=models.BooleanField(default=True),
),
migrations.RenameField(
model_name="order",
old_name="sales_channel",
new_name="sales_channel_type",
),
migrations.AddField(
model_name="saleschannel",
name="organizer",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="sales_channels",
to="pretixbase.organizer",
),
),
migrations.AddField(
model_name="discount",
name="limit_sales_channels",
field=models.ManyToManyField(to="pretixbase.saleschannel"),
),
migrations.AddField(
model_name="event",
name="limit_sales_channels",
field=models.ManyToManyField(to="pretixbase.saleschannel"),
),
migrations.AddField(
model_name="item",
name="limit_sales_channels",
field=models.ManyToManyField(to="pretixbase.saleschannel"),
),
migrations.AddField(
model_name="itemvariation",
name="limit_sales_channels",
field=models.ManyToManyField(to="pretixbase.saleschannel"),
),
migrations.AddField(
model_name="checkinlist",
name="auto_checkin_sales_channels",
field=models.ManyToManyField(to="pretixbase.saleschannel"),
),
migrations.AddField(
model_name="order",
name="sales_channel",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="pretixbase.saleschannel",
),
),
migrations.AlterUniqueTogether(
name="saleschannel",
unique_together={("organizer", "identifier")},
),
]
@@ -0,0 +1,84 @@
# Generated by Django 4.2.8 on 2024-03-24 17:55
from django.db import migrations
from i18nfield.strings import LazyI18nString
from pretix.base.channels import get_all_sales_channel_types
def create_sales_channels(apps, schema_editor):
channel_types = get_all_sales_channel_types()
type_to_channel = dict()
full_discount_set = set()
full_set = set()
Organizer = apps.get_model("pretixbase", "Organizer")
for o in Organizer.objects.all().iterator():
for i, t in enumerate(channel_types.values()):
if not t.default_created:
continue
type_to_channel[t.identifier, o.pk] = o.sales_channels.get_or_create(
type=t.identifier,
defaults=dict(
position=i,
identifier=t.identifier,
label=LazyI18nString.from_gettext(t.verbose_name),
),
)[0]
full_set.add(t.identifier)
if t.discounts_supported:
full_discount_set.add(t.identifier)
Event = apps.get_model("pretixbase", "Event")
for d in Event.objects.all().iterator():
if set(d.sales_channels) != full_set:
d.all_sales_channels = False
d.save()
for s in d.sales_channels:
d.limit_sales_channels.add(type_to_channel[s, d.organizer_id])
Item = apps.get_model("pretixbase", "Item")
for d in Item.objects.select_related("event").iterator():
if set(d.sales_channels) != full_set:
d.all_sales_channels = False
d.save()
for s in d.sales_channels:
d.limit_sales_channels.add(type_to_channel[s, d.event.organizer_id])
ItemVariation = apps.get_model("pretixbase", "ItemVariation")
for d in ItemVariation.objects.select_related("item__event").iterator():
if set(d.sales_channels) != full_set:
d.all_sales_channels = False
d.save()
for s in d.sales_channels:
d.limit_sales_channels.add(type_to_channel[s, d.item.event.organizer_id])
Discount = apps.get_model("pretixbase", "Discount")
for d in Discount.objects.select_related("event").iterator():
if set(d.sales_channels) != full_discount_set:
d.all_sales_channels = False
d.save()
for s in d.sales_channels:
d.limit_sales_channels.add(type_to_channel[s, d.event.organizer_id])
CheckinList = apps.get_model("pretixbase", "CheckinList")
for c in CheckinList.objects.select_related("event").iterator():
for s in c.auto_checkin_sales_channel_types:
c.auto_checkin_sales_channels.add(type_to_channel[s, c.event.organizer_id])
Order = apps.get_model("pretixbase", "Order")
for (k, orgid), v in type_to_channel.items():
Order.objects.filter(sales_channel_type=k, event__organizer_id=orgid, sales_channel__isnull=True).update(
sales_channel=v
)
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0265_saleschannel_and_more"),
]
operations = [
migrations.RunPython(create_sales_channels, migrations.RunPython.noop),
]
@@ -0,0 +1,46 @@
# Generated by Django 4.2.8 on 2024-03-25 13:34
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0266_saleschannel_migrate_data"),
]
operations = [
migrations.RemoveField(
model_name="checkinlist",
name="auto_checkin_sales_channel_types",
),
migrations.RemoveField(
model_name="discount",
name="sales_channels",
),
migrations.RemoveField(
model_name="event",
name="sales_channels",
),
migrations.RemoveField(
model_name="item",
name="sales_channels",
),
migrations.RemoveField(
model_name="itemvariation",
name="sales_channels",
),
migrations.RemoveField(
model_name="order",
name="sales_channel_type",
),
migrations.AlterField(
model_name="order",
name="sales_channel",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="pretixbase.saleschannel",
),
),
]
@@ -0,0 +1,32 @@
# Generated by Django 4.2.8 on 2024-07-01 09:26
from django.db import migrations, models
import pretix.base.models.orders
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0267_remove_old_sales_channels"),
]
operations = [
migrations.RemoveField(
model_name="subevent",
name="items",
),
migrations.RemoveField(
model_name="subevent",
name="variations",
),
migrations.AlterField(
model_name="order",
name="internal_secret",
field=models.CharField(
default=pretix.base.models.orders.generate_secret,
max_length=32,
null=True,
),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 4.2.13 on 2024-07-17 14:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0268_remove_subevent_items_remove_subevent_variations_and_more'),
]
operations = [
migrations.AddField(
model_name='order',
name='api_meta',
field=models.JSONField(default=dict),
),
]
+15 -6
View File
@@ -38,7 +38,6 @@ from i18nfield.strings import LazyI18nString
from phonenumber_field.phonenumber import to_python
from phonenumbers import SUPPORTED_REGIONS
from pretix.base.channels import get_all_sales_channels
from pretix.base.forms.questions import guess_country
from pretix.base.modelimport import (
DatetimeColumnMixin, DecimalColumnMixin, ImportColumn, SubeventColumnMixin,
@@ -538,18 +537,28 @@ class Expires(DatetimeColumnMixin, ImportColumn):
class Saleschannel(ImportColumn):
identifier = 'sales_channel'
verbose_name = gettext_lazy('Sales channel')
default_value = None
initial = 'static:web'
@cached_property
def channels(self):
return list(self.event.organizer.sales_channels.all())
def static_choices(self):
return [
(sc.identifier, sc.verbose_name) for sc in get_all_sales_channels().values()
(c.identifier, str(c.label)) for c in self.channels
]
def clean(self, value, previous_values):
if not value:
value = 'web'
if value not in get_all_sales_channels():
matches = [
p for p in self.channels
if p.identifier == value or any((v and v == value) for v in i18n_flat(p.label))
]
if len(matches) == 0:
raise ValidationError(_("Please enter a valid sales channel."))
return value
if len(matches) > 1:
raise ValidationError(_("Please enter a valid sales channel."))
return matches[0]
def assign(self, value, order, position, invoice_address, **kwargs):
order.sales_channel = value
+4 -4
View File
@@ -28,9 +28,9 @@ from django.utils.translation import gettext as _, gettext_lazy, pgettext_lazy
from pretix.base.modelimport import (
BooleanColumnMixin, DatetimeColumnMixin, DecimalColumnMixin, ImportColumn,
IntegerColumnMixin, i18n_flat,
IntegerColumnMixin, SubeventColumnMixin, i18n_flat,
)
from pretix.base.models import ItemVariation, Quota, Seat, Voucher
from pretix.base.models import ItemVariation, Quota, Seat, SubEvent, Voucher
from pretix.base.signals import voucher_import_columns
@@ -55,11 +55,11 @@ class CodeColumn(ImportColumn):
obj.code = value
class SubeventColumn(ImportColumn):
class SubeventColumn(SubeventColumnMixin, ImportColumn):
identifier = 'subevent'
verbose_name = pgettext_lazy('subevents', 'Date')
def assign(self, value, obj: Voucher, **kwargs):
def assign(self, value, obj: SubEvent, **kwargs):
obj.subevent = value
+2 -1
View File
@@ -51,7 +51,8 @@ from .orders import (
generate_secret,
)
from .organizer import (
Organizer, Organizer_SettingsStore, Team, TeamAPIToken, TeamInvite,
Organizer, Organizer_SettingsStore, SalesChannel, Team, TeamAPIToken,
TeamInvite,
)
from .seating import Seat, SeatCategoryMapping, SeatingPlan
from .tax import TaxRule
+6 -7
View File
@@ -46,7 +46,6 @@ from django_scopes import ScopedManager, scopes_disabled
from pretix.base.media import MEDIA_TYPES
from pretix.base.models import LoggedModel
from pretix.base.models.fields import MultiStringField
from pretix.helpers import PostgresWindowFrame
@@ -100,13 +99,13 @@ class CheckinList(LoggedModel):
verbose_name=_('Automatically check out everyone at'),
null=True, blank=True
)
auto_checkin_sales_channels = MultiStringField(
default=[],
blank=True,
auto_checkin_sales_channels = models.ManyToManyField(
"SalesChannel",
verbose_name=_('Sales channels to automatically check in'),
help_text=_('All items on this check-in list will be automatically marked as checked-in when purchased through '
'any of the selected sales channels. This option can be useful when tickets sold at the box office '
'are not checked again before entry and should be considered validated directly upon purchase.')
help_text=_('This option is deprecated and will be removed in the next months. As a replacement, our new plugin '
'"Auto check-in" can be used. When we remove this option, we will automatically migrate your event '
'to use the new plugin.'),
blank=True,
)
rules = models.JSONField(default=dict, blank=True)
+8 -5
View File
@@ -34,7 +34,6 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager
from pretix.base.decimal import round_decimal
from pretix.base.models import fields
from pretix.base.models.base import LoggedModel
@@ -65,10 +64,14 @@ class Discount(LoggedModel):
default=0,
verbose_name=_("Position")
)
sales_channels = fields.MultiStringField(
verbose_name=_('Sales channels'),
default=['web'],
blank=False,
all_sales_channels = models.BooleanField(
verbose_name=_("All supported sales channels"),
default=True,
)
limit_sales_channels = models.ManyToManyField(
"SalesChannel",
verbose_name=_("Sales channels"),
blank=True,
)
available_from = models.DateTimeField(
+79 -19
View File
@@ -36,6 +36,7 @@ import logging
import os
import string
import uuid
import warnings
from collections import Counter, OrderedDict, defaultdict
from datetime import datetime, time, timedelta
from operator import attrgetter
@@ -66,7 +67,6 @@ from django_scopes import ScopedManager, scopes_disabled
from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.models.base import LoggedModel
from pretix.base.models.fields import MultiStringField
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.timemachine import time_machine_now
from pretix.base.validators import EventSlugBanlistValidator
@@ -304,8 +304,12 @@ class EventMixin:
return safe_string(json.dumps(eventdict))
@classmethod
def annotated(cls, qs, channel='web', voucher=None):
from pretix.base.models import Item, ItemVariation, Quota
def annotated(cls, qs, channel, voucher=None):
# Channel can currently be a SalesChannel or a str, since we need that compatibility, but a SalesChannel
# makes the query SIGNIFICANTLY faster
from pretix.base.models import Item, ItemVariation, Quota, SalesChannel
assert isinstance(channel, (SalesChannel, str))
sq_active_item = Item.objects.using(settings.DATABASE_REPLICA).filter_available(channel=channel, voucher=voucher).filter(
Q(variations__isnull=True)
@@ -316,18 +320,23 @@ class EventMixin:
q_variation = (
Q(active=True)
& Q(sales_channels__contains=channel)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()))
& Q(item__active=True)
& Q(Q(item__available_from__isnull=True) | Q(item__available_from__lte=time_machine_now()))
& Q(Q(item__available_until__isnull=True) | Q(item__available_until__gte=time_machine_now()))
& Q(Q(item__category__isnull=True) | Q(item__category__is_addon=False))
& Q(item__sales_channels__contains=channel)
& Q(item__require_bundling=False)
& Q(quotas__pk=OuterRef('pk'))
)
if isinstance(channel, str):
q_variation &= Q(Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=channel))
q_variation &= Q(Q(item__all_sales_channels=True) | Q(item__limit_sales_channels__identifier=channel))
else:
q_variation &= Q(Q(all_sales_channels=True) | Q(limit_sales_channels=channel))
q_variation &= Q(Q(item__all_sales_channels=True) | Q(item__limit_sales_channels=channel))
if voucher:
if voucher.variation_id:
q_variation &= Q(pk=voucher.variation_id)
@@ -467,6 +476,7 @@ class EventMixin:
return best_state_found, num_tickets_found, num_tickets_possible
def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
assert isinstance(sales_channel, str) or sales_channel is None
qs_annotated = self._seats(ignore_voucher=ignore_voucher)
qs = qs_annotated.filter(has_order=False, has_cart=False, has_voucher=False)
@@ -495,10 +505,13 @@ class EventMixin:
return qs.filter(q)
def default_sales_channels():
from ..channels import get_all_sales_channels
def default_sales_channels(): # kept for legacy migration
from ..channels import get_all_sales_channel_types
return list(get_all_sales_channels().keys())
if "PYTEST_CURRENT_TEST" not in os.environ:
warnings.warn('Method should not be used in new code.', DeprecationWarning)
return list(get_all_sales_channel_types().keys())
@settings_hierarkey.add(parent_field='organizer', cache_namespace='event')
@@ -535,8 +548,10 @@ class Event(EventMixin, LoggedModel):
:type plugins: str
:param has_subevents: Enable event series functionality
:type has_subevents: bool
:param sales_channels: A list of sales channel identifiers, that this event is available for sale on
:type sales_channels: list
:param all_sales_channels: A flag indicating that this event is available on all channels and limit_sales_channels will be ignored.
:type all_sales_channels: bool
:param limit_sales_channels: A list of sales channel identifiers, that this event is available for sale on
:type limit_sales_channels: list
"""
settings_namespace = 'event'
@@ -628,10 +643,14 @@ class Event(EventMixin, LoggedModel):
auto_now=True, db_index=True
)
sales_channels = MultiStringField(
verbose_name=_('Restrict to specific sales channels'),
help_text=_('Only sell tickets for this event on the following sales channels.'),
default=default_sales_channels,
all_sales_channels = models.BooleanField(
verbose_name=_("Sell on all sales channels"),
default=True,
)
limit_sales_channels = models.ManyToManyField(
"SalesChannel",
verbose_name=_("Restrict to specific sales channels"),
blank=True,
)
objects = ScopedManager(organizer='organizer')
@@ -805,10 +824,17 @@ class Event(EventMixin, LoggedModel):
if other.date_admission:
self.date_admission = self.date_from + (other.date_admission - other.date_from)
self.testmode = other.testmode
self.sales_channels = other.sales_channels
self.all_sales_channels = other.all_sales_channels
self.save()
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
if not self.all_sales_channels:
self.limit_sales_channels.set(
self.organizer.sales_channels.filter(
identifier__in=other.limit_sales_channels.values_list("identifier", flat=True)
)
)
if not skip_meta_data:
for emv in EventMetaValue.objects.filter(event=other):
emv.pk = None
@@ -846,12 +872,17 @@ class Event(EventMixin, LoggedModel):
item_map = {}
variation_map = {}
for i in Item.objects.filter(event=other).prefetch_related('variations'):
for i in Item.objects.filter(event=other).prefetch_related(
'variations', 'limit_sales_channels', 'require_membership_types',
'variations__limit_sales_channels', 'variations__require_membership_types',
):
vars = list(i.variations.all())
require_membership_types = list(i.require_membership_types.all())
limit_sales_channels = list(i.limit_sales_channels.all())
item_map[i.pk] = i
i.pk = None
i.event = self
i._prefetched_objects_cache = {}
if i.picture:
i.picture.save(os.path.basename(i.picture.name), i.picture)
if i.category_id:
@@ -868,12 +899,23 @@ class Event(EventMixin, LoggedModel):
if require_membership_types and other.organizer_id == self.organizer_id:
i.require_membership_types.set(require_membership_types)
if not i.all_sales_channels:
i.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels]))
for v in vars:
require_membership_types = list(v.require_membership_types.all())
limit_sales_channels = list(v.limit_sales_channels.all())
variation_map[v.pk] = v
v.pk = None
v.item = i
v._prefetched_objects_cache = {}
v.save(force_insert=True)
if require_membership_types and other.organizer_id == self.organizer_id:
v.require_membership_types.set(require_membership_types)
if not v.all_sales_channels:
v.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels]))
for i in self.items.filter(hidden_if_item_available__isnull=False):
i.hidden_if_item_available = item_map[i.hidden_if_item_available_id]
i.save()
@@ -911,6 +953,7 @@ class Event(EventMixin, LoggedModel):
vars = list(q.variations.all())
oldid = q.pk
q.pk = None
q._prefetched_objects_cache = {}
q.event = self
q.closed = False
q.save(force_insert=True)
@@ -922,11 +965,15 @@ class Event(EventMixin, LoggedModel):
q.variations.add(variation_map[v.pk])
self.items.filter(hidden_if_available_id=oldid).update(hidden_if_available=q)
for d in Discount.objects.filter(event=other).prefetch_related('condition_limit_products'):
for d in Discount.objects.filter(event=other).prefetch_related(
'condition_limit_products', 'benefit_limit_products', 'limit_sales_channels'
):
c_items = list(d.condition_limit_products.all())
b_items = list(d.benefit_limit_products.all())
limit_sales_channels = list(d.limit_sales_channels.all())
d.pk = None
d.event = self
d._prefetched_objects_cache = {}
d.save(force_insert=True)
d.log_action('pretix.object.cloned')
for i in c_items:
@@ -936,12 +983,16 @@ class Event(EventMixin, LoggedModel):
if i.pk in item_map:
d.benefit_limit_products.add(item_map[i.pk])
if not d.all_sales_channels:
d.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels]))
question_map = {}
for q in Question.objects.filter(event=other).prefetch_related('items', 'options'):
items = list(q.items.all())
opts = list(q.options.all())
question_map[q.pk] = q
q.pk = None
q._prefetched_objects_cache = {}
q.event = self
q.save(force_insert=True)
q.log_action('pretix.object.cloned')
@@ -972,10 +1023,14 @@ class Event(EventMixin, LoggedModel):
_walk_rules(i)
checkin_list_map = {}
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related('limit_products'):
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related(
'limit_products', 'auto_checkin_sales_channels'
):
items = list(cl.limit_products.all())
auto_checkin_sales_channels = list(cl.auto_checkin_sales_channels.all())
checkin_list_map[cl.pk] = cl
cl.pk = None
cl._prefetched_objects_cache = {}
cl.event = self
rules = cl.rules
_walk_rules(rules)
@@ -984,6 +1039,8 @@ class Event(EventMixin, LoggedModel):
cl.log_action('pretix.object.cloned')
for i in items:
cl.limit_products.add(item_map[i.pk])
if auto_checkin_sales_channels:
cl.auto_checkin_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in auto_checkin_sales_channels]))
if other.seating_plan:
if other.seating_plan.organizer_id == self.organizer_id:
@@ -1487,8 +1544,11 @@ class SubEvent(EventMixin, LoggedModel):
return qs_annotated
@classmethod
def annotated(cls, qs, channel='web', voucher=None):
def annotated(cls, qs, channel, voucher=None):
from .items import SubEventItem, SubEventItemVariation
from .organizer import SalesChannel
assert isinstance(channel, (str, SalesChannel))
qs = super().annotated(qs, channel, voucher=voucher)
qs = qs.annotate(
+43 -13
View File
@@ -34,8 +34,10 @@
# License for the specific language governing permissions and limitations under the License.
import calendar
import os
import sys
import uuid
import warnings
from collections import Counter, OrderedDict
from datetime import date, datetime, time, timedelta
from decimal import Decimal, DecimalException
@@ -61,7 +63,6 @@ from django_countries.fields import Country
from django_scopes import ScopedManager
from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.models import fields
from pretix.base.models.base import LoggedModel
from pretix.base.models.fields import MultiStringField
from pretix.base.models.tax import TaxedPrice
@@ -270,14 +271,24 @@ class SubEventItemVariation(models.Model):
def filter_available(qs, channel='web', voucher=None, allow_addons=False):
# Channel can currently be a SalesChannel or a str, since we need that compatibility, but a SalesChannel
# makes the query SIGNIFICANTLY faster
from .organizer import SalesChannel
assert isinstance(channel, (SalesChannel, str))
q = (
# IMPORTANT: If this is updated, also update the ItemVariation query
# in models/event.py: EventMixin.annotated()
Q(active=True)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()) | Q(available_from_mode='info'))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()) | Q(available_until_mode='info'))
& Q(sales_channels__contains=channel) & Q(require_bundling=False)
& Q(require_bundling=False)
)
if isinstance(channel, str):
q &= Q(Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=channel))
else:
q &= Q(Q(all_sales_channels=True) | Q(limit_sales_channels=channel))
if not allow_addons:
q &= Q(Q(category__isnull=True) | Q(category__is_addon=False))
@@ -353,8 +364,10 @@ class Item(LoggedModel):
:type original_price: decimal.Decimal
:param require_approval: If set to ``True``, orders containing this product can only be processed and paid after approved by an administrator
:type require_approval: bool
:param sales_channels: Sales channels this item is available on.
:type sales_channels: bool
:param all_sales_channels: A flag indicating that this item is available on all channels and limit_sales_channels will be ignored.
:type all_sales_channels: bool
:param limit_sales_channels: A list of sales channel identifiers, that this item is available for sale on.
:type limit_sales_channels: list
:param issue_giftcard: If ``True``, buying this product will give you a gift card with the value of the product's price
:type issue_giftcard: bool
:param validity_mode: Instruction how to set ``valid_from``/``valid_until`` on tickets, ``null`` is default event validity.
@@ -609,9 +622,14 @@ class Item(LoggedModel):
help_text=_('If set, this will be displayed next to the current price to show that the current price is a '
'discounted one. This is just a cosmetic setting and will not actually impact pricing.')
)
sales_channels = fields.MultiStringField(
verbose_name=_('Sales channels'),
default=['web'],
all_sales_channels = models.BooleanField(
verbose_name=_("Sell on all sales channels"),
default=True,
)
limit_sales_channels = models.ManyToManyField(
"SalesChannel",
verbose_name=_("Restrict to specific sales channels"),
help_text=_('Only sell tickets for this product on the selected sales channels.'),
blank=True,
)
issue_giftcard = models.BooleanField(
@@ -1033,9 +1051,13 @@ class Item(LoggedModel):
return None, None
def _all_sales_channels_identifiers():
from pretix.base.channels import get_all_sales_channels
return list(get_all_sales_channels().keys())
def _all_sales_channels_identifiers(): # kept for legacy migrations
from pretix.base.channels import get_all_sales_channel_types
if "PYTEST_CURRENT_TEST" not in os.environ:
warnings.warn('Method should not be used in new code.', DeprecationWarning)
return list(get_all_sales_channel_types().keys())
class ItemVariation(models.Model):
@@ -1058,6 +1080,10 @@ class ItemVariation(models.Model):
:param require_approval: If set to ``True``, orders containing this variation can only be processed and paid after
approval by an administrator
:type require_approval: bool
:param all_sales_channels: A flag indicating that this variation is available on all channels and limit_sales_channels will be ignored.
:type all_sales_channels: bool
:param limit_sales_channels: A list of sales channel identifiers, that this variation is available for sale on.
:type limit_sales_channels: list
"""
item = models.ForeignKey(
@@ -1143,9 +1169,13 @@ class ItemVariation(models.Model):
default=Item.UNAVAIL_MODE_HIDDEN,
max_length=16,
)
sales_channels = fields.MultiStringField(
verbose_name=_('Sales channels'),
default=_all_sales_channels_identifiers,
all_sales_channels = models.BooleanField(
verbose_name=_("Sell on all sales channels the product is sold on"),
default=True,
)
limit_sales_channels = models.ManyToManyField(
"SalesChannel",
verbose_name=_("Restrict to specific sales channels"),
help_text=_('The sales channel selection for the product as a whole takes precedence, so if a sales channel is '
'selected here but not on product level, the variation will not be available.'),
blank=True,
+12 -4
View File
@@ -187,8 +187,8 @@ class Order(LockModel, LoggedModel):
:type require_approval: bool
:param meta_info: Additional meta information on the order, JSON-encoded.
:type meta_info: str
:param sales_channel: Identifier of the sales channel this order was created through.
:type sales_channel: str
:param sales_channel: Foreign key to the sales channel this order was created through.
:type sales_channel: SalesChannel
"""
STATUS_PENDING = "n"
@@ -299,13 +299,21 @@ class Order(LockModel, LoggedModel):
verbose_name=_("Meta information"),
null=True, blank=True
)
api_meta = models.JSONField(
verbose_name=_("API meta information"),
null=False, blank=True,
default=dict
)
last_modified = models.DateTimeField(
auto_now=True, db_index=False
)
require_approval = models.BooleanField(
default=False
)
sales_channel = models.CharField(max_length=190, default="web")
sales_channel = models.ForeignKey(
"SalesChannel",
on_delete=models.PROTECT,
)
email_known_to_work = models.BooleanField(
default=False,
verbose_name=_('E-mail address verified')
@@ -1932,7 +1940,7 @@ class OrderPayment(models.Model):
trigger_pdf=not send_mail or not self.order.event.settings.invoice_email_attachment
)
if send_mail and self.order.sales_channel in self.order.event.settings.mail_sales_channel_placed_paid:
if send_mail and self.order.sales_channel.identifier in self.order.event.settings.mail_sales_channel_placed_paid:
self._send_paid_mail(invoice, user, mail_text)
if self.order.event.settings.mail_send_order_paid_attendee:
for p in self.order.positions.all():
+77
View File
@@ -46,7 +46,9 @@ from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.timezone import get_current_timezone, make_aware, now
from django.utils.translation import gettext_lazy as _
from django_scopes import ScopedManager, scope
from i18nfield.fields import I18nCharField
from i18nfield.strings import LazyI18nString
from pretix.base.models.base import LoggedModel
from pretix.base.validators import OrganizerSlugBanlistValidator
@@ -104,6 +106,8 @@ class Organizer(LoggedModel):
if is_new:
kwargs.pop('update_fields', None) # does not make sense here
self.set_defaults()
with scope(organizer=self):
self.create_default_sales_channels()
else:
self.get_cache().clear()
return obj
@@ -212,6 +216,24 @@ class Organizer(LoggedModel):
else:
return get_connection(fail_silently=False)
def create_default_sales_channels(self):
from pretix.base.channels import get_all_sales_channel_types
i = 0
for channel in get_all_sales_channel_types().values():
if not channel.default_created:
continue
self.sales_channels.get_or_create(
identifier=channel.identifier,
defaults={
'label': LazyI18nString.from_gettext(channel.verbose_name),
'type': channel.identifier,
},
position=i
)
i += 1
def generate_invite_token():
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)
@@ -504,3 +526,58 @@ class OrganizerFooterLink(models.Model):
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
self.organizer.cache.clear()
class SalesChannel(LoggedModel):
organizer = models.ForeignKey('Organizer', on_delete=models.CASCADE, related_name='sales_channels')
label = I18nCharField(
max_length=200,
verbose_name=_("Name"),
)
identifier = models.CharField(
verbose_name=_("Identifier"),
max_length=200,
validators=[
RegexValidator(
regex=r"^[a-zA-Z0-9.\-_]+$",
message=_("The identifier may only contain letters, numbers, dots, dashes, and underscores."),
),
],
)
type = models.CharField(
verbose_name=_("Type"),
max_length=200,
)
position = models.PositiveIntegerField(
default=0,
verbose_name=_("Position")
)
configuration = models.JSONField(default=dict)
objects = ScopedManager(organizer="organizer")
class Meta:
ordering = ("position", "type", "identifier", "id")
unique_together = ("organizer", "identifier")
def __str__(self):
return str(self.label)
@cached_property
def type_instance(self):
from ..channels import get_all_sales_channel_types
types = get_all_sales_channel_types()
return types[self.type]
@property
def icon(self):
return self.type_instance.icon
def allow_delete(self):
from . import Order
if self.type_instance.default_created:
return False
return not Order.objects.filter(sales_channel=self).exists()
+28 -13
View File
@@ -185,7 +185,7 @@ class Seat(models.Model):
@classmethod
def annotated(cls, qs, event_id, subevent, ignore_voucher_id=None, minimal_distance=0,
ignore_order_id=None, ignore_cart_id=None, distance_only_within_row=False):
ignore_order_id=None, ignore_cart_id=None, distance_only_within_row=False, annotate_ids=False):
from . import CartPosition, Order, OrderPosition, Voucher
vqs = Voucher.objects.filter(
@@ -214,17 +214,24 @@ class Seat(models.Model):
)
if ignore_cart_id:
cqs = cqs.exclude(cart_id=ignore_cart_id)
qs_annotated = qs.annotate(
has_order=Exists(
opqs
),
has_cart=Exists(
cqs
),
has_voucher=Exists(
vqs
if annotate_ids:
qs_annotated = qs.annotate(
orderposition_id=Subquery(opqs.values('id')),
cartposition_id=Subquery(cqs.values('id')),
voucher_id=Subquery(vqs.values('id')),
)
else:
qs_annotated = qs.annotate(
has_order=Exists(
opqs
),
has_cart=Exists(
cqs
),
has_voucher=Exists(
vqs
)
)
)
if minimal_distance > 0:
# TODO: Is there a more performant implementation on PostgreSQL using
@@ -235,7 +242,11 @@ class Seat(models.Model):
Power(F('y') - OuterRef('y'), Value(2), output_field=models.FloatField())
)
).filter(
Q(has_order=True) | Q(has_cart=True) | Q(has_voucher=True),
(
(Q(orderposition_id__isnull=False) | Q(cartposition_id__isnull=False) | Q(voucher_id__isnull=False))
if annotate_ids else
(Q(has_order=True) | Q(has_cart=True) | Q(has_voucher=True))
),
distance__lt=minimal_distance ** 2
)
if distance_only_within_row:
@@ -243,10 +254,14 @@ class Seat(models.Model):
qs_annotated = qs_annotated.annotate(has_closeby_taken=Exists(sq_closeby))
return qs_annotated
def is_available(self, ignore_cart=None, ignore_orderpos=None, ignore_voucher_id=None, sales_channel='web',
def is_available(self, ignore_cart=None, ignore_orderpos=None, ignore_voucher_id=None,
sales_channel='web',
ignore_distancing=False, distance_ignore_cart_id=None):
from .orders import Order
from .organizer import SalesChannel
if isinstance(sales_channel, SalesChannel):
sales_channel = sales_channel.identifier
if self.blocked and sales_channel not in self.event.settings.seating_allow_blocked_seats_for_channel:
return False
opqs = self.orderposition_set.filter(
+4 -5
View File
@@ -56,7 +56,6 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
from i18nfield.strings import LazyI18nString
from pretix.base.channels import get_all_sales_channels
from pretix.base.forms import I18nMarkdownTextarea, PlaceholderValidator
from pretix.base.models import (
CartPosition, Event, GiftCard, InvoiceAddress, Order, OrderPayment,
@@ -417,8 +416,8 @@ class BasePaymentProvider:
forms.MultipleChoiceField(
label=_('Restrict to specific sales channels'),
choices=(
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
if c.payment_restrictions_supported
(c.identifier, c.label) for c in self.event.organizer.sales_channels.all()
if c.type_instance.payment_restrictions_supported
),
initial=['web'],
widget=forms.CheckboxSelectMultiple,
@@ -588,7 +587,7 @@ class BasePaymentProvider:
return rel_date.datetime(self.event).date()
def _is_available_by_time(self, now_dt=None, cart_id=None, order=None):
now_dt = now_dt or now()
now_dt = now_dt or time_machine_now()
tz = ZoneInfo(self.event.settings.timezone)
try:
@@ -853,7 +852,7 @@ class BasePaymentProvider:
if str(ia.country) != '' and str(ia.country) not in restricted_countries:
return False
if order.sales_channel not in self.settings.get('_restrict_to_sales_channels', as_type=list, default=['web']):
if order.sales_channel.identifier not in self.settings.get('_restrict_to_sales_channels', as_type=list, default=['web']):
return False
return self._is_available_by_time(order=order)
+48 -9
View File
@@ -49,7 +49,7 @@ from io import BytesIO
import jsonschema
import reportlab.rl_config
from bidi.algorithm import get_display
from bidi import get_display
from django.conf import settings
from django.contrib.staticfiles import finders
from django.core.exceptions import ValidationError
@@ -956,7 +956,7 @@ class Renderer:
)
canvas.restoreState()
def _draw_textarea(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
def _text_paragraph(self, op: OrderPosition, order: Order, o: dict, legacy_lineheight=False, override_fontsize=None):
font = o['fontfamily']
# Since pdfmetrics.registerFont is global, we want to make sure that no one tries to sneak in a font, they
@@ -970,12 +970,13 @@ class Renderer:
if o['italic']:
font += ' I'
fontsize = override_fontsize if override_fontsize is not None else float(o['fontsize'])
try:
ad = getAscentDescent(font, float(o['fontsize']))
ad = getAscentDescent(font, fontsize)
except KeyError: # font not known, fall back
logger.warning(f'Use of unknown font "{font}"')
font = 'Open Sans'
ad = getAscentDescent(font, float(o['fontsize']))
ad = getAscentDescent(font, fontsize)
align_map = {
'left': TA_LEFT,
@@ -985,16 +986,17 @@ class Renderer:
# lineheight display differs from browser canvas. This calc is just empirical values to get
# reportlab render similarly to browser canvas.
# for backwards compatability use „uncorrected“ lineheight of 1.0 instead of 1.15
lineheight = float(o['lineheight']) * 1.15 if 'lineheight' in o else 1.0
lineheight = float(o['lineheight']) * 1.15 if not legacy_lineheight or 'lineheight' in o else 1.0
style = ParagraphStyle(
name=uuid.uuid4().hex,
fontName=font,
fontSize=float(o['fontsize']),
leading=lineheight * float(o['fontsize']),
fontSize=fontsize,
leading=lineheight * fontsize,
# for backwards compatability use autoLeading if no lineheight is given
autoLeading='off' if 'lineheight' in o else 'max',
autoLeading='off' if not legacy_lineheight or 'lineheight' in o else 'max',
textColor=Color(o['color'][0] / 255, o['color'][1] / 255, o['color'][2] / 255),
alignment=align_map[o['align']]
alignment=align_map[o['align']],
splitLongWords=o.get('splitlongwords', True),
)
# add an almost-invisible space &hairsp; after hyphens as word-wrap in ReportLab only works on space chars
text = conditional_escape(
@@ -1013,6 +1015,41 @@ class Renderer:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
p = Paragraph(text, style=style)
return p, ad, lineheight
def _draw_textcontainer(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
fontsize = float(o['fontsize'])
height = float(o['height']) * mm
width = float(o['width']) * mm
while True:
p, ad, lineheight = self._text_paragraph(op, order, o, override_fontsize=fontsize)
w, h = p.wrapOn(canvas, width, 1000 * mm)
widths = p.getActualLineWidths0()
if not widths:
break
actual_w = max(widths)
if not o.get('autoresize', False) or (h <= height and actual_w <= width) or fontsize <= 1.0:
break
if h > height: # we can do larger steps for height
fontsize -= max(1.0, fontsize * .1)
else:
fontsize -= max(.25, fontsize * .025)
canvas.saveState()
# The ascent/descent offsets here are not really proven to be correct, they're just empirical values to get
# reportlab render similarly to browser canvas.
canvas.translate(float(o['left']) * mm, float(o['bottom']) * mm + height)
canvas.rotate(o.get('rotation', 0) * -1)
if o.get('verticalalign', 'top') == 'top':
p.drawOn(canvas, 0, - h)
elif o.get('verticalalign', 'top') == 'middle':
p.drawOn(canvas, 0, (-height - h) / 2)
elif o.get('verticalalign', 'top') == 'bottom':
p.drawOn(canvas, 0, -height)
canvas.restoreState()
def _draw_textarea(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
p, ad, lineheight = self._text_paragraph(op, order, o, legacy_lineheight=True)
w, h = p.wrapOn(canvas, float(o['width']) * mm, 1000 * mm)
# p_size = p.wrap(float(o['width']) * mm, 1000 * mm)
canvas.saveState()
@@ -1051,6 +1088,8 @@ class Renderer:
self._draw_barcodearea(canvas, op, order, o)
elif o['type'] == "imagearea":
self._draw_imagearea(canvas, op, order, o)
elif o['type'] == "textcontainer":
self._draw_textcontainer(canvas, op, order, o)
elif o['type'] == "textarea":
self._draw_textarea(canvas, op, order, o)
elif o['type'] == "poweredby":
+49 -11
View File
@@ -52,12 +52,11 @@ from django.utils.translation import (
)
from django_scopes import scopes_disabled
from pretix.base.channels import get_all_sales_channels
from pretix.base.i18n import language
from pretix.base.media import MEDIA_TYPES
from pretix.base.models import (
CartPosition, Event, InvoiceAddress, Item, ItemVariation, Seat,
SeatCategoryMapping, Voucher,
CartPosition, Event, InvoiceAddress, Item, ItemVariation, SalesChannel,
Seat, SeatCategoryMapping, Voucher,
)
from pretix.base.models.event import SubEvent
from pretix.base.models.orders import OrderFee
@@ -275,8 +274,8 @@ class CartManager:
AddOperation: 30
}
def __init__(self, event: Event, cart_id: str, invoice_address: InvoiceAddress=None, widget_data=None,
sales_channel='web'):
def __init__(self, event: Event, cart_id: str, sales_channel: SalesChannel,
invoice_address: InvoiceAddress=None, widget_data=None, expiry=None):
self.event = event
self.cart_id = cart_id
self.real_now_dt = now()
@@ -288,6 +287,7 @@ class CartManager:
self._variations_cache = {}
self._seated_cache = {}
self._expiry = None
self._explicit_expiry = expiry
self.invoice_address = invoice_address
self._widget_data = widget_data or {}
self._sales_channel = sales_channel
@@ -306,7 +306,12 @@ class CartManager:
return self._seated_cache[item, subevent]
def _calculate_expiry(self):
self._expiry = self.real_now_dt + timedelta(minutes=self.event.settings.get('reservation_time', as_type=int))
if self._explicit_expiry:
self._expiry = self._explicit_expiry
else:
self._expiry = self.real_now_dt + timedelta(
minutes=self.event.settings.get('reservation_time', as_type=int)
)
def _check_presale_dates(self):
if self.event.presale_start and time_machine_now(self.real_now_dt) < self.event.presale_start:
@@ -384,7 +389,7 @@ class CartManager:
})
def _check_max_cart_size(self):
if not get_all_sales_channels()[self._sales_channel].unlimited_items_per_order:
if not self._sales_channel.type_instance.unlimited_items_per_order:
cartsize = self.positions.filter(addon_to__isnull=True).count()
cartsize += sum([op.count for op in self._operations if isinstance(op, self.AddOperation) and not op.addon_to])
cartsize -= len([1 for op in self._operations if isinstance(op, self.RemoveOperation) if
@@ -422,8 +427,13 @@ class CartManager:
elif op.item.media_policy == Item.MEDIA_POLICY_REUSE:
raise CartError(error_messages['media_usage_not_implemented'])
if self._sales_channel not in op.item.sales_channels or (op.variation and self._sales_channel not in op.variation.sales_channels):
raise CartError(error_messages['unavailable'])
if not op.item.all_sales_channels:
if self._sales_channel.identifier not in (s.identifier for s in op.item.limit_sales_channels.all()):
raise CartError(error_messages['unavailable'])
if op.variation and not op.variation.all_sales_channels:
if self._sales_channel.identifier not in (s.identifier for s in op.variation.limit_sales_channels.all()):
raise CartError(error_messages['unavailable'])
if op.subevent and op.item.pk in op.subevent.item_overrides and not op.subevent.item_overrides[op.item.pk].is_available():
raise CartError(error_messages['not_for_sale'])
@@ -457,7 +467,14 @@ class CartManager:
raise CartError(error_messages['ended'])
seated = self._is_seated(op.item, op.subevent)
if seated and (not op.seat or (op.seat.blocked and self._sales_channel not in self.event.settings.seating_allow_blocked_seats_for_channel)):
if (
seated and (
not op.seat or (
op.seat.blocked and
self._sales_channel.identifier not in self.event.settings.seating_allow_blocked_seats_for_channel
)
)
):
raise CartError(error_messages['seat_invalid'])
elif op.seat and not seated:
raise CartError(error_messages['seat_forbidden'])
@@ -1371,7 +1388,7 @@ class CartManager:
discount_results = apply_discounts(
self.event,
self._sales_channel,
self._sales_channel.identifier,
[
(cp.item_id, cp.subevent_id, cp.line_price_gross, bool(cp.addon_to), cp.is_bundled, cp.listed_price - cp.price_after_voucher)
for cp in positions
@@ -1505,6 +1522,11 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
except InvoiceAddress.DoesNotExist:
pass
try:
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
except SalesChannel.DoesNotExist:
raise CartError("Invalid sales channel.")
try:
try:
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia, widget_data=widget_data,
@@ -1526,6 +1548,10 @@ def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='e
:param session: Session ID of a guest
"""
with language(locale), time_machine_now_assigned(override_now_dt):
try:
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
except SalesChannel.DoesNotExist:
raise CartError("Invalid sales channel.")
try:
try:
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
@@ -1546,6 +1572,10 @@ def remove_cart_position(self, event: Event, position: int, cart_id: str=None, l
:param session: Session ID of a guest
"""
with language(locale), time_machine_now_assigned(override_now_dt):
try:
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
except SalesChannel.DoesNotExist:
raise CartError("Invalid sales channel.")
try:
try:
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
@@ -1565,6 +1595,10 @@ def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel
:param session: Session ID of a guest
"""
with language(locale), time_machine_now_assigned(override_now_dt):
try:
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
except SalesChannel.DoesNotExist:
raise CartError("Invalid sales channel.")
try:
try:
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
@@ -1593,6 +1627,10 @@ def set_cart_addons(self, event: Event, addons: List[dict], cart_id: str=None, l
ia = InvoiceAddress.objects.get(pk=invoice_address)
except InvoiceAddress.DoesNotExist:
pass
try:
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
except SalesChannel.DoesNotExist:
raise CartError("Invalid sales channel.")
try:
try:
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia, sales_channel=sales_channel)
+3 -3
View File
@@ -1154,12 +1154,12 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
)
@receiver(order_placed, dispatch_uid="autocheckin_order_placed")
@receiver(order_placed, dispatch_uid="legacy_autocheckin_order_placed")
def order_placed(sender, **kwargs):
order = kwargs['order']
event = sender
cls = list(event.checkin_lists.filter(auto_checkin_sales_channels__contains=order.sales_channel).prefetch_related(
cls = list(event.checkin_lists.filter(auto_checkin_sales_channels=order.sales_channel).prefetch_related(
'limit_products'))
if not cls:
return
@@ -1171,7 +1171,7 @@ def order_placed(sender, **kwargs):
checkin_created.send(event, checkin=ci)
@receiver(periodic_task, dispatch_uid="autocheckin_exit_all")
@receiver(periodic_task, dispatch_uid="autocheckout_exit_all")
@scopes_disabled()
def process_exit_all(sender, **kwargs):
qs = CheckinList.objects.filter(
+6 -3
View File
@@ -420,7 +420,7 @@ def invoice_pdf_task(invoice: int):
def invoice_qualified(order: Order):
if order.total == Decimal('0.00') or order.require_approval or \
order.sales_channel not in order.event.settings.get('invoice_generate_sales_channels'):
order.sales_channel.identifier not in order.event.settings.get('invoice_generate_sales_channels'):
return False
return True
@@ -443,8 +443,11 @@ def build_preview_invoice_pdf(event):
locale = event.settings.locale
with rolledback_transaction(), language(locale, event.settings.region):
order = event.orders.create(status=Order.STATUS_PENDING, datetime=timezone.now(),
expires=timezone.now(), code="PREVIEW", total=100 * event.tax_rules.count())
order = event.orders.create(
status=Order.STATUS_PENDING, datetime=timezone.now(),
expires=timezone.now(), code="PREVIEW", total=100 * event.tax_rules.count(),
sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
invoice = Invoice(
order=order, event=event, invoice_no="PREVIEW",
date=timezone.now().date(), locale=locale, organizer=event.organizer
+50 -27
View File
@@ -62,7 +62,6 @@ from django.utils.translation import gettext as _, gettext_lazy, ngettext_lazy
from django_scopes import scopes_disabled
from pretix.api.models import OAuthApplication
from pretix.base.channels import get_all_sales_channels
from pretix.base.email import get_email_context
from pretix.base.i18n import get_language_without_region, language
from pretix.base.media import MEDIA_TYPES
@@ -76,7 +75,7 @@ from pretix.base.models.orders import (
BlockedTicketSecret, InvoiceAddress, OrderFee, OrderRefund,
generate_secret,
)
from pretix.base.models.organizer import TeamAPIToken
from pretix.base.models.organizer import SalesChannel, TeamAPIToken
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
from pretix.base.payment import GiftCardPayment, PaymentException
from pretix.base.reldate import RelativeDateWrapper
@@ -650,8 +649,7 @@ def _check_date(event: Event, now_dt: datetime):
def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: datetime, positions: List[CartPosition],
address: InvoiceAddress = None,
sales_channel='web', customer=None):
sales_channel: SalesChannel, address: InvoiceAddress=None, customer=None):
err = None
_check_date(event, time_machine_now_dt)
@@ -775,7 +773,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
if cp.seat:
# Unlike quotas (which we blindly trust as long as the position is not expired), we check seats every
# time, since we absolutely can not overbook a seat.
if not cp.seat.is_available(ignore_cart=cp, ignore_voucher_id=cp.voucher_id, sales_channel=sales_channel):
if not cp.seat.is_available(ignore_cart=cp, ignore_voucher_id=cp.voucher_id, sales_channel=sales_channel.identifier):
err = err or error_messages['seat_unavailable']
delete(cp)
continue
@@ -873,7 +871,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
sorted_positions = [cp for cp in sorted_positions if cp.pk and cp.pk not in deleted_positions]
discount_results = apply_discounts(
event,
sales_channel,
sales_channel.identifier,
[
(cp.item_id, cp.subevent_id, cp.line_price_gross, bool(cp.addon_to), cp.is_bundled, cp.listed_price - cp.price_after_voucher)
for cp in sorted_positions
@@ -959,12 +957,11 @@ def _get_fees(positions: List[CartPosition], payment_requests: List[dict], addre
return fees
def _create_order(event: Event, email: str, positions: List[CartPosition], now_dt: datetime,
payment_requests: List[dict], locale: str=None, address: InvoiceAddress=None,
meta_info: dict=None, sales_channel: str='web', shown_total=None,
customer=None, valid_if_pending=False):
def _create_order(event: Event, *, email: str, positions: List[CartPosition], now_dt: datetime,
payment_requests: List[dict], sales_channel: SalesChannel, locale: str=None,
address: InvoiceAddress=None, meta_info: dict=None, shown_total=None,
customer=None, valid_if_pending=False, api_meta: dict=None):
payments = []
sales_channel = get_all_sales_channels()[sales_channel]
try:
validate_memberships_in_order(customer, positions, event, lock=True, testmode=event.testmode)
@@ -986,10 +983,11 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
datetime=now_dt,
locale=get_language_without_region(locale),
total=total,
testmode=True if sales_channel.testmode_supported and event.testmode else False,
testmode=True if sales_channel.type_instance.testmode_supported and event.testmode else False,
meta_info=json.dumps(meta_info or {}),
api_meta=api_meta or {},
require_approval=require_approval,
sales_channel=sales_channel.identifier,
sales_channel=sales_channel,
customer=customer,
valid_if_pending=valid_if_pending,
)
@@ -1099,7 +1097,7 @@ def _order_placed_email_attendee(event: Event, order: Order, position: OrderPosi
def _perform_order(event: Event, payment_requests: List[dict], position_ids: List[str],
email: str, locale: str, address: int, meta_info: dict=None, sales_channel: str='web',
shown_total=None, customer=None):
shown_total=None, customer=None, api_meta: dict=None):
for p in payment_requests:
p['pprov'] = event.get_payment_providers(cached=True)[p['provider']]
if not p['pprov']:
@@ -1108,6 +1106,11 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
if customer:
customer = event.organizer.customers.get(pk=customer)
try:
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
except SalesChannel.DoesNotExist:
raise OrderError("Invalid sales channel.")
if email == settings.PRETIX_EMAIL_NONE_VALUE:
email = None
@@ -1186,9 +1189,21 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
if 'sleep-after-quota-check' in debugflags_var.get():
sleep(2)
order, payment_objs = _create_order(event, email, positions, real_now_dt, payment_requests,
locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel,
shown_total=shown_total, customer=customer, valid_if_pending=valid_if_pending)
order, payment_objs = _create_order(
event,
email=email,
positions=positions,
now_dt=real_now_dt,
payment_requests=payment_requests,
locale=locale,
address=addr,
meta_info=meta_info,
sales_channel=sales_channel,
shown_total=shown_total,
customer=customer,
valid_if_pending=valid_if_pending,
api_meta=api_meta,
)
try:
for p in payment_objs:
@@ -1272,7 +1287,7 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
email_attendees_template = event.settings.mail_text_order_placed_attendee
subject_attendees_template = event.settings.mail_subject_order_placed_attendee
if sales_channel in event.settings.mail_sales_channel_placed_paid:
if sales_channel.identifier in event.settings.mail_sales_channel_placed_paid:
_order_placed_email(event, order, email_template, subject_template, log_entry, invoice, payment_objs,
is_free=free_order_flow)
if email_attendees:
@@ -1424,7 +1439,7 @@ def send_download_reminders(sender, **kwargs):
if days is None:
continue
if o.sales_channel not in event.settings.mail_sales_channel_download_reminder:
if o.sales_channel.identifier not in event.settings.mail_sales_channel_download_reminder:
continue
reminder_date = (o.first_date - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
@@ -1926,9 +1941,13 @@ class OrderChangeManager:
if not item.is_available() or (variation and not variation.is_available()):
raise OrderError(error_messages['unavailable'])
if self.order.sales_channel not in item.sales_channels or (
variation and self.order.sales_channel not in variation.sales_channels):
raise OrderError(error_messages['unavailable'])
if not item.all_sales_channels:
if self.order.sales_channel.identifier not in (s.identifier for s in item.limit_sales_channels.all()):
raise OrderError(error_messages['unavailable'])
if variation and not variation.all_sales_channels:
if self.order.sales_channel.identifier not in (s.identifier for s in variation.limit_sales_channels.all()):
raise OrderError(error_messages['unavailable'])
if subevent and item.pk in subevent.item_overrides and not subevent.item_overrides[item.pk].is_available():
raise OrderError(error_messages['not_for_sale'])
@@ -2025,9 +2044,12 @@ class OrderChangeManager:
# This also prevents accidental removal through the UI because a hidden product will no longer
# be part of the input.
(a.variation and a.variation.unavailability_reason(has_voucher=True, subevent=a.subevent))
or (a.variation and self.order.sales_channel not in a.variation.sales_channels)
or (a.variation and not a.variation.all_sales_channels and not a.variation.limit_sales_channels.contains(self.order.sales_channel))
or a.item.unavailability_reason(has_voucher=True, subevent=a.subevent)
or self.order.sales_channel not in item.sales_channels
or (
not item.all_sales_channels and
not item.limit_sales_channels.contains(self.order.sales_channel)
)
)
if is_unavailable:
continue
@@ -2853,12 +2875,13 @@ class OrderChangeManager:
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
def perform_order(self, event: Event, payments: List[dict], positions: List[str],
email: str=None, locale: str=None, address: int=None, meta_info: dict=None,
sales_channel: str='web', shown_total=None, customer=None, override_now_dt: datetime=None):
sales_channel: str='web', shown_total=None, customer=None, override_now_dt: datetime=None,
api_meta: dict=None):
with language(locale), time_machine_now_assigned(override_now_dt):
try:
try:
return _perform_order(event, payments, positions, email, locale, address, meta_info,
sales_channel, shown_total, customer)
sales_channel, shown_total, customer, api_meta)
except LockTimeoutException:
self.retry()
except (MaxRetriesExceededError, LockTimeoutException):
@@ -3129,7 +3152,7 @@ def signal_listener_issue_memberships(sender: Event, order: Order, **kwargs):
if order.status != Order.STATUS_PAID or not order.customer:
return
for p in order.positions.all():
if p.item.grant_membership_type_id:
if p.item.grant_membership_type_id and not p.granted_memberships.exists():
create_membership(order.customer, p)
+5 -2
View File
@@ -28,7 +28,8 @@ from django.db.models import Q
from pretix.base.decimal import round_decimal
from pretix.base.models import (
AbstractPosition, InvoiceAddress, Item, ItemAddOn, ItemVariation, Voucher,
AbstractPosition, InvoiceAddress, Item, ItemAddOn, ItemVariation,
SalesChannel, Voucher,
)
from pretix.base.models.event import Event, SubEvent
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
@@ -164,12 +165,14 @@ def apply_discounts(event: Event, sales_channel: str,
:param positions: Tuple of the form ``(item_id, subevent_id, line_price_gross, is_addon_to, is_bundled, voucher_discount)``
:return: A list of ``(new_gross_price, discount)`` tuples in the same order as the input
"""
if isinstance(sales_channel, SalesChannel):
sales_channel = sales_channel.identifier
new_prices = {}
discount_qs = event.discounts.filter(
Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()),
Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()),
sales_channels__contains=sales_channel,
Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=sales_channel),
active=True,
).prefetch_related('condition_limit_products', 'benefit_limit_products').order_by('position', 'pk')
for discount in discount_qs:
+1
View File
@@ -105,6 +105,7 @@ def preview(event: int, provider: str):
order = event.orders.create(status=Order.STATUS_PENDING, datetime=now(),
email='sample@pretix.eu',
locale=event.settings.locale,
sales_channel=event.organizer.sales_channels.get(identifier="web"),
expires=now(), code="PREVIEW1234", total=119)
scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
+17 -1
View File
@@ -1478,6 +1478,10 @@ DEFAULTS = {
min_value=1,
required=True,
widget=forms.NumberInput(),
help_text=_('With an increased limit, a customer may request more than one ticket for a specific product '
'using the same, unique email address. However, regardless of this setting, they will need to '
'fill the waiting list form multiple times if they want more than one ticket, as every entry only '
'grants one single ticket at a time.'),
)
},
'show_checkin_number_user': {
@@ -3359,7 +3363,9 @@ Your {organizer} team""")) # noqa: W291
},
'seating_allow_blocked_seats_for_channel': {
'default': [],
'type': list
'type': list,
'serializer_class': serializers.ListField,
'serializer_kwargs': lambda: dict(child=serializers.CharField()),
},
'seating_distance_within_row': {
'default': 'False',
@@ -3797,6 +3803,16 @@ def validate_event_settings(event, settings_dict):
'payment_term_last': _('The last payment date cannot be before the end of presale.')
})
if settings_dict.get('seating_allow_blocked_seats_for_channel'):
allowed_channels = set(event.organizer.sales_channels.values_list("identifier", flat=True))
for channel in settings_dict['seating_allow_blocked_seats_for_channel']:
if channel not in allowed_channels:
raise ValidationError({
'seating_allow_blocked_seats_for_channel': _('The value "{identifier}" is not a valid sales channel.').format(
identifier=channel
)
})
if isinstance(event, Event):
validate_event_settings.send(sender=event, settings_dict=settings_dict)
+5 -2
View File
@@ -294,13 +294,16 @@ This signal is sent out when a notification is sent.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
register_sales_channels = django.dispatch.Signal()
register_sales_channel_types = django.dispatch.Signal()
"""
This signal is sent out to get all known sales channels types. Receivers should return an
instance of a subclass of ``pretix.base.channels.SalesChannel`` or a list of such
instance of a subclass of ``pretix.base.channels.SalesChannelType`` or a list of such
instances.
"""
register_sales_channels = DeprecatedSignal() # TODO: remove me
register_data_exporters = EventPluginSignal()
"""
This signal is sent out to get all known data exporters. Receivers should return a
@@ -0,0 +1,19 @@
{% load rich_text %}
{% load static %}
{% load i18n %}
{% if widget.wrap_label %}<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %}>{% endif %}
{% include "django/forms/widgets/input.html" %}
{% if widget.wrap_label %}
{% if "." in widget.value.instance.type_instance.icon %}
<img class="fa-like-image" src="{% static widget.value.instance.type_instance.icon %}" alt="">
{% else %}
<span class="fa fa-fw fa-{{ widget.value.instance.type_instance.icon }} text-muted"></span>
{% endif %}
{% if widget.plugin_missing %}
<del>
{% endif %}
{{ widget.label }}{% if widget.plugin_missing %}</del>
<span class="fa fa-info-circle" data-toggle="tooltip" title="{% trans "This sales channel cannot be used properly since the respective plugin is not active for this event." %}"></span>
{% endif %}
</label>
{% endif %}
+17
View File
@@ -438,3 +438,20 @@ class ButtonGroupRadioSelect(forms.RadioSelect):
attrs['icon'] = self.option_icons[value]
opt = super().create_option(name, value, label, selected, index, subindex, attrs)
return opt
class SalesChannelCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
option_template_name = 'pretixbase/forms/widgets/checkbox_sales_channel_option.html'
def __init__(self, event, attrs=None, choices=()):
self.event = event
super().__init__(attrs, choices)
def create_option(
self, name, value, label, selected, index, subindex=None, attrs=None
):
plugin = value.instance.type_instance.required_event_plugin
return {
**super().create_option(name, value, label, selected, index, subindex, attrs),
"plugin_missing": plugin and plugin not in self.event.get_plugins(),
}
+7 -11
View File
@@ -30,11 +30,12 @@ from django_scopes.forms import (
SafeModelChoiceField, SafeModelMultipleChoiceField,
)
from pretix.base.channels import get_all_sales_channels
from pretix.base.forms.widgets import SplitDateTimePickerWidget
from pretix.base.models import Gate
from pretix.base.models.checkin import Checkin, CheckinList
from pretix.control.forms import ItemMultipleChoiceField
from pretix.control.forms import (
ItemMultipleChoiceField, SalesChannelCheckboxSelectMultiple,
)
from pretix.control.forms.widgets import Select2
@@ -66,14 +67,9 @@ class CheckinListForm(forms.ModelForm):
kwargs.pop('locales', None)
super().__init__(**kwargs)
self.fields['limit_products'].queryset = self.event.items.all()
self.fields['auto_checkin_sales_channels'] = forms.MultipleChoiceField(
label=self.fields['auto_checkin_sales_channels'].label,
help_text=self.fields['auto_checkin_sales_channels'].help_text,
required=self.fields['auto_checkin_sales_channels'].required,
choices=(
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
),
widget=forms.CheckboxSelectMultiple
self.fields['auto_checkin_sales_channels'].queryset = self.event.organizer.sales_channels.all()
self.fields['auto_checkin_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(
self.event, choices=self.fields['auto_checkin_sales_channels'].widget.choices
)
if not self.event.organizer.gates.exists():
@@ -123,13 +119,13 @@ class CheckinListForm(forms.ModelForm):
'gates': forms.CheckboxSelectMultiple(attrs={
'class': 'scrolling-multiple-choice'
}),
'auto_checkin_sales_channels': forms.CheckboxSelectMultiple(),
'exit_all_at': NextTimeInput(attrs={'class': 'timepickerfield'}),
}
field_classes = {
'limit_products': ItemMultipleChoiceField,
'gates': SafeModelMultipleChoiceField,
'subevent': SafeModelChoiceField,
'auto_checkin_sales_channels': SafeModelMultipleChoiceField,
'exit_all_at': NextTimeField,
}
+14 -12
View File
@@ -22,13 +22,16 @@
from decimal import Decimal
from django import forms
from django.utils.translation import gettext_lazy as _
from django_scopes.forms import SafeModelMultipleChoiceField
from pretix.base.channels import get_all_sales_channels
from pretix.base.channels import get_all_sales_channel_types
from pretix.base.forms import I18nModelForm
from pretix.base.forms.widgets import SplitDateTimePickerWidget
from pretix.base.models import Discount
from pretix.control.forms import ItemMultipleChoiceField, SplitDateTimeField
from pretix.control.forms import (
ItemMultipleChoiceField, SalesChannelCheckboxSelectMultiple,
SplitDateTimeField,
)
class DiscountForm(I18nModelForm):
@@ -38,7 +41,8 @@ class DiscountForm(I18nModelForm):
fields = [
'active',
'internal_name',
'sales_channels',
'all_sales_channels',
'limit_sales_channels',
'available_from',
'available_until',
'subevent_mode',
@@ -60,6 +64,7 @@ class DiscountForm(I18nModelForm):
'available_until': SplitDateTimeField,
'condition_limit_products': ItemMultipleChoiceField,
'benefit_limit_products': ItemMultipleChoiceField,
'limit_sales_channels': SafeModelMultipleChoiceField,
}
widgets = {
'subevent_mode': forms.RadioSelect,
@@ -83,15 +88,12 @@ class DiscountForm(I18nModelForm):
self.event = kwargs['event']
super().__init__(*args, **kwargs)
self.fields['sales_channels'] = forms.MultipleChoiceField(
label=_('Sales channels'),
required=True,
choices=(
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
if c.discounts_supported
),
widget=forms.CheckboxSelectMultiple,
self.fields['limit_sales_channels'].queryset = self.event.organizer.sales_channels.filter(
type__in=[k for k, v in get_all_sales_channel_types().items() if v.discounts_supported]
)
self.fields['limit_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(self.event, attrs={
'data-inverse-dependency': '<[name$=all_sales_channels]',
}, choices=self.fields['limit_sales_channels'].widget.choices)
self.fields['condition_limit_products'].queryset = self.event.items.all()
self.fields['benefit_limit_products'].queryset = self.event.items.all()
self.fields['condition_min_count'].required = False
+20 -21
View File
@@ -44,9 +44,7 @@ from django.conf import settings
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
from django.core.validators import MaxValueValidator
from django.db.models import Prefetch, Q, prefetch_related_objects
from django.forms import (
CheckboxSelectMultiple, formset_factory, inlineformset_factory,
)
from django.forms import formset_factory, inlineformset_factory
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.html import escape, format_html
@@ -54,12 +52,12 @@ from django.utils.safestring import mark_safe
from django.utils.timezone import get_current_timezone_name
from django.utils.translation import gettext, gettext_lazy as _, pgettext_lazy
from django_countries.fields import LazyTypedChoiceField
from django_scopes.forms import SafeModelMultipleChoiceField
from i18nfield.forms import (
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextInput,
)
from pytz import common_timezones
from pretix.base.channels import get_all_sales_channels
from pretix.base.forms import (
I18nMarkdownTextarea, I18nModelForm, PlaceholderValidator, SettingsForm,
)
@@ -73,8 +71,8 @@ from pretix.base.settings import (
)
from pretix.base.validators import multimail_validate
from pretix.control.forms import (
MultipleLanguagesWidget, SlugWidget, SplitDateTimeField,
SplitDateTimePickerWidget,
MultipleLanguagesWidget, SalesChannelCheckboxSelectMultiple, SlugWidget,
SplitDateTimeField, SplitDateTimePickerWidget,
)
from pretix.control.forms.widgets import Select2
from pretix.helpers.countries import CachedCountries
@@ -378,16 +376,10 @@ class EventUpdateForm(I18nModelForm):
required=False,
help_text=_('You need to configure the custom domain in the webserver beforehand.')
)
self.fields['sales_channels'] = forms.MultipleChoiceField(
label=self.fields['sales_channels'].label,
help_text=self.fields['sales_channels'].help_text,
required=self.fields['sales_channels'].required,
initial=self.fields['sales_channels'].initial,
choices=(
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
),
widget=forms.CheckboxSelectMultiple
)
self.fields['limit_sales_channels'].queryset = self.event.organizer.sales_channels.all()
self.fields['limit_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(self.event, attrs={
'data-inverse-dependency': '<[name$=all_sales_channels]',
}, choices=self.fields['limit_sales_channels'].widget.choices)
def clean_domain(self):
d = self.cleaned_data['domain']
@@ -444,7 +436,8 @@ class EventUpdateForm(I18nModelForm):
'location',
'geo_lat',
'geo_lon',
'sales_channels'
'all_sales_channels',
'limit_sales_channels',
]
field_classes = {
'date_from': SplitDateTimeField,
@@ -452,6 +445,7 @@ class EventUpdateForm(I18nModelForm):
'date_admission': SplitDateTimeField,
'presale_start': SplitDateTimeField,
'presale_end': SplitDateTimeField,
'limit_sales_channels': SafeModelMultipleChoiceField,
}
widgets = {
'date_from': SplitDateTimePickerWidget(),
@@ -459,7 +453,6 @@ class EventUpdateForm(I18nModelForm):
'date_admission': SplitDateTimePickerWidget(attrs={'data-date-default': '#id_date_from_0'}),
'presale_start': SplitDateTimePickerWidget(),
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}),
'sales_channels': CheckboxSelectMultiple(),
}
@@ -915,7 +908,7 @@ class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm):
locale_names = dict(settings.LANGUAGES)
self.fields['invoice_language'].choices = [('__user__', _('The user\'s language'))] + [(a, locale_names[a]) for a in event.settings.locales]
self.fields['invoice_generate_sales_channels'].choices = (
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
(c.identifier, c.label) for c in event.organizer.sales_channels.all()
)
self.fields['invoice_numbers_counter_length'].validators.append(MaxValueValidator(15))
@@ -961,7 +954,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
]
mail_sales_channel_placed_paid = forms.MultipleChoiceField(
choices=lambda: [(ident, sc.verbose_name) for ident, sc in get_all_sales_channels().items()],
choices=[],
label=_('Sales channels for checkout emails'),
help_text=_('The order placed and paid emails will only be send to orders from these sales channels. '
'The online shop must be enabled.'),
@@ -972,7 +965,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
)
mail_sales_channel_download_reminder = forms.MultipleChoiceField(
choices=lambda: [(ident, sc.verbose_name) for ident, sc in get_all_sales_channels().items()],
choices=[],
label=_('Sales channels'),
help_text=_('This email will only be send to orders from these sales channels. The online shop must be enabled.'),
widget=forms.CheckboxSelectMultiple(
@@ -1367,6 +1360,12 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
self.fields['mail_html_renderer'].choices = [
(r.identifier, r.verbose_name) for r in event.get_html_mail_renderers().values()
]
self.fields['mail_sales_channel_placed_paid'].choices = (
(c.identifier, c.label) for c in event.organizer.sales_channels.all()
)
self.fields['mail_sales_channel_download_reminder'].choices = (
(c.identifier, c.label) for c in event.organizer.sales_channels.all()
)
prefetch_related_objects([self.event.organizer], Prefetch('meta_properties'))
self.event.meta_values_cached = self.event.meta_values.select_related('property').all()
+96 -19
View File
@@ -48,19 +48,20 @@ from django.utils.formats import date_format, localize
from django.utils.functional import cached_property
from django.utils.timezone import get_current_timezone, make_aware, now
from django.utils.translation import gettext, gettext_lazy as _, pgettext_lazy
from django_countries.fields import CountryField
from django_scopes.forms import SafeModelChoiceField
from pretix.base.channels import get_all_sales_channels
from pretix.base.forms.widgets import (
DatePickerWidget, SplitDateTimePickerWidget, TimePickerWidget,
)
from pretix.base.models import (
Checkin, CheckinList, Device, Event, EventMetaProperty, EventMetaValue,
Gate, Invoice, InvoiceAddress, Item, Order, OrderPayment, OrderPosition,
OrderRefund, Organizer, Question, QuestionAnswer, Quota, SubEvent,
SubEventMetaValue, Team, TeamAPIToken, TeamInvite, Voucher,
OrderRefund, Organizer, Question, QuestionAnswer, Quota, SalesChannel,
SubEvent, SubEventMetaValue, Team, TeamAPIToken, TeamInvite, Voucher,
)
from pretix.base.signals import register_payment_providers
from pretix.control.forms import SplitDateTimeField
from pretix.control.forms.widgets import Select2, Select2ItemVarQuota
from pretix.control.signals import order_search_filter_q
from pretix.helpers.countries import CachedCountries
@@ -68,7 +69,7 @@ from pretix.helpers.database import (
get_deterministic_ordering, rolledback_transaction,
)
from pretix.helpers.dicts import move_to_end
from pretix.helpers.i18n import i18ncomp
from pretix.helpers.i18n import get_format_without_seconds, i18ncomp
PAYMENT_PROVIDERS = []
@@ -579,9 +580,11 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
required=False,
label=_('Maximal sum of payments and refunds'),
)
sales_channel = forms.ChoiceField(
sales_channel = SafeModelChoiceField(
label=_('Sales channel'),
required=False,
queryset=SalesChannel.objects.none(),
to_field_name="identifier",
)
has_checkin = forms.NullBooleanField(
required=False,
@@ -604,9 +607,7 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
del self.fields['subevents_from']
del self.fields['subevents_to']
self.fields['sales_channel'].choices = [('', '')] + [
(k, v.verbose_name) for k, v in get_all_sales_channels().items()
]
self.fields['sales_channel'].queryset = self.event.organizer.sales_channels.all()
locale_names = dict(settings.LANGUAGES)
self.fields['locale'].choices = [('', '')] + [(a, locale_names[a]) for a in self.event.settings.locales]
@@ -688,11 +689,71 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
)
self.fields['quota'].widget.choices = self.fields['quota'].choices
for q in self.event.questions.all():
self.fields['question_{}'.format(q.pk)] = forms.CharField(
label=q.question,
required=False,
help_text=_('Exact matches only')
)
kwargs = {
"label": q.question,
"required": False,
}
fname = 'question_{}'.format(q.pk)
if q.type == Question.TYPE_NUMBER:
self.fields[fname] = forms.DecimalField(
help_text=_('Exact matches only'),
**kwargs,
)
elif q.type == Question.TYPE_BOOLEAN:
self.fields[fname] = forms.ChoiceField(
choices=(
("", ""),
("True", _("Yes")),
("False", _("No")),
),
**kwargs,
)
elif q.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
self.fields[fname] = forms.ModelChoiceField(
queryset=q.options,
widget=forms.Select,
to_field_name='identifier',
empty_label='',
**kwargs,
)
elif q.type == Question.TYPE_COUNTRYCODE:
self.fields[fname] = CountryField(
countries=CachedCountries,
blank=True, null=True, blank_label=' ',
).formfield(
**kwargs,
widget=forms.Select,
empty_label=' ',
)
elif q.type == Question.TYPE_DATE:
self.fields[fname] = forms.DateField(
widget=DatePickerWidget(),
help_text=_('Exact matches only'),
**kwargs,
)
elif q.type == Question.TYPE_TIME:
self.fields[fname] = forms.TimeField(
widget=TimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
help_text=_('Exact matches only'),
**kwargs,
)
elif q.type == Question.TYPE_DATETIME:
self.fields[fname] = SplitDateTimeField(
widget=SplitDateTimePickerWidget(
time_format=get_format_without_seconds('TIME_INPUT_FORMATS'),
min_date=q.valid_datetime_min,
max_date=q.valid_datetime_max
),
help_text=_('Exact matches only'),
**kwargs,
)
elif q.type == Question.TYPE_FILE:
continue
else:
self.fields[fname] = forms.CharField(
help_text=_('Exact matches only'),
**kwargs,
)
def filter_qs(self, qs):
fdata = self.cleaned_data
@@ -719,7 +780,7 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
if fdata.get('comment'):
qs = qs.filter(comment__icontains=fdata.get('comment'))
if fdata.get('sales_channel'):
qs = qs.filter(sales_channel=fdata.get('sales_channel'))
qs = qs.filter(sales_channel__identifier=fdata.get('sales_channel').identifier)
if fdata.get('total'):
qs = qs.filter(total=fdata.get('total'))
if fdata.get('email_known_to_work') is not None:
@@ -788,11 +849,24 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
).distinct()
for q in self.event.questions.all():
if fdata.get(f'question_{q.pk}'):
answers = QuestionAnswer.objects.filter(
question_id=q.pk,
orderposition__order_id=OuterRef('pk'),
answer__iexact=fdata.get(f'question_{q.pk}')
)
if q.type == Question.TYPE_BOOLEAN:
answers = QuestionAnswer.objects.filter(
question_id=q.pk,
orderposition__order_id=OuterRef('pk'),
answer__exact=fdata.get(f'question_{q.pk}')
)
elif q.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
answers = QuestionAnswer.objects.filter(
question_id=q.pk,
orderposition__order_id=OuterRef('pk'),
options=fdata.get(f'question_{q.pk}')
)
else:
answers = QuestionAnswer.objects.filter(
question_id=q.pk,
orderposition__order_id=OuterRef('pk'),
answer__iexact=fdata.get(f'question_{q.pk}')
)
qs = qs.annotate(**{f'q_{q.pk}': Exists(answers)}).filter(**{f'q_{q.pk}': True})
return qs
@@ -2578,6 +2652,9 @@ class DeviceFilterForm(FilterForm):
if fdata.get('gate'):
qs = qs.filter(gate=fdata['gate'])
if fdata.get('software_brand'):
qs = qs.filter(software_brand=fdata['software_brand'])
if fdata.get('state') == 'active':
qs = qs.filter(revoked=False)
elif fdata.get('state') == 'revoked':
+27 -38
View File
@@ -40,7 +40,6 @@ from urllib.parse import urlencode
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import UploadedFile
from django.db.models import Max
from django.forms.formsets import DELETION_FIELD_NAME
from django.urls import reverse
@@ -55,7 +54,6 @@ from django_scopes.forms import (
)
from i18nfield.forms import I18nFormField, I18nTextarea
from pretix.base.channels import get_all_sales_channels
from pretix.base.forms import I18nFormSet, I18nMarkdownTextarea, I18nModelForm
from pretix.base.forms.widgets import DatePickerWidget
from pretix.base.models import (
@@ -64,8 +62,9 @@ from pretix.base.models import (
from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
from pretix.base.signals import item_copy_data
from pretix.control.forms import (
ButtonGroupRadioSelect, ItemMultipleChoiceField, SizeValidationMixin,
SplitDateTimeField, SplitDateTimePickerWidget,
ButtonGroupRadioSelect, ExtFileField, ItemMultipleChoiceField,
SalesChannelCheckboxSelectMultiple, SplitDateTimeField,
SplitDateTimePickerWidget,
)
from pretix.control.forms.widgets import Select2, Select2ItemVarMulti
from pretix.helpers.models import modelcopy
@@ -413,7 +412,7 @@ class ItemCreateForm(I18nModelForm):
'checkin_text',
'free_price',
'original_price',
'sales_channels',
'all_sales_channels',
'issue_giftcard',
'require_approval',
'allow_waitinglist',
@@ -443,9 +442,6 @@ class ItemCreateForm(I18nModelForm):
if src.picture:
self.instance.picture.save(os.path.basename(src.picture.name), src.picture)
else:
# Add to all sales channels by default
self.instance.sales_channels = list(get_all_sales_channels().keys())
self.instance.position = (self.event.items.aggregate(p=Max('position'))['p'] or 0) + 1
if not self.instance.admission:
@@ -474,6 +470,8 @@ class ItemCreateForm(I18nModelForm):
})
if self.cleaned_data.get('copy_from'):
if not self.instance.all_sales_channels:
self.instance.limit_sales_channels.set(self.cleaned_data['copy_from'].limit_sales_channels.all())
self.instance.require_membership_types.set(
self.cleaned_data['copy_from'].require_membership_types.all()
)
@@ -563,6 +561,13 @@ class TicketNullBooleanSelect(forms.NullBooleanSelect):
class ItemUpdateForm(I18nModelForm):
picture = ExtFileField(
label=_('Product picture'),
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
required=False,
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['tax_rule'].queryset = self.instance.event.tax_rules.all()
@@ -574,14 +579,10 @@ class ItemUpdateForm(I18nModelForm):
if self.event.tax_rules.exists():
self.fields['tax_rule'].required = True
self.fields['description'].widget.attrs['rows'] = '4'
self.fields['sales_channels'] = forms.MultipleChoiceField(
label=_('Sales channels'),
required=False,
choices=(
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
),
widget=forms.CheckboxSelectMultiple
)
self.fields['limit_sales_channels'].queryset = self.event.organizer.sales_channels.all()
self.fields['limit_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(self.event, attrs={
'data-inverse-dependency': '<[name$=all_sales_channels]',
}, choices=self.fields['limit_sales_channels'].widget.choices)
change_decimal_field(self.fields['default_price'], self.event.currency)
self.fields['available_from_mode'].widget = ButtonGroupRadioSelect(
@@ -756,14 +757,6 @@ class ItemUpdateForm(I18nModelForm):
return d
def clean_picture(self):
value = self.cleaned_data.get('picture')
if isinstance(value, UploadedFile) and value.size > settings.FILE_UPLOAD_MAX_SIZE_IMAGE:
raise forms.ValidationError(_("Please do not upload files larger than {size}!").format(
size=SizeValidationMixin._sizeof_fmt(settings.FILE_UPLOAD_MAX_SIZE_IMAGE)
))
return value
class Meta:
model = Item
localized_fields = '__all__'
@@ -772,7 +765,8 @@ class ItemUpdateForm(I18nModelForm):
'name',
'internal_name',
'active',
'sales_channels',
'all_sales_channels',
'limit_sales_channels',
'admission',
'personalized',
'description',
@@ -829,6 +823,7 @@ class ItemUpdateForm(I18nModelForm):
'hidden_if_item_available': SafeModelChoiceField,
'grant_membership_type': SafeModelChoiceField,
'require_membership_types': SafeModelMultipleChoiceField,
'limit_sales_channels': SafeModelMultipleChoiceField,
}
widgets = {
'available_from': SplitDateTimePickerWidget(),
@@ -900,18 +895,10 @@ class ItemVariationForm(I18nModelForm):
qs = kwargs.pop('membership_types')
super().__init__(*args, **kwargs)
change_decimal_field(self.fields['default_price'], self.event.currency)
self.fields['sales_channels'] = forms.MultipleChoiceField(
label=_('Sales channels'),
required=False,
choices=(
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
),
help_text=_('The sales channel selection for the product as a whole takes precedence, so if a sales channel is '
'selected here but not on product level, the variation will not be available.'),
widget=forms.CheckboxSelectMultiple
)
if not self.instance.pk:
self.initial.setdefault('sales_channels', list(get_all_sales_channels().keys()))
self.fields['limit_sales_channels'].queryset = self.event.organizer.sales_channels.all()
self.fields['limit_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(self.event, attrs={
'data-inverse-dependency': '<[name$=all_sales_channels]',
}, choices=self.fields['limit_sales_channels'].widget.choices)
self.fields['description'].widget.attrs['rows'] = 3
if qs:
@@ -983,12 +970,14 @@ class ItemVariationForm(I18nModelForm):
'available_from_mode',
'available_until',
'available_until_mode',
'sales_channels',
'all_sales_channels',
'limit_sales_channels',
'hide_without_voucher',
]
field_classes = {
'available_from': SplitDateTimeField,
'available_until': SplitDateTimeField,
'limit_sales_channels': SafeModelMultipleChoiceField,
}
widgets = {
'available_from': SplitDateTimePickerWidget(),
+49 -3
View File
@@ -50,6 +50,7 @@ from django_scopes.forms import SafeModelChoiceField
from i18nfield.forms import (
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextInput,
)
from i18nfield.strings import LazyI18nString
from phonenumber_field.formfields import PhoneNumberField
from pytz import common_timezones
@@ -57,7 +58,8 @@ from pretix.api.models import WebHook
from pretix.api.webhooks import get_all_webhook_events
from pretix.base.customersso.oidc import oidc_validate_and_complete_config
from pretix.base.forms import (
I18nMarkdownTextarea, I18nModelForm, PlaceholderValidator, SettingsForm,
SECRET_REDACTED, I18nMarkdownTextarea, I18nModelForm, PlaceholderValidator,
SecretKeySettingsField, SettingsForm,
)
from pretix.base.forms.questions import (
NamePartsFormField, WrappedPhoneNumberPrefixWidget, get_country_by_locale,
@@ -68,7 +70,8 @@ from pretix.base.forms.widgets import (
)
from pretix.base.models import (
Customer, Device, EventMetaProperty, Gate, GiftCard, GiftCardAcceptance,
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium, Team,
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium,
SalesChannel, Team,
)
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
from pretix.base.models.organizer import OrganizerFooterLink
@@ -956,7 +959,7 @@ class SSOProviderForm(I18nModelForm):
label=pgettext_lazy('sso_oidc', 'Client ID'),
required=False,
)
config_oidc_client_secret = forms.CharField(
config_oidc_client_secret = SecretKeySettingsField(
label=pgettext_lazy('sso_oidc', 'Client secret'),
required=False,
)
@@ -1013,7 +1016,13 @@ class SSOProviderForm(I18nModelForm):
if self.instance and self.instance.method == method:
f.initial = self.instance.configuration.get(suffix)
def _unmask_secret_fields(self):
for k, v in self.cleaned_data.items():
if isinstance(self.fields.get(k), SecretKeySettingsField) and self.cleaned_data.get(k) == SECRET_REDACTED:
self.cleaned_data[k] = self.fields[k].initial
def clean(self):
self._unmask_secret_fields()
data = self.cleaned_data
if not data.get("method"):
return data
@@ -1090,3 +1099,40 @@ class GiftCardAcceptanceInviteForm(forms.Form):
if self.organizer.gift_card_acceptor_acceptance.filter(acceptor=acceptor).exists():
raise ValidationError(_('The selected organizer has already been invited.'))
return acceptor
class SalesChannelForm(I18nModelForm):
class Meta:
model = SalesChannel
fields = ['label', 'identifier']
widgets = {
'default': forms.TextInput(),
}
def __init__(self, *args, **kwargs):
self.type = kwargs.pop("type")
super().__init__(*args, **kwargs)
if not self.type.multiple_allowed or (self.instance and self.instance.pk):
self.fields["identifier"].initial = self.type.identifier
self.fields["identifier"].disabled = True
self.fields["label"].initial = LazyI18nString.from_gettext(self.type.verbose_name)
def clean(self):
d = super().clean()
if self.instance.pk:
d["identifier"] = self.instance.identifier
elif self.type.multiple_allowed:
d["identifier"] = self.type.identifier + "." + d["identifier"]
else:
d["identifier"] = self.type.identifier
if not self.instance.pk:
# self.event is actually the organizer, sorry I18nModelForm!
if self.event.sales_channels.filter(identifier=d["identifier"]).exists():
raise ValidationError(
_("A sales channel with the same identifier already exists.")
)
return d
+8
View File
@@ -98,6 +98,14 @@ class ControlFieldRenderer(FieldRenderer):
attrs = ''
return '<div class="{klass}"{attrs}>{html}</div>'.format(klass=self.get_form_group_class(), html=html, attrs=attrs)
def wrap_widget(self, html):
if isinstance(self.widget, CheckboxInput):
css_class = "checkbox"
if self.field.field.disabled:
css_class += " disabled"
html = f'<div class="{css_class}">{html}</div>'
return html
class ControlFieldWithVisibilityRenderer(ControlFieldRenderer):
def __init__(self, *args, **kwargs):
+9 -2
View File
@@ -49,6 +49,7 @@ from pretix.base.forms import (
I18nModelForm, MarkdownTextarea, PlaceholderValidator,
)
from pretix.base.forms.widgets import format_placeholders_help_text
from pretix.base.i18n import language
from pretix.base.models import Item, Voucher
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
from pretix.control.forms.widgets import Select2, Select2ItemVarQuota
@@ -289,8 +290,9 @@ class VoucherBulkForm(VoucherForm):
)
}),
required=False,
help_text=_('You can either supply a list of email addresses with one email address per line, or a CSV file with a title column '
'and one or more of the columns "email", "number", "name", or "tag".')
help_text=_('You can either supply a list of email addresses with one email address per line, or the contents '
'of a CSV file with a title column and one or more of the columns "email", "number", "name", '
'or "tag".')
)
Recipient = namedtuple('Recipient', 'email number name tag')
@@ -332,6 +334,11 @@ class VoucherBulkForm(VoucherForm):
super().__init__(*args, **kwargs)
self._set_field_placeholders('send_subject', ['event', 'name'])
self._set_field_placeholders('send_message', ['event', 'voucher_list', 'name'])
with language(self.instance.event.settings.locale, self.instance.event.settings.region):
for f in ("send_subject", "send_message"):
self.fields[f].initial = str(self.fields[f].initial)
if 'seat' in self.fields:
self.fields['seats'] = forms.CharField(
label=_("Specific seat IDs"),
+3
View File
@@ -357,6 +357,9 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.membershiptype.created': _('The membership type has been created.'),
'pretix.membershiptype.changed': _('The membership type has been changed.'),
'pretix.membershiptype.deleted': _('The membership type has been deleted.'),
'pretix.saleschannel.created': _('The sales channel has been created.'),
'pretix.saleschannel.changed': _('The sales channel has been changed.'),
'pretix.saleschannel.deleted': _('The sales channel has been deleted.'),
'pretix.customer.created': _('The account has been created.'),
'pretix.customer.changed': _('The account has been changed.'),
'pretix.customer.membership.created': _('A membership for this account has been added.'),
+7
View File
@@ -502,6 +502,13 @@ def get_organizer_navigation(request):
}),
'active': url.url_name == 'organizer.settings.mail',
},
{
'label': _('Sales channels'),
'url': reverse('control:organizer.channels', kwargs={
'organizer': request.organizer.slug
}),
'active': url.url_name.startswith('organizer.channel'),
},
{
'label': _('Webhooks'),
'url': reverse('control:organizer.webhooks', kwargs={
@@ -1,6 +1,7 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load static %}
{% load urlreplace %}
{% block title %}{% trans "Check-in lists" %}{% endblock %}
{% block inside %}
@@ -137,9 +138,14 @@
{% endif %}
{% endif %}
<td>
{% for channel in cl.auto_checkin_sales_channels %}
<span class="fa fa-{{ channel.icon }} text-muted"
data-toggle="tooltip" title="{% trans channel.verbose_name %}"></span>
{% for channel in cl.auto_checkin_sales_channels.all %}
{% if "." in channel.icon %}
<img src="{% static channel.icon %}" class="fa-like-image"
data-toggle="tooltip" title="{{ channel.label }}">
{% else %}
<span class="fa fa-{{ channel.icon }} text-muted"
data-toggle="tooltip" title="{{ channel.label }}"></span>
{% endif %}
{% endfor %}
</td>
<td>
@@ -1,5 +1,6 @@
{% extends "pretixcontrol/event/settings_base.html" %}
{% load i18n %}
{% load static %}
{% load bootstrap3 %}
{% block inside %}
<h1>{% trans "Payment settings" %}</h1>
@@ -30,8 +31,13 @@
</td>
<td class="iconcol">
{% for channel in provider.sales_channels %}
<span class="fa fa-{{ channel.icon }} text-muted"
data-toggle="tooltip" title="{% trans channel.verbose_name %}"></span>
{% if "." in channel.icon %}
<img src="{% static channel.icon %}" class="fa-like-image"
data-toggle="tooltip" title="{{ channel.label }}">
{% else %}
<span class="fa fa-{{ channel.icon }} text-muted"
data-toggle="tooltip" title="{{ channel.label }}"></span>
{% endif %}
{% endfor %}
</td>
<td class="text-right flip">

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