Compare commits

...
52 Commits
Author SHA1 Message Date
Kara Engelhardt 9520c411db Format js code with prettier 2026-11-13 16:41:15 +01:00
Kara Engelhardt 749211c206 Add prettier config 2026-11-13 16:41:15 +01:00
Raphael Michel f346acb0d2 API: Only require transmission fields depending on method logic (#6765)
* API: Do not mark all invoice transmission fields as required

* Fix test
2026-11-13 14:34:41 +01:00
Phin Wolkwitz 0478559fd7 Accounting report: Add annotation and full daterange… (Z#23245563) (#6742)
Add an annotation and full daterange in accounting reports if "all time" is selected
2026-11-13 12:33:22 +01:00
Raphael Michel 137059dd4e Customer account login: Add rate limit (Z#23252382) (#6760)
* Customer account login: Add rate limit

* Apply suggestion from @raphaelm
2026-11-11 10:29:51 +01:00
Raphael Michel bd0b55bdd3 Reusable media API: Limit data exposed on cross-organizer requests (Z#23253153) (#6761) 2026-11-11 10:29:02 +01:00
Raphael Michel ceff9de5ee Customer step: Fix login URL for SSO in weird domain setup (#6687)
The current code links to a 404 page if the event has a custom domain
but the organizer uses the default system domain.
2026-11-10 14:44:48 +01:00
Lukas Bockstaller 38b467cef7 Add option to auto transmit invoices after modification by the customer (Z#23251416) (#6713)
* Add option to auto transmit invoices after modification by the customer

* Add caveat for peppol etc. to setting help text

* auto retransmit invoices after change

* code style
2026-11-10 14:41:19 +01:00
Raphael Michel ae95df7ea2 Merge branch 'ticketoutputpdf-security' into 'master'
[SECURITY] Fix missing permission check for ticket layout API (CVE-2026-125744)

See merge request pretix/pretix!60
2026-11-10 13:54:25 +01:00
d362f16417 Documentation for questionnaires API changes (#6704)
* Add docs pages, deprecation notes

* Add data field docs

* Items personalized deprecation

* Move paragraph

* fix indentation

* Document questionnaires resource

* Apply batched suggestions from code review

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

* rename to order_presale / position_presale

* _presale -> _purchase

* children -> rows

* change datafields API endpoint URLs

* removal of question change api endpoints

* update answers objects

* add print_on_invoice_label

* document restrictions

* Update personalized attribute

* fix inconsistent type

* Update doc/api/resources/datafields.rst

Co-authored-by: Raphael Michel <michel@pretix.eu>

* rename dependency_question -> dependency_datafield, document label/help_text fields for static text blocks

* add print_on_invoice_label to examples

* Add markers for new features

---------

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
Co-authored-by: Raphael Michel <michel@pretix.eu>
Co-authored-by: Raphael Michel <michel@rami.io>
2026-11-10 13:21:50 +01:00
Raphael Michel 7f1a0fd58f [SECURITY] Fix missing permission check for ticket layout API (CVE-2026-125744) 2026-11-10 13:13:31 +01:00
Martin Gross 6d05cfea8b Badges: Add Herma 9010 (Z#23253294) (#6759) 2026-11-10 12:29:47 +01:00
pajowu c1b8259075 Replace deprecated chained @classmethod (#6757) 2026-11-10 12:14:28 +01:00
pajowu e394054ae3 Fix duplicate default check in TicketLayoutSerializer (#6748) 2026-11-06 12:24:33 +01:00
Raphael Michel dda2a18fdf Week calendar: Fix navigation after dateutil 4.0 upgrade (#6746) 2026-11-06 10:03:55 +01:00
dependabot[bot] 68bd182f79 Bump arabic-reshaper from 3.0.1 to 3.1.0 (#6744)
Bumps [arabic-reshaper](https://github.com/mpcabd/python-arabic-reshaper) from 3.0.1 to 3.1.0.
- [Release notes](https://github.com/mpcabd/python-arabic-reshaper/releases)
- [Commits](https://github.com/mpcabd/python-arabic-reshaper/compare/v3.0.1...v3.1.0)

---
updated-dependencies:
- dependency-name: arabic-reshaper
  dependency-version: 3.1.0
  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>
2026-11-05 20:03:54 +01:00
dependabot[bot] 2ee6ea7692 Update pytest-asyncio requirement from >=1.4.1 to >=1.4.1.post1 (#6743)
Updates the requirements on [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) to permit the latest version.
- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases)
- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v1.4.1...v1.4.1.post1)

---
updated-dependencies:
- dependency-name: pytest-asyncio
  dependency-version: 1.4.1.post1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-05 20:03:22 +01:00
dependabot[bot] a37305f8f4 Update flake8 requirement from ==7.4.* to ==7.5.* (#6745)
Updates the requirements on [flake8](https://github.com/pycqa/flake8) to permit the latest version.
- [Commits](https://github.com/pycqa/flake8/compare/7.4.0...7.5.0)

---
updated-dependencies:
- dependency-name: flake8
  dependency-version: 7.5.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-05 20:02:57 +01:00
Richard Schreiber bd4b049c16 Fix parameters for localization of SeatProtected errors (#6740) 2026-11-05 15:39:03 +01:00
Phin Wolkwitz cb852a8fa7 Fix itemcount in list beneath cart in event series (Z#23251632) (#6727) 2026-11-05 13:03:44 +01:00
Richard Schreiber 86dd5619e0 Move JavaScript importmap to top (#6734) 2026-11-05 12:39:21 +01:00
pajowu b1a1d66156 ticketoutputpdf api: save background to permanent file (#6735) 2026-11-05 10:33:08 +01:00
pajowu e229697d2a Fix changing sold-out addons if hide_sold_out is set (#6714)
The old item was shown in the frontend and you could de-select it, but this change was never applied
2026-11-04 20:37:05 +01:00
dependabot[bot] f9c7378da7 Update fakeredis requirement from ==2.39.* to ==2.40.* (#6737)
Updates the requirements on [fakeredis](https://github.com/cunla/fakeredis-py) to permit the latest version.
- [Release notes](https://github.com/cunla/fakeredis-py/releases)
- [Commits](https://github.com/cunla/fakeredis-py/compare/v2.39.0...v2.40.0)

---
updated-dependencies:
- dependency-name: fakeredis
  dependency-version: 2.40.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-04 20:21:03 +01:00
dependabot[bot] 86152a182a Update python-dateutil requirement from ==3.0.* to ==4.0.* (#6736)
Updates the requirements on [python-dateutil](https://github.com/dateutil/dateutil) to permit the latest version.
- [Release notes](https://github.com/dateutil/dateutil/releases)
- [Changelog](https://github.com/dateutil/dateutil/blob/master/NEWS)
- [Commits](https://github.com/dateutil/dateutil/compare/3.0.0...4.0.0)

---
updated-dependencies:
- dependency-name: python-dateutil
  dependency-version: 4.0.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-04 20:20:12 +01:00
dependabot[bot] 76899600b0 Update pytest-asyncio requirement from >=1.4.0 to >=1.4.1 (#6739)
Updates the requirements on [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) to permit the latest version.
- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases)
- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v1.4.0...v1.4.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-04 20:17:40 +01:00
pajowu 04a0b009a6 Accept a list of sales_channels in (Sub)Event.annotated, filter_available (Z#23252067) (#6723) 2026-11-04 18:49:19 +01:00
pajowu 4e9d60d238 Fix tests loading local pretix.cfg (#6705)
test/sqlite.cfg does not exist, so pretix.testutils.settings never set PRETIX_CONFIG_FILE, leading to pretix.settings loading the local pretix.cfg

Not setting VITE_DEV_MODE outside CI only worked because most devs probably have set django.debug=on in their pretix.cfg
2026-11-04 17:44:11 +01:00
Raphael Michel fe0d0ceb2e Drop importlib_metadata dependency (#6720)
No longer needed as a polyfill and starts to break things
2026-11-04 17:44:02 +01:00
dependabot[bot] 8457603555 Update sphinx requirement from ==9.1.* to ==9.0.* (#6731)
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/v9.0.4/CHANGES.rst)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v9.1.0rc1...v9.0.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-04 09:52:27 +01:00
dependabot[bot] ae35af596f Update tlds requirement from >=2026093003 to >=2026102900 (#6729)
Updates the requirements on [tlds](https://github.com/kichik/tlds) to permit the latest version.
- [Commits](https://github.com/kichik/tlds/commits)

---
updated-dependencies:
- dependency-name: tlds
  dependency-version: '2026102900'
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-04 09:52:20 +01:00
Christiaan de Die le Clercq 47c3493372 Translations: Update Dutch
Currently translated at 96.9% (6255 of 6452 strings)

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

powered by weblate
2026-11-04 09:52:05 +01:00
Isaac Raño 85d65d17b4 Translations: Update Galician
Currently translated at 18.7% (1209 of 6452 strings)

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

powered by weblate
2026-11-04 09:52:05 +01:00
vihumelo e50c845e09 Translations: Update Portuguese (Brazil)
Currently translated at 98.8% (257 of 260 strings)

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

powered by weblate
2026-11-04 09:52:05 +01:00
vihumelo c84d9061da Translations: Update Portuguese (Brazil)
Currently translated at 92.9% (5995 of 6452 strings)

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

powered by weblate
2026-11-04 09:52:05 +01:00
CVZ-es 25c9659655 Translations: Update Spanish
Currently translated at 100.0% (6452 of 6452 strings)

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

powered by weblate
2026-11-04 09:52:05 +01:00
CVZ-es b6bce961c3 Translations: Update French
Currently translated at 100.0% (6452 of 6452 strings)

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

powered by weblate
2026-11-04 09:52:05 +01:00
Mie Frydensbjerg 1de0455ebc Translations: Update Danish
Currently translated at 62.1% (4009 of 6452 strings)

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

powered by weblate
2026-11-04 09:52:05 +01:00
dependabot[bot] 9fdafa57de Update reportlab requirement from ==5.0.* to ==5.1.* (#6730)
Updates the requirements on [reportlab](https://www.reportlab.com/) to permit the latest version.

---
updated-dependencies:
- dependency-name: reportlab
  dependency-version: 5.1.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-04 09:51:36 +01:00
50bcfc8c74 OIDC: Expose SSO attributes to customer_signed_in signal (#6603)
* OIDC: Expose SSO attributes to customer_signed_in signal

* Change OIDC claim name

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

* Document the changed signature of the customer_signed_in signal

* Put docs on right signal

* Fix test assertions

---------

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
Co-authored-by: Raphael Michel <michel@rami.io>
2026-11-04 09:48:03 +01:00
Raphael Michel fa552fe7d1 Permissions: Add helptext for outgoingmails:read (Z#23251039) (#6707) 2026-11-04 09:20:29 +01:00
dependabot[bot] cbfc24dd64 Update css-inline requirement from ==0.22.* to ==0.23.* (#6683)
Updates the requirements on [css-inline](https://github.com/Stranger6667/css-inline) to permit the latest version.
- [Release notes](https://github.com/Stranger6667/css-inline/releases)
- [Changelog](https://github.com/Stranger6667/css-inline/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stranger6667/css-inline/compare/c-v0.22.0...c-v0.23.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-03 17:21:28 +01:00
dependabot[bot] 6e0c2d69a5 Bump pycparser from 3.11 to 3.12 (#6701)
Bumps [pycparser](https://github.com/eliben/pycparser) from 3.11 to 3.12.
- [Release notes](https://github.com/eliben/pycparser/releases)
- [Commits](https://github.com/eliben/pycparser/compare/release_v3.11...release_v3.12)

---
updated-dependencies:
- dependency-name: pycparser
  dependency-version: '3.12'
  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>
2026-11-03 17:19:46 +01:00
dependabot[bot]andRaphael Michel 34ed175abe Update dnspython requirement from ==2.8.* to ==2.9.* (#6671)
* Update dnspython requirement from ==2.8.* to ==2.9.*

Updates the requirements on [dnspython](https://github.com/rthalley/dnspython) to permit the latest version.
- [Release notes](https://github.com/rthalley/dnspython/releases)
- [Changelog](https://github.com/rthalley/dnspython/blob/main/doc/whatsnew.rst)
- [Commits](https://github.com/rthalley/dnspython/compare/v2.8.0rc1...v2.9.0)

---
updated-dependencies:
- dependency-name: dnspython
  dependency-version: 2.9.0
  dependency-type: direct:production
...

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

* Update pyproject.toml

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Raphael Michel <michel@pretix.eu>
2026-11-03 17:19:17 +01:00
dependabot[bot] a9aaf8638e Update pytest-mock requirement from ==3.16.* to ==3.17.* (#6715)
Updates the requirements on [pytest-mock](https://github.com/pytest-dev/pytest-mock) to permit the latest version.
- [Release notes](https://github.com/pytest-dev/pytest-mock/releases)
- [Changelog](https://github.com/pytest-dev/pytest-mock/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.16.0...v3.17.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-03 17:18:21 +01:00
Raphael MichelandRaphael Michel a613affa05 Sentry: Scrub secrets from event details (#6218)
* Sentry: Scrub secrets from event details

* Apply suggestions from code review

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
2026-11-03 16:45:35 +01:00
Raphael Michel cd0ed78b76 Tests: Do not rely on actual DNS for peppol validation (#6711) 2026-11-03 16:44:31 +01:00
Raphael Michel 4889abd75c PDF editor: Warn about missing scannable QR code (#6726)
* PDF editor: Warn about missing scannable QR code

* Fix
2026-11-03 16:43:59 +01:00
Raphael Michel 0a73faa8bd Bump to i18nfield 1.12.0 2026-11-03 10:38:37 +01:00
Tobias Kunze def5042661 Ignore spaces in bank transfer queries (#6724)
When copying an order code from e.g. the order list, it's easy to
include spaces when pasting, and then this does not match anything. And
as the JavaScript side of the search *does* compare the stripped string
(normalizeQuery), just deleting the whitespace does not trigger the
order to be found either, you have delete one more letter.
2026-11-03 09:12:38 +01:00
Richard Schreiber e6f8f178af Extend log_action filter and make recursive (Z#23247315) (#6598)
* Extend log_action filter and make recursive

* fix flake8

* fix type mismatch

* add "salt" to sensitive keys

* match "key" (e.g. see payone)

* add "auth" to sensitive keys

* clean lists recursively

* improve clean method name as it also cleans list recursively

* Add test
2026-10-29 10:52:00 +01:00
Raphael Michel 5f5380e8b1 Bump version to 2026.10.0.dev0 2026-10-29 09:59:11 +01:00
127 changed files with 5584 additions and 3449 deletions

No files matched your search

-1
View File
@@ -1 +0,0 @@
/*
+1 -1
View File
@@ -56,7 +56,7 @@ calculated automatically.
},
"answers": [
{
"question": 1,
"datafield": "A1B2C3D4",
"answer": "23",
"options": []
}
+18 -8
View File
@@ -3,6 +3,14 @@
Cart positions
==============
.. note::
This API is affected by an upcoming larger change to our API. The API documentation already reflects the future
version. Features or fields not yet available are marked with **NEW**.
The new version is expected to be released for hosted pretix early December and for self-hosted pretix with
version 2027.1.
You can find more information `on our blog <https://pretix.eu/about/en/blog/20261110-questionnaires/>`_.
The API provides limited access to the cart position data model. This API currently only allows creating and deleting
cart positions to reserve quota.
@@ -31,12 +39,13 @@ voucher integer Internal ID of
addon_to integer Internal ID of the position this position is an add-on for (or ``null``)
is_bundled boolean If ``addon_to`` is set, this shows whether this is a bundled product or an addon product
subevent integer ID of the date inside an event series this position belongs to (or ``null``)
answers list of objects Answers to user-defined questions
├ question integer Internal ID of the answered question
answers list of objects Values of data fields (answers to user-defined questions)
├ datafield string **NEW** The data field's ``identifier`` (internal reference)
├ answer string Text representation of the answer
├ question_identifier string The question's ``identifier`` field
├ options list of integers Internal IDs of selected option(s)s (only for choice types)
└ option_identifiers list of strings The ``identifier`` fields of the selected option(s)s
├ option_identifiers list of strings The ``identifier`` fields of the selected option(s)s
├ question integer **Deprecated.** Internal ID of the data field
└ question_identifier string **Read-only, deprecated.** The data field's ``identifier`` (internal reference)
seat objects The assigned seat (or ``null``)
├ id integer Internal ID of the seat instance
├ name string Human-readable seat name
@@ -199,7 +208,8 @@ Cart position endpoints
* ``bundled`` (optional, expect a list of nested objects of cart positions)
* ``answers``
* ``question``
* ``datafield`` **NEW**
* ``question`` **DEPRECATED**
* ``answer``
* ``options``
@@ -223,7 +233,7 @@ Cart position endpoints
"attendee_email": null,
"answers": [
{
"question": 1,
"datafield": "A1B2C3D4",
"answer": "23",
"options": []
}
@@ -285,7 +295,7 @@ Cart position endpoints
"attendee_email": null,
"answers": [
{
"question": 1,
"datafield": "A1B2C3D4",
"answer": "23",
"options": []
}
@@ -303,7 +313,7 @@ Cart position endpoints
"attendee_email": null,
"answers": [
{
"question": 1,
"datafield": "A1B2C3D4",
"answer": "23",
"options": []
}
+37 -26
View File
@@ -5,6 +5,14 @@
Check-in
========
.. note::
This API is affected by an upcoming larger change to our API. The API documentation already reflects the future
version. Features or fields not yet available are marked with **NEW**.
The new version is expected to be released for hosted pretix early December and for self-hosted pretix with
version 2027.1.
You can find more information `on our blog <https://pretix.eu/about/en/blog/20261110-questionnaires/>`_.
This page describes special APIs built for ticket scanning apps. For managing check-in configuration or other operations,
please also see :ref:`rest-checkinlists`. The check-in list API also contains endpoints to obtain statistics or log
failed scans.
@@ -24,41 +32,43 @@ Checking a ticket in
passed needs to be from a distinct event.
:query string expand: Expand a field inside the ``position`` object into a full object. Currently ``subevent``, ``item``, ``variation``, and ``answers.question`` are supported. Can be passed multiple times.
:<json string secret: Scanned QR code corresponding to the ``secret`` attribute of a ticket.
:<json string source_type: Type of source the ``secret`` was obtained form. Defaults to ``"barcode"``.
:<json array lists: List of check-in list IDs to search on. No two check-in lists may be from the same event.
:<json string type: Send ``"exit"`` for an exit and ``"entry"`` (default) for an entry.
:<json datetime datetime: Specifies the datetime of the check-in. If not supplied, the current time will be used.
:<json boolean force: Specifies that the check-in should succeed regardless of revoked barcode, previous check-ins or required
:>json string secret: Scanned QR code corresponding to the ``secret`` attribute of a ticket.
:>json string source_type: Type of source the ``secret`` was obtained form. Defaults to ``"barcode"``.
:>json array lists: List of check-in list IDs to search on. No two check-in lists may be from the same event.
:>json string type: Send ``"exit"`` for an exit and ``"entry"`` (default) for an entry.
:>json datetime datetime: Specifies the datetime of the check-in. If not supplied, the current time will be used.
:>json boolean force: Specifies that the check-in should succeed regardless of revoked barcode, previous check-ins or required
questions that have not been filled. This is usually used to upload offline scans that already happened,
because there's no point in validating them since they happened whether they are valid or not. Defaults to ``false``.
:<json boolean questions_supported: When this parameter is set to ``true``, handling of questions is supported. If
:>json boolean questions_supported: When this parameter is set to ``true``, handling of questions is supported. If
you do not implement question handling in your user interface, you **must**
set this to ``false``. In that case, questions will just be ignored. Defaults
to ``true``.
:<json boolean ignore_unpaid: Specifies that the check-in should succeed even if the order is in pending state.
:>json boolean ignore_unpaid: Specifies that the check-in should succeed even if the order is in pending state.
Defaults to ``false`` and only works when ``include_pending`` is set on the check-in
list.
:<json object answers: If questions are supported/required, you may/must supply a mapping of question IDs to their
respective answers. The answers should always be strings. In case of (multiple-)choice-type
:>json object answers: If questions are supported/required, you may/must supply a mapping of ``identifier`` (`internal
reference`) of data fields to their respective answers (**NEW**).
For backwards compatibility, a mapping of data field ``id`` to answer is supported, as well.
The answers should always be strings. In case of (multiple-)choice-type
answers, the string should contain the (comma-separated) IDs of the selected options.
:<json string nonce: You can set this parameter to a unique random value to identify this check-in. If you're sending
:>json string nonce: You can set this parameter to a unique random value to identify this check-in. If you're sending
this request twice with the same nonce, the second request will also succeed but will always
create only one check-in object even when the previous request was successful as well. This
allows for a certain level of idempotency and enables you to re-try after a connection failure.
:<json string exchange_medium_type: To perform an exchange to a reusable medium, pass the type of the new reusable medium
:<json string exchange_medium_identifier: To perform an exchange to a reusable media, pass the identifier of the new medium
:<json boolean use_order_locale: Specifies that pretix should use the customer's language (``locale`` field from the
:>json string exchange_medium_type: To perform an exchange to a reusable medium, pass the type of the new reusable medium
:>json string exchange_medium_identifier: To perform an exchange to a reusable media, pass the identifier of the new medium
:>json boolean use_order_locale: Specifies that pretix should use the customer's language (``locale`` field from the
order) when building texts (currently only the ``reason_explanation`` response field).
Defaults to ``false`` in which case the server will determine the language (currently
the event default language, might change in the future with support for the
``Accept-Language`` header).
:<json boolean simulate: Do not actually perform the check-in, only simulate the response. The ``position`` response
:>json boolean simulate: Do not actually perform the check-in, only simulate the response. The ``position`` response
object will not reflect the simulated changes.
:>json string status: ``"ok"``, ``"incomplete"``, ``"exchange"``, or ``"error"``
:>json string reason: Reason code, only set on status ``"error"``, see below for possible values.
:>json string reason_explanation: Human-readable explanation, only set on status ``"error"`` and reason ``"rules"``, can be null.
:>json object position: Copy of the matching order position (if any was found). The contents are the same as the
:<json string status: ``"ok"``, ``"incomplete"``, ``"exchange"``, or ``"error"``
:<json string reason: Reason code, only set on status ``"error"``, see below for possible values.
:<json string reason_explanation: Human-readable explanation, only set on status ``"error"`` and reason ``"rules"``, can be null.
:<json object position: Copy of the matching order position (if any was found). The contents are the same as the
:ref:`order-position-resource`, with the following differences: (1) The ``checkins`` value
will only include check-ins for the selected list. (2) An additional boolean property
``require_attention`` will inform you whether either the order or the item have the
@@ -66,13 +76,13 @@ Checking a ticket in
back to values from a parent product or from invoice addresses. (4) Additional properties
``order__status``, ``order__valid_if_pending``, ``order__require_approval``, and
``order__locale`` are included with details form the order for convenience.
:>json boolean require_attention: Whether or not the ``require_attention`` flag is set on the item or order.
:>json list checkin_texts: List of additional texts to show to the user.
:>json object list: Excerpt of information about the matching :ref:`check-in list <rest-checkinlists>` (if any was found),
:<json boolean require_attention: Whether or not the ``require_attention`` flag is set on the item or order.
:<json list checkin_texts: List of additional texts to show to the user.
:<json object list: Excerpt of information about the matching :ref:`check-in list <rest-checkinlists>` (if any was found),
including the attributes ``id``, ``name``, ``event``, ``subevent``, and ``include_pending``.
:>json object questions: List of questions to be answered for check-in, only set on status ``"incomplete"``.
:>json object media_policy: Reusable media policy (see documentation on items), only set on status ``"exchange"``.
:>json object media_type: Reusable media type (see documentation on items), only set on status ``"exchange"``.
:<json object questions: List of questions to be answered for check-in, only set on status ``"incomplete"``.
:<json object media_policy: Reusable media policy (see documentation on items), only set on status ``"exchange"``.
:<json object media_type: Reusable media type (see documentation on items), only set on status ``"exchange"``.
**Example request**:
@@ -92,7 +102,7 @@ Checking a ticket in
"datetime": null,
"questions_supported": true,
"answers": {
"4": "XS"
"A1B2C3D4": "XS"
}
}
@@ -318,6 +328,7 @@ Performing a ticket search
],
"answers": [
{
"datafield": "A1B2C3D4",
"question": 12,
"answer": "Foo",
"options": []
+21 -10
View File
@@ -5,6 +5,14 @@
Check-in lists
==============
.. note::
This API is affected by an upcoming larger change to our API. The API documentation already reflects the future
version. Features or fields not yet available are marked with **NEW**.
The new version is expected to be released for hosted pretix early December and for self-hosted pretix with
version 2027.1.
You can find more information `on our blog <https://pretix.eu/about/en/blog/20261110-questionnaires/>`_.
Resource description
--------------------
@@ -569,6 +577,7 @@ Order position endpoints
],
"answers": [
{
"datafield": "A1B2C3D4",
"question": 12,
"answer": "Foo",
"options": []
@@ -609,27 +618,29 @@ Order position endpoints
:query boolean untrusted_input: If set to true, the lookup parameter is **always** interpreted as a ``secret``, never
as an ``id``. This should be always set if you are passing through untrusted, scanned
data to avoid guessing of ticket IDs.
:<json boolean questions_supported: When this parameter is set to ``true``, handling of questions is supported. If
:>json boolean questions_supported: When this parameter is set to ``true``, handling of questions is supported. If
you do not implement question handling in your user interface, you **must**
set this to ``false``. In that case, questions will just be ignored. Defaults
to ``true``.
:<json boolean canceled_supported: When this parameter is set to ``true``, the response code ``canceled`` may be
:>json boolean canceled_supported: When this parameter is set to ``true``, the response code ``canceled`` may be
returned. Otherwise, canceled orders will return ``unpaid``. (**Deprecated**, in
the future, this will be ignored and ``canceled`` may always be returned.)
:<json datetime datetime: Specifies the datetime of the check-in. If not supplied, the current time will be used.
:<json boolean force: Specifies that the check-in should succeed regardless of revoked barcode, previous check-ins or required
:>json datetime datetime: Specifies the datetime of the check-in. If not supplied, the current time will be used.
:>json boolean force: Specifies that the check-in should succeed regardless of revoked barcode, previous check-ins or required
questions that have not been filled. This is usually used to upload offline scans that already happened,
because there's no point in validating them since they happened whether they are valid or not. Defaults to ``false``.
:<json string type: Send ``"exit"`` for an exit and ``"entry"`` (default) for an entry.
:<json boolean ignore_unpaid: Specifies that the check-in should succeed even if the order is in pending state.
:>json string type: Send ``"exit"`` for an exit and ``"entry"`` (default) for an entry.
:>json boolean ignore_unpaid: Specifies that the check-in should succeed even if the order is in pending state.
Defaults to ``false`` and only works when ``include_pending`` is set on the check-in
list.
:<json string nonce: You can set this parameter to a unique random value to identify this check-in. If you're sending
:>json string nonce: You can set this parameter to a unique random value to identify this check-in. If you're sending
this request twice with the same nonce, the second request will also succeed but will always
create only one check-in object even when the previous request was successful as well. This
allows for a certain level of idempotency and enables you to re-try after a connection failure.
:<json object answers: If questions are supported/required, you may/must supply a mapping of question IDs to their
respective answers. The answers should always be strings. In case of (multiple-)choice-type
:>json object answers: If questions are supported/required, you may/must supply a mapping of ``identifier`` (`internal
reference`) of data fields to their respective answers (**NEW**).
For backwards compatibility, a mapping of data field ``id`` to answer is supported, as well.
The answers should always be strings. In case of (multiple-)choice-type
answers, the string should contain the (comma-separated) IDs of the selected options.
**Example request**:
@@ -648,7 +659,7 @@ Order position endpoints
"questions_supported": true,
"canceled_supported": true,
"answers": {
"4": "XS"
"A1B2C3D4": "XS"
}
}
+250
View File
@@ -0,0 +1,250 @@
.. _rest-datafieldoptions:
Data field options
================
.. note::
This API is part of an upcoming larger change to our API. The API documentation already reflects the future
version. Until the change, this resource is still available at ``/questions/(id)/options/``.
The new version is expected to be released for hosted pretix early December and for self-hosted pretix with
version 2027.1.
You can find more information `on our blog <https://pretix.eu/about/en/blog/20261110-questionnaires/>`_.
Resource description
--------------------
:ref:`Data fields <rest-datafields>` of type "choice" or "multiple choice" can have different options attached.
As with data fields, data field options exist on two resource paths, depending on whether
the data field is attached to orders or order positions. Therefore, each endpoint below has two
different URLs.
The options resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the option
position integer An integer, used for sorting
identifier string An arbitrary string that can be used for matching with
other sources.
answer multi-lingual string The displayed value of this option
===================================== ========================== =======================================================
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/datafields/position/(datafield)/options/
/api/v1/organizers/(organizer)/events/(event)/datafields/order/(datafield)/options/
Returns a list of all options for a given data field.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/datafields/position/11/options/ 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": 2,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"identifier": "LVETRWVU",
"position": 1,
"answer": {"en": "S"}
},
{
"id": 2,
"identifier": "DFEMJWMJ",
"position": 2,
"answer": {"en": "M"}
},
{
"id": 3,
"identifier": "W9AH7RDE",
"position": 3,
"answer": {"en": "L"}
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:query boolean active: If set to ``true`` or ``false``, only data fields with this value for the field ``active`` will be
returned.
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param datafield: The ``id`` field of the data field to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/data field does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/datafields/position/(datafield)/options/(id)/
/api/v1/organizers/(organizer)/events/(event)/datafields/order/(datafield)/options/(id)/
Returns information on one option, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/datafields/position/1/options/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,
"identifier": "LVETRWVU",
"position": 1,
"answer": {"en": "S"}
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param datafield: The ``id`` field of the data field to fetch
:param id: The ``id`` field of the option to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/datafields/position/(datafield)/options/
/api/v1/organizers/(organizer)/events/(event)/datafields/order/(datafield)/options/
Create a new option
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/datafields/position/1/options/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"identifier": "LVETRWVU",
"position": 1,
"answer": {"en": "S"}
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 1,
"identifier": "LVETRWVU",
"position": 1,
"answer": {"en": "S"}
}
:param organizer: The ``slug`` field of the organizer of the event/data field to create a option for
:param event: The ``slug`` field of the event to create a option for
:param datafield: The ``id`` field of the data field to create a option for
:statuscode 201: no error
:statuscode 400: The option 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 this resource.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/datafields/position/(datafield)/options/(id)/
/api/v1/organizers/(organizer)/events/(event)/datafields/order/(datafield)/options/(id)/
Update an option. 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 ``id`` field.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/datafields/position/1/options/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"position": 3
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"identifier": "LVETRWVU",
"position": 1,
"answer": {"en": "S"}
}
: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 data field to modify
:param id: The ``id`` field of the option to modify
:statuscode 200: no error
:statuscode 400: The option could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/datafields/position/(datafield)/options/(id)/
/api/v1/organizers/(organizer)/events/(event)/datafields/order/(datafield)/options/(id)/
Delete an option.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/datafields/position/1/options/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 datafield: The ``id`` field of the data field to modify
:param id: The ``id`` field of the option to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
+428
View File
@@ -0,0 +1,428 @@
.. _rest-datafields:
Data fields
=========
.. note::
This API is part of an upcoming larger change to our API. The API documentation already reflects the future
version. Until the change, please keep using the endpoints at ``/questions/``.
The new version is expected to be released for hosted pretix early December and for self-hosted pretix with
version 2027.1.
You can find more information `on our blog <https://pretix.eu/about/en/blog/20261110-questionnaires/>`_.
Resource description
--------------------
Data fields define additional fields related to an order or an order position.
By referencing data fields in a questionnaire, you can have customers or staff fill them out -- during or
after checkout (web or POS), during check-in in the ticket scanner app, or in the backend.
There are two different resource paths for data fields, depending on whether the data field is attached
to orders or order positions. Therefore, each endpoint below has two different URLs.
The data field resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the data field
internal_name string **Required.** An internal field label
identifier string An arbitrary string that can be used for matching with
other sources.
Must be unique per event, across all types of data fields.
type string **Required.** The expected type of data. Valid options:
* ``N`` – number
* ``S`` – one-line string
* ``T`` – multi-line string
* ``B`` – boolean
* ``C`` – choice from a list
* ``M`` – multiple choice from a list
* ``F`` – file upload
* ``D`` – date
* ``H`` – time
* ``W`` – date and time
* ``CC`` – country code (ISO 3666-1 alpha-2)
* ``TEL`` – telephone number
show_during_checkin boolean If ``true``, the value of the data field will be shown
during check-in (if the check-in client supports it).
*Only supported on per-position data fields.*
print_on_invoice boolean If ``true``, the data field will be shown on
invoices.
*Only supported on per-position data fields.*
print_on_invoice_label multi-lingual string **Required if ``print_on_invoice`` is ``true``.** Label
to print on the invoice before the value of this field.
Otherwise `null`.
options list of objects In case of data field type ``C`` or ``M``, this lists the
available objects. Only writable during creation,
use :ref:`separate endpoint <rest-datafieldoptions>` to modify this later.
├ id integer Internal ID of the option
├ position integer An integer, used for sorting
├ identifier string An arbitrary string that can be used for matching with
other sources.
└ answer multi-lingual string The displayed value of this option
valid_number_min string Minimum value for number fields (optional)
valid_number_max string Maximum value for number fields (optional)
valid_date_min date Minimum value for date fields (optional)
valid_date_max date Maximum value for date fields (optional)
valid_datetime_min datetime Minimum value for date and time fields (optional)
valid_datetime_max datetime Maximum value for date and time fields (optional)
valid_file_portrait boolean Turn on file validation for portrait photos
valid_string_length_min integer Minimum length for string fields (optional)
valid_string_length_max integer Maximum length for string fields (optional)
referenced_by_questionnaires list of integers **Read-only.** List of questionnaire IDs that reference
this data field
===================================== ========================== =======================================================
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/datafields/position/
/api/v1/organizers/(organizer)/events/(event)/datafields/order/
Returns a list of all data fields within a given event.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/datafields/position/ 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,
"internal_name": "T-Shirt size",
"type": "C",
"identifier": "WY3TP9SL",
"show_during_checkin": false,
"print_on_invoice": false,
"print_on_invoice_label": null,
"valid_number_min": null,
"valid_number_max": null,
"valid_date_min": null,
"valid_date_max": null,
"valid_datetime_min": null,
"valid_datetime_max": null,
"valid_string_length_min": null,
"valid_string_length_max": null,
"valid_file_portrait": false,
"options": [
{
"id": 1,
"identifier": "LVETRWVU",
"position": 0,
"answer": {"en": "S"}
},
{
"id": 2,
"identifier": "DFEMJWMJ",
"position": 1,
"answer": {"en": "M"}
},
{
"id": 3,
"identifier": "W9AH7RDE",
"position": 2,
"answer": {"en": "L"}
}
],
"referenced_by_questionnaires": [1]
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id`` and ``position``.
Default: ``position``
:query string identifier: Only return questions with the given identifier string
:query boolean ask_during_checkin: Only return questions that are or are not to be asked during check-in
:query boolean required: Only return questions that are or are not required to fill in
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/datafields/position/(id)/
/api/v1/organizers/(organizer)/events/(event)/datafields/order/(id)/
Returns information on one data field, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/datafields/position/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,
"internal_name": "T-Shirt size",
"type": "C",
"identifier": "WY3TP9SL",
"show_during_checkin": false,
"print_on_invoice": false,
"print_on_invoice_label": null,
"valid_number_min": null,
"valid_number_max": null,
"valid_date_min": null,
"valid_date_max": null,
"valid_datetime_min": null,
"valid_datetime_max": null,
"valid_string_length_min": null,
"valid_string_length_max": null,
"valid_file_portrait": false,
"options": [
{
"id": 1,
"identifier": "LVETRWVU",
"position": 0,
"answer": {"en": "S"}
},
{
"id": 2,
"identifier": "DFEMJWMJ",
"position": 1,
"answer": {"en": "M"}
},
{
"id": 3,
"identifier": "W9AH7RDE",
"position": 2,
"answer": {"en": "L"}
}
],
"referenced_by_questionnaires": [1]
}
: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 question to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/datafields/position/
/api/v1/organizers/(organizer)/events/(event)/datafields/order/
Create a new question
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/datafields/position/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"internal_name": "T-Shirt size",
"type": "C",
"show_during_checkin": false,
"print_on_invoice": false,
"print_on_invoice_label": null,
"options": [
{
"answer": {"en": "S"}
},
{
"answer": {"en": "M"}
},
{
"answer": {"en": "L"}
}
]
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 1,
"internal_name": "T-Shirt size",
"type": "C",
"identifier": "WY3TP9SL",
"show_during_checkin": false,
"print_on_invoice": false,
"print_on_invoice_label": null,
"valid_number_min": null,
"valid_number_max": null,
"valid_date_min": null,
"valid_date_max": null,
"valid_datetime_min": null,
"valid_datetime_max": null,
"valid_string_length_min": null,
"valid_string_length_max": null,
"valid_file_portrait": false,
"options": [
{
"id": 1,
"identifier": "LVETRWVU",
"position": 0,
"answer": {"en": "S"}
},
{
"id": 2,
"identifier": "DFEMJWMJ",
"position": 1,
"answer": {"en": "M"}
},
{
"id": 3,
"identifier": "W9AH7RDE",
"position": 2,
"answer": {"en": "L"}
}
],
"referenced_by_questionnaires": [1]
}
:param organizer: The ``slug`` field of the organizer of the event to create an item for
:param event: The ``slug`` field of the event to create an item for
:statuscode 201: no error
:statuscode 400: The item 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 this resource.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/datafields/position/(id)/
/api/v1/organizers/(organizer)/events/(event)/datafields/order/(id)/
Update a data field. 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 ``options`` field. If
you need to update/delete options please use the nested dedicated endpoints.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/datafields/position/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"internal_name": "Clothing size"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"internal_name": "Clothing size",
"type": "C",
"identifier": "WY3TP9SL",
"show_during_checkin": false,
"print_on_invoice": false,
"print_on_invoice_label": null,
"valid_number_min": null,
"valid_number_max": null,
"valid_date_min": null,
"valid_date_max": null,
"valid_datetime_min": null,
"valid_datetime_max": null,
"valid_string_length_min": null,
"valid_string_length_max": null,
"valid_file_portrait": false,
"options": [
{
"id": 1,
"identifier": "LVETRWVU",
"position": 0,
"answer": {"en": "S"}
},
{
"id": 2,
"identifier": "DFEMJWMJ",
"position": 1,
"answer": {"en": "M"}
},
{
"id": 3,
"identifier": "W9AH7RDE",
"position": 2,
"answer": {"en": "L"}
}
],
"referenced_by_questionnaires": [1]
}
: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 question to modify
:statuscode 200: no error
:statuscode 400: The question could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/datafields/position/(id)/
/api/v1/organizers/(organizer)/events/(event)/datafields/order/(id)/
Delete a data field.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/datafields/position/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 question to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
+3
View File
@@ -21,6 +21,9 @@ at :ref:`plugin-docs`.
item_add-ons
item_meta_properties
item_program_times
datafields
datafield_options
questionnaires
questions
question_options
quotas
+13 -5
View File
@@ -37,12 +37,9 @@ tax_rule integer The internal
admission boolean ``true`` for items that grant admission to the event
(such as primary tickets) and ``false`` for others
(such as add-ons or merchandise).
personalized boolean ``true`` for items that require personalization according
personalized boolean **DEPRECATED.** ``true`` for items that require personalization according
to event settings. Only affects system-level fields, not
custom questions. Currently only allowed for products with
``admission`` set to ``true``. For backwards compatibility,
when creating new items and this field is not given, it defaults
to the same value as ``admission``.
custom questions. Defaults to ``false``.
position integer An integer, used for sorting
picture file A product picture to be displayed in the shop
(can be ``null``).
@@ -233,6 +230,17 @@ meta_data object Values set fo
The ``program_times`` attribute has been added.
.. versionchanged:: 2027.1
The ``personalized`` attribute has been deprecated.
For compatibility with existing applications:
* When retrieving products, `personalized` is `True` if any attached questionnaire asks for `attendee_name` and/or `attendee_email`.
* When creating a new product, setting the `personalized` field to `True` will attach the "Attendee data" questionnaire to the new product. If no such questionnaire exists, it will be created.
Before, the `personalized` field defaulted to the same value as `admission`. Now, it defaults to `false`.
* When updating a product, the `personalized` field cannot be changed, returning an HTTP 400 error if attempted.
Notes
-----
+63 -12
View File
@@ -9,6 +9,14 @@
Orders
======
.. note::
This API is affected by an upcoming larger change to our API. The API documentation already reflects the future
version. Features or fields not yet available are marked with **NEW**.
The new version is expected to be released for hosted pretix early December and for self-hosted pretix with
version 2027.1.
You can find more information `on our blog <https://pretix.eu/about/en/blog/20261110-questionnaires/>`_.
Order resource
--------------
@@ -119,6 +127,11 @@ cancellation_date datetime Time of order c
plugin_data object Additional data added by plugins.
use_gift_cards list of strings List of unique gift card secrets that are used to pay
for this order.
answers list of objects **NEW** Values of data fields (answers to user-defined questions)
├ datafield string The data field's ``identifier`` (internal reference)
├ answer string Text representation of the answer
├ options list of integers Internal IDs of selected option(s)s (only for choice types)
└ option_identifiers list of strings The ``identifier`` fields of the selected option(s)s
===================================== ========================== =======================================================
@@ -162,6 +175,10 @@ use_gift_cards list of strings List of unique
The ``use_gift_cards`` attribute has been added.
.. versionchanged:: 2027.1
The ``answers`` attribute has been added.
.. _order-position-resource:
Order position resource
@@ -231,12 +248,13 @@ print_logs list of objects List of print j
downloads list of objects List of ticket download options
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
└ url string Download URL
answers list of objects Answers to user-defined questions
├ question integer Internal ID of the answered question
├ answer string Text representation of the answer (URL if answer is a file)
├ question_identifier string The question's ``identifier`` field
answers list of objects Values of data fields (answers to user-defined questions)
├ datafield string **NEW** The data field's ``identifier`` (internal reference)
├ answer string Text representation of the answer
├ options list of integers Internal IDs of selected option(s)s (only for choice types)
└ option_identifiers list of strings The ``identifier`` fields of the selected option(s)s
├ option_identifiers list of strings The ``identifier`` fields of the selected option(s)s
├ question integer **Deprecated.** Internal ID of the data field
└ question_identifier string **Read-only, deprecated.** The data field's ``identifier`` (internal reference)
seat objects The assigned seat. Can be ``null``.
├ id integer Internal ID of the seat instance
├ name string Human-readable seat name
@@ -452,10 +470,11 @@ List of all orders
],
"answers": [
{
"datafield": "WY3TP9SL",
"question": 12,
"question_identifier": "WY3TP9SL",
"answer": "Foo",
"option_idenfiters": [],
"option_identifiers": [],
"options": []
}
],
@@ -488,7 +507,15 @@ List of all orders
],
"refunds": [],
"cancellation_date": null,
"plugin_data": {}
"plugin_data": {},
"answers": [
{
"datafield": "XYZ12345",
"answer": "42",
"option_identifiers": [],
"options": []
}
]
}
]
}
@@ -699,6 +726,7 @@ Fetching individual orders
],
"answers": [
{
"datafield": "WY3TP9SL",
"question": 12,
"question_identifier": "WY3TP9SL",
"answer": "Foo",
@@ -735,7 +763,15 @@ Fetching individual orders
],
"refunds": [],
"cancellation_date": null,
"plugin_data": {}
"plugin_data": {},
"answers": [
{
"datafield": "XYZ12345",
"answer": "42",
"option_identifiers": [],
"options": []
}
]
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -1079,7 +1115,8 @@ Creating orders
* ``discount`` (optional, only possible if ``price`` is set; attention: if this is set to not-``null`` on any position, automatic calculation of discounts will not run)
* ``answers``
* ``question``
* ``datafield`` (**NEW**)
* ``question`` (**DEPRECATED**)
* ``answer``
* ``options``
@@ -1114,6 +1151,12 @@ Creating orders
order isn't completely paid after its creation with ``use_gift_cards``, then a subsequent request to the payment
endpoint is needed.
* **NEW** ``answers`` (optional). Values for order-level data fields.
* ``datafield``
* ``answer``
* ``options``
If you want to use add-on products, you need to set the ``positionid`` fields of all positions manually
to incrementing integers starting with ``1``. Then, you can reference one of these
IDs in the ``addon_to`` field of another position. Note that all add_ons for a specific position need to come
@@ -1161,6 +1204,12 @@ Creating orders
"internal_reference": "",
"vat_id": ""
},
"answers": [
{
"datafield": "XYZ12345",
"answer": "42"
}
],
"positions": [
{
"positionid": 1,
@@ -1174,7 +1223,7 @@ Creating orders
"addon_to": null,
"answers": [
{
"question": 1,
"datafield": "WY3TP9SL",
"answer": "23",
"options": []
}
@@ -1684,6 +1733,7 @@ List of all order positions
],
"answers": [
{
"datafield": "WY3TP9SL",
"question": 12,
"question_identifier": "WY3TP9SL",
"answer": "Foo",
@@ -1863,6 +1913,7 @@ Fetching individual positions
],
"answers": [
{
"datafield": "WY3TP9SL",
"question": 12,
"question_identifier": "WY3TP9SL",
"answer": "Foo",
@@ -1971,7 +2022,7 @@ Manipulating individual positions
* ``answers``: If specified, you will need to provide **all** answers for this order position.
Validation is handled the same way as when creating orders through the API. You are therefore
expected to provide ``question``, ``answer``, and possibly ``options``. ``question_identifier``
expected to provide ``datafield``, ``answer``, and possibly ``options``. ``question`` is deprecated. ``question_identifier``
and ``option_identifiers`` will be ignored. As a special case, you can submit the magic value
``"file:keep"`` as the answer to a file question to keep the current value without re-uploading it.
@@ -2067,7 +2118,7 @@ Manipulating individual positions
* ``state``
* ``answers``: Validation is handled the same way as when creating orders through the API. You are therefore
expected to provide ``question``, ``answer``, and possibly ``options``. ``question_identifier``
expected to provide ``datafield``, ``answer``, and possibly ``options``. ``question`` is deprecated. ``question_identifier``
and ``option_identifiers`` will be ignored. As a special case, you can submit the magic value
``"file:keep"`` as the answer to a file question to keep the current value without re-uploading it.
+2 -224
View File
@@ -1,229 +1,7 @@
Question options
================
Resource description
--------------------
.. warning:: This resource is **deprecated**.
Questions of type "choice" or "multiple choice" can have different options attached.
The options resource contains the following public fields:
New applications must use the :ref:`rest-datafieldoptions` resource.
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the option
position integer An integer, used for sorting
identifier string An arbitrary string that can be used for matching with
other sources.
answer multi-lingual string The displayed value of this option
===================================== ========================== =======================================================
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/questions/(question)/options/
Returns a list of all options for a given question.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/questions/11/options/ 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": 2,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"identifier": "LVETRWVU",
"position": 1,
"answer": {"en": "S"}
},
{
"id": 2,
"identifier": "DFEMJWMJ",
"position": 2,
"answer": {"en": "M"}
},
{
"id": 3,
"identifier": "W9AH7RDE",
"position": 3,
"answer": {"en": "L"}
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:query boolean active: If set to ``true`` or ``false``, only questions with this value for the field ``active`` will be
returned.
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param question: The ``id`` field of the question to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/question does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/questions/(question)/options/(id)/
Returns information on one option, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/questions/1/options/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,
"identifier": "LVETRWVU",
"position": 1,
"answer": {"en": "S"}
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param question: The ``id`` field of the question to fetch
:param id: The ``id`` field of the option to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/questions/(question)/options/
Creates a new option
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/questions/1/options/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"identifier": "LVETRWVU",
"position": 1,
"answer": {"en": "S"}
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 1,
"identifier": "LVETRWVU",
"position": 1,
"answer": {"en": "S"}
}
:param organizer: The ``slug`` field of the organizer of the event/question to create a option for
:param event: The ``slug`` field of the event to create a option for
:param question: The ``id`` field of the question to create a option for
:statuscode 201: no error
:statuscode 400: The option 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 this resource.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/questions/(question)/options/(id)/
Update an option. 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 ``id`` field.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/questions/1/options/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"position": 3
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"identifier": "LVETRWVU",
"position": 1,
"answer": {"en": "S"}
}
: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 question to modify
:param id: The ``id`` field of the option to modify
:statuscode 200: no error
:statuscode 400: The option could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/questions/(id)/options/(id)/
Delete an option.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/questions/1/options/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 question to modify
:param id: The ``id`` field of the option to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
+379
View File
@@ -0,0 +1,379 @@
.. _rest-questionnaires:
Questionnaires
=========
.. note::
This API is part of an upcoming larger change to our API. The API documentation already reflects the future
version. Until the change, please keep using the endpoints at ``/questions/``.
The new version is expected to be released for hosted pretix early December and for self-hosted pretix with
version 2027.1.
You can find more information `on our blog <https://pretix.eu/about/en/blog/20261110-questionnaires/>`_.
Questionnaires are attached to one or more products, and a product can have multiple questionnaires.
They consist of questionnaire rows, each of which can be one of the following:
Reference to system field
To reference a system field, use one of the identifier strings from the table below in the row's `datafield` attribute.
Currently, system fields are only defined on order positions, not on orders.
=============== =========================== ============================
Container Identifier Label
=============== =========================== ============================
Order position `"@attendee_name_parts"` Attendee name
Order position `"@attendee_email"` Attendee email
Order position `"@company"` Company
Order position `"@street"` Street
Order position `"@zipcode"` ZIP code
Order position `"@city"` City
Order position `"@state"` State
Order position `"@country"` Country
=============== =========================== ============================
Reference to user-defined data field
To reference a :ref:`user-defined data field <rest-datafields>`, use its internal reference (`identifier`)
in the row's `datafield` attribute.
Static explanation text / subheading
To add a static text block, set the row's `datafield` attribute to `null`. In this case, the text
from the `label` attribute is displayed as a subheading, and the `help_text` attribute as a normal paragraph.
Resource description
--------------------
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the questionnaire
type string The type of questionnaire. Valid options:
* ``order_purchase`` — Order-wide, before purchase
* ``position_purchase`` — Per product, before purchase
* ``position_attendee`` — Per product, via attendee link
* ``position_checkin`` — Per product, at check-in
* ``position_hidden`` — Per product, hidden
internal_name string Internal label for the questionnaire.
items list of integers List of item IDs this questionnaire is assigned to.
*Only supported on per-product questionnaires.*
position integer An integer, used for sorting
all_sales_channels boolean If ``true`` (default), the questions from this questionnaire
are asked on all sales channels.
limit_sales_channels list of strings List of sales channel identifiers the questions are asked on
if ``all_sales_channels`` is ``false``.
rows list of objects List of all questionnaire rows.
├ datafield string Reference to a data field (see above)
├ required boolean If ``true``, the question needs to be filled out.
├ label multi-lingual string The field label shown to the customer. In case of a static
text block, the subheading.
├ help_text multi-lingual string The help text shown to the customer.
├ dependency_datafield string ``datafield`` reference of a different row in the same
questionnaire.
The current question will only be shown if the question
given in this attribute is set to one of the values given
in ``dependency_values``.
├ dependency_values list of strings If ``dependency_datafield`` is set to reference a boolean
data field, this should be ``["True"]`` or ``["False"]``.
Otherwise, it should be a list of ``identifier`` values
of question options.
├ rendered_help_text string **Read-only.** Markdown contents from help_text rendered
to HTML
===================================== ========================== =======================================================
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/questionnaires/
Returns a list of all questionnaires within a given event.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/questionnaires/ 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": 2,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"type": "position_purchase",
"internal_name": "Attendee data",
"items": [1, 2],
"position": 0,
"all_sales_channels": true,
"limit_sales_channels": [],
"rows": [
{
"datafield": "@attendee_name_parts",
"required": false,
"label": {"en": "Attendee name"},
"help_text": {},
"dependency_datafield": null,
"dependency_values": [],
"rendered_help_text": ""
},
{
"datafield": "@attendee_email",
"required": false,
"label": {"en": "Attendee email"},
"help_text": {"en": "*We'll send you our program guide a few days before the event.*"},
"dependency_datafield": null,
"dependency_values": [],
"rendered_help_text": "<em>We'll send you our program guide a few days before the event.</em>"
}
]
},
{
"id": 2,
"type": "order_purchase",
"internal_name": "Per-order questions",
"items": [],
"position": 0,
"all_sales_channels": true,
"limit_sales_channels": [],
"rows": [
{
"datafield": "87Y8DCS9",
"required": false,
"label": {"en": "How did you find us?"},
"help_text": {},
"dependency_datafield": null,
"dependency_values": [],
"rendered_help_text": ""
}
]
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id`` and ``position``.
Default: ``position``
:query string type: Only return questionnaires with the given type
:query string items: Only return questionnaires associated with the given item (internal item ID)
:query boolean sales_channel: Only return questionnaires visible on the given sales channel (sales channel identifier string)
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/questionnaires/(id)/
Returns information on one questionnaire, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/questionnaires/2/ 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": 2,
"type": "order_purchase",
"internal_name": "Per-order questions",
"items": [],
"position": 0,
"all_sales_channels": true,
"limit_sales_channels": [],
"rows": [
{
"datafield": "87Y8DCS9",
"required": false,
"label": {"en": "How did you find us?"},
"help_text": {},
"dependency_datafield": null,
"dependency_values": [],
"rendered_help_text": ""
}
]
}
: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 questionnaire to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/questionnaires/
Create a new questionnaire
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/questionnaires/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"type": "order_purchase",
"internal_name": "Per-order questions",
"position": 0,
"all_sales_channels": true,
"rows": [
{
"datafield": "87Y8DCS9",
"required": false,
"label": {"en": "How did you find us?"}
}
]
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 2,
"type": "order_purchase",
"internal_name": "Per-order questions",
"items": [],
"position": 0,
"all_sales_channels": true,
"limit_sales_channels": [],
"rows": [
{
"datafield": "87Y8DCS9",
"required": false,
"label": {"en": "How did you find us?"},
"help_text": {},
"dependency_datafield": null,
"dependency_values": [],
"rendered_help_text": ""
}
]
}
:param organizer: The ``slug`` field of the organizer of the event to create a questionnaire for
:param event: The ``slug`` field of the event to create a questionnaire for
:statuscode 201: no error
:statuscode 400: The questionnaire 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 this resource.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/questionnaires/(id)/
Update a questionnaire. 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 only change all ``rows`` en bloc, or keep all of them unchanged in case of a ``PATCH``. If you
change ``rows``, you need to specify all fields of the resource.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/questionnaires/2/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"internal_name": "Questions asked per order"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 2,
"type": "order_purchase",
"internal_name": "Questions asked per order",
"items": [],
"position": 0,
"all_sales_channels": true,
"limit_sales_channels": [],
"rows": [
{
"datafield": "87Y8DCS9",
"required": false,
"label": {"en": "How did you find us?"},
"help_text": {},
"dependency_datafield": null,
"dependency_values": [],
"rendered_help_text": ""
}
]
}
:param organizer: The ``slug`` field of the organizer of the questionnaire to modify
:param event: The ``slug`` field of the event of the questionnaire to modify
:param id: The ``id`` field of the questionnaire to modify
:statuscode 200: no error
:statuscode 400: The questionnaire could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/questionnaires/(id)/
Delete a questionnaire.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/questionnaires/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 of the questionnaire to modify
:param event: The ``slug`` field of the event of the questionnaire to modify
:param id: The ``id`` field of the questionnaire to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
+40 -1
View File
@@ -8,10 +8,23 @@
Questions
=========
.. warning:: This resource is **deprecated**. It is only provided for compatibility with existing applications
and should not be used in new development.
New applications must use the :ref:`rest-datafields` and :ref:`rest-questionnaires` resources.
The new version is expected to be released for hosted pretix early December and for self-hosted pretix with
version 2027.1.
You can find more information `on our blog <https://pretix.eu/about/en/blog/20261110-questionnaires/>`_.
This resource will continue to work in read-only mode on events where the
:ref:`questionnaire configuration <rest-questionnaires>` is compatible with the old data model.
If incompatible configuration is detected, all endpoints return HTTP 410.
Resource description
--------------------
Questions define additional fields that need to be filled out by customers during checkout.
Questions define additional fields related to an order position that need to be filled out by customers during checkout.
The question resource contains the following public fields:
.. rst-class:: rest-resource-table
@@ -84,6 +97,10 @@ dependency_value string An old version
The ``show_during_checkin`` attribute has been added.
.. versionchanged:: 2026.[TODO]
The questions resource was deprecated and replaced by the :ref:`rest-datafields` and :ref:`rest-questionnaires` resources.
Endpoints
---------
@@ -249,6 +266,13 @@ Endpoints
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/questions/
.. warning:: This endpoint is **deprecated** and will be removed as part of an upcoming API change.
The new version is expected to be released for hosted pretix early December and for self-hosted
pretix with version 2027.1.
You can find more information `on our blog <https://pretix.eu/about/en/blog/20261110-questionnaires/>`_.
After the change, instead create a :ref:`data field <rest-datafields>` first, then add
it to a :ref:`questionnaire <rest-questionnaires>`.
Creates a new question
**Example request**:
@@ -351,6 +375,13 @@ Endpoints
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/questions/(id)/
.. warning:: This endpoint is **deprecated** and will be removed as part of an upcoming API change.
The new version is expected to be released for hosted pretix early December and for self-hosted
pretix with version 2027.1.
You can find more information `on our blog <https://pretix.eu/about/en/blog/20261110-questionnaires/>`_.
After the change, instead update the relevant :ref:`data field <rest-datafields>`, or
the reference to it inside a :ref:`questionnaire <rest-questionnaires>`.
Update a question. 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.
@@ -437,6 +468,14 @@ Endpoints
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/questions/(id)/
.. warning:: This endpoint is **deprecated** and will be removed as part of an upcoming API change.
The new version is expected to be released for hosted pretix early December and for self-hosted
pretix with version 2027.1.
You can find more information `on our blog <https://pretix.eu/about/en/blog/20261110-questionnaires/>`_.
After the change, insteadremove the questionnaire child from its :ref:`questionnaire <rest-questionnaires>`,
or delete the relevant :ref:`data field <rest-datafields>`.
Delete a question.
**Example request**:
+1 -1
View File
@@ -1,4 +1,4 @@
sphinx==9.1.*
sphinx==9.0.*
sphinx-rtd-theme~=3.1.0
sphinxcontrib-httpdomain~=2.1.0
sphinxcontrib-images~=1.0.1
+1 -1
View File
@@ -1,5 +1,5 @@
-e ../
sphinx==9.1.*
sphinx==9.0.*
sphinx-rtd-theme~=3.1.0
sphinxcontrib-httpdomain~=2.1.0
sphinxcontrib-images~=1.0.1
+2
View File
@@ -5,6 +5,7 @@ import ts from 'typescript-eslint'
import stylistic from '@stylistic/eslint-plugin'
import vue from 'eslint-plugin-vue'
import vuePug from 'eslint-plugin-vue-pug'
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
const ignores = globalIgnores([
'**/node_modules',
@@ -161,4 +162,5 @@ export default defineConfig([
},
},
},
eslintPluginPrettierRecommended,
])
+127
View File
@@ -21,9 +21,12 @@
"@vue/eslint-config-typescript": "^14.7.0",
"@vue/language-plugin-pug": "^3.2.5",
"eslint": "^10.0.3",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.7",
"eslint-plugin-vue": "^10.8.0",
"eslint-plugin-vue-pug": "^1.0.0-alpha.5",
"globals": "^17.4.0",
"prettier": "3.10.1",
"pug": "^3.0.3",
"sass-embedded": "^1.98.0",
"smol-toml": "^1.6.1",
@@ -92,6 +95,17 @@
"dev": true,
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@dual-bundle/import-meta-resolve": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.2.1.tgz",
"integrity": "sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==",
"dev": true,
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/JounQin"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
@@ -718,6 +732,19 @@
"node": ">=14"
}
},
"node_modules/@pkgr/core": {
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz",
"integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/pkgr"
}
},
"node_modules/@rolldown/binding-android-arm-eabi": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz",
@@ -1966,6 +1993,53 @@
}
}
},
"node_modules/eslint-config-prettier": {
"version": "10.1.8",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
"funding": {
"url": "https://opencollective.com/eslint-config-prettier"
},
"peerDependencies": {
"eslint": ">=7.0.0"
}
},
"node_modules/eslint-plugin-prettier": {
"version": "5.5.7",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.7.tgz",
"integrity": "sha512-WjHyn3GrFd+ZR0rzsLiM4zJZ35PZMOE0SUIxzGmnWNG0126yNfJKJzObmmkYDwHJfGlkz3PSlzyG9ez/LJntwg==",
"dev": true,
"license": "MIT",
"dependencies": {
"prettier-linter-helpers": "^1.0.1",
"synckit": "^0.12.3"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint-plugin-prettier"
},
"peerDependencies": {
"@types/eslint": ">=8.0.0",
"eslint": ">=8.0.0",
"eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0",
"prettier": ">=3.0.0"
},
"peerDependenciesMeta": {
"@types/eslint": {
"optional": true
},
"eslint-config-prettier": {
"optional": true
}
}
},
"node_modules/eslint-plugin-vue": {
"version": "10.8.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.8.0.tgz",
@@ -2190,6 +2264,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-diff": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
"integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@@ -3361,6 +3442,35 @@
"node": ">= 0.8.0"
}
},
"node_modules/prettier": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.10.1.tgz",
"integrity": "sha512-70vp3ZUYoPlFbF9uiTdXHUUCGEIp7d4+6p+8DYbkm2jP7x/3i/paXVF6dopSI3zd/CEitrZN1rc4i85xWmmXlg==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prettier-linter-helpers": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz",
"integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-diff": "^1.1.2"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/promise": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
@@ -4300,6 +4410,23 @@
"node": ">=16.0.0"
}
},
"node_modules/synckit": {
"version": "0.12.3",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.12.3.tgz",
"integrity": "sha512-U+/AXT6LWsxnj2vz9HeUuPWBNgdWxjyIEjEPdbJARC9txmgPzo2VEdqnFo4hHaNyMPtycyVJrSaQYeQv/PoyyA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@dual-bundle/import-meta-resolve": "^4.2.1",
"@pkgr/core": "^0.3.6"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/synckit"
}
},
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+3
View File
@@ -39,9 +39,12 @@
"@vue/eslint-config-typescript": "^14.7.0",
"@vue/language-plugin-pug": "^3.2.5",
"eslint": "^10.0.3",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.7",
"eslint-plugin-vue": "^10.8.0",
"eslint-plugin-vue-pug": "^1.0.0-alpha.5",
"globals": "^17.4.0",
"prettier": "3.10.1",
"pug": "^3.0.3",
"sass-embedded": "^1.98.0",
"smol-toml": "^1.6.1",
+13 -16
View File
@@ -27,16 +27,16 @@ classifiers = [
]
dependencies = [
"arabic-reshaper==3.0.1", # Support for Arabic in reportlab
"arabic-reshaper==3.1.0", # Support for Arabic in reportlab
"babel",
"BeautifulSoup4==4.15.*",
"bleach==6.4.*",
"celery==5.6.*",
"chardet==5.2.*",
"cryptography>=50.0.2",
"css-inline==0.22.*",
"css-inline==0.23.*",
"defusedcsv>=3.0.0",
"dnspython==2.*",
"dnspython==2.9.*",
"Django[argon2]==5.2.*,>=5.2.17",
"django-bootstrap3==26.3",
"django-compressor==4.6.0",
@@ -46,7 +46,7 @@ dependencies = [
"django-formtools==2.7",
"django-hierarkey==2.0.*,>=2.0.2",
"django-hijack==3.7.*",
"django-i18nfield==1.11.*",
"django-i18nfield==1.12.*",
"django-libsass==0.9",
"django-localflavor==5.1",
"django-markup",
@@ -58,17 +58,14 @@ dependencies = [
"django-scopes==2.1.*",
"django-statici18n==2.8.*",
"djangorestframework>=3.18.3",
"dnspython==2.8.*",
"drf_ujson2==1.7.*",
"geoip2==5.*",
"importlib_metadata==9.*", # Polyfill, we can probably drop this once we require Python 3.10+
"isoweek",
"jsonschema",
"kombu==5.6.*",
"libsass==0.23.*",
"lxml",
"markdown==3.11.2", # 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
"markdown==3.11.2",
"mt-940==4.30.*",
"oauthlib==3.3.*",
"openpyxl==3.1.*",
@@ -82,23 +79,23 @@ dependencies = [
"protobuf==7.36.*",
"psycopg2-binary",
"pycountry",
"pycparser==3.11",
"pycparser==3.12",
"pycryptodome==3.24.*",
"pypdf==6.20.*",
"python-bidi==0.6.*", # Support for Arabic in reportlab
"python-dateutil==3.0.*",
"python-dateutil==4.0.*",
"pytz",
"pytz-deprecation-shim==0.1.*",
"pyuca",
"qrcode==8.2",
"redis==7.4.*",
"reportlab==5.0.*",
"reportlab==5.1.*",
"requests==2.34.*",
"sentry-sdk==2.71.*",
"sepaxml==2.7.*",
"stripe==7.9.*",
"text-unidecode==1.*",
"tlds>=2026093003",
"tlds>=2026102900",
"tqdm==4.*",
"ua-parser==1.0.*",
"vobject==0.9.*",
@@ -112,17 +109,17 @@ dev = [
"aiohttp==3.14.*",
"coverage",
"coveralls",
"fakeredis==2.39.*",
"flake8==7.4.*",
"fakeredis==2.40.*",
"flake8==7.5.*",
"freezegun",
"isort==9.0.*",
"pep8-naming==0.15.*",
"potypo",
"pytest-asyncio>=1.4.0",
"pytest-asyncio>=1.4.1.post1",
"pytest-cache",
"pytest-cov",
"pytest-django==4.*",
"pytest-mock==3.16.*",
"pytest-mock==3.17.*",
"pytest-sugar",
"pytest-xdist==3.8.*",
"pytest-playwright",
+1 -1
View File
@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "2026.9.0"
__version__ = "2026.10.0.dev0"
+2
View File
@@ -197,6 +197,8 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
'linked_giftcard',
'info',
'notes',
# When adding fields, check if they should be hidden in ReusableMediaViewSet
# for cross-organizer lookups
)
+1
View File
@@ -117,6 +117,7 @@ class TransmissionInfoSerializer(serializers.Serializer):
super().__init__(*args, **kwargs)
for k, v in transmission_type.invoice_address_form_fields.items():
self.fields[k] = form_field_to_serializer_field(v)
self.fields[k].required = False # invoice_address_form_fields_required is checked by parent serializer
class InvoiceAddressSerializer(I18nAwareModelSerializer):
+11 -2
View File
@@ -183,9 +183,18 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
type=s.validated_data["type"],
identifier=s.validated_data["identifier"],
)
m.customer = None # not relevant for cross-organizer
s = self.get_serializer(m)
return Response({"result": s.data})
data = s.data
# Remove fields that are not relevant for cross-organizer usage
data["customer"] = None
data["linked_orderposition"] = None
data["linked_orderpositions"] = []
data["info"] = {}
data["notes"] = None
data["claim_token"] = None
data["label"] = None
return Response({"result": data})
except ReusableMedium.DoesNotExist:
mt = MEDIA_TYPES.get(s.validated_data["type"])
if mt:
+2
View File
@@ -264,6 +264,8 @@ def oidc_validate_authorization(provider, code, redirect_uri, pkce_code_verifier
)
)
profile['oidc_claims'] = userinfo
return profile
+3 -3
View File
@@ -29,6 +29,7 @@ from typing import List, Optional, Protocol
from django.conf import settings
from django.db import DatabaseError, transaction
from django.utils.functional import classproperty
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
@@ -101,9 +102,8 @@ class OutboundSyncProvider:
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
@classmethod
@property
def display_name(cls):
@classproperty
def display_name(cls): # noqa: N805
return str(cls.identifier)
@classmethod
+22 -19
View File
@@ -131,6 +131,27 @@ class PeppolIdValidator:
def __init__(self, validate_online=False):
self.validate_online = validate_online
def _validate_peppol_id_online(self, value):
base_hostnames = ['participant.sml.prod.tech.peppol.org', 'participant.sml.test.tech.peppol.org']
smp_id = base64.b32encode(hashlib.sha256(value.lower().encode()).digest()).decode().rstrip("=")
for base_hostname in base_hostnames:
smp_domain = f'{smp_id}.iso6523-actorid-upis.{base_hostname}'
resolver = dns.resolver.Resolver()
try:
answers = resolver.resolve(smp_domain, 'NAPTR', lifetime=1.0)
if answers:
return value
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
# ID not registered, do not set found=True
pass
except Exception: # noqa
# Error likely on our end or infrastructure is down, allow user to proceed
return value
raise ValidationError(
_("The Peppol participant ID is not registered on the Peppol network."),
)
def __call__(self, value):
if ":" not in value:
raise ValidationError(_("A Peppol participant ID always starts with a prefix, followed by a colon (:)."))
@@ -146,25 +167,7 @@ class PeppolIdValidator:
params={"number": prefix})
if self.validate_online:
base_hostnames = ['participant.sml.prod.tech.peppol.org', 'participant.sml.test.tech.peppol.org']
smp_id = base64.b32encode(hashlib.sha256(value.lower().encode()).digest()).decode().rstrip("=")
for base_hostname in base_hostnames:
smp_domain = f'{smp_id}.iso6523-actorid-upis.{base_hostname}'
resolver = dns.resolver.Resolver()
try:
answers = resolver.resolve(smp_domain, 'NAPTR', lifetime=1.0)
if answers:
return value
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
# ID not registered, do not set found=True
pass
except Exception: # noqa
# Error likely on our end or infrastructure is down, allow user to proceed
return value
raise ValidationError(
_("The Peppol participant ID is not registered on the Peppol network."),
)
value = self._validate_peppol_id_online(value)
return value
+21 -4
View File
@@ -151,12 +151,29 @@ class LoggingMixin:
logentry = LogEntry(content_object=self, user=user, action_type=action, event=event,
organizer_id=organizer_id, **kwargs)
if isinstance(data, dict):
sensitivekeys = ['password', 'secret', 'api_key']
# sensitivekeys are matched whether contained, not exactly
sensitivekeys = [
'auth',
'key',
'token',
'password',
'Password',
'_pass',
'salt',
'secret',
]
for sensitivekey in sensitivekeys:
for k, v in data.items():
if (sensitivekey in k) and v:
def clean_recursive(d):
data = d
data_iterator = d.items() if isinstance(d, dict) else enumerate(d)
for k, v in data_iterator:
if v and any([sensitivekey in str(k) for sensitivekey in sensitivekeys]):
data[k] = "********"
elif isinstance(v, (list, dict)):
data[k] = clean_recursive(v)
return data
data = clean_recursive(data)
logentry.data = json.dumps(data, cls=CustomJSONEncoder, sort_keys=True)
elif data:
+20 -6
View File
@@ -83,6 +83,8 @@ from .organizer import Organizer, Team
if TYPE_CHECKING:
from hierarkey.proxy import HierarkeyProxy
from .organizer import SalesChannel
logger = logging.getLogger(__name__)
@@ -336,12 +338,18 @@ class EventMixin:
return safe_string(json.dumps(eventdict))
@classmethod
def annotated(cls, qs, channel, voucher=None):
def annotated(cls, qs, channel: "str | SalesChannel | list[SalesChannel]", 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))
assert isinstance(channel, (SalesChannel, str, list))
if isinstance(channel, SalesChannel):
channel = [channel]
if isinstance(channel, list):
assert all(isinstance(c, SalesChannel) for c in channel)
sq_active_item = Item.objects.using(settings.DATABASE_REPLICA).filter_available(channel=channel, voucher=voucher).filter(
Q(variations__isnull=True)
@@ -364,8 +372,8 @@ class EventMixin:
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))
q_variation &= Q(Q(all_sales_channels=True) | Q(limit_sales_channels__in=channel))
q_variation &= Q(Q(item__all_sales_channels=True) | Q(item__limit_sales_channels__in=channel))
if voucher:
if voucher.variation_id:
@@ -1661,11 +1669,17 @@ class SubEvent(EventMixin, LoggedModel):
return qs_annotated
@classmethod
def annotated(cls, qs, channel, voucher=None):
def annotated(cls, qs, channel: "str | SalesChannel | list[SalesChannel]", voucher=None):
from .items import SubEventItem, SubEventItemVariation
from .organizer import SalesChannel
assert isinstance(channel, (str, SalesChannel))
assert isinstance(channel, (SalesChannel, str, list))
if isinstance(channel, SalesChannel):
channel = [channel]
if isinstance(channel, list):
assert all(isinstance(c, SalesChannel) for c in channel)
qs = super().annotated(qs, channel, voucher=voucher)
qs = qs.annotate(
+9 -2
View File
@@ -313,7 +313,14 @@ def filter_available(qs, channel='web', voucher=None, allow_addons=False, allow_
# makes the query SIGNIFICANTLY faster
from .organizer import SalesChannel
assert isinstance(channel, (SalesChannel, str))
assert isinstance(channel, (SalesChannel, str, list))
if isinstance(channel, SalesChannel):
channel = [channel]
if isinstance(channel, list):
assert all(isinstance(c, SalesChannel) for c in channel)
q = (
# IMPORTANT: If this is updated, also update the ItemVariation query
# in models/event.py: EventMixin.annotated()
@@ -325,7 +332,7 @@ def filter_available(qs, channel='web', voucher=None, allow_addons=False, allow_
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))
q &= Q(Q(all_sales_channels=True) | Q(limit_sales_channels__in=channel))
if not allow_addons:
q &= Q(Q(category__isnull=True) | Q(category__is_addon=False))
+1
View File
@@ -330,5 +330,6 @@ def register_default_organizer_permissions(sender, **kwargs):
PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "No access")),
PermissionOption(actions=("read",), label=pgettext_lazy("permission_level", "View")),
],
help_text=_("View access includes the option to e.g. abort pending emails or retry failed emails."),
),
]
+1 -1
View File
@@ -22,9 +22,9 @@
import os
import sys
from enum import Enum
from importlib import metadata
from typing import List
import importlib_metadata as metadata
from django.apps import AppConfig, apps
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
+2 -1
View File
@@ -2176,7 +2176,8 @@ class OrderChangeManager:
subevent=a.subevent,
voucher=None,
base_qs=Item.objects.filter(pk=a.item.pk),
allow_addons=True
allow_addons=True,
ignore_hide_sold_out_for_item_ids={a.item.pk}
)
if (not items) or items[0].current_unavailability_reason:
return False
+2 -2
View File
@@ -126,10 +126,10 @@ def generate_seats(event, subevent, plan, mapping, blocked_guids=None):
for s in current_seats.values():
if s.has_op:
raise SeatProtected(_('You can not change the plan since seat "%s" is not present in the new plan and is '
'already sold.', s.name))
'already sold.'), s.name)
if s.has_v:
raise SeatProtected(_('You can not change the plan since seat "%s" is not present in the new plan and is '
'already used in a voucher.', s.name))
'already used in a voucher.'), s.name)
Seat.objects.bulk_create(create_seats)
CartPosition.objects.filter(addon_to__seat__in=[s.pk for s in current_seats.values()]).delete()
+4 -1
View File
@@ -1199,11 +1199,14 @@ The ``sender`` keyword argument will contain the organizer.
customer_signed_in = OrganizerPluginSignal(allow_legacy_plugins=True)
"""
Arguments: ``customer``
Arguments: ``customer``, ``profile``
This signal is sent out every time a customer signs in. The ``customer`` object
is given as the first argument.
In case of SSO login, the full claims will be passed in the ``profile`` argument,
e.g. as ``oidc_claims`` for OIDC. Otherwise, ``profile`` will be ``None``.
The ``sender`` keyword argument will contain the organizer.
"""
+1 -1
View File
@@ -23,10 +23,10 @@ import json
import logging
import pathlib
import re
from importlib import metadata
from urllib.parse import urljoin
from urllib.request import urlopen
import importlib_metadata as metadata
from django import template
from django.conf import settings
from django.utils.safestring import mark_safe
@@ -16,6 +16,7 @@
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixcontrol/scss/main.scss" %}" />
<link rel="stylesheet" type="text/x-scss" href="{% static "lightbox/css/lightbox.scss" %}" />
{% endcompress %}
{% vite_importmap %}
{% if DEBUG %}
<script type="text/javascript" src="{% url 'javascript-catalog' lang=request.LANGUAGE_CODE %}"></script>
{% else %}
@@ -87,7 +88,6 @@
<meta name="theme-color" content="#3b1c4a">
<meta name="referrer" content="origin">
{% vite_importmap %}
{% block custom_header %}{% endblock %}
</head>
<body data-datetimeformat="{{ js_datetime_format }}" data-timeformat="{{ js_time_format }}"
@@ -225,6 +225,14 @@
</div>
</div>
<div class="col-md-3" id="editor-toolbox-area">
{% if checkin_qr_warning %}
<div class="alert alert-warning" id="nocheckin-notice">
{% blocktrans trimmed %}
This layout does not contain a QR code that can be used to scan tickets at check-in.
{% endblocktrans %}
</div>
{% endif %}
<div class="panel panel-default" id="toolbox">
<div class="panel-heading">
<span id="toolbox-heading">
+2 -1
View File
@@ -31,7 +31,8 @@
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import importlib_metadata as metadata
from importlib import metadata as metadata
from django.conf import settings
from django.contrib import messages
from django.db import transaction
+2
View File
@@ -65,6 +65,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
)
maxfilesize = settings.FILE_UPLOAD_MAX_SIZE_IMAGE
minfilesize = 10
checkin_qr_warning = False
title = None
def get(self, request, *args, **kwargs):
@@ -292,6 +293,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
ctx['title'] = self.title
ctx['locales'] = [p for p in settings.LANGUAGES if p[0] in self.request.event.settings.locales]
ctx['maxfilesize'] = self.maxfilesize
ctx['checkin_qr_warning'] = self.checkin_qr_warning
return ctx
+6 -6
View File
@@ -4,16 +4,16 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-28 10:48+0000\n"
"PO-Revision-Date: 2026-07-31 18:00+0000\n"
"Last-Translator: Nikolai <nikolai@lengefeldt.de>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix/da/"
">\n"
"PO-Revision-Date: 2026-10-29 17:00+0000\n"
"Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix/"
"da/>\n"
"Language: da\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 2026.7.1\n"
"X-Generator: Weblate 2026.10.1\n"
#: pretix/_base_settings.py
msgid "English"
@@ -34962,7 +34962,7 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html
#, python-format
msgid "%(num)s currently available"
msgstr "%(num)s pt. tilgængelig"
msgstr "%(num)s pt. ledig"
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar.html
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html
+32 -35
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-28 10:48+0000\n"
"PO-Revision-Date: 2026-10-26 23:00+0000\n"
"Last-Translator: Tim <plicnetwork@gmail.com>\n"
"PO-Revision-Date: 2026-10-29 17:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
"es/>\n"
"Language: es\n"
@@ -7656,17 +7656,14 @@ msgid "This gift card was used in the meantime. Please try again."
msgstr "Mientras tanto, esta tarjeta de regalo se utilizó. Inténtalo de nuevo."
#: pretix/base/payment.py
#, fuzzy
#| msgid "Gift card code"
msgid "Gift card not found."
msgstr "Código de la tarjeta de regalo"
msgstr "No se ha encontrado la tarjeta regalo."
#: pretix/base/payment.py
#, fuzzy
#| msgid "Only test gift cards can be used in test mode."
msgid "Only test gift card can be used in test mode and vice versa."
msgstr ""
"En el modo de prueba solo se pueden utilizar tarjetas regalo de prueba."
"En el modo de prueba solo se pueden utilizar tarjetas regalo de prueba, y "
"viceversa."
#: pretix/base/payment.py
msgid ""
@@ -16689,12 +16686,9 @@ msgid "Phone"
msgstr "Teléfono"
#: pretix/control/forms/organizer.py
#, fuzzy
#| msgctxt "sso_oidc"
#| msgid "Base URL"
msgctxt "sso_oidc"
msgid "Base URL / issuer"
msgstr "URL de base"
msgstr "URL base / emisor"
#: pretix/control/forms/organizer.py
msgctxt "sso_oidc"
@@ -16767,21 +16761,16 @@ msgstr ""
"endpoint de autorización. Introducir como: {example}"
#: pretix/control/forms/organizer.py pretix/control/views/organizer.py
#, fuzzy
#| msgctxt "sso_oidc"
#| msgid "Base URL"
msgid "Base URL / issuer"
msgstr "URL de base"
msgstr "URL base / emisor"
#: pretix/control/forms/organizer.py
msgid "Invalidate old client secret and generate a new one"
msgstr "Invalidar el antiguo secreto del cliente y generar uno nuevo"
#: pretix/control/forms/organizer.py pretix/control/views/organizer.py
#, fuzzy
#| msgid "will be retried"
msgid "(will be generated)"
msgstr "se volverá a intentar"
msgstr "(se generará)"
#: pretix/control/forms/organizer.py
msgid "Organizer short name"
@@ -26404,16 +26393,16 @@ msgstr "Cliente SSO:"
#: pretix/control/templates/pretixcontrol/organizers/ssoclient_edit.html
#: pretix/control/templates/pretixcontrol/organizers/ssoclients.html
#, fuzzy
#| msgid "Create a new SSO client"
msgid "Create credentials for a new SSO client"
msgstr "Crear un nuevo cliente SSO"
msgstr "Crear credenciales para un nuevo cliente de SSO"
#: pretix/control/templates/pretixcontrol/organizers/ssoclient_edit.html
msgid ""
"After submitting this form, client credentials for your SSO client will be "
"generated."
msgstr ""
"Una vez enviado este formulario, se generarán las credenciales de cliente "
"para tu cliente SSO."
#: pretix/control/templates/pretixcontrol/organizers/ssoclient_edit.html
#: pretix/control/templates/pretixcontrol/organizers/ssoclients.html
@@ -26423,6 +26412,9 @@ msgid ""
"href=\"%(url)s\">Using pretix as an SSO provider with external SSO clients</"
"a>."
msgstr ""
"Para obtener más información, consulta nuestra documentación en <a href=\"%"
"(url)s\">Uso de Pretix como proveedor de SSO con clientes de SSO externos</a>"
"."
#: pretix/control/templates/pretixcontrol/organizers/ssoclients.html
msgid ""
@@ -26430,6 +26422,9 @@ msgid ""
" \t\"Login with pretix\" in external systems\n"
" "
msgstr ""
"\n"
" \t«Iniciar sesión con pretix» en sistemas externos\n"
" "
#: pretix/control/templates/pretixcontrol/organizers/ssoclients.html
msgid ""
@@ -26454,17 +26449,13 @@ msgid "This SSO provider cannot be deleted since it has already been used."
msgstr "Este proveedor de SSO no se puede eliminar porque ya se ha utilizado."
#: pretix/control/templates/pretixcontrol/organizers/ssoprovider_edit.html
#, fuzzy
#| msgid "Delete SSO provider:"
msgid "External SSO provider:"
msgstr "Eliminar proveedor de SSO:"
msgstr "Proveedor externo de SSO:"
#: pretix/control/templates/pretixcontrol/organizers/ssoprovider_edit.html
#: pretix/control/templates/pretixcontrol/organizers/ssoproviders.html
#, fuzzy
#| msgid "Create a new SSO provider"
msgid "Connect to an external SSO provider"
msgstr "Agregar un nuevo proveedor SSO"
msgstr "Conectarse a un proveedor externo de SSO"
#: pretix/control/templates/pretixcontrol/organizers/ssoprovider_edit.html
#: pretix/control/templates/pretixcontrol/organizers/ssoproviders.html
@@ -26473,6 +26464,8 @@ msgid ""
"For more information, refer to our documentation on <a href=\"%(url)s\" "
"target=\"_blank\">Using pretix as an SSO client</a>."
msgstr ""
"Para obtener más información, consulta nuestra documentación sobre <a "
"href=\"%(url)s\" target=\"_blank\">El uso de pretix como cliente de SSO</a>."
#: pretix/control/templates/pretixcontrol/organizers/ssoprovider_edit.html
msgctxt "sso"
@@ -26485,6 +26478,9 @@ msgid ""
" \tConnect to an external login provider\n"
" "
msgstr ""
"\n"
" \tConectarse a un proveedor de inicio de sesión externo\n"
" "
#: pretix/control/templates/pretixcontrol/organizers/ssoproviders.html
msgid ""
@@ -30025,15 +30021,10 @@ msgid "The provider has been created."
msgstr "Se ha creado el proveedor."
#: pretix/control/views/organizer.py
#, fuzzy
#| msgid ""
#| "Your changes have been saved. Please note down the following client "
#| "secret, it will never be shown again: {secret}"
msgid ""
"Please note down the following client secret, it will never be shown again."
msgstr ""
"Se han guardado los cambios. Anote el siguiente secreto de cliente; no se "
"volverá a mostrar nunca más: {secret}"
"Anota el siguiente secreto de cliente; no volverá a mostrarse nunca más."
#: pretix/control/views/organizer.py
msgid ""
@@ -32586,6 +32577,12 @@ msgid ""
"one of those get linked to their own separate order page restricted to only "
"that product."
msgstr ""
"Es posible que se envíen varios correos electrónicos a la misma dirección de "
"correo si un pedido contiene varios productos para asistentes y se restringe "
"el acceso a los productos y, al mismo tiempo, se limita el envío de correos "
"únicamente a los asistentes. Esto es intencionado, ya que cada uno de ellos "
"se vincula a su propia página de pedido independiente, restringida "
"exclusivamente a ese producto."
#: pretix/plugins/sendmail/forms.py
msgid "Filter check-in status"
@@ -33498,7 +33495,7 @@ msgstr "MobilePay"
#: pretix/plugins/stripe/payment.py
msgid "Wero"
msgstr ""
msgstr "Wero"
#: pretix/plugins/stripe/payment.py
msgid "Destination"
+34 -36
View File
@@ -4,10 +4,10 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-28 10:48+0000\n"
"PO-Revision-Date: 2026-10-27 17:00+0000\n"
"PO-Revision-Date: 2026-10-29 17:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/fr/"
">\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/"
"fr/>\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -7707,16 +7707,14 @@ msgid "This gift card was used in the meantime. Please try again."
msgstr "Cette carte-cadeau a été utilisée entre-temps. Veuillez réessayer."
#: pretix/base/payment.py
#, fuzzy
#| msgid "Gift card code"
msgid "Gift card not found."
msgstr "Code carte-cadeau"
msgstr "Carte cadeau introuvable."
#: pretix/base/payment.py
#, fuzzy
#| msgid "Only test gift cards can be used in test mode."
msgid "Only test gift card can be used in test mode and vice versa."
msgstr "Seules les cartes-cadeaux de test peuvent être utilisées en mode test."
msgstr ""
"Seule la carte cadeau de test peut être utilisée en mode test, et "
"inversement."
#: pretix/base/payment.py
msgid ""
@@ -16850,12 +16848,9 @@ msgid "Phone"
msgstr "Téléphone"
#: pretix/control/forms/organizer.py
#, fuzzy
#| msgctxt "sso_oidc"
#| msgid "Base URL"
msgctxt "sso_oidc"
msgid "Base URL / issuer"
msgstr "URL de base"
msgstr "URL de base / émetteur"
#: pretix/control/forms/organizer.py
msgctxt "sso_oidc"
@@ -16928,21 +16923,16 @@ msgstr ""
"Saisissez-les sous la forme suivante : {example}"
#: pretix/control/forms/organizer.py pretix/control/views/organizer.py
#, fuzzy
#| msgctxt "sso_oidc"
#| msgid "Base URL"
msgid "Base URL / issuer"
msgstr "URL de base"
msgstr "URL de base / émetteur"
#: pretix/control/forms/organizer.py
msgid "Invalidate old client secret and generate a new one"
msgstr "Invalider l’ancienne clé secrète client et en générer une nouvelle"
#: pretix/control/forms/organizer.py pretix/control/views/organizer.py
#, fuzzy
#| msgid "will be retried"
msgid "(will be generated)"
msgstr "sera réessayé"
msgstr "(sera généré)"
#: pretix/control/forms/organizer.py
msgid "Organizer short name"
@@ -26606,16 +26596,16 @@ msgstr "Client SSO :"
#: pretix/control/templates/pretixcontrol/organizers/ssoclient_edit.html
#: pretix/control/templates/pretixcontrol/organizers/ssoclients.html
#, fuzzy
#| msgid "Create a new SSO client"
msgid "Create credentials for a new SSO client"
msgstr "Créer un nouveau client SSO"
msgstr "Créer des identifiants pour un nouveau client SSO"
#: pretix/control/templates/pretixcontrol/organizers/ssoclient_edit.html
msgid ""
"After submitting this form, client credentials for your SSO client will be "
"generated."
msgstr ""
"Une fois ce formulaire envoyé, les identifiants de votre client SSO seront "
"générés."
#: pretix/control/templates/pretixcontrol/organizers/ssoclient_edit.html
#: pretix/control/templates/pretixcontrol/organizers/ssoclients.html
@@ -26625,6 +26615,9 @@ msgid ""
"href=\"%(url)s\">Using pretix as an SSO provider with external SSO clients</"
"a>."
msgstr ""
"Pour plus d'informations, consultez notre documentation sur <a href=\"%(url)"
"s\">L'utilisation de Pretix en tant que fournisseur SSO avec des clients SSO "
"externes</a>."
#: pretix/control/templates/pretixcontrol/organizers/ssoclients.html
msgid ""
@@ -26632,6 +26625,9 @@ msgid ""
" \t\"Login with pretix\" in external systems\n"
" "
msgstr ""
"\n"
" \t« Se connecter avec pretix » dans les systèmes externes\n"
" "
#: pretix/control/templates/pretixcontrol/organizers/ssoclients.html
msgid ""
@@ -26659,17 +26655,13 @@ msgstr ""
"Ce fournisseur SSO ne peut pas être supprimé car il a déjà été utilisé."
#: pretix/control/templates/pretixcontrol/organizers/ssoprovider_edit.html
#, fuzzy
#| msgid "Delete SSO provider:"
msgid "External SSO provider:"
msgstr "Supprimer le fournisseur SSO :"
msgstr "Fournisseur SSO externe :"
#: pretix/control/templates/pretixcontrol/organizers/ssoprovider_edit.html
#: pretix/control/templates/pretixcontrol/organizers/ssoproviders.html
#, fuzzy
#| msgid "Create a new SSO provider"
msgid "Connect to an external SSO provider"
msgstr "Créer un nouveau fournisseur SSO"
msgstr "Se connecter à un fournisseur SSO externe"
#: pretix/control/templates/pretixcontrol/organizers/ssoprovider_edit.html
#: pretix/control/templates/pretixcontrol/organizers/ssoproviders.html
@@ -26678,6 +26670,8 @@ msgid ""
"For more information, refer to our documentation on <a href=\"%(url)s\" "
"target=\"_blank\">Using pretix as an SSO client</a>."
msgstr ""
"Pour plus d'informations, consultez notre documentation sur <a href=\"%(url)"
"s\" target=\"_blank\">L'utilisation de pretix en tant que client SSO</a>."
#: pretix/control/templates/pretixcontrol/organizers/ssoprovider_edit.html
msgctxt "sso"
@@ -26690,6 +26684,9 @@ msgid ""
" \tConnect to an external login provider\n"
" "
msgstr ""
"\n"
" \tSe connecter à un fournisseur d'authentification externe\n"
" "
#: pretix/control/templates/pretixcontrol/organizers/ssoproviders.html
msgid ""
@@ -30269,15 +30266,10 @@ msgid "The provider has been created."
msgstr "Le fournisseur a été créé."
#: pretix/control/views/organizer.py
#, fuzzy
#| msgid ""
#| "Your changes have been saved. Please note down the following client "
#| "secret, it will never be shown again: {secret}"
msgid ""
"Please note down the following client secret, it will never be shown again."
msgstr ""
"Vos modifications ont été enregistrées. Veuillez noter la clé secrète client "
"suivante, elle ne sera plus jamais affichée : {secret}"
"Veuillez noter le secret client suivant ; il ne s'affichera plus jamais."
#: pretix/control/views/organizer.py
msgid ""
@@ -32862,6 +32854,12 @@ msgid ""
"one of those get linked to their own separate order page restricted to only "
"that product."
msgstr ""
"Il se peut que plusieurs e-mails soient envoyés à la même adresse e-mail si "
"une commande comprend plusieurs produits destinés à des participants, "
"lorsque vous limitez l'accès aux produits tout en restreignant les e-mails "
"aux seuls participants. Ce comportement est normal, car chacun de ces "
"produits est associé à sa propre page de commande, dont l'accès est limité à "
"ce produit uniquement."
#: pretix/plugins/sendmail/forms.py
msgid "Filter check-in status"
@@ -33777,7 +33775,7 @@ msgstr "MobilePay"
#: pretix/plugins/stripe/payment.py
msgid "Wero"
msgstr ""
msgstr "Wero"
#: pretix/plugins/stripe/payment.py
msgid "Destination"
+7 -8
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-28 10:48+0000\n"
"PO-Revision-Date: 2026-04-21 11:05+0000\n"
"Last-Translator: Sandra Rial Pérez <sandrarial@gestiontickets.online>\n"
"PO-Revision-Date: 2026-11-02 11:32+0000\n"
"Last-Translator: Isaac Raño <isaac@ovixia.com>\n"
"Language-Team: Galician <https://translate.pretix.eu/projects/pretix/pretix/"
"gl/>\n"
"Language: gl\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.17\n"
"X-Generator: Weblate 2026.10.1\n"
#: pretix/_base_settings.py
msgid "English"
@@ -93,7 +93,7 @@ msgstr "Hebreo"
#: pretix/_base_settings.py
msgid "Hungarian"
msgstr ""
msgstr "Húngaro"
#: pretix/_base_settings.py
msgid "Indonesian"
@@ -153,7 +153,7 @@ msgstr "Español (América Latina)"
#: pretix/_base_settings.py
msgid "Thai"
msgstr ""
msgstr "Tailandés"
#: pretix/_base_settings.py
msgid "Turkish"
@@ -409,7 +409,7 @@ msgstr ""
#: pretix/api/serializers/organizer.py pretix/control/views/organizer.py
#, python-format
msgid "You've been invited to join %(organizer)s"
msgstr ""
msgstr "Convidáronte a unirte a %(organizer)s"
#: pretix/api/serializers/organizer.py pretix/control/views/organizer.py
msgid "This user already has been invited for this team."
@@ -429,9 +429,8 @@ msgid "Medium connected to other event"
msgstr "O medio conectou a outro acontecemento"
#: pretix/api/views/checkin.py
#, fuzzy
msgid "You cannot exchange a medium for a medium."
msgstr "No puede cancelar este pedido."
msgstr "Non se pode trocar un medio por outro."
#: pretix/api/views/checkin.py
#, fuzzy
+33 -8
View File
@@ -7,16 +7,16 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-28 10:48+0000\n"
"PO-Revision-Date: 2026-10-19 02:00+0000\n"
"PO-Revision-Date: 2026-11-03 08:12+0000\n"
"Last-Translator: Christiaan de Die le Clercq <contact@techwolf12.nl>\n"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/"
">\n"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/>"
"\n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 2026.10.1\n"
"X-Generator: Weblate 2026.11\n"
#: pretix/_base_settings.py
msgid "English"
@@ -8379,12 +8379,12 @@ msgstr "Niet ingesteld"
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative date cannot be expressed as \"before\" for \"{}\""
msgstr ""
msgstr "Een relatieve datum kan niet worden uitgedrukt als \"voor\" voor \"{}\""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative date cannot be expressed as \"after\" for \"{}\""
msgstr ""
msgstr "Een relatieve datum kan niet worden uitgedrukt als \"na\" voor \"{}\""
#: pretix/base/reldate.py
#, python-brace-format
@@ -8415,7 +8415,7 @@ msgstr "Evenement geannuleerd"
#: pretix/base/services/cancelevent.py
msgid "Confirm event cancellation and bulk refund"
msgstr ""
msgstr "Evenementannulering en bulkterugbetaling bevestigen"
#: pretix/base/services/cart.py pretix/base/services/modelimport.py
#: pretix/base/services/orders.py
@@ -9355,6 +9355,7 @@ msgstr "Een voucher kan niet worden aangemaakt zonder code."
msgid ""
"Voucher codes must be unique. Code \"{code}\" already exists in this import."
msgstr ""
"Vouchercodes moeten uniek zijn. Code \"{code}\" bestaat al in deze import."
#: pretix/base/services/modelimport.py
#, python-brace-format
@@ -9363,13 +9364,18 @@ msgid ""
msgid_plural ""
"Voucher codes must be unique. Import contains existing voucher codes {code}."
msgstr[0] ""
"Vouchercodes moeten uniek zijn. Import bevat al bestaande vouchercode {code}."
msgstr[1] ""
"Vouchercodes moeten uniek zijn. Import bevat al bestaande vouchercodes {code}"
"."
#: pretix/base/services/modelimport.py
msgid ""
"Vouchers could not be imported, probably due to a voucher code already being "
"in use."
msgstr ""
"Vouchers konden niet worden geïmporteerd, waarschijnlijk omdat een "
"vouchercode al in gebruik is."
#: pretix/base/services/orders.py
msgid ""
@@ -10431,7 +10437,7 @@ msgstr ""
#: pretix/base/settings.py
msgid "No dates match your criteria."
msgstr ""
msgstr "Geen data voldoen aan je zoekcriteria."
#: pretix/base/settings.py
msgctxt "subevents"
@@ -10632,6 +10638,10 @@ msgid ""
"their order without selecting a payment method. This is useful whenever "
"orders are not created by the same person who is making the payment."
msgstr ""
"Als uitgestelde betaling is toegestaan op een verkoopkanaal, kunnen klanten "
"hun bestelling afronden zonder een betaalmethode te kiezen. Dit is handig "
"wanneer bestellingen niet worden geplaatst door dezelfde persoon als degene "
"die de betaling uitvoert."
#: pretix/base/settings.py
msgid "Show start date"
@@ -11603,6 +11613,10 @@ msgid ""
"the email address above. Please note that you still need to add a contact "
"email address that will be shared with all emails you send."
msgstr ""
"Als je dit instelt, verwijst de contactlink in de voettekst hiernaartoe in "
"plaats van naar het bovenstaande e-mailadres. Let op dat je nog steeds een "
"contact-e-mailadres moet toevoegen dat wordt meegestuurd met alle e-mails "
"die je verzendt."
#: pretix/base/settings.py pretix/control/forms/event.py
msgid "Imprint URL"
@@ -13494,6 +13508,8 @@ msgid ""
"Don't share this code with anyone. The %(instance)s team will never ask you "
"for it."
msgstr ""
"Deel deze code met niemand. Het %(instance)s-team zal je hier nooit om "
"vragen."
#: pretix/base/templates/pretixbase/email/cancel_confirm.txt
#: pretix/base/templates/pretixbase/email/export_failed.txt
@@ -13502,6 +13518,8 @@ msgid ""
"Thanks, \n"
"The %(instance)s Team"
msgstr ""
"Bedankt, \n"
"Het %(instance)s-team"
#: pretix/base/templates/pretixbase/email/email_footer.html
#, python-format
@@ -13698,11 +13716,14 @@ msgstr "Ga verder in een nieuw tabblad"
#: pretix/base/templates/pretixbase/framebreak.html
msgid "For security reasons, the following step is only possible in a new tab."
msgstr ""
"Om veiligheidsredenen is de volgende stap alleen mogelijk in een nieuw "
"tabblad."
#: pretix/base/templates/pretixbase/framebreak.html
msgid ""
"If the new tab did not open automatically, please click the following button:"
msgstr ""
"Als het nieuwe tabblad niet automatisch opent, klik dan op de volgende knop:"
#: pretix/base/templates/pretixbase/framebreak.html
#: pretix/presale/templates/pretixpresale/event/cookies.html
@@ -37134,6 +37155,10 @@ msgid ""
"before retrying can help. As a last step, try using a different browser or "
"another device (for example, a desktop or laptop computer)."
msgstr ""
"Als het probleem zich blijft voordoen, kan het helpen om alle "
"browservensters te sluiten en cookies te verwijderen voordat je het opnieuw "
"probeert. Als laatste stap kun je proberen een andere browser of een ander "
"apparaat (bijvoorbeeld een desktop of laptop) te gebruiken."
#: pretix/presale/templates/pretixpresale/organizers/customer_membership.html
msgid "Your membership"
+151 -54
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-28 10:48+0000\n"
"PO-Revision-Date: 2026-09-02 08:35+0000\n"
"Last-Translator: Renne Rocha <renne@rocha.dev.br>\n"
"PO-Revision-Date: 2026-10-30 03:00+0000\n"
"Last-Translator: vihumelo <victordimelo@gmail.com>\n"
"Language-Team: Portuguese (Brazil) <https://translate.pretix.eu/projects/"
"pretix/pretix/pt_BR/>\n"
"Language: pt_BR\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 2026.8.1\n"
"X-Generator: Weblate 2026.10.1\n"
#: pretix/_base_settings.py
msgid "English"
@@ -411,7 +411,7 @@ msgstr ""
#: pretix/api/serializers/organizer.py pretix/control/views/organizer.py
#, python-format
msgid "You've been invited to join %(organizer)s"
msgstr ""
msgstr "Você recebeu um convite para integrar %(organizer)s"
#: pretix/api/serializers/organizer.py pretix/control/views/organizer.py
msgid "This user already has been invited for this team."
@@ -607,6 +607,9 @@ msgid ""
"This includes related events like creation, deletion, opening or closing of "
"quotas. No webhook is sent for changes to the resulting availability."
msgstr ""
"Isso inclui eventos relacionados, como a criação, exclusão, abertura ou "
"fechamento de cotas. Nenhum webhook é enviado em decorrência de alterações "
"na disponibilidade."
#: pretix/api/webhooks.py
msgid "Shop taken live"
@@ -1678,7 +1681,7 @@ msgstr "Endereço"
#: pretix/base/settings.py pretix/control/forms/filter.py
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
msgid "ZIP code"
msgstr "Código Postal"
msgstr "CEP"
#: pretix/base/exporters/invoices.py pretix/base/exporters/orderlist.py
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
@@ -3622,6 +3625,9 @@ msgid ""
"The Peppol participant ID prefix %(number)s is not known to our system. "
"Please reach out to us if you are sure this ID is correct."
msgstr ""
"Nosso sistema não reconhece o prefixo %(number)s do identificador de "
"participante da Peppol. Se você tem certeza de que esse identificador está "
"correto, entre em contato conosco."
#: pretix/base/invoicing/peppol.py
#, python-format
@@ -5600,6 +5606,9 @@ msgid ""
"prior to their usage. Therefore, the selected media policy does not make "
"sense for this media type."
msgstr ""
"O tipo de mídia selecionado exige que todas as mídias sejam cadastradas no "
"sistema antes de serem utilizadas. Portanto, a política de mídia selecionada "
"não é adequada para esse tipo de mídia."
#: pretix/base/models/items.py
msgid ""
@@ -5821,7 +5830,7 @@ msgstr "Código do país (ISO 3166-1 alfa-2)"
#: pretix/base/models/items.py
msgid "Asked on"
msgstr ""
msgstr "Pergunta feita em"
#: pretix/base/models/items.py pretix/base/models/organizer.py
msgid ""
@@ -6171,12 +6180,12 @@ msgstr "retornou"
#: pretix/base/models/media.py
msgctxt "reusable_medium"
msgid "Claim token"
msgstr ""
msgstr "Token de vinculação"
#: pretix/base/models/media.py
msgctxt "reusable_medium"
msgid "Label"
msgstr ""
msgstr "Rótulo"
#: pretix/base/models/media.py
#, fuzzy
@@ -6190,6 +6199,9 @@ msgid ""
"validity. If multiple tickets are valid at once, this will lead to failed "
"check-ins."
msgstr ""
"Se você vincular mais de um ingresso, certifique-se de que os períodos de "
"validade não se sobreponham. Se vários ingressos estiverem válidos ao mesmo "
"tempo, poderão ocorrer falhas no check-in."
#: pretix/base/models/memberships.py
#: pretix/presale/templates/pretixpresale/organizers/customer_memberships.html
@@ -8119,6 +8131,8 @@ msgstr "Apenas API"
msgid ""
"Menu item will only show up if the user has permission for general settings."
msgstr ""
"Esta opção só será exibida no menu se o usuário tiver permissão para acessar "
"as configurações gerais."
#: pretix/base/permissions.py
msgctxt "permission_level"
@@ -8136,6 +8150,8 @@ msgid ""
"This includes access to all settings not listed explicitly below, including "
"plugin settings."
msgstr ""
"Isso inclui o acesso a todas as configurações que não estão explicitamente "
"listadas abaixo, incluindo as configurações dos plugins."
#: pretix/base/permissions.py
#: pretix/control/templates/pretixcontrol/event/payment.html
@@ -8165,7 +8181,7 @@ msgstr "Produtos, cotas e perguntas"
#: pretix/base/permissions.py
msgid "Also includes related objects like categories or discounts."
msgstr ""
msgstr "Também inclui elementos relacionados, como categorias ou descontos."
#: pretix/base/permissions.py
msgctxt "permission_level"
@@ -8219,12 +8235,14 @@ msgstr "Acessar eventos existentes"
#: pretix/base/permissions.py
msgctxt "permission_level"
msgid "Access existing and create new events"
msgstr ""
msgstr "Acessar eventos existentes e criar novos eventos"
#: pretix/base/permissions.py
msgid ""
"The level of access to events is determined in detail by the settings below."
msgstr ""
"O nível de acesso aos eventos é definido detalhadamente pelas configurações "
"abaixo."
#: pretix/base/permissions.py pretix/control/navigation.py
#: pretix/control/templates/pretixcontrol/event/invoicing.html
@@ -8241,12 +8259,17 @@ msgid ""
"This includes access to all organizer-level functionality not listed "
"explicitly below, including plugin settings."
msgstr ""
"Isso inclui o acesso a todas as funcionalidades disponíveis no nível do "
"organizador que não estejam explicitamente listadas abaixo, incluindo as "
"configurações dos plugins."
#: pretix/base/permissions.py
msgid ""
"Includes the ability to give someone (including oneself) additional "
"permissions."
msgstr ""
"Inclui a possibilidade de conceder permissões adicionais a alguém (inclusive "
"a si mesmo)."
#: pretix/base/permissions.py pretix/control/navigation.py
msgid "Devices"
@@ -8257,6 +8280,8 @@ msgid ""
"Includes the ability to give access to events and data oneself does not have "
"access to."
msgstr ""
"Inclui a possibilidade de conceder acesso a eventos e dados aos quais você "
"mesmo não tem acesso."
#: pretix/base/permissions.py
#, fuzzy
@@ -8349,22 +8374,22 @@ msgstr "Não definido"
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative date cannot be expressed as \"before\" for \"{}\""
msgstr ""
msgstr "Uma data relativa não pode ser expressa como \"antes de\" para \"{}\""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative date cannot be expressed as \"after\" for \"{}\""
msgstr ""
msgstr "Uma data relativa não pode ser expressa como \"depois de\" para \"{}\""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative time cannot be expressed as \"before\" for \"{}\""
msgstr ""
msgstr "Um tempo relativo não pode ser expresso como \"antes de\" para \"{}\""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative time cannot be expressed as \"after\" for \"{}\""
msgstr ""
msgstr "Um tempo relativo não pode ser expresso como \"depois de\" para \"{}\""
#: pretix/base/secrets.py
msgid "Random (default, works with all pretix apps)"
@@ -8385,7 +8410,7 @@ msgstr "Evento cancelado"
#: pretix/base/services/cancelevent.py
msgid "Confirm event cancellation and bulk refund"
msgstr ""
msgstr "Confirmar o cancelamento do evento e o reembolso em massa"
#: pretix/base/services/cart.py pretix/base/services/modelimport.py
#: pretix/base/services/orders.py
@@ -8933,13 +8958,15 @@ msgstr "Você precisa responder a perguntas para concluir este check-in."
#: pretix/base/services/checkin.py
msgid "Ticket needs to be exchanged to a suitable medium."
msgstr ""
msgstr "O ingresso precisa ser trocado por um meio adequado."
#: pretix/base/services/checkin.py
msgid ""
"This ticket has already been exchanged for a reusable medium that now needs "
"to be used instead."
msgstr ""
"Este ingresso já foi trocado por uma mídia reutilizável, que agora deve ser "
"utilizado em seu lugar."
#: pretix/base/services/checkin.py
msgid "This ticket has already been redeemed."
@@ -9105,62 +9132,44 @@ msgid "You are receiving this email because you placed an order for {event}."
msgstr "Você está recebendo este email porque fez um pedido para {event}."
#: pretix/base/services/media.py
#, fuzzy
#| msgid "Invalid input type."
msgid "Invalid medium type."
msgstr "Tipo de entrada inválido."
msgstr "Tipo de mídia inválido."
#: pretix/base/services/media.py
#, fuzzy
#| msgid "The selected media type is not enabled in your organizer settings."
msgid "Medium type is not enabled for organizer."
msgstr ""
"O tipo de mídia selecionado não está habilitado nas configurações do "
"organizador."
msgstr "O tipo de mídia não está habilitado para o organizador."
#: pretix/base/services/media.py
msgid "Incorrect medium type for product."
msgstr ""
msgstr "Tipo de mídia incorreto para o produto."
#: pretix/base/services/media.py
#, fuzzy
#| msgid "This ticket has already been redeemed."
msgid "Ticket is already exchanged for reusable medium."
msgstr "Este ingresso já foi resgatado."
msgstr "Este ingresso já foi trocado por uma mídia reutilizável."
#: pretix/base/services/media.py
#, fuzzy
#| msgid "Reusable Medium ID"
msgid "Reusable medium not found."
msgstr "ID de mídia reutilizável"
msgstr "Mídia reutilizável não encontrada."
#: pretix/base/services/media.py
#, fuzzy
#| msgid "The reusable medium has been created."
msgid "Reusable medium is inactive or expired."
msgstr "A mídia reutilizável foi criada."
msgstr "A mídia reutilizável está inativa ou expirada."
#: pretix/base/services/media.py
#, fuzzy
#| msgid "The reusable medium has been created."
msgid "Reusable medium not found and could not be created."
msgstr "A mídia reutilizável foi criada."
msgstr "A mídia reutilizável não foi encontrada e não pôde ser criada."
#: pretix/base/services/media.py
#, fuzzy
#| msgid "Reusable media type"
msgid "Reusable medium already exists."
msgstr "Tipo de mídia reutilizável"
msgstr "A mídia reutilizável já existe."
#: pretix/base/services/media.py
#, fuzzy
#| msgid "The reusable medium has been created."
msgid "Reusable medium could not be created."
msgstr "A mídia reutilizável foi criada."
msgstr "Não foi possível criar a mídia reutilizável."
#: pretix/base/services/media.py
msgid "Product does not support medium exchange."
msgstr ""
msgstr "O produto não permite a troca por mídia."
#: pretix/base/services/memberships.py
#, python-brace-format
@@ -9325,6 +9334,8 @@ msgstr "Um cupom não pode ser criado sem um código."
msgid ""
"Voucher codes must be unique. Code \"{code}\" already exists in this import."
msgstr ""
"Os códigos dos cupons devem ser únicos. O código \"{code}\" já existe nesta "
"importação."
#: pretix/base/services/modelimport.py
#, python-brace-format
@@ -9333,13 +9344,19 @@ msgid ""
msgid_plural ""
"Voucher codes must be unique. Import contains existing voucher codes {code}."
msgstr[0] ""
"Os códigos dos cupons devem ser únicos. A importação contém um código de "
"cupom já existente: {code}."
msgstr[1] ""
"Os códigos dos cupons devem ser únicos. A importação contém códigos de "
"cupons já existentes: {code}."
#: pretix/base/services/modelimport.py
msgid ""
"Vouchers could not be imported, probably due to a voucher code already being "
"in use."
msgstr ""
"Não foi possível importar os cupons, provavelmente porque um dos códigos já "
"está em uso."
#: pretix/base/services/orders.py
msgid ""
@@ -9917,7 +9934,7 @@ msgstr ""
#: pretix/base/settings.py
msgid "Enforce the usage of issued reusable media for check-in"
msgstr ""
msgstr "Exigir o uso de mídias reutilizáveis emitidas para realizar o check-in"
#: pretix/base/settings.py
msgid ""
@@ -9925,6 +9942,10 @@ msgid ""
"medium has been created and linked to a ticket. Keeping this option turned "
"off will treat the reusable medium and ticket as equals."
msgstr ""
"Se esta opção estiver ativada, o código de barras do ingresso não será mais "
"aceito caso uma mídia reutilizável tenha sido criada e vinculada a ele. Se a "
"opção permanecer desativada, a mídia reutilizável e o ingresso serão "
"considerados equivalentes."
#: pretix/base/settings.py
msgid "Length of barcodes"
@@ -10392,12 +10413,12 @@ msgstr ""
#: pretix/base/settings.py
msgid "No dates match your criteria."
msgstr ""
msgstr "Nenhuma data corresponde aos seus critérios."
#: pretix/base/settings.py
msgctxt "subevents"
msgid "Text for empty date results"
msgstr ""
msgstr "Texto exibido quando a data não retorna resultados"
#: pretix/base/settings.py
msgctxt "subevents"
@@ -10408,6 +10429,11 @@ msgid ""
"touch with you to arrange further dates. We do not recommend more than one "
"or two sentences."
msgstr ""
"Este texto será exibido quando o calendário ou a lista de datas estiver "
"vazio, por exemplo, quando não houver datas disponíveis em determinado mês "
"ou quando um filtro selecionado pelo usuário não retornar resultados. Você "
"pode usar esse espaço para informar como entrar em contato com você para "
"combinar novas datas. Recomendamos usar apenas uma ou duas frases."
#: pretix/base/settings.py
msgid "Guidance text"
@@ -10593,6 +10619,9 @@ msgid ""
"their order without selecting a payment method. This is useful whenever "
"orders are not created by the same person who is making the payment."
msgstr ""
"Se o pagamento posterior for permitido em um canal de vendas, os clientes "
"poderão concluir seus pedidos sem selecionar uma forma de pagamento. Isso é "
"útil quando a pessoa que faz o pedido não é a mesma que realiza o pagamento."
#: pretix/base/settings.py
msgid "Show start date"
@@ -11561,6 +11590,10 @@ msgid ""
"the email address above. Please note that you still need to add a contact "
"email address that will be shared with all emails you send."
msgstr ""
"Se você configurar esta opção, o link de contato no rodapé direcionará para "
"o endereço informado aqui, em vez de utilizar o endereço de e-mail acima. "
"Observe que ainda será necessário informar um endereço de e-mail de contato, "
"que será incluído em todos os e-mails enviados."
#: pretix/base/settings.py pretix/control/forms/event.py
msgid "Imprint URL"
@@ -13156,7 +13189,7 @@ msgstr ""
#: pretix/base/settings.py
#, python-brace-format
msgid "VAT-ID is not supported for \"{}\"."
msgstr ""
msgstr "O VAT-ID não é compatível para \"{}\"."
#: pretix/base/settings.py
msgid "The last payment date cannot be before the end of presale."
@@ -13451,6 +13484,8 @@ msgid ""
"Don't share this code with anyone. The %(instance)s team will never ask you "
"for it."
msgstr ""
"Não compartilhe este código com ninguém. A equipe do %(instance)s nunca "
"solicitará esse código."
#: pretix/base/templates/pretixbase/email/cancel_confirm.txt
#: pretix/base/templates/pretixbase/email/export_failed.txt
@@ -13459,6 +13494,8 @@ msgid ""
"Thanks, \n"
"The %(instance)s Team"
msgstr ""
"Obrigado,\n"
"Time %(instance)s"
#: pretix/base/templates/pretixbase/email/email_footer.html
#, python-format
@@ -14021,6 +14058,8 @@ msgid ""
"You entered an URL, which is not allowed. Please remove %(match)s from your "
"input."
msgstr ""
"Você inseriu uma URL, o que não é permitido. Remova %(match)s do texto "
"informado."
#: pretix/base/views/errors.py
msgid ""
@@ -14281,6 +14320,8 @@ msgid ""
"You cannot choose a team that would give you more access than you have on "
"the event you are copying."
msgstr ""
"Você não pode escolher um time que lhe conceda um nível de acesso superior "
"ao que você possui no evento que está copiando."
#: pretix/control/forms/event.py
msgid "Copy configuration from"
@@ -14295,6 +14336,8 @@ msgid ""
"You cannot choose an event on which you have less access than the team you "
"selected in the previous step."
msgstr ""
"Você não pode escolher um evento no qual tenha um nível de acesso inferior "
"ao do time selecionado na etapa anterior."
#: pretix/control/forms/event.py pretix/control/forms/item.py
#: pretix/control/forms/subevents.py
@@ -14397,7 +14440,7 @@ msgstr "Data de pagamento"
#: pretix/control/forms/event.py
msgid "same as above"
msgstr ""
msgstr "o mesmo que acima"
#: pretix/control/forms/event.py
#, fuzzy
@@ -14479,6 +14522,13 @@ msgid ""
"configured whenever possible. Gross prices may still change if they are "
"impossible to derive from a rounded net price."
msgstr ""
"Recomendado para a emissão de faturas eletrônicas quando você vende "
"principalmente para consumidores finais. Os preços brutos ou líquidos de "
"alguns produtos podem ser ajustados automaticamente para garantir o "
"arredondamento correto do total do pedido. O sistema procura manter os "
"preços brutos conforme configurados sempre que possível. No entanto, esses "
"preços ainda podem sofrer alterações quando não for possível calculá-los a "
"partir de um preço líquido arredondado."
#: pretix/control/forms/event.py
msgid "Generate invoices for Sales channels"
@@ -16648,7 +16698,7 @@ msgstr "Você pode definir apenas um domínio de organizador."
#: pretix/control/forms/organizer.py
msgid "Provided by a plugin"
msgstr ""
msgstr "Fornecido por um plugin"
#: pretix/control/forms/organizer.py
msgid ""
@@ -16906,7 +16956,7 @@ msgstr "terceiro"
#: pretix/control/forms/rrule.py
msgctxt "rrule"
msgid "fourth"
msgstr ""
msgstr "quarto"
#: pretix/control/forms/rrule.py
msgctxt "rrule"
@@ -16925,7 +16975,7 @@ msgstr "Dia do fim de semana"
#: pretix/control/forms/subevents.py
msgctxt "subevent"
msgid "Skip dates that overlap with any existing date"
msgstr ""
msgstr "Ignorar datas que se sobreponham a qualquer data existente"
#: pretix/control/forms/subevents.py
msgctxt "subevent"
@@ -16935,6 +16985,10 @@ msgid ""
"This respects even inactive dates and works best if all dates have both a "
"start and end time."
msgstr ""
"Isso pode ser útil se todas as datas ocorrerem no mesmo local e você quiser "
"evitar a criação de datas recorrentes que entrem em conflito com eventos "
"especiais já existentes. Essa opção também considera as datas inativas e "
"funciona melhor quando todas as datas têm horários de início e término."
#: pretix/control/forms/subevents.py
msgid "Keep the current values"
@@ -16984,6 +17038,9 @@ msgid ""
"because at least one of the selected vouchers has already been redeemed "
"%(max_redeemed)s times."
msgstr ""
"Você não pode reduzir o número máximo de resgates para %(max_usages)s, pois "
"pelo menos um dos cupons selecionados já foi resgatato %(max_redeemed)s "
"vezes."
#: pretix/control/forms/vouchers.py
#, fuzzy
@@ -17017,6 +17074,8 @@ msgid ""
"Changing the maximum number of usages in bulk is not supported if any of the "
"selected vouchers is assigned a seat."
msgstr ""
"Não é possível alterar em massa o número máximo de utilizações se algum dos "
"cupons selecionados estiver vinculado a um assento."
#: pretix/control/forms/vouchers.py
msgctxt "subevent"
@@ -17024,18 +17083,24 @@ msgid ""
"Changing the date in bulk is not supported if any of the selected vouchers "
"is assigned a seat."
msgstr ""
"Não é possível alterar a data em massa se algum dos cupons selecionados "
"estiver vinculado a um assento."
#: pretix/control/forms/vouchers.py
msgid ""
"Changing the product to a quota is not supported if any of the selected "
"vouchers is assigned a seat."
msgstr ""
"Não é possível alterar o produto para uma cota se algum dos cupons "
"selecionados estiver vinculado a um assento."
#: pretix/control/forms/vouchers.py
msgid ""
"This change cannot be completed because not all assigned seats of the "
"vouchers are still available"
msgstr ""
"Esta alteração não pode ser concluída porque nem todos os assentos "
"vinculados aos cupons ainda estão disponíveis"
#: pretix/control/forms/vouchers.py
msgid "Codes"
@@ -18198,6 +18263,8 @@ msgid ""
"The ticket #{positionid} was exchanged for reusable medium "
"{medium_identifier}."
msgstr ""
"O ingresso #{positionid} foi trocado pela mídia reutilizável "
"{medium_identifier}."
#: pretix/control/logdisplay.py
msgid "The medium has been connected to a new gift card."
@@ -18255,6 +18322,8 @@ msgstr "Um código de emergência de dois fatores foi gerado."
#: pretix/control/logdisplay.py
msgid "Drift and throttle values for two-factor devices have been reset."
msgstr ""
"Os dados de sincronização e de controle de tentativas dos dispositivos de "
"autenticação de dois fatores foram redefinidos."
#: pretix/control/logdisplay.py
#, python-brace-format
@@ -20067,6 +20136,8 @@ msgid ""
"This ticket needs to be exchanged into a <strong>%(media_type)s</strong> "
"reusable medium. <strong>%(media_policy)s</strong>."
msgstr ""
"Este ingresso precisa ser trocado por uma mídia reutilizável do tipo <strong>"
"%(media_type)s</strong>. <strong>%(media_policy)s</strong>."
#: pretix/control/templates/pretixcontrol/checkin/simulator.html
msgid "Special attention required"
@@ -20322,6 +20393,18 @@ msgid ""
"Thanks, \n"
"The %(instance)s Team\n"
msgstr ""
"Olá,\n"
"\n"
"Recebemos uma solicitação para redefinir a senha da sua conta %(instance)s. "
"Para definir uma nova senha, acesse o link abaixo:\n"
"\n"
"%(url)s\n"
"\n"
"Se você não fez essa solicitação, pode ignorar este e-mail com segurança — "
"sua senha não será alterada.\n"
"\n"
"Obrigado,\n"
"Equipe %(instance)s\n"
#: pretix/control/templates/pretixcontrol/email/invitation.txt
#, fuzzy, python-format
@@ -20385,14 +20468,17 @@ msgid ""
"\n"
"We noticed a new sign-in to your %(instance)s account:\n"
msgstr ""
"Olá,\n"
"\n"
"Identificamos um novo acesso à sua conta %(instance)s:\n"
#: pretix/control/templates/pretixcontrol/email/login_notice.txt
msgid "Browser"
msgstr ""
msgstr "Navegador"
#: pretix/control/templates/pretixcontrol/email/login_notice.txt
msgid "Operating system"
msgstr ""
msgstr "Sistema operacional"
#: pretix/control/templates/pretixcontrol/email/login_notice.txt
#, python-format
@@ -20407,6 +20493,14 @@ msgid ""
"Thanks, \n"
"The %(instance)s Team\n"
msgstr ""
"Se foi você, nenhuma ação é necessária.\n"
"\n"
"Se você não reconhece este acesso, altere sua senha imediatamente:\n"
"\n"
"%(url)s\n"
"\n"
"Obrigado,\n"
"Equipe %(instance)s\n"
#: pretix/control/templates/pretixcontrol/email/security_notice.txt
#, fuzzy, python-format
@@ -22387,6 +22481,9 @@ msgid ""
"debugging problems in your pretix installation, and verifying that your "
"usage of pretix is in compliance with the Enterprise license you purchased."
msgstr ""
"Isso tem duas finalidades: coletar informações úteis que possam ajudar a "
"diagnosticar problemas na sua instalação do pretix e verificar se o uso do "
"pretix está em conformidade com a licença Enterprise que você adquiriu."
#: pretix/control/templates/pretixcontrol/global_sysreport.html
msgid "First month of license term:"
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-30 07:26+0000\n"
"PO-Revision-Date: 2026-03-25 08:00+0000\n"
"Last-Translator: Renne Rocha <renne@rocha.dev.br>\n"
"PO-Revision-Date: 2026-10-30 03:00+0000\n"
"Last-Translator: vihumelo <victordimelo@gmail.com>\n"
"Language-Team: Portuguese (Brazil) <https://translate.pretix.eu/projects/"
"pretix/pretix-js/pt_BR/>\n"
"Language: pt_BR\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 5.16.2\n"
"X-Generator: Weblate 2026.10.1\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js
msgid "Marked as paid"
@@ -430,7 +430,7 @@ msgstr "Pressione Ctrl+C para copiar!"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
msgid "Edit"
msgstr ""
msgstr "Editar"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
msgid "Visualize"
+8
View File
@@ -196,6 +196,14 @@ OPTIONS = OrderedDict([
'offsets': [46 * mm, 46 * mm],
'pagesize': pagesizes.A4,
}),
('herma_75x40', {
'name': 'HERMA 75 x 40 mm (9010)',
'cols': 2,
'rows': 6,
'margins': [28.5 * mm, 25 * mm, 28.5 * mm, 25 * mm],
'offsets': [85 * mm, 40 * mm],
'pagesize': pagesizes.A4,
}),
('herma_88.9x33.87', {
'name': 'HERMA 88,9 x 33,87 mm (4515)',
'cols': 2,
+5
View File
@@ -219,6 +219,11 @@ TEMPLATES = {
"pagesize": (75 * mm, 52 * mm),
"layout": _simple_template(75 * mm, 52 * mm),
},
"75x40": {
"label": format_lazy(_("{width} x {height} mm label"), width=75, height=40),
"pagesize": (75 * mm, 40 * mm),
"layout": _simple_template(75 * mm, 40 * mm),
},
"70x36": {
"label": format_lazy(_("{width} x {height} mm label"), width=70, height=36),
"pagesize": (70 * mm, 36 * mm),
+1 -1
View File
@@ -239,7 +239,7 @@ class ActionView(View):
})
def get(self, request, *args, **kwargs):
u = request.GET.get('query', '')
u = request.GET.get('query', '').strip()
if len(u) < 2:
return JsonResponse({'results': []})
+28 -17
View File
@@ -27,6 +27,7 @@ from decimal import Decimal
from django import forms
from django.db.models import F, Sum
from django.db.models.aggregates import Min
from django.db.models.functions import Coalesce
from django.utils.formats import date_format
from django.utils.html import escape
@@ -41,7 +42,8 @@ from reportlab.platypus import (
from pretix.base.exporter import BaseExporter
from pretix.base.models import (
GiftCardTransaction, OrderFee, OrderPayment, OrderRefund, Transaction,
GiftCardTransaction, Order, OrderFee, OrderPayment, OrderRefund,
Transaction,
)
from pretix.base.templatetags.money import money_filter, tax_rate_format
from pretix.base.timeframes import (
@@ -116,25 +118,34 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
dt_start, df_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(
now(), form_data["date_range"], self.timezone
)
if dt_start:
filters.append(
_("Begin")
+ ": "
+ date_format(
else:
dt_start = Order.objects.filter(
event__in=self.events,
).aggregate(min_datetime=Min('datetime'))['min_datetime']
df_end = now()
if dt_start:
filters.append(
"{label}: {value}{hint}".format(
label=_("Begin"),
value=date_format(
dt_start.astimezone(self.timezone), "SHORT_DATETIME_FORMAT"
)
),
hint="" if form_data["date_range"] else " ({})".format(_("first order received"))
)
if df_end:
filters.append(
_("End")
+ ": "
+ date_format(
(df_end - datetime.timedelta.resolution).astimezone(
self.timezone
),
"SHORT_DATETIME_FORMAT",
)
)
if df_end:
filters.append(
_("End")
+ ": "
+ date_format(
(df_end - datetime.timedelta.resolution).astimezone(
self.timezone
),
"SHORT_DATETIME_FORMAT",
)
)
if not form_data["no_testmode"]:
filters.append(_("Report includes test orders which may be deleted later!"))
+15 -2
View File
@@ -98,19 +98,32 @@ class TicketLayoutSerializer(I18nAwareModelSerializer):
return d
def validate(self, attrs):
if attrs.get('default') and self.context['event'].ticket_layouts.filter(default=True).exists:
if attrs.get('default') and self.context['event'].ticket_layouts.filter(default=True).exists():
raise ValidationError('You cannot have two layouts with default = True')
return attrs
def create(self, validated_data):
validated_data["event"] = self.context["event"]
return super().create(validated_data)
background = validated_data.pop("background", None)
instance = super().create(validated_data)
if background:
instance.background.save('background.pdf', background)
return instance
def update(self, instance, validated_data):
background = validated_data.pop("background", None)
if background:
if instance.background and TicketLayout.objects.filter(background=instance.background).count() == 1:
instance.background.delete()
instance.background.save('background.pdf', background)
return super().update(instance, validated_data)
class TicketLayoutViewSet(viewsets.ModelViewSet):
serializer_class = TicketLayoutSerializer
queryset = TicketLayout.objects.none()
lookup_field = 'id'
write_permission = 'event.settings.general:write'
def get_queryset(self):
return self.request.event.ticket_layouts.all()
@@ -235,6 +235,7 @@ class LayoutGetDefault(EventPermissionRequiredMixin, View):
class LayoutEditorView(BaseEditorView):
checkin_qr_warning = True
@cached_property
def layout(self):
@@ -1,271 +1,298 @@
import type { I18nString, SubEvent } from './i18n'
import type { I18nString, SubEvent } from "./i18n";
const settingsEl = document.getElementById('api-settings')
const { urls } = JSON.parse(settingsEl.textContent || '{}') as { urls: {
lists: string
questions: string
} }
const settingsEl = document.getElementById("api-settings");
const { urls } = JSON.parse(settingsEl.textContent || "{}") as {
urls: {
lists: string;
questions: string;
};
};
// interfaces generated from api docs
export interface PaginatedResponse<T> {
count: number
next: string | null
previous: string | null
results: T[]
count: number;
next: string | null;
previous: string | null;
results: T[];
}
export interface CheckinList {
id: number
name: string
all_products: boolean
limit_products: number[]
subevent: SubEvent | null
position_count?: number
checkin_count?: number
include_pending: boolean
allow_multiple_entries: boolean
allow_entry_after_exit: boolean
rules: Record<string, unknown>
exit_all_at: string | null
addon_match: boolean
ignore_in_statistics?: boolean
consider_tickets_used?: boolean
id: number;
name: string;
all_products: boolean;
limit_products: number[];
subevent: SubEvent | null;
position_count?: number;
checkin_count?: number;
include_pending: boolean;
allow_multiple_entries: boolean;
allow_entry_after_exit: boolean;
rules: Record<string, unknown>;
exit_all_at: string | null;
addon_match: boolean;
ignore_in_statistics?: boolean;
consider_tickets_used?: boolean;
}
export interface Checkin {
id: number
list: number
datetime: string
type: 'entry' | 'exit'
gate: number | null
device: number | null
device_id: number | null
auto_checked_in: boolean
id: number;
list: number;
datetime: string;
type: "entry" | "exit";
gate: number | null;
device: number | null;
device_id: number | null;
auto_checked_in: boolean;
}
export interface Seat {
id: number
name: string
zone_name: string
row_name: string
row_label: string | null
seat_number: string
seat_label: string | null
seat_guid: string
id: number;
name: string;
zone_name: string;
row_name: string;
row_label: string | null;
seat_number: string;
seat_label: string | null;
seat_guid: string;
}
export interface Position {
id: number
order: string
positionid: number
canceled?: boolean
item: { id?: number; name: I18nString; internal_name?: string; admission?: boolean }
variation: { id?: number; value: I18nString } | null
price: string
attendee_name: string
attendee_name_parts: Record<string, string>
attendee_email: string | null
company?: string | null
street?: string | null
zipcode?: string | null
city?: string | null
country?: string | null
state?: string | null
voucher?: number | null
voucher_budget_use?: string | null
tax_rate: string
tax_value: string
tax_code?: string | null
tax_rule: number | null
secret: string
addon_to: number | null
subevent: SubEvent | null
discount?: number | null
blocked: string[] | null
valid_from: string | null
valid_until: string | null
pseudonymization_id: string
seat: Seat | null
checkins: Checkin[]
downloads?: { output: string; url: string }[]
answers: Answer[]
pdf_data?: Record<string, unknown>
plugin_data?: Record<string, unknown>
id: number;
order: string;
positionid: number;
canceled?: boolean;
item: {
id?: number;
name: I18nString;
internal_name?: string;
admission?: boolean;
};
variation: { id?: number; value: I18nString } | null;
price: string;
attendee_name: string;
attendee_name_parts: Record<string, string>;
attendee_email: string | null;
company?: string | null;
street?: string | null;
zipcode?: string | null;
city?: string | null;
country?: string | null;
state?: string | null;
voucher?: number | null;
voucher_budget_use?: string | null;
tax_rate: string;
tax_value: string;
tax_code?: string | null;
tax_rule: number | null;
secret: string;
addon_to: number | null;
subevent: SubEvent | null;
discount?: number | null;
blocked: string[] | null;
valid_from: string | null;
valid_until: string | null;
pseudonymization_id: string;
seat: Seat | null;
checkins: Checkin[];
downloads?: { output: string; url: string }[];
answers: Answer[];
pdf_data?: Record<string, unknown>;
plugin_data?: Record<string, unknown>;
// Additional fields from checkin list positions endpoint
order__status?: string
order__valid_if_pending?: boolean
order__require_approval?: boolean
order__locale?: string
require_attention?: boolean
addons?: Addon[]
order__status?: string;
order__valid_if_pending?: boolean;
order__require_approval?: boolean;
order__locale?: string;
require_attention?: boolean;
addons?: Addon[];
}
export interface Answer {
question: number | AnswerQuestion
answer: string
question_identifier: string
options: number[]
option_identifiers: string[]
question: number | AnswerQuestion;
answer: string;
question_identifier: string;
options: number[];
option_identifiers: string[];
}
export interface AnswerQuestion {
id: number
question: I18nString
help_text?: I18nString
type: string
required: boolean
position: number
items: number[]
identifier: string
ask_during_checkin: boolean
show_during_checkin: boolean
hidden?: boolean
print_on_invoice?: boolean
options: QuestionOption[]
valid_number_min?: string | null
valid_number_max?: string | null
valid_date_min?: string | null
valid_date_max?: string | null
valid_datetime_min?: string | null
valid_datetime_max?: string | null
valid_file_portrait?: boolean
valid_string_length_max?: number | null
dependency_question?: number | null
dependency_values?: string[]
id: number;
question: I18nString;
help_text?: I18nString;
type: string;
required: boolean;
position: number;
items: number[];
identifier: string;
ask_during_checkin: boolean;
show_during_checkin: boolean;
hidden?: boolean;
print_on_invoice?: boolean;
options: QuestionOption[];
valid_number_min?: string | null;
valid_number_max?: string | null;
valid_date_min?: string | null;
valid_date_max?: string | null;
valid_datetime_min?: string | null;
valid_datetime_max?: string | null;
valid_file_portrait?: boolean;
valid_string_length_max?: number | null;
dependency_question?: number | null;
dependency_values?: string[];
}
export interface QuestionOption {
id: number
identifier: string
position: number
answer: I18nString
id: number;
identifier: string;
position: number;
answer: I18nString;
}
export interface Addon {
item: { name: I18nString; internal_name?: string }
variation: { value: I18nString } | null
item: { name: I18nString; internal_name?: string };
variation: { value: I18nString } | null;
}
export interface CheckinStatusVariation {
id: number
value: string
checkin_count: number
position_count: number
id: number;
value: string;
checkin_count: number;
position_count: number;
}
export interface CheckinStatusItem {
id: number
name: string
checkin_count: number
admission: boolean
position_count: number
variations: CheckinStatusVariation[]
id: number;
name: string;
checkin_count: number;
admission: boolean;
position_count: number;
variations: CheckinStatusVariation[];
}
export interface CheckinStatus {
checkin_count: number
position_count: number
inside_count: number
event?: { name: string }
items?: CheckinStatusItem[]
checkin_count: number;
position_count: number;
inside_count: number;
event?: { name: string };
items?: CheckinStatusItem[];
}
export interface RedeemRequest {
questions_supported: boolean
canceled_supported: boolean
ignore_unpaid: boolean
type: 'entry' | 'exit'
answers: Record<string, string>
datetime?: string | null
force?: boolean
nonce?: string
questions_supported: boolean;
canceled_supported: boolean;
ignore_unpaid: boolean;
type: "entry" | "exit";
answers: Record<string, string>;
datetime?: string | null;
force?: boolean;
nonce?: string;
}
export interface RedeemResponseList {
id: number
name: string
event: string
subevent: number | null
include_pending: boolean
id: number;
name: string;
event: string;
subevent: number | null;
include_pending: boolean;
}
export interface RedeemResponse {
status: 'ok' | 'error' | 'incomplete'
reason?: 'invalid' | 'unpaid' | 'blocked' | 'invalid_time' | 'canceled' | 'already_redeemed' | 'product' | 'rules' | 'ambiguous' | 'revoked' | 'unapproved' | 'error'
reason_explanation?: string | null
position?: Position
questions?: AnswerQuestion[]
checkin_texts?: string[]
require_attention?: boolean
list?: RedeemResponseList
status: "ok" | "error" | "incomplete";
reason?:
| "invalid"
| "unpaid"
| "blocked"
| "invalid_time"
| "canceled"
| "already_redeemed"
| "product"
| "rules"
| "ambiguous"
| "revoked"
| "unapproved"
| "error";
reason_explanation?: string | null;
position?: Position;
questions?: AnswerQuestion[];
checkin_texts?: string[];
require_attention?: boolean;
list?: RedeemResponseList;
}
const CSRF_TOKEN = document.querySelector<HTMLInputElement>('input[name=csrfmiddlewaretoken]')?.value ?? ''
const CSRF_TOKEN =
document.querySelector<HTMLInputElement>("input[name=csrfmiddlewaretoken]")
?.value ?? "";
function handleAuthError (response: Response): void {
function handleAuthError(response: Response): void {
if ([401, 403].includes(response.status)) {
window.location.href = '/control/login?next=' + encodeURIComponent(
window.location.pathname + window.location.search
) + window.location.hash
window.location.href =
"/control/login?next=" +
encodeURIComponent(window.location.pathname + window.location.search) +
window.location.hash;
}
}
export const api = {
// generic fetch wrapper, not sure if this should be exposed
async fetch <T> (url: string, options?: RequestInit): Promise<T> {
const response = await fetch(url, options)
handleAuthError(response)
async fetch<T>(url: string, options?: RequestInit): Promise<T> {
const response = await fetch(url, options);
handleAuthError(response);
if (!response.ok && response.status !== 400 && response.status !== 404) {
throw new Error('HTTP status ' + response.status)
throw new Error("HTTP status " + response.status);
}
return response.json()
return response.json();
},
async fetchCheckinLists (endsAfter?: string): Promise<PaginatedResponse<CheckinList>> {
const cutoff = endsAfter ?? moment().subtract(8, 'hours').toISOString()
const url = `${urls.lists}?exclude=checkin_count&exclude=position_count&expand=subevent&ends_after=${cutoff}`
return api.fetch(url)
async fetchCheckinLists(
endsAfter?: string,
): Promise<PaginatedResponse<CheckinList>> {
const cutoff = endsAfter ?? moment().subtract(8, "hours").toISOString();
const url = `${urls.lists}?exclude=checkin_count&exclude=position_count&expand=subevent&ends_after=${cutoff}`;
return api.fetch(url);
},
async fetchCheckinList (listId: string): Promise<CheckinList> {
return api.fetch(`${urls.lists}${listId}/?expand=subevent`)
async fetchCheckinList(listId: string): Promise<CheckinList> {
return api.fetch(`${urls.lists}${listId}/?expand=subevent`);
},
async fetchNextPage<T> (nextUrl: string): Promise<PaginatedResponse<T>> {
return api.fetch(nextUrl)
async fetchNextPage<T>(nextUrl: string): Promise<PaginatedResponse<T>> {
return api.fetch(nextUrl);
},
async fetchStatus (listId: number): Promise<CheckinStatus> {
return api.fetch(`${urls.lists}${listId}/status/`)
async fetchStatus(listId: number): Promise<CheckinStatus> {
return api.fetch(`${urls.lists}${listId}/status/`);
},
async searchPositions (listId: number, query: string): Promise<PaginatedResponse<Position>> {
const url = `${urls.lists}${listId}/positions/?ignore_status=true&expand=subevent&expand=item&expand=variation&check_rules=true&search=${encodeURIComponent(query)}`
return api.fetch(url)
async searchPositions(
listId: number,
query: string,
): Promise<PaginatedResponse<Position>> {
const url = `${urls.lists}${listId}/positions/?ignore_status=true&expand=subevent&expand=item&expand=variation&check_rules=true&search=${encodeURIComponent(query)}`;
return api.fetch(url);
},
async redeemPosition (
async redeemPosition(
listId: number,
positionId: string,
data: RedeemRequest,
untrusted: boolean = false
untrusted: boolean = false,
): Promise<RedeemResponse> {
let url = `${urls.lists}${listId}/positions/${encodeURIComponent(positionId)}/redeem/?expand=item&expand=subevent&expand=variation&expand=answers.question&expand=addons`
if (untrusted) url += '&untrusted_input=true'
let url = `${urls.lists}${listId}/positions/${encodeURIComponent(positionId)}/redeem/?expand=item&expand=subevent&expand=variation&expand=answers.question&expand=addons`;
if (untrusted) url += "&untrusted_input=true";
const response = await fetch(url, {
method: 'POST',
method: "POST",
headers: {
'X-CSRFToken': CSRF_TOKEN,
'Content-Type': 'application/json',
"X-CSRFToken": CSRF_TOKEN,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
})
});
handleAuthError(response)
handleAuthError(response);
if (response.status === 404) {
return { status: 'error', reason: 'invalid' }
return { status: "error", reason: "invalid" };
}
if (!response.ok && response.status !== 400) {
throw new Error('HTTP status ' + response.status)
throw new Error("HTTP status " + response.status);
}
return response.json()
}
}
return response.json();
},
};
@@ -1,292 +1,316 @@
<script setup lang="ts">
import { ref, computed, nextTick, onMounted, onUnmounted } from 'vue'
import { api } from '../api'
import type { CheckinList, Position, RedeemResponse, CheckinStatus, AnswerQuestion } from '../api'
import { STRINGS, i18nstringLocalize, formatSubevent, formatAnswer } from '../i18n'
import { ref, computed, nextTick, onMounted, onUnmounted } from "vue";
import { api } from "../api";
import type {
CheckinList,
Position,
RedeemResponse,
CheckinStatus,
AnswerQuestion,
} from "../api";
import {
STRINGS,
i18nstringLocalize,
formatSubevent,
formatAnswer,
} from "../i18n";
import CheckinlistSelect from './checkinlist-select.vue'
import SearchresultItem from './searchresult-item.vue'
import Datefield from './datefield.vue'
import Timefield from './timefield.vue'
import Datetimefield from './datetimefield.vue'
import CheckinlistSelect from "./checkinlist-select.vue";
import SearchresultItem from "./searchresult-item.vue";
import Datefield from "./datefield.vue";
import Timefield from "./timefield.vue";
import Datetimefield from "./datetimefield.vue";
const props = defineProps<{
eventName?: string
}>()
eventName?: string;
}>();
const COUNTRIES = JSON.parse(document.querySelector('#countries')?.textContent ?? '[]')
const COUNTRIES = JSON.parse(
document.querySelector("#countries")?.textContent ?? "[]",
);
let clearTimeoutId: ReturnType<typeof setTimeout> | null = null
const answers = ref<Record<string, string>>({})
let clearTimeoutId: ReturnType<typeof setTimeout> | null = null;
const answers = ref<Record<string, string>>({});
// Checkin list selection
const checkinlist = ref<CheckinList | null>(null)
const subevent = computed(() => formatSubevent(checkinlist.value?.subevent))
const checkinlist = ref<CheckinList | null>(null);
const subevent = computed(() => formatSubevent(checkinlist.value?.subevent));
function selectList (list: CheckinList) {
checkinlist.value = list
location.hash = '#' + list.id
refocus()
fetchStatus()
function selectList(list: CheckinList) {
checkinlist.value = list;
location.hash = "#" + list.id;
refocus();
fetchStatus();
}
function switchList () {
location.hash = ''
checkinlist.value = null
function switchList() {
location.hash = "";
checkinlist.value = null;
}
// Entry/Exit type
const type = ref<'entry' | 'exit'>('entry')
const type = ref<"entry" | "exit">("entry");
function switchType () {
type.value = type.value === 'exit' ? 'entry' : 'exit'
refocus()
function switchType() {
type.value = type.value === "exit" ? "entry" : "exit";
refocus();
}
// Input/query
const inputEl = ref<HTMLInputElement>()
const query = ref('')
const inputEl = ref<HTMLInputElement>();
const query = ref("");
function refocus () {
function refocus() {
nextTick(() => {
inputEl.value?.focus()
})
inputEl.value?.focus();
});
}
function inputKeyup (e: KeyboardEvent) {
if (e.key === 'Enter') {
startSearch(true)
} else if (query.value === '') {
clear()
function inputKeyup(e: KeyboardEvent) {
if (e.key === "Enter") {
startSearch(true);
} else if (query.value === "") {
clear();
}
}
function globalKeydown (e: KeyboardEvent | Event) {
function globalKeydown(e: KeyboardEvent | Event) {
if (!(e instanceof KeyboardEvent)) {
refocus()
return
refocus();
return;
}
if (document.activeElement?.classList.contains('searchresult')) {
if (e.key === 'ArrowDown') {
(document.activeElement.nextElementSibling as HTMLElement)?.focus()
e.preventDefault()
return
if (document.activeElement?.classList.contains("searchresult")) {
if (e.key === "ArrowDown") {
(document.activeElement.nextElementSibling as HTMLElement)?.focus();
e.preventDefault();
return;
}
if (e.key === 'ArrowUp') {
(document.activeElement.previousElementSibling as HTMLElement)?.focus()
e.preventDefault()
return
if (e.key === "ArrowUp") {
(document.activeElement.previousElementSibling as HTMLElement)?.focus();
e.preventDefault();
return;
}
}
const nodeName = document.activeElement?.nodeName?.toLowerCase()
if (nodeName !== 'input' && nodeName !== 'textarea') {
const nodeName = document.activeElement?.nodeName?.toLowerCase();
if (nodeName !== "input" && nodeName !== "textarea") {
if (e.key?.match(/^[a-z0-9A-Z+/=<>#]$/)) {
query.value = ''
refocus()
query.value = "";
refocus();
}
}
}
// Status
const status = ref<CheckinStatus | null>(null)
const statusLoading = ref(0)
let statusInterval: ReturnType<typeof setInterval> | null = null
const status = ref<CheckinStatus | null>(null);
const statusLoading = ref(0);
let statusInterval: ReturnType<typeof setInterval> | null = null;
async function fetchStatus () {
if (!checkinlist.value) return
async function fetchStatus() {
if (!checkinlist.value) return;
statusLoading.value++
statusLoading.value++;
try {
status.value = await api.fetchStatus(checkinlist.value.id)
status.value = await api.fetchStatus(checkinlist.value.id);
} finally {
statusLoading.value--
statusLoading.value--;
}
}
// Check/redeem
const checkLoading = ref(false)
const checkError = ref<string | null>(null)
const checkResult = ref<RedeemResponse | null>(null)
const checkLoading = ref(false);
const checkError = ref<string | null>(null);
const checkResult = ref<RedeemResponse | null>(null);
const checkResultAddons = computed(() => {
if (!checkResult.value?.position?.addons) return ''
return checkResult.value.position.addons.map((addon) => {
if (addon.variation) {
return `+ ${addon.item.internal_name || i18nstringLocalize(addon.item.name)} – ${i18nstringLocalize(addon.variation.value)}`
}
return '+ ' + (addon.item.internal_name || i18nstringLocalize(addon.item.name))
}).join('\n')
})
if (!checkResult.value?.position?.addons) return "";
return checkResult.value.position.addons
.map((addon) => {
if (addon.variation) {
return `+ ${addon.item.internal_name || i18nstringLocalize(addon.item.name)} – ${i18nstringLocalize(addon.variation.value)}`;
}
return (
"+ " + (addon.item.internal_name || i18nstringLocalize(addon.item.name))
);
})
.join("\n");
});
const checkResultSubevent = computed(() => formatSubevent(checkResult.value?.position?.subevent))
const checkResultSubevent = computed(() =>
formatSubevent(checkResult.value?.position?.subevent),
);
const checkResultItemvar = computed(() => {
if (!checkResult.value?.position) return ''
if (!checkResult.value?.position) return "";
if (checkResult.value.position.variation) {
return `${i18nstringLocalize(checkResult.value.position.item.name)} – ${i18nstringLocalize(checkResult.value.position.variation.value)}`
return `${i18nstringLocalize(checkResult.value.position.item.name)} – ${i18nstringLocalize(checkResult.value.position.variation.value)}`;
}
return i18nstringLocalize(checkResult.value.position.item.name)
})
return i18nstringLocalize(checkResult.value.position.item.name);
});
const checkResultText = computed(() => {
if (!checkResult.value) return ''
if (checkResult.value.status === 'ok') {
return type.value === 'exit' ? STRINGS['result.exit'] : STRINGS['result.ok']
} else if (checkResult.value.status === 'incomplete') {
return STRINGS['result.questions']
} else if (checkResult.value.reason && STRINGS['result.' + checkResult.value.reason]) {
return STRINGS['result.' + checkResult.value.reason]
if (!checkResult.value) return "";
if (checkResult.value.status === "ok") {
return type.value === "exit"
? STRINGS["result.exit"]
: STRINGS["result.ok"];
} else if (checkResult.value.status === "incomplete") {
return STRINGS["result.questions"];
} else if (
checkResult.value.reason &&
STRINGS["result." + checkResult.value.reason]
) {
return STRINGS["result." + checkResult.value.reason];
}
return checkResult.value.reason ?? ''
})
return checkResult.value.reason ?? "";
});
const checkResultColor = computed(() => {
if (!checkResult.value) return ''
if (checkResult.value.status === 'ok') return 'green'
if (checkResult.value.status === 'incomplete') return 'purple'
if (checkResult.value.reason === 'already_redeemed') return 'orange'
return 'red'
})
if (!checkResult.value) return "";
if (checkResult.value.status === "ok") return "green";
if (checkResult.value.status === "incomplete") return "purple";
if (checkResult.value.reason === "already_redeemed") return "orange";
return "red";
});
// Modals
const showUnpaidModal = ref(false)
const showQuestionsModal = ref(false)
const questionsModalEl = ref<HTMLFormElement>()
const showUnpaidModal = ref(false);
const showQuestionsModal = ref(false);
const questionsModalEl = ref<HTMLFormElement>();
function answerSetM (qid: string, opid: string, checked: boolean) {
let arr = answers.value[qid] ? answers.value[qid].split(',') : []
function answerSetM(qid: string, opid: string, checked: boolean) {
let arr = answers.value[qid] ? answers.value[qid].split(",") : [];
if (checked && !arr.includes(opid)) {
arr.push(opid)
arr.push(opid);
} else if (!checked) {
arr = arr.filter(o => opid !== o)
arr = arr.filter((o) => opid !== o);
}
answers.value[qid] = arr.join(',')
answers.value[qid] = arr.join(",");
}
// Search
const searchLoading = ref(false)
const searchResults = ref<Position[] | null>(null)
const searchNextUrl = ref<string | null>(null)
const searchError = ref<unknown>(null)
const resultEls = ref<InstanceType<typeof SearchresultItem>[]>([])
const searchLoading = ref(false);
const searchResults = ref<Position[] | null>(null);
const searchNextUrl = ref<string | null>(null);
const searchError = ref<unknown>(null);
const resultEls = ref<InstanceType<typeof SearchresultItem>[]>([]);
function selectResult (position: Position) {
check(position.secret, false, false, false, false)
function selectResult(position: Position) {
check(position.secret, false, false, false, false);
}
async function startSearch (fallbackToScan: boolean) {
async function startSearch(fallbackToScan: boolean) {
if (query.value.length >= 32 && fallbackToScan) {
check(query.value, false, false, true, true)
return
check(query.value, false, false, true, true);
return;
}
checkResult.value = null
searchLoading.value = true
searchError.value = null
searchResults.value = []
answers.value = {}
checkResult.value = null;
searchLoading.value = true;
searchError.value = null;
searchResults.value = [];
answers.value = {};
if (clearTimeoutId) {
clearTimeout(clearTimeoutId)
clearTimeoutId = null
clearTimeout(clearTimeoutId);
clearTimeoutId = null;
}
try {
const data = await api.searchPositions(checkinlist.value!.id, query.value)
searchLoading.value = false
const data = await api.searchPositions(checkinlist.value!.id, query.value);
searchLoading.value = false;
if (data.results) {
searchResults.value = data.results
searchNextUrl.value = data.next
searchResults.value = data.results;
searchNextUrl.value = data.next;
if (data.results.length) {
if (data.results[0].secret === query.value) {
nextTick(() => {
inputEl.value?.blur()
resultEls.value[0]?.el?.click()
})
inputEl.value?.blur();
resultEls.value[0]?.el?.click();
});
} else {
nextTick(() => {
resultEls.value[0]?.el?.focus()
})
resultEls.value[0]?.el?.focus();
});
}
} else {
nextTick(() => {
inputEl.value?.blur()
})
inputEl.value?.blur();
});
}
} else {
searchError.value = data
searchError.value = data;
}
clearTimeoutId = setTimeout(clear, 30 * 1000)
clearTimeoutId = setTimeout(clear, 30 * 1000);
} catch (e) {
searchLoading.value = false
searchResults.value = []
searchError.value = e
clearTimeoutId = setTimeout(clear, 30 * 1000)
searchLoading.value = false;
searchResults.value = [];
searchError.value = e;
clearTimeoutId = setTimeout(clear, 30 * 1000);
}
}
async function searchNext () {
if (!searchNextUrl.value) return
async function searchNext() {
if (!searchNextUrl.value) return;
searchLoading.value = true
searchError.value = null
searchLoading.value = true;
searchError.value = null;
if (clearTimeoutId) {
clearTimeout(clearTimeoutId)
clearTimeoutId = null
clearTimeout(clearTimeoutId);
clearTimeoutId = null;
}
try {
const data = await api.fetchNextPage<Position>(searchNextUrl.value)
searchLoading.value = false
const data = await api.fetchNextPage<Position>(searchNextUrl.value);
searchLoading.value = false;
if (data.results) {
searchResults.value = [...(searchResults.value ?? []), ...data.results]
searchNextUrl.value = data.next
searchResults.value = [...(searchResults.value ?? []), ...data.results];
searchNextUrl.value = data.next;
} else {
searchError.value = data
searchError.value = data;
}
clearTimeoutId = setTimeout(clear, 30 * 1000)
clearTimeoutId = setTimeout(clear, 30 * 1000);
} catch (e) {
searchLoading.value = false
searchError.value = e
clearTimeoutId = setTimeout(clear, 30 * 1000)
searchLoading.value = false;
searchError.value = e;
clearTimeoutId = setTimeout(clear, 30 * 1000);
}
}
async function check (
async function check(
id: string,
ignoreUnpaid: boolean,
keepAnswers: boolean,
fallbackToSearch: boolean,
untrusted: boolean
untrusted: boolean,
) {
if (!keepAnswers) {
answers.value = {}
answers.value = {};
} else if (showQuestionsModal.value) {
if (!questionsModalEl.value?.reportValidity()) return
if (!questionsModalEl.value?.reportValidity()) return;
}
showUnpaidModal.value = false
showQuestionsModal.value = false
checkLoading.value = true
checkError.value = null
checkResult.value = {} as RedeemResponse
showUnpaidModal.value = false;
showQuestionsModal.value = false;
checkLoading.value = true;
checkError.value = null;
checkResult.value = {} as RedeemResponse;
if (clearTimeoutId) {
clearTimeout(clearTimeoutId)
clearTimeoutId = null
clearTimeout(clearTimeoutId);
clearTimeoutId = null;
}
nextTick(() => {
inputEl.value?.blur()
})
inputEl.value?.blur();
});
try {
const data = await api.redeemPosition(
@@ -299,73 +323,87 @@ async function check (
type: type.value,
answers: answers.value,
},
untrusted
)
untrusted,
);
checkLoading.value = false
checkResult.value = data
checkLoading.value = false;
checkResult.value = data;
if (checkinlist.value?.include_pending && data.status === 'error' && data.reason === 'unpaid') {
showUnpaidModal.value = true
if (
checkinlist.value?.include_pending &&
data.status === "error" &&
data.reason === "unpaid"
) {
showUnpaidModal.value = true;
nextTick(() => {
document.querySelector<HTMLButtonElement>('.modal-unpaid .btn-primary')?.focus()
})
} else if (data.status === 'incomplete' && data.questions) {
showQuestionsModal.value = true
document
.querySelector<HTMLButtonElement>(".modal-unpaid .btn-primary")
?.focus();
});
} else if (data.status === "incomplete" && data.questions) {
showQuestionsModal.value = true;
for (const q of data.questions) {
if (!answers.value[q.id.toString()]) {
answers.value[q.id.toString()] = ''
answers.value[q.id.toString()] = "";
}
;(q as AnswerQuestion).question = i18nstringLocalize(q.question)
(q as AnswerQuestion).question = i18nstringLocalize(q.question);
for (const o of q.options) {
;(o as { answer: string }).answer = i18nstringLocalize(o.answer)
(o as { answer: string }).answer = i18nstringLocalize(o.answer);
}
}
nextTick(() => {
document.querySelector<HTMLElement>('.modal-questions input, .modal-questions select, .modal-questions textarea')?.focus()
})
} else if (data.status === 'error' && data.reason === 'invalid' && fallbackToSearch) {
startSearch(false)
document
.querySelector<HTMLElement>(
".modal-questions input, .modal-questions select, .modal-questions textarea",
)
?.focus();
});
} else if (
data.status === "error" &&
data.reason === "invalid" &&
fallbackToSearch
) {
startSearch(false);
} else {
clearTimeoutId = setTimeout(clear, 30 * 1000)
fetchStatus()
clearTimeoutId = setTimeout(clear, 30 * 1000);
fetchStatus();
}
} catch (e) {
checkLoading.value = false
checkResult.value = {} as RedeemResponse
checkError.value = String(e)
clearTimeoutId = setTimeout(clear, 30 * 1000)
checkLoading.value = false;
checkResult.value = {} as RedeemResponse;
checkError.value = String(e);
clearTimeoutId = setTimeout(clear, 30 * 1000);
}
}
function clear () {
query.value = ''
searchLoading.value = false
searchResults.value = null
searchNextUrl.value = null
searchError.value = null
checkLoading.value = false
checkError.value = null
checkResult.value = null
showUnpaidModal.value = false
showQuestionsModal.value = false
answers.value = {}
function clear() {
query.value = "";
searchLoading.value = false;
searchResults.value = null;
searchNextUrl.value = null;
searchError.value = null;
checkLoading.value = false;
checkError.value = null;
checkResult.value = null;
showUnpaidModal.value = false;
showQuestionsModal.value = false;
answers.value = {};
}
onMounted(() => {
window.addEventListener('focus', globalKeydown)
document.addEventListener('visibilitychange', globalKeydown)
document.addEventListener('keydown', globalKeydown)
statusInterval = setInterval(fetchStatus, 120 * 1000)
})
window.addEventListener("focus", globalKeydown);
document.addEventListener("visibilitychange", globalKeydown);
document.addEventListener("keydown", globalKeydown);
statusInterval = setInterval(fetchStatus, 120 * 1000);
});
onUnmounted(() => {
window.removeEventListener('focus', globalKeydown)
document.removeEventListener('visibilitychange', globalKeydown)
document.removeEventListener('keydown', globalKeydown)
if (statusInterval) clearInterval(statusInterval)
if (clearTimeoutId) clearTimeout(clearTimeoutId)
})
window.removeEventListener("focus", globalKeydown);
document.removeEventListener("visibilitychange", globalKeydown);
document.removeEventListener("keydown", globalKeydown);
if (statusInterval) clearInterval(statusInterval);
if (clearTimeoutId) clearTimeout(clearTimeoutId);
});
</script>
<template lang="pug">
.container
@@ -1,17 +1,17 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { CheckinList } from '../api'
import { formatSubevent } from '../i18n'
import { computed } from "vue";
import type { CheckinList } from "../api";
import { formatSubevent } from "../i18n";
const props = defineProps<{
list: CheckinList
}>()
list: CheckinList;
}>();
defineEmits<{
selected: [list: CheckinList]
}>()
selected: [list: CheckinList];
}>();
const subevent = computed(() => formatSubevent(props.list.subevent))
const subevent = computed(() => formatSubevent(props.list.subevent));
</script>
<template lang="pug">
a.list-group-item(href="#", @click.prevent="$emit('selected', list)")
@@ -1,86 +1,86 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { api } from '../api'
import type { CheckinList } from '../api'
import { STRINGS } from '../i18n'
import CheckinlistItem from './checkinlist-item.vue'
import { ref, onMounted } from "vue";
import { api } from "../api";
import type { CheckinList } from "../api";
import { STRINGS } from "../i18n";
import CheckinlistItem from "./checkinlist-item.vue";
const emit = defineEmits<{
selected: [list: CheckinList]
}>()
selected: [list: CheckinList];
}>();
const loading = ref(false)
const error = ref<unknown>(null)
const lists = ref<CheckinList[] | null>(null)
const nextUrl = ref<string | null>(null)
const loading = ref(false);
const error = ref<unknown>(null);
const lists = ref<CheckinList[] | null>(null);
const nextUrl = ref<string | null>(null);
async function load () {
loading.value = true
error.value = null
async function load() {
loading.value = true;
error.value = null;
try {
if (location.hash) {
const listId = location.hash.substring(1)
const listId = location.hash.substring(1);
try {
const data = await api.fetchCheckinList(listId)
loading.value = false
const data = await api.fetchCheckinList(listId);
loading.value = false;
if (data.id) {
emit('selected', data)
emit("selected", data);
} else {
location.hash = ''
load()
location.hash = "";
load();
}
} catch {
location.hash = ''
load()
location.hash = "";
load();
}
return
return;
}
const data = await api.fetchCheckinLists()
loading.value = false
const data = await api.fetchCheckinLists();
loading.value = false;
if (data.results) {
lists.value = data.results
nextUrl.value = data.next
lists.value = data.results;
nextUrl.value = data.next;
} else if (data.results === 0) {
error.value = STRINGS['checkinlist.none']
error.value = STRINGS["checkinlist.none"];
} else {
error.value = data
error.value = data;
}
} catch (e) {
loading.value = false
error.value = e
loading.value = false;
error.value = e;
}
}
async function loadNext () {
if (!nextUrl.value) return
async function loadNext() {
if (!nextUrl.value) return;
loading.value = true
error.value = null
loading.value = true;
error.value = null;
try {
const data = await api.fetchNextPage<CheckinList>(nextUrl.value)
loading.value = false
const data = await api.fetchNextPage<CheckinList>(nextUrl.value);
loading.value = false;
if (data.results) {
lists.value.push(...data.results)
nextUrl.value = data.next
lists.value.push(...data.results);
nextUrl.value = data.next;
} else if (data.results === 0) {
error.value = STRINGS['checkinlist.none']
error.value = STRINGS["checkinlist.none"];
} else {
error.value = data
error.value = data;
}
} catch (e) {
loading.value = false
error.value = e
loading.value = false;
error.value = e;
}
}
onMounted(() => {
load()
})
load();
});
</script>
<template lang="pug">
.panel.panel-primary.checkinlist-select
@@ -1,18 +1,18 @@
<script setup lang="ts">
import { ref, watch, onMounted, onUnmounted } from 'vue'
import { dateFormat, datetimeLocale } from '../i18n'
import { ref, watch, onMounted, onUnmounted } from "vue";
import { dateFormat, datetimeLocale } from "../i18n";
const props = defineProps<{
required?: boolean
modelValue?: string
id?: string
}>()
required?: boolean;
modelValue?: string;
id?: string;
}>();
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
"update:modelValue": [value: string];
}>();
const input = ref<HTMLInputElement>()
const input = ref<HTMLInputElement>();
const opts = {
format: dateFormat,
@@ -20,44 +20,50 @@ const opts = {
useCurrent: false,
showClear: props.required,
icons: {
time: 'fa fa-clock-o',
date: 'fa fa-calendar',
up: 'fa fa-chevron-up',
down: 'fa fa-chevron-down',
previous: 'fa fa-chevron-left',
next: 'fa fa-chevron-right',
today: 'fa fa-screenshot',
clear: 'fa fa-trash',
close: 'fa fa-remove',
time: "fa fa-clock-o",
date: "fa fa-calendar",
up: "fa fa-chevron-up",
down: "fa fa-chevron-down",
previous: "fa fa-chevron-left",
next: "fa fa-chevron-right",
today: "fa fa-screenshot",
clear: "fa fa-trash",
close: "fa fa-remove",
},
}
};
watch(() => props.modelValue, (val) => {
if (val) {
$(input.value!).data('DateTimePicker').date(moment(val))
}
})
watch(
() => props.modelValue,
(val) => {
if (val) {
$(input.value!).data("DateTimePicker").date(moment(val));
}
},
);
onMounted(() => {
$(input.value!)
.datetimepicker(opts)
.trigger('change')
.on('dp.change', function (this: HTMLElement) {
emit('update:modelValue', $(this).data('DateTimePicker').date().format('YYYY-MM-DD'))
})
.trigger("change")
.on("dp.change", function (this: HTMLElement) {
emit(
"update:modelValue",
$(this).data("DateTimePicker").date().format("YYYY-MM-DD"),
);
});
if (!props.modelValue) {
$(input.value!).data('DateTimePicker').viewDate(moment().hour(0).minute(0).second(0).millisecond(0))
$(input.value!)
.data("DateTimePicker")
.viewDate(moment().hour(0).minute(0).second(0).millisecond(0));
} else {
$(input.value!).data('DateTimePicker').date(moment(props.modelValue))
$(input.value!).data("DateTimePicker").date(moment(props.modelValue));
}
})
});
onUnmounted(() => {
$(input.value!)
.off()
.datetimepicker('destroy')
})
$(input.value!).off().datetimepicker("destroy");
});
</script>
<template lang="pug">
input.form-control(:id="id", ref="input", :required="required")
@@ -1,18 +1,18 @@
<script setup lang="ts">
import { ref, watch, onMounted, onUnmounted } from 'vue'
import { datetimeFormat, datetimeLocale, timezone } from '../i18n'
import { ref, watch, onMounted, onUnmounted } from "vue";
import { datetimeFormat, datetimeLocale, timezone } from "../i18n";
const props = defineProps<{
required?: boolean
modelValue?: string
id?: string
}>()
required?: boolean;
modelValue?: string;
id?: string;
}>();
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
"update:modelValue": [value: string];
}>();
const input = ref<HTMLInputElement>()
const input = ref<HTMLInputElement>();
const opts = {
format: datetimeFormat,
@@ -21,44 +21,50 @@ const opts = {
useCurrent: false,
showClear: props.required,
icons: {
time: 'fa fa-clock-o',
date: 'fa fa-calendar',
up: 'fa fa-chevron-up',
down: 'fa fa-chevron-down',
previous: 'fa fa-chevron-left',
next: 'fa fa-chevron-right',
today: 'fa fa-screenshot',
clear: 'fa fa-trash',
close: 'fa fa-remove',
time: "fa fa-clock-o",
date: "fa fa-calendar",
up: "fa fa-chevron-up",
down: "fa fa-chevron-down",
previous: "fa fa-chevron-left",
next: "fa fa-chevron-right",
today: "fa fa-screenshot",
clear: "fa fa-trash",
close: "fa fa-remove",
},
}
};
watch(() => props.modelValue, (val) => {
if (val) {
$(input.value!).data('DateTimePicker').date(moment(val))
}
})
watch(
() => props.modelValue,
(val) => {
if (val) {
$(input.value!).data("DateTimePicker").date(moment(val));
}
},
);
onMounted(() => {
$(input.value!)
.datetimepicker(opts)
.trigger('change')
.on('dp.change', function (this: HTMLElement) {
emit('update:modelValue', $(this).data('DateTimePicker').date().toISOString())
})
.trigger("change")
.on("dp.change", function (this: HTMLElement) {
emit(
"update:modelValue",
$(this).data("DateTimePicker").date().toISOString(),
);
});
if (!props.modelValue) {
$(input.value!).data('DateTimePicker').viewDate(moment().hour(0).minute(0).second(0).millisecond(0))
$(input.value!)
.data("DateTimePicker")
.viewDate(moment().hour(0).minute(0).second(0).millisecond(0));
} else {
$(input.value!).data('DateTimePicker').date(moment(props.modelValue))
$(input.value!).data("DateTimePicker").date(moment(props.modelValue));
}
})
});
onUnmounted(() => {
$(input.value!)
.off()
.datetimepicker('destroy')
})
$(input.value!).off().datetimepicker("destroy");
});
</script>
<template lang="pug">
input.form-control(:id="id", ref="input", :required="required")
@@ -1,35 +1,43 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import type { Position } from '../api'
import { STRINGS, i18nstringLocalize, formatSubevent } from '../i18n'
import { computed, ref } from "vue";
import type { Position } from "../api";
import { STRINGS, i18nstringLocalize, formatSubevent } from "../i18n";
const props = defineProps<{
position: Position
}>()
position: Position;
}>();
defineEmits<{
selected: [position: Position]
}>()
selected: [position: Position];
}>();
const rootEl = ref<HTMLAnchorElement>()
const rootEl = ref<HTMLAnchorElement>();
const status = computed(() => {
if (props.position.checkins.length) return 'redeemed'
if (props.position.order__status === 'n' && props.position.order__valid_if_pending) return 'pending_valid'
if (props.position.order__status === 'n' && props.position.order__require_approval) return 'require_approval'
return props.position.order__status
})
if (props.position.checkins.length) return "redeemed";
if (
props.position.order__status === "n" &&
props.position.order__valid_if_pending
)
return "pending_valid";
if (
props.position.order__status === "n" &&
props.position.order__require_approval
)
return "require_approval";
return props.position.order__status;
});
const itemvar = computed(() => {
if (props.position.variation) {
return `${i18nstringLocalize(props.position.item.name)} – ${i18nstringLocalize(props.position.variation.value)}`
return `${i18nstringLocalize(props.position.item.name)} – ${i18nstringLocalize(props.position.variation.value)}`;
}
return i18nstringLocalize(props.position.item.name)
})
return i18nstringLocalize(props.position.item.name);
});
const subevent = computed(() => formatSubevent(props.position.subevent))
const subevent = computed(() => formatSubevent(props.position.subevent));
defineExpose({ el: rootEl })
defineExpose({ el: rootEl });
</script>
<template lang="pug">
a.list-group-item.searchresult(ref="rootEl", href="#", @click.prevent="$emit('selected', position)")
@@ -1,18 +1,18 @@
<script setup lang="ts">
import { ref, watch, onMounted, onUnmounted } from 'vue'
import { timeFormat, datetimeLocale } from '../i18n'
import { ref, watch, onMounted, onUnmounted } from "vue";
import { timeFormat, datetimeLocale } from "../i18n";
const props = defineProps<{
required?: boolean
modelValue?: string
id?: string
}>()
required?: boolean;
modelValue?: string;
id?: string;
}>();
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
"update:modelValue": [value: string];
}>();
const input = ref<HTMLInputElement>()
const input = ref<HTMLInputElement>();
const opts = {
format: timeFormat,
@@ -20,44 +20,50 @@ const opts = {
useCurrent: false,
showClear: props.required,
icons: {
time: 'fa fa-clock-o',
date: 'fa fa-calendar',
up: 'fa fa-chevron-up',
down: 'fa fa-chevron-down',
previous: 'fa fa-chevron-left',
next: 'fa fa-chevron-right',
today: 'fa fa-screenshot',
clear: 'fa fa-trash',
close: 'fa fa-remove',
time: "fa fa-clock-o",
date: "fa fa-calendar",
up: "fa fa-chevron-up",
down: "fa fa-chevron-down",
previous: "fa fa-chevron-left",
next: "fa fa-chevron-right",
today: "fa fa-screenshot",
clear: "fa fa-trash",
close: "fa fa-remove",
},
}
};
watch(() => props.modelValue, (val) => {
if (val) {
$(input.value!).data('DateTimePicker').date(moment(val))
}
})
watch(
() => props.modelValue,
(val) => {
if (val) {
$(input.value!).data("DateTimePicker").date(moment(val));
}
},
);
onMounted(() => {
$(input.value!)
.datetimepicker(opts)
.trigger('change')
.on('dp.change', function (this: HTMLElement) {
emit('update:modelValue', $(this).data('DateTimePicker').date().format('HH:mm:ss'))
})
.trigger("change")
.on("dp.change", function (this: HTMLElement) {
emit(
"update:modelValue",
$(this).data("DateTimePicker").date().format("HH:mm:ss"),
);
});
if (!props.modelValue) {
$(input.value!).data('DateTimePicker').viewDate(moment().hour(0).minute(0).second(0).millisecond(0))
$(input.value!)
.data("DateTimePicker")
.viewDate(moment().hour(0).minute(0).second(0).millisecond(0));
} else {
$(input.value!).data('DateTimePicker').date(moment(props.modelValue))
$(input.value!).data("DateTimePicker").date(moment(props.modelValue));
}
})
});
onUnmounted(() => {
$(input.value!)
.off()
.datetimepicker('destroy')
})
$(input.value!).off().datetimepicker("destroy");
});
</script>
<template lang="pug">
input.form-control(:id="id", ref="input", :required="required")
@@ -1,106 +1,115 @@
const body = document.body
const body = document.body;
export const timezone = body.dataset.timezone ?? 'UTC'
export const datetimeFormat = body.dataset.datetimeformat ?? 'L LT'
export const dateFormat = body.dataset.dateformat ?? 'L'
export const timeFormat = body.dataset.timeformat ?? 'LT'
export const datetimeLocale = body.dataset.datetimelocale ?? 'en'
export const pretixLocale = body.dataset.pretixlocale ?? 'en'
export const timezone = body.dataset.timezone ?? "UTC";
export const datetimeFormat = body.dataset.datetimeformat ?? "L LT";
export const dateFormat = body.dataset.dateformat ?? "L";
export const timeFormat = body.dataset.timeformat ?? "LT";
export const datetimeLocale = body.dataset.datetimelocale ?? "en";
export const pretixLocale = body.dataset.pretixlocale ?? "en";
moment.locale(datetimeLocale)
moment.locale(datetimeLocale);
export function gettext (msgid: string): string {
if (typeof django !== 'undefined' && typeof django.gettext !== 'undefined') {
return django.gettext(msgid)
export function gettext(msgid: string): string {
if (typeof django !== "undefined" && typeof django.gettext !== "undefined") {
return django.gettext(msgid);
}
return msgid
return msgid;
}
export function ngettext (singular: string, plural: string, count: number): string {
if (typeof django !== 'undefined' && typeof django.ngettext !== 'undefined') {
return django.ngettext(singular, plural, count)
export function ngettext(
singular: string,
plural: string,
count: number,
): string {
if (typeof django !== "undefined" && typeof django.ngettext !== "undefined") {
return django.ngettext(singular, plural, count);
}
return plural
return plural;
}
export type I18nString = string | Record<string, string> | null | undefined
export type I18nString = string | Record<string, string> | null | undefined;
export function i18nstringLocalize (obj: I18nString): string {
export function i18nstringLocalize(obj: I18nString): string {
// external
return i18nstring_localize(obj)
return i18nstring_localize(obj);
}
export const STRINGS: Record<string, string> = {
'checkinlist.select': gettext('Select a check-in list'),
'checkinlist.none': gettext('No active check-in lists found.'),
'checkinlist.switch': gettext('Switch check-in list'),
'results.headline': gettext('Search results'),
'results.none': gettext('No tickets found'),
'check.headline': gettext('Result'),
'check.attention': gettext('This ticket requires special attention'),
'scantype.switch': gettext('Switch direction'),
'scantype.entry': gettext('Entry'),
'scantype.exit': gettext('Exit'),
'input.placeholder': gettext('Scan a ticket or search and press return…'),
'pagination.next': gettext('Load more'),
'status.p': gettext('Valid'),
'status.n': gettext('Unpaid'),
'status.c': gettext('Canceled'),
'status.e': gettext('Canceled'),
'status.pending_valid': gettext('Confirmed'),
'status.require_approval': gettext('Approval pending'),
'status.redeemed': gettext('Redeemed'),
'modal.cancel': gettext('Cancel'),
'modal.continue': gettext('Continue'),
'modal.unpaid.head': gettext('Ticket not paid'),
'modal.unpaid.text': gettext('This ticket is not yet paid. Do you want to continue anyways?'),
'modal.questions': gettext('Additional information required'),
'result.ok': gettext('Valid ticket'),
'result.exit': gettext('Exit recorded'),
'result.already_redeemed': gettext('Ticket already used'),
'result.questions': gettext('Information required'),
'result.invalid': gettext('Unknown ticket'),
'result.product': gettext('Ticket type not allowed here'),
'result.unpaid': gettext('Ticket not paid'),
'result.rules': gettext('Entry not allowed'),
'result.revoked': gettext('Ticket code revoked/changed'),
'result.blocked': gettext('Ticket blocked'),
'result.invalid_time': gettext('Ticket not valid at this time'),
'result.canceled': gettext('Order canceled'),
'result.ambiguous': gettext('Ticket code is ambiguous on list'),
'result.unapproved': gettext('Order not approved'),
'status.checkin': gettext('Checked-in Tickets'),
'status.position': gettext('Valid Tickets'),
'status.inside': gettext('Currently inside'),
yes: gettext('Yes'),
no: gettext('No'),
}
"checkinlist.select": gettext("Select a check-in list"),
"checkinlist.none": gettext("No active check-in lists found."),
"checkinlist.switch": gettext("Switch check-in list"),
"results.headline": gettext("Search results"),
"results.none": gettext("No tickets found"),
"check.headline": gettext("Result"),
"check.attention": gettext("This ticket requires special attention"),
"scantype.switch": gettext("Switch direction"),
"scantype.entry": gettext("Entry"),
"scantype.exit": gettext("Exit"),
"input.placeholder": gettext("Scan a ticket or search and press return…"),
"pagination.next": gettext("Load more"),
"status.p": gettext("Valid"),
"status.n": gettext("Unpaid"),
"status.c": gettext("Canceled"),
"status.e": gettext("Canceled"),
"status.pending_valid": gettext("Confirmed"),
"status.require_approval": gettext("Approval pending"),
"status.redeemed": gettext("Redeemed"),
"modal.cancel": gettext("Cancel"),
"modal.continue": gettext("Continue"),
"modal.unpaid.head": gettext("Ticket not paid"),
"modal.unpaid.text": gettext(
"This ticket is not yet paid. Do you want to continue anyways?",
),
"modal.questions": gettext("Additional information required"),
"result.ok": gettext("Valid ticket"),
"result.exit": gettext("Exit recorded"),
"result.already_redeemed": gettext("Ticket already used"),
"result.questions": gettext("Information required"),
"result.invalid": gettext("Unknown ticket"),
"result.product": gettext("Ticket type not allowed here"),
"result.unpaid": gettext("Ticket not paid"),
"result.rules": gettext("Entry not allowed"),
"result.revoked": gettext("Ticket code revoked/changed"),
"result.blocked": gettext("Ticket blocked"),
"result.invalid_time": gettext("Ticket not valid at this time"),
"result.canceled": gettext("Order canceled"),
"result.ambiguous": gettext("Ticket code is ambiguous on list"),
"result.unapproved": gettext("Order not approved"),
"status.checkin": gettext("Checked-in Tickets"),
"status.position": gettext("Valid Tickets"),
"status.inside": gettext("Currently inside"),
yes: gettext("Yes"),
no: gettext("No"),
};
export interface SubEvent {
name: Record<string, string>
date_from: string
name: Record<string, string>;
date_from: string;
}
export function formatSubevent (subevent: SubEvent | null | undefined): string {
if (!subevent) return ''
const name = i18nstringLocalize(subevent.name)
const date = moment.utc(subevent.date_from).tz(timezone).format(datetimeFormat)
return `${name} · ${date}`
export function formatSubevent(subevent: SubEvent | null | undefined): string {
if (!subevent) return "";
const name = i18nstringLocalize(subevent.name);
const date = moment
.utc(subevent.date_from)
.tz(timezone)
.format(datetimeFormat);
return `${name} · ${date}`;
}
export interface Question {
type: string
type: string;
}
export function formatAnswer (value: string, question: Question): string {
if (question.type === 'B' && value === 'True') {
return STRINGS['yes']
} else if (question.type === 'B' && value === 'False') {
return STRINGS['no']
} else if (question.type === 'W' && value) {
return moment(value).tz(timezone).format('L LT')
} else if (question.type === 'D' && value) {
return moment(value).format('L')
export function formatAnswer(value: string, question: Question): string {
if (question.type === "B" && value === "True") {
return STRINGS["yes"];
} else if (question.type === "B" && value === "False") {
return STRINGS["no"];
} else if (question.type === "W" && value) {
return moment(value).tz(timezone).format("L LT");
} else if (question.type === "D" && value) {
return moment(value).format("L");
}
return value
return value;
}
@@ -1,17 +1,17 @@
import { createApp } from 'vue'
import { createApp } from "vue";
// import './scss/main.scss'
import App from './components/app.vue'
import App from "./components/app.vue";
const mountEl = document.querySelector<HTMLElement>('#app')!
const mountEl = document.querySelector<HTMLElement>("#app")!;
const app = createApp(App, mountEl.dataset)
app.mount('#app')
const app = createApp(App, mountEl.dataset);
app.mount("#app");
app.config.errorHandler = (error, _vm, info) => {
// vue fatals on errors by default, which is a weird choice
// https://github.com/vuejs/core/issues/3525
// https://github.com/vuejs/router/discussions/2435
console.error('[VUE]', info, error)
}
console.error("[VUE]", info, error);
};
+7
View File
@@ -75,6 +75,7 @@ class AuthenticationForm(forms.Form):
'unverified': _("You have not yet activated your account and set a password. Please click the link in the "
"email we sent you. In case you cannot find it, click \"Forgot your password?\" to receive "
"a new email."),
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
}
def __init__(self, request=None, *args, **kwargs):
@@ -93,6 +94,12 @@ class AuthenticationForm(forms.Form):
email = self.cleaned_data.get('email')
password = self.cleaned_data.get('password')
if rate_limit("customer_login", include_ip_from_request=self.request, max_num=10, expire_time=300):
raise forms.ValidationError(
self.error_messages['rate_limit'],
code='rate_limit',
)
if email is not None and password:
try:
u = self.request.organizer.customers.get(email=email.lower(), provider__isnull=True)
+9
View File
@@ -20,6 +20,7 @@
# <https://www.gnu.org/licenses/>.
#
import sys
from collections import Counter
from datetime import datetime
from typing import Optional
@@ -472,3 +473,11 @@ def _item_num_options(item):
def get_item_option_count(items):
return sum(_item_num_options(item) for item in items)
def get_item_var_sums(positions, subevent):
itemvarsums = Counter()
for pos in positions:
if pos.subevent == subevent:
itemvarsums[pos.variation or pos.item] += 1
return itemvarsums
@@ -55,7 +55,7 @@
<div class="col-md-6 col-md-offset-3">
{% for provider in request.organizer.sso_providers.all %}
{% if provider.is_active %}
<a href="{% eventurl request.organizer "presale:organizer.customer.login" provider=provider.pk %}?next={% if request.domain_mode == 'event' or request.domain_mode == 'organizer_alternative' %}{{ request.scheme }}://{{ request.get_host }}{% endif %}{{ request.get_full_path|urlencode }}"
<a href="{% abseventurl request.organizer "presale:organizer.customer.login" provider=provider.pk %}?next={% if request.domain_mode == 'event' or request.domain_mode == 'organizer_alternative' %}{{ request.scheme }}://{{ request.get_host }}{% endif %}{{ request.get_full_path|urlencode }}"
class="btn btn-primary btn-lg btn-block" data-open-in-popup-window>
{{ provider.button_label }}
</a>
@@ -125,10 +125,10 @@
<div class="col-md-8 col-sm-6 col-xs-12">
<h{{ headline_level|default:3|add:2 }} class="h5" id="{{ form_prefix }}item-{{ item.pk }}-{{ var.pk }}-legend">
{{ var }}
{% if cart.itemvarsums|getitem:var %}
{% if itemvarsums|getitem:var %}
<span class="textbubble-success">
{% icon "shopping-cart" %}
{% blocktrans trimmed count amount=cart.itemvarsums|getitem:var %}
{% blocktrans trimmed count amount=itemvarsums|getitem:var %}
{{ amount }}× in your cart
{% plural %}
{{ amount }}× in your cart
@@ -299,10 +299,10 @@
<div class="product-description {% if item.picture %}with-picture{% endif %}">
<h{{ headline_level|default:3|add:1 }} class="h4" id="{{ form_prefix }}item-{{ item.pk }}-legend">
{{ item.name }}
{% if cart.itemvarsums|getitem:item %}
{% if itemvarsums|getitem:item %}
<span class="textbubble-success">
{% icon "shopping-cart" %}
{% blocktrans trimmed count amount=cart.itemvarsums|getitem:item %}
{% blocktrans trimmed count amount=itemvarsums|getitem:item %}
{{ amount }}× in your cart
{% plural %}
{{ amount }}× in your cart
+1 -6
View File
@@ -33,7 +33,7 @@
# License for the specific language governing permissions and limitations under the License.
import copy
import warnings
from collections import Counter, defaultdict
from collections import defaultdict
from datetime import datetime, timedelta
from decimal import Decimal
from functools import wraps
@@ -266,10 +266,6 @@ class CartMixin:
minutes_left = None
seconds_left = None
itemvarsums = Counter()
for p in cartpos:
itemvarsums[p.variation or p.item] += 1
return {
'positions': positions,
'invoice_address': self.invoice_address,
@@ -291,7 +287,6 @@ class CartMixin:
not self.request.event.settings.display_net_prices and
sum(c.price_includes_rounding_correction for c in positions) + sum(f.price_includes_rounding_correction for f in fees)
),
'itemvarsums': itemvarsums,
'current_selected_payments': [
p for p in self.current_selected_payments(lcp, fees, self.invoice_address)
if p.get('multi_use_supported')
+3 -1
View File
@@ -70,7 +70,8 @@ from pretix.helpers.http import redirect_to_url
from pretix.helpers.safedownload import check_token
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.productlist import (
get_item_option_count, item_group_by_category, prepare_item_list_for_shop,
get_item_option_count, get_item_var_sums, item_group_by_category,
prepare_item_list_for_shop,
)
from pretix.presale.views import (
CartMixin, EventViewMixin, allow_cors_if_namespaced,
@@ -701,6 +702,7 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, CartMixin, TemplateView
)
context['cart'] = self.get_cart()
context['itemvarsums'] = get_item_var_sums(context['cart'].get('raw', []), self.subevent)
context['show_cart'] = context['cart']['positions']
# Regroup those by category
+2 -2
View File
@@ -156,7 +156,7 @@ class LoginView(RedirectBackMixin, FormView):
"""Security check complete. Log the user in."""
customer = form.get_customer()
customer_login(self.request, customer)
customer_signed_in.send(customer.organizer, customer=customer)
customer_signed_in.send(customer.organizer, customer=customer, profile=None)
return HttpResponseRedirect(self.get_success_url())
@@ -901,7 +901,7 @@ class SSOLoginReturnView(RedirectBackMixin, View):
popup_origin
)
customer_signed_in.send(customer.organizer, customer=customer)
customer_signed_in.send(customer.organizer, customer=customer, profile=profile)
if popup_origin:
return render(self.request, 'pretixpresale/postmessage.html', {
+3 -1
View File
@@ -72,7 +72,8 @@ from pretix.helpers.http import redirect_to_url
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.ical import get_public_ical
from pretix.presale.productlist import (
get_item_option_count, item_group_by_category, prepare_item_list_for_shop,
get_item_option_count, get_item_var_sums, item_group_by_category,
prepare_item_list_for_shop,
)
from pretix.presale.signals import seatingframe_html_head
from pretix.presale.views.organizer import (
@@ -244,6 +245,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
context['display_add_to_cart'] = display_add_to_cart
context['cart'] = self.get_cart()
context['itemvarsums'] = get_item_var_sums(context['cart'].get('raw', []), self.subevent)
context['has_addon_choices'] = any(cp.has_addon_choices for cp in get_cart(self.request))
templating_context = PlaceholderContext(event_or_subevent=self.subevent or self.request.event, event=self.request.event)
+4 -1
View File
@@ -76,7 +76,7 @@ from pretix.base.models.tax import TaxedPrice
from pretix.base.payment import PaymentException
from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_pdf, invoice_pdf_task,
invoice_qualified,
invoice_qualified, transmit_invoice,
)
from pretix.base.services.orders import (
OrderChangeManager, OrderError, _try_auto_refund, cancel_order,
@@ -907,6 +907,9 @@ class OrderModify(EventViewMixin, OrderDetailMixin, OrderQuestionsViewMixin, Tem
self.order.log_action('pretix.event.order.invoice.reissued', data={
'invoice': inv.pk
})
transmit_invoice.apply_async(args=(c.event_id, c.pk, True))
if c.pk != inv.pk:
transmit_invoice.apply_async(args=(inv.event_id, inv.pk, True))
messages.success(self.request, _('The invoice has been reissued.'))
except Exception as e:
self.order.log_action("pretix.event.order.invoice.failed", data={
+6 -1
View File
@@ -35,6 +35,7 @@ import calendar
import hashlib
import math
import operator
import re
from collections import defaultdict
from datetime import date, datetime, time, timedelta
from functools import reduce
@@ -381,7 +382,11 @@ class EventListMixin:
def _set_week_year(self):
if 'date' in self.request.GET:
try:
iso = dateutil.parser.isoparse(self.request.GET.get('date')).isocalendar()
input = self.request.GET.get('date')
# Our parameters for the single-digit weeks of the year are 2027-W2, as emitted by Django's "W" format code,
# but dateutil 4.0 is strict about ISO and only accepts 2027-W02 as valid.
input = re.sub('-W([0-9])$', '-W0\\1', input)
iso = dateutil.parser.isoparse(input).isocalendar()
except ValueError:
iso = time_machine_now().isocalendar()
self.year = iso[0]
+35 -1
View File
@@ -19,12 +19,18 @@
# 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 re
import typing
import weakref
from celery.exceptions import Retry
from sentry_sdk import Hub
from sentry_sdk.integrations import django as djangosentry
from sentry_sdk.utils import capture_internal_exceptions
from sentry_sdk.scrubber import EventScrubber
from sentry_sdk.utils import AnnotatedValue, capture_internal_exceptions
if typing.TYPE_CHECKING:
from sentry_sdk._types import Event
def _make_event_processor(weak_request, integration):
@@ -96,3 +102,31 @@ def setup_custom_filters():
hub = Hub.current
with hub.configure_scope() as scope:
scope.add_event_processor(ignore_retry)
class PretixEventScrubber(EventScrubber):
secret_re = re.compile(
'.*(pretixsecret|ramiiosecret|PRIVATE KEY|://[^/@:]*:[^/@:]+@).*',
re.IGNORECASE
)
def scrub_dict(self, d: object) -> None:
if not isinstance(d, dict):
return
super().scrub_dict(d)
self._scrub_dict_for_known_secrets(d)
def _scrub_dict_for_known_secrets(self, d: dict):
for k, v in d.items():
if isinstance(v, str) and self.secret_re.match(v):
d[k] = AnnotatedValue.substituted_because_contains_sensitive_data()
def scrub_exception(self, event: "Event") -> None:
with capture_internal_exceptions():
if "exception" in event:
self.scrub_dict(event["exception"])
def scrub_event(self, event: "Event") -> None:
super().scrub_event(event)
self.scrub_exception(event)
+4 -4
View File
@@ -41,7 +41,7 @@ from json import loads
from pathlib import Path
from urllib.parse import urlparse
import importlib_metadata as metadata
from importlib import metadata
from django.core.exceptions import ImproperlyConfigured
from django.utils.crypto import get_random_string
from kombu import Queue
@@ -721,9 +721,9 @@ if config.has_option('sentry', 'dsn') and not any(c in sys.argv for c in ('shell
from sentry_sdk.integrations.logging import (
LoggingIntegration, ignore_logger,
)
from sentry_sdk.scrubber import DEFAULT_DENYLIST, EventScrubber
from sentry_sdk.scrubber import DEFAULT_DENYLIST
from .sentry import PretixSentryIntegration, setup_custom_filters
from .sentry import PretixSentryIntegration, PretixEventScrubber, setup_custom_filters
SENTRY_TOKEN = config.get('sentry', 'traces_sample_token', fallback='')
SENTRY_ENABLE_LOGS = config.getboolean('sentry', 'enable_logs', fallback=False)
@@ -759,7 +759,7 @@ if config.has_option('sentry', 'dsn') and not any(c in sys.argv for c in ('shell
traces_sampler=traces_sampler,
environment=urlparse(SITE_URL).netloc,
release=__version__,
event_scrubber=EventScrubber(denylist=pretix_denylist, recursive=True),
event_scrubber=PretixEventScrubber(denylist=pretix_denylist, recursive=True),
send_default_pii=False,
propagate_traces=False, # see https://github.com/getsentry/sentry-python/issues/1717
)
@@ -1,72 +1,81 @@
<script setup lang="ts">
import { computed } from 'vue'
import { rules as rawRules, allItems, activeItems, allProducts, limitProducts } from './django-interop'
import { convertToDNF } from './jsonlogic-boolalg'
import { computed } from "vue";
import {
rules as rawRules,
allItems,
activeItems,
allProducts,
limitProducts,
} from "./django-interop";
import { convertToDNF } from "./jsonlogic-boolalg";
import RulesEditor from './checkin-rules-editor.vue'
import RulesVisualization from './checkin-rules-visualization.vue'
import RulesEditor from "./checkin-rules-editor.vue";
import RulesVisualization from "./checkin-rules-visualization.vue";
const gettext = (window as any).gettext
const gettext = (window as any).gettext;
const missingItems = computed(() => {
// This computed variable contains list of item or variation names that
// a) Are allowed on the checkin list according to all_products or include_products
// b) Are not matched by ANY logical branch of the rule.
// The list will be empty if there is a "catch-all" rule.
let productsSeen = {}
let variationsSeen = {}
let rules = convertToDNF(rawRules.value)
let branchWithoutProductFilter = false
let productsSeen = {};
let variationsSeen = {};
let rules = convertToDNF(rawRules.value);
let branchWithoutProductFilter = false;
if (!rules.or) {
rules = { or: [rules] }
rules = { or: [rules] };
}
for (let part of rules.or) {
if (!part.and) {
part = { and: [part] }
part = { and: [part] };
}
let thisBranchWithoutProductFilter = true
let thisBranchWithoutProductFilter = true;
for (let subpart of part.and) {
if (subpart.inList) {
if (subpart.inList[0].var === 'product' && subpart.inList[1]) {
thisBranchWithoutProductFilter = false
if (subpart.inList[0].var === "product" && subpart.inList[1]) {
thisBranchWithoutProductFilter = false;
for (let listentry of subpart.inList[1].objectList) {
productsSeen[parseInt(listentry.lookup[1])] = true
productsSeen[parseInt(listentry.lookup[1])] = true;
}
} else if (subpart.inList[0].var === 'variation' && subpart.inList[1]) {
thisBranchWithoutProductFilter = false
} else if (subpart.inList[0].var === "variation" && subpart.inList[1]) {
thisBranchWithoutProductFilter = false;
for (let listentry_ of subpart.inList[1].objectList) {
variationsSeen[parseInt(listentry_.lookup[1])] = true
variationsSeen[parseInt(listentry_.lookup[1])] = true;
}
}
}
}
if (thisBranchWithoutProductFilter) {
branchWithoutProductFilter = true
break
branchWithoutProductFilter = true;
break;
}
}
if (branchWithoutProductFilter || (!Object.keys(productsSeen).length && !Object.keys(variationsSeen).length)) {
if (
branchWithoutProductFilter ||
(!Object.keys(productsSeen).length && !Object.keys(variationsSeen).length)
) {
// At least one branch with no product filters at all – that's fine.
return []
return [];
}
let missing = []
let missing = [];
for (const item of activeItems.value) {
if (productsSeen[item.id]) continue
if (!allProducts.value && !limitProducts.value.includes(item.id)) continue
if (productsSeen[item.id]) continue;
if (!allProducts.value && !limitProducts.value.includes(item.id)) continue;
if (item.variations.length > 0) {
for (let variation of item.variations) {
if (variationsSeen[variation.id]) continue
missing.push(item.name + ' – ' + variation.name)
if (variationsSeen[variation.id]) continue;
missing.push(item.name + " – " + variation.name);
}
} else {
missing.push(item.name)
missing.push(item.name);
}
}
return missing
})
return missing;
});
</script>
<template lang="pug">
#rules-editor.form-inline
@@ -98,5 +107,4 @@ const missingItems = computed(() => {
li(v-for="h in missingItems", :key="h") {{ h }}
p {{ gettext("Please double-check if this was intentional.") }}
</template>
<style lang="stylus">
</style>
<style lang="stylus"></style>
@@ -1,230 +1,258 @@
<script setup lang="ts">
/* eslint-disable vue/no-mutating-props */
import { computed } from 'vue'
import { TEXTS, VARS, TYPEOPS } from './constants'
import { productSelectURL, variationSelectURL, gateSelectURL } from './django-interop'
import LookupSelect2 from './lookup-select2.vue'
import Datetimefield from './datetimefield.vue'
import Timefield from './timefield.vue'
import { computed } from "vue";
import { TEXTS, VARS, TYPEOPS } from "./constants";
import {
productSelectURL,
variationSelectURL,
gateSelectURL,
} from "./django-interop";
import LookupSelect2 from "./lookup-select2.vue";
import Datetimefield from "./datetimefield.vue";
import Timefield from "./timefield.vue";
const props = defineProps<{
rule: any
level: number
index: number
}>()
rule: any;
level: number;
index: number;
}>();
const emit = defineEmits<{
remove: []
duplicate: []
}>()
remove: [];
duplicate: [];
}>();
const operator = computed(() => Object.keys(props.rule)[0])
const operands = computed(() => props.rule[operator.value])
const operator = computed(() => Object.keys(props.rule)[0]);
const operands = computed(() => props.rule[operator.value]);
const variable = computed(() => {
const op = operator.value
if (op === 'and' || op === 'or') {
return op
const op = operator.value;
if (op === "and" || op === "or") {
return op;
} else if (props.rule[op]?.[0]) {
if (props.rule[op][0]['entries_since']) return 'entries_since'
if (props.rule[op][0]['entries_before']) return 'entries_before'
if (props.rule[op][0]['entries_days_since']) return 'entries_days_since'
if (props.rule[op][0]['entries_days_before']) return 'entries_days_before'
return props.rule[op][0]['var']
if (props.rule[op][0]["entries_since"]) return "entries_since";
if (props.rule[op][0]["entries_before"]) return "entries_before";
if (props.rule[op][0]["entries_days_since"]) return "entries_days_since";
if (props.rule[op][0]["entries_days_before"]) return "entries_days_before";
return props.rule[op][0]["var"];
}
return null
})
return null;
});
const rightoperand = computed(() => {
const op = operator.value
if (op === 'and' || op === 'or') return null
return props.rule[op]?.[1] ?? null
})
const op = operator.value;
if (op === "and" || op === "or") return null;
return props.rule[op]?.[1] ?? null;
});
const classObject = computed(() => ({
'checkin-rule': true,
['checkin-rule-' + variable.value]: true
}))
"checkin-rule": true,
["checkin-rule-" + variable.value]: true,
}));
const vartype = computed(() => VARS[variable.value]?.type)
const vartype = computed(() => VARS[variable.value]?.type);
const timeType = computed(() => {
if (vartype.value === 'int_by_datetime') {
return props.rule[operator.value]?.[0]?.[variable.value]?.[0]?.buildTime?.[0]
if (vartype.value === "int_by_datetime") {
return props.rule[operator.value]?.[0]?.[variable.value]?.[0]
?.buildTime?.[0];
}
return rightoperand.value?.buildTime?.[0]
})
return rightoperand.value?.buildTime?.[0];
});
const timeTolerance = computed(() => {
const op = operator.value
if ((op === 'isBefore' || op === 'isAfter') && props.rule[op]?.[2] !== undefined) {
return props.rule[op][2]
const op = operator.value;
if (
(op === "isBefore" || op === "isAfter") &&
props.rule[op]?.[2] !== undefined
) {
return props.rule[op][2];
}
return null
})
return null;
});
const timeValue = computed(() => {
if (vartype.value === 'int_by_datetime') {
return props.rule[operator.value]?.[0]?.[variable.value]?.[0]?.buildTime?.[1]
if (vartype.value === "int_by_datetime") {
return props.rule[operator.value]?.[0]?.[variable.value]?.[0]
?.buildTime?.[1];
}
return rightoperand.value?.buildTime?.[1]
})
return rightoperand.value?.buildTime?.[1];
});
const cardinality = computed(() => TYPEOPS[vartype.value]?.[operator.value]?.cardinality)
const operators = computed(() => TYPEOPS[vartype.value])
const cardinality = computed(
() => TYPEOPS[vartype.value]?.[operator.value]?.cardinality,
);
const operators = computed(() => TYPEOPS[vartype.value]);
function setVariable (event: Event) {
const target = event.target as HTMLSelectElement
const currentOp = Object.keys(props.rule)[0]
let currentVal = props.rule[currentOp]
function setVariable(event: Event) {
const target = event.target as HTMLSelectElement;
const currentOp = Object.keys(props.rule)[0];
let currentVal = props.rule[currentOp];
if (target.value === 'and' || target.value === 'or') {
if (currentVal[0]?.var) currentVal = []
props.rule[target.value] = currentVal
delete props.rule[currentOp]
if (target.value === "and" || target.value === "or") {
if (currentVal[0]?.var) currentVal = [];
props.rule[target.value] = currentVal;
delete props.rule[currentOp];
} else {
if (currentVal !== 'and' && currentVal !== 'or' && currentVal[0] && VARS[target.value]?.type === vartype.value) {
if (vartype.value === 'int_by_datetime') {
const currentData = props.rule[currentOp][0][variable.value]
props.rule[currentOp][0] = { [target.value]: JSON.parse(JSON.stringify(currentData)) }
if (
currentVal !== "and" &&
currentVal !== "or" &&
currentVal[0] &&
VARS[target.value]?.type === vartype.value
) {
if (vartype.value === "int_by_datetime") {
const currentData = props.rule[currentOp][0][variable.value];
props.rule[currentOp][0] = {
[target.value]: JSON.parse(JSON.stringify(currentData)),
};
} else {
props.rule[currentOp][0].var = target.value
props.rule[currentOp][0].var = target.value;
}
} else if (VARS[target.value]?.type === 'int_by_datetime') {
delete props.rule[currentOp]
props.rule['!!'] = [{ [target.value]: [{ buildTime: [null, null] }] }]
} else if (VARS[target.value]?.type === "int_by_datetime") {
delete props.rule[currentOp];
props.rule["!!"] = [{ [target.value]: [{ buildTime: [null, null] }] }];
} else {
delete props.rule[currentOp]
props.rule['!!'] = [{ var: target.value }]
delete props.rule[currentOp];
props.rule["!!"] = [{ var: target.value }];
}
}
}
function setOperator (event: Event) {
const target = event.target as HTMLSelectElement
const currentOp = Object.keys(props.rule)[0]
const currentVal = props.rule[currentOp]
delete props.rule[currentOp]
props.rule[target.value] = currentVal
function setOperator(event: Event) {
const target = event.target as HTMLSelectElement;
const currentOp = Object.keys(props.rule)[0];
const currentVal = props.rule[currentOp];
delete props.rule[currentOp];
props.rule[target.value] = currentVal;
}
function setRightOperandNumber (event: Event) {
const val = parseInt((event.target as HTMLInputElement).value)
function setRightOperandNumber(event: Event) {
const val = parseInt((event.target as HTMLInputElement).value);
if (props.rule[operator.value].length === 1) {
props.rule[operator.value].push(val)
props.rule[operator.value].push(val);
} else {
props.rule[operator.value][1] = val
props.rule[operator.value][1] = val;
}
}
function setTimeTolerance (event: Event) {
const val = parseInt((event.target as HTMLInputElement).value)
function setTimeTolerance(event: Event) {
const val = parseInt((event.target as HTMLInputElement).value);
if (props.rule[operator.value].length === 2) {
props.rule[operator.value].push(val)
props.rule[operator.value].push(val);
} else {
props.rule[operator.value][2] = val
props.rule[operator.value][2] = val;
}
}
function setTimeType (event: Event) {
const val = (event.target as HTMLSelectElement).value
const time = { buildTime: [val] }
if (vartype.value === 'int_by_datetime') {
props.rule[operator.value][0][variable.value][0] = time
function setTimeType(event: Event) {
const val = (event.target as HTMLSelectElement).value;
const time = { buildTime: [val] };
if (vartype.value === "int_by_datetime") {
props.rule[operator.value][0][variable.value][0] = time;
} else {
if (props.rule[operator.value].length === 1) {
props.rule[operator.value].push(time)
props.rule[operator.value].push(time);
} else {
props.rule[operator.value][1] = time
props.rule[operator.value][1] = time;
}
if (val === 'custom') {
props.rule[operator.value][2] = 0
if (val === "custom") {
props.rule[operator.value][2] = 0;
}
}
}
function setTimeValue (val: string) {
if (vartype.value === 'int_by_datetime') {
props.rule[operator.value][0][variable.value][0]['buildTime'][1] = val
function setTimeValue(val: string) {
if (vartype.value === "int_by_datetime") {
props.rule[operator.value][0][variable.value][0]["buildTime"][1] = val;
} else {
props.rule[operator.value][1]['buildTime'][1] = val
props.rule[operator.value][1]["buildTime"][1] = val;
}
}
function setRightOperandProductList (val: { id: any; text: string }[]) {
const products = { objectList: val.map(item => ({ lookup: ['product', item.id, item.text] })) }
function setRightOperandProductList(val: { id: any; text: string }[]) {
const products = {
objectList: val.map((item) => ({
lookup: ["product", item.id, item.text],
})),
};
if (props.rule[operator.value].length === 1) {
props.rule[operator.value].push(products)
props.rule[operator.value].push(products);
} else {
props.rule[operator.value][1] = products
props.rule[operator.value][1] = products;
}
}
function setRightOperandVariationList (val: { id: any; text: string }[]) {
const products = { objectList: val.map(item => ({ lookup: ['variation', item.id, item.text] })) }
function setRightOperandVariationList(val: { id: any; text: string }[]) {
const products = {
objectList: val.map((item) => ({
lookup: ["variation", item.id, item.text],
})),
};
if (props.rule[operator.value].length === 1) {
props.rule[operator.value].push(products)
props.rule[operator.value].push(products);
} else {
props.rule[operator.value][1] = products
props.rule[operator.value][1] = products;
}
}
function setRightOperandGateList (val: { id: any; text: string }[]) {
const products = { objectList: val.map(item => ({ lookup: ['gate', item.id, item.text] })) }
function setRightOperandGateList(val: { id: any; text: string }[]) {
const products = {
objectList: val.map((item) => ({ lookup: ["gate", item.id, item.text] })),
};
if (props.rule[operator.value].length === 1) {
props.rule[operator.value].push(products)
props.rule[operator.value].push(products);
} else {
props.rule[operator.value][1] = products
props.rule[operator.value][1] = products;
}
}
function setRightOperandEnum (event: Event) {
const val = (event.target as HTMLSelectElement).value
function setRightOperandEnum(event: Event) {
const val = (event.target as HTMLSelectElement).value;
if (props.rule[operator.value].length === 1) {
props.rule[operator.value].push(val)
props.rule[operator.value].push(val);
} else {
props.rule[operator.value][1] = val
props.rule[operator.value][1] = val;
}
}
function addOperand () {
props.rule[operator.value].push({ '': [] })
function addOperand() {
props.rule[operator.value].push({ "": [] });
}
function wrapWithOR () {
const r = JSON.parse(JSON.stringify(props.rule))
delete props.rule[operator.value]
props.rule.or = [r]
function wrapWithOR() {
const r = JSON.parse(JSON.stringify(props.rule));
delete props.rule[operator.value];
props.rule.or = [r];
}
function wrapWithAND () {
const r = JSON.parse(JSON.stringify(props.rule))
delete props.rule[operator.value]
props.rule.and = [r]
function wrapWithAND() {
const r = JSON.parse(JSON.stringify(props.rule));
delete props.rule[operator.value];
props.rule.and = [r];
}
function cutOut () {
const cop = Object.keys(operands.value[0])[0]
const r = operands.value[0][cop]
delete props.rule[operator.value]
props.rule[cop] = r
function cutOut() {
const cop = Object.keys(operands.value[0])[0];
const r = operands.value[0][cop];
delete props.rule[operator.value];
props.rule[cop] = r;
}
function remove () {
emit('remove')
function remove() {
emit("remove");
}
function duplicate () {
emit('duplicate')
function duplicate() {
emit("duplicate");
}
function removeChild (index: number) {
props.rule[operator.value].splice(index, 1)
function removeChild(index: number) {
props.rule[operator.value].splice(index, 1);
}
function duplicateChild (index: number) {
const r = JSON.parse(JSON.stringify(props.rule[operator.value][index]))
props.rule[operator.value].splice(index, 0, r)
function duplicateChild(index: number) {
const r = JSON.parse(JSON.stringify(props.rule[operator.value][index]));
props.rule[operator.value].splice(index, 0, r);
}
</script>
<template lang="pug">
@@ -1,13 +1,13 @@
<script setup lang="ts">
import { computed } from 'vue'
import { TEXTS } from './constants'
import { rules } from './django-interop'
import CheckinRule from './checkin-rule.vue'
import { computed } from "vue";
import { TEXTS } from "./constants";
import { rules } from "./django-interop";
import CheckinRule from "./checkin-rule.vue";
const hasRules = computed(() => !!Object.keys(rules.value).length)
const hasRules = computed(() => !!Object.keys(rules.value).length);
function addRule () {
rules.value.and = []
function addRule() {
rules.value.and = [];
}
</script>
<template lang="pug">
@@ -1,36 +1,36 @@
<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
import { rules } from './django-interop'
import VizNode from './viz-node.vue'
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from "vue";
import { rules } from "./django-interop";
import VizNode from "./viz-node.vue";
declare const d3: any
declare const d3: any;
const svg = ref<SVGSVGElement | null>(null)
const maximized = ref(false)
const zoom = ref<any>(null)
const defaultScale = ref(1)
const zoomTransform = ref(d3.zoomTransform({ k: 1, x: 0, y: 0 }))
const svg = ref<SVGSVGElement | null>(null);
const maximized = ref(false);
const zoom = ref<any>(null);
const defaultScale = ref(1);
const zoomTransform = ref(d3.zoomTransform({ k: 1, x: 0, y: 0 }));
const boxWidth = 300
const boxHeight = 62
const paddingX = 50
const marginX = 50
const marginY = 20
const boxWidth = 300;
const boxHeight = 62;
const paddingX = 50;
const marginX = 50;
const marginY = 20;
interface GraphNode {
rule: any
column: number
children: string[]
y?: number
parent?: GraphNode
rule: any;
column: number;
children: string[];
y?: number;
parent?: GraphNode;
}
interface Graph {
nodes_by_id: Record<string, GraphNode>
children: string[]
columns: number
height: number
y?: number
nodes_by_id: Record<string, GraphNode>;
children: string[];
columns: number;
height: number;
y?: number;
}
const graph = computed<Graph>(() => {
@@ -69,175 +69,198 @@ const graph = computed<Graph>(() => {
children: [],
columns: -1,
height: 1,
}
};
// Step 1: Start building the graph by finding all nodes and edges
let counter = 0
const _add_to_graph = (rule: any): [string[], string[]] => { // returns [heads, tails]
if (typeof rule !== 'object' || rule === null) {
const node_id = (counter++).toString()
let counter = 0;
const _add_to_graph = (rule: any): [string[], string[]] => {
// returns [heads, tails]
if (typeof rule !== "object" || rule === null) {
const node_id = (counter++).toString();
graphData.nodes_by_id[node_id] = {
rule: rule,
column: -1,
children: [],
}
return [[node_id], [node_id]]
};
return [[node_id], [node_id]];
}
const operator = Object.keys(rule)[0]
const operands = rule[operator]
const operator = Object.keys(rule)[0];
const operands = rule[operator];
if (operator === 'and') {
let children: string[] = []
let tails: string[] | null = null
operands.reverse()
if (operator === "and") {
let children: string[] = [];
let tails: string[] | null = null;
operands.reverse();
for (const operand of operands) {
const [new_children, new_tails] = _add_to_graph(operand)
const [new_children, new_tails] = _add_to_graph(operand);
for (const new_child of new_tails) {
graphData.nodes_by_id[new_child].children.push(...children)
graphData.nodes_by_id[new_child].children.push(...children);
for (const c of children) {
graphData.nodes_by_id[c].parent = graphData.nodes_by_id[new_child]
graphData.nodes_by_id[c].parent = graphData.nodes_by_id[new_child];
}
}
if (tails === null) {
tails = new_tails
tails = new_tails;
}
children = new_children
children = new_children;
}
return [children, tails!]
} else if (operator === 'or') {
const children: string[] = []
const tails: string[] = []
return [children, tails!];
} else if (operator === "or") {
const children: string[] = [];
const tails: string[] = [];
for (const operand of operands) {
const [new_children, new_tails] = _add_to_graph(operand)
children.push(...new_children)
tails.push(...new_tails)
const [new_children, new_tails] = _add_to_graph(operand);
children.push(...new_children);
tails.push(...new_tails);
}
return [children, tails]
return [children, tails];
} else {
const node_id = (counter++).toString()
const node_id = (counter++).toString();
graphData.nodes_by_id[node_id] = {
rule: rule,
column: -1,
children: [],
}
return [[node_id], [node_id]]
};
return [[node_id], [node_id]];
}
}
graphData.children = _add_to_graph(JSON.parse(JSON.stringify(rules.value)))[0]
};
graphData.children = _add_to_graph(
JSON.parse(JSON.stringify(rules.value)),
)[0];
// Step 2: We compute the "column" of every node, which is the maximum number of hops required to reach the
// node from the root node
const _set_column_to_min = (nodes: GraphNode[], mincol: number) => {
for (const node of nodes) {
if (mincol > node.column) {
node.column = mincol
graphData.columns = Math.max(mincol + 1, graphData.columns)
_set_column_to_min(node.children.map(nid => graphData.nodes_by_id[nid]), mincol + 1)
node.column = mincol;
graphData.columns = Math.max(mincol + 1, graphData.columns);
_set_column_to_min(
node.children.map((nid) => graphData.nodes_by_id[nid]),
mincol + 1,
);
}
}
}
_set_column_to_min(graphData.children.map(nid => graphData.nodes_by_id[nid]), 0)
};
_set_column_to_min(
graphData.children.map((nid) => graphData.nodes_by_id[nid]),
0,
);
// Step 3: Align each node on a grid. The x position is already given by the column computed above, but we still
// need the y position. This part of the algorithm is opinionated and probably not yet the nicest solution we
// can use!
const _set_y = (node: Graph | GraphNode, offset: number): number => {
if (typeof node.y === 'undefined') {
if (typeof node.y === "undefined") {
// We only take the first value we found for each node
node.y = offset
node.y = offset;
}
let used = 0
let used = 0;
for (const cid of node.children) {
used += Math.max(0, _set_y(graphData.nodes_by_id[cid], offset + used) - 1)
used++
used += Math.max(
0,
_set_y(graphData.nodes_by_id[cid], offset + used) - 1,
);
used++;
}
return used
}
_set_y(graphData, 0)
return used;
};
_set_y(graphData, 0);
// Step 4: Compute the "height" of the graph by looking at the node with the highest y value
graphData.height = 1
graphData.height = 1;
for (const node of [...Object.values(graphData.nodes_by_id)]) {
graphData.height = Math.max(graphData.height, (node.y ?? 0) + 1)
graphData.height = Math.max(graphData.height, (node.y ?? 0) + 1);
}
return graphData
})
return graphData;
});
const contentWidth = computed(() => {
return graph.value.columns * (boxWidth + marginX) + 2 * paddingX
})
return graph.value.columns * (boxWidth + marginX) + 2 * paddingX;
});
const contentHeight = computed(() => {
return graph.value.height * (boxHeight + marginY)
})
return graph.value.height * (boxHeight + marginY);
});
const viewBox = computed(() => {
return `0 0 ${contentWidth.value} ${contentHeight.value}`
})
return `0 0 ${contentWidth.value} ${contentHeight.value}`;
});
function createZoom () {
if (!svg.value) return
function createZoom() {
if (!svg.value) return;
const viewportHeight = svg.value.clientHeight
const viewportWidth = svg.value.clientWidth
defaultScale.value = 1
const viewportHeight = svg.value.clientHeight;
const viewportWidth = svg.value.clientWidth;
defaultScale.value = 1;
zoom.value = d3
.zoom()
.scaleExtent([Math.min(defaultScale.value * 0.5, 1), Math.max(5, contentHeight.value / viewportHeight, contentWidth.value / viewportWidth)])
.extent([[0, 0], [viewportWidth, viewportHeight]])
.scaleExtent([
Math.min(defaultScale.value * 0.5, 1),
Math.max(
5,
contentHeight.value / viewportHeight,
contentWidth.value / viewportWidth,
),
])
.extent([
[0, 0],
[viewportWidth, viewportHeight],
])
.filter((event: any) => {
const wheeled = event.type === 'wheel'
const mouseDrag
= event.type === 'mousedown'
|| event.type === 'mouseup'
|| event.type === 'mousemove'
const touch
= event.type === 'touchstart'
|| event.type === 'touchmove'
|| event.type === 'touchstop'
return (wheeled || mouseDrag || touch) && maximized.value
const wheeled = event.type === "wheel";
const mouseDrag =
event.type === "mousedown" ||
event.type === "mouseup" ||
event.type === "mousemove";
const touch =
event.type === "touchstart" ||
event.type === "touchmove" ||
event.type === "touchstop";
return (wheeled || mouseDrag || touch) && maximized.value;
})
.wheelDelta((event: any) => {
// In contrast to default implementation, do not use a factor 10 if ctrl is pressed
return -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002)
})
.on('zoom', (event: any) => {
zoomTransform.value = event.transform
return (
-event.deltaY *
(event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002)
);
})
.on("zoom", (event: any) => {
zoomTransform.value = event.transform;
});
const initTransform = d3.zoomIdentity
.scale(defaultScale.value)
.translate(0, 0)
zoomTransform.value = initTransform
.translate(0, 0);
zoomTransform.value = initTransform;
// This sets correct d3 internal state for the initial centering
d3.select(svg.value)
.call(zoom.value.transform, initTransform)
d3.select(svg.value).call(zoom.value.transform, initTransform);
const svgSelection = d3.select(svg.value).call(zoom.value)
svgSelection.on('touchmove.zoom', null)
const svgSelection = d3.select(svg.value).call(zoom.value);
svgSelection.on("touchmove.zoom", null);
// TODO touch support
}
watch(maximized, () => {
nextTick(() => {
createZoom()
})
})
createZoom();
});
});
onMounted(() => {
createZoom()
window.addEventListener('resize', createZoom)
})
createZoom();
window.addEventListener("resize", createZoom);
});
onUnmounted(() => {
window.removeEventListener('resize', createZoom)
})
window.removeEventListener("resize", createZoom);
});
</script>
<template lang="pug">
div(:class="'checkin-rules-visualization ' + (maximized ? 'maximized' : '')")
@@ -1,20 +1,20 @@
/* global gettext, pgettext */
export const TEXTS = {
and: gettext('All of the conditions below (AND)'),
or: gettext('At least one of the conditions below (OR)'),
date_from: gettext('Event start'),
date_to: gettext('Event end'),
date_admission: gettext('Event admission'),
date_custom: gettext('custom date and time'),
date_customtime: gettext('custom time'),
date_tolerance: gettext('Tolerance (minutes)'),
condition_add: gettext('Add condition'),
minutes: gettext('minutes'),
duplicate: gettext('Duplicate'),
status_present: pgettext('entry_status', 'present'),
status_absent: pgettext('entry_status', 'absent'),
}
and: gettext("All of the conditions below (AND)"),
or: gettext("At least one of the conditions below (OR)"),
date_from: gettext("Event start"),
date_to: gettext("Event end"),
date_admission: gettext("Event admission"),
date_custom: gettext("custom date and time"),
date_customtime: gettext("custom time"),
date_tolerance: gettext("Tolerance (minutes)"),
condition_add: gettext("Add condition"),
minutes: gettext("minutes"),
duplicate: gettext("Duplicate"),
status_present: pgettext("entry_status", "present"),
status_absent: pgettext("entry_status", "absent"),
};
export const TYPEOPS = {
// Every change to our supported JSON logic must be done
@@ -26,173 +26,173 @@ export const TYPEOPS = {
// * in pretixscan-ios
product: {
inList: {
label: gettext('is one of'),
label: gettext("is one of"),
cardinality: 2,
}
},
},
variation: {
inList: {
label: gettext('is one of'),
label: gettext("is one of"),
cardinality: 2,
}
},
},
gate: {
inList: {
label: gettext('is one of'),
label: gettext("is one of"),
cardinality: 2,
}
},
},
datetime: {
isBefore: {
label: gettext('is before'),
label: gettext("is before"),
cardinality: 2,
},
isAfter: {
label: gettext('is after'),
label: gettext("is after"),
cardinality: 2,
},
},
enum_entry_status: {
'==': {
label: gettext('='),
"==": {
label: gettext("="),
cardinality: 2,
},
},
int_by_datetime: {
'<': {
label: '<',
"<": {
label: "<",
cardinality: 2,
},
'<=': {
label: '≤',
"<=": {
label: "≤",
cardinality: 2,
},
'>': {
label: '>',
">": {
label: ">",
cardinality: 2,
},
'>=': {
label: '≥',
">=": {
label: "≥",
cardinality: 2,
},
'==': {
label: '=',
"==": {
label: "=",
cardinality: 2,
},
'!=': {
label: '≠',
"!=": {
label: "≠",
cardinality: 2,
},
},
int: {
'<': {
label: '<',
"<": {
label: "<",
cardinality: 2,
},
'<=': {
label: '≤',
"<=": {
label: "≤",
cardinality: 2,
},
'>': {
label: '>',
">": {
label: ">",
cardinality: 2,
},
'>=': {
label: '≥',
">=": {
label: "≥",
cardinality: 2,
},
'==': {
label: '=',
"==": {
label: "=",
cardinality: 2,
},
'!=': {
label: '≠',
"!=": {
label: "≠",
cardinality: 2,
},
},
}
};
export const VARS = {
product: {
label: gettext('Product'),
type: 'product',
label: gettext("Product"),
type: "product",
},
variation: {
label: gettext('Product variation'),
type: 'variation',
label: gettext("Product variation"),
type: "variation",
},
gate: {
label: gettext('Gate'),
type: 'gate',
label: gettext("Gate"),
type: "gate",
},
now: {
label: gettext('Current date and time'),
type: 'datetime',
label: gettext("Current date and time"),
type: "datetime",
},
now_isoweekday: {
label: gettext('Current day of the week (1 = Monday, 7 = Sunday)'),
type: 'int',
label: gettext("Current day of the week (1 = Monday, 7 = Sunday)"),
type: "int",
},
entry_status: {
label: gettext('Current entry status'),
type: 'enum_entry_status',
label: gettext("Current entry status"),
type: "enum_entry_status",
},
entries_number: {
label: gettext('Number of previous entries'),
type: 'int',
label: gettext("Number of previous entries"),
type: "int",
},
entries_today: {
label: gettext('Number of previous entries since midnight'),
type: 'int',
label: gettext("Number of previous entries since midnight"),
type: "int",
},
entries_since: {
label: gettext('Number of previous entries since'),
type: 'int_by_datetime',
label: gettext("Number of previous entries since"),
type: "int_by_datetime",
},
entries_before: {
label: gettext('Number of previous entries before'),
type: 'int_by_datetime',
label: gettext("Number of previous entries before"),
type: "int_by_datetime",
},
entries_days: {
label: gettext('Number of days with a previous entry'),
type: 'int',
label: gettext("Number of days with a previous entry"),
type: "int",
},
entries_days_since: {
label: gettext('Number of days with a previous entry since'),
type: 'int_by_datetime',
label: gettext("Number of days with a previous entry since"),
type: "int_by_datetime",
},
entries_days_before: {
label: gettext('Number of days with a previous entry before'),
type: 'int_by_datetime',
label: gettext("Number of days with a previous entry before"),
type: "int_by_datetime",
},
minutes_since_last_entry: {
label: gettext('Minutes since last entry (-1 on first entry)'),
type: 'int',
label: gettext("Minutes since last entry (-1 on first entry)"),
type: "int",
},
minutes_since_first_entry: {
label: gettext('Minutes since first entry (-1 on first entry)'),
type: 'int',
label: gettext("Minutes since first entry (-1 on first entry)"),
type: "int",
},
}
};
export const DATETIME_OPTIONS = {
format: document.body.dataset.datetimeformat,
locale: document.body.dataset.datetimelocale,
useCurrent: false,
icons: {
time: 'fa fa-clock-o',
date: 'fa fa-calendar',
up: 'fa fa-chevron-up',
down: 'fa fa-chevron-down',
previous: 'fa fa-chevron-left',
next: 'fa fa-chevron-right',
today: 'fa fa-screenshot',
clear: 'fa fa-trash',
close: 'fa fa-remove'
}
}
time: "fa fa-clock-o",
date: "fa fa-calendar",
up: "fa fa-chevron-up",
down: "fa fa-chevron-down",
previous: "fa fa-chevron-left",
next: "fa fa-chevron-right",
today: "fa fa-screenshot",
clear: "fa fa-trash",
close: "fa fa-remove",
},
};
export const TIME_OPTIONS = {
...DATETIME_OPTIONS,
format: document.body.dataset.timeformat,
}
};
@@ -1,21 +1,24 @@
<script setup lang="ts">
import { ref, watch, onMounted, onUnmounted } from 'vue'
import { DATETIME_OPTIONS } from './constants'
import { ref, watch, onMounted, onUnmounted } from "vue";
import { DATETIME_OPTIONS } from "./constants";
const props = defineProps<{
required?: boolean
value?: string
}>()
required?: boolean;
value?: string;
}>();
const emit = defineEmits<{
input: [value: string]
}>()
input: [value: string];
}>();
const input = ref<HTMLInputElement | null>(null)
const input = ref<HTMLInputElement | null>(null);
watch(() => props.value, (val) => {
$(input.value).data('DateTimePicker').date(moment(val))
})
watch(
() => props.value,
(val) => {
$(input.value).data("DateTimePicker").date(moment(val));
},
);
onMounted(() => {
$(input.value)
@@ -23,22 +26,22 @@ onMounted(() => {
...DATETIME_OPTIONS,
showClear: props.required,
})
.trigger('change')
.on('dp.change', function (this: HTMLElement) {
emit('input', $(this).data('DateTimePicker').date().toISOString())
})
.trigger("change")
.on("dp.change", function (this: HTMLElement) {
emit("input", $(this).data("DateTimePicker").date().toISOString());
});
if (!props.value) {
$(input.value).data('DateTimePicker').viewDate(moment().hour(0).minute(0).second(0).millisecond(0))
$(input.value)
.data("DateTimePicker")
.viewDate(moment().hour(0).minute(0).second(0).millisecond(0));
} else {
$(input.value).data('DateTimePicker').date(moment(props.value))
$(input.value).data("DateTimePicker").date(moment(props.value));
}
})
});
onUnmounted(() => {
$(input.value)
.off()
.datetimepicker('destroy')
})
$(input.value).off().datetimepicker("destroy");
});
</script>
<template lang="pug">
input.form-control(ref="input")
@@ -1,70 +1,95 @@
import { ref, watch } from 'vue'
import { ref, watch } from "vue";
export const allProducts = ref(false)
export const limitProducts = ref<number[]>([])
export const allProducts = ref(false);
export const limitProducts = ref<number[]>([]);
function updateProducts () {
allProducts.value = document.querySelector<HTMLInputElement>('#id_all_products')?.checked ?? false
limitProducts.value = Array.from(document.querySelectorAll<HTMLInputElement>('input[name=limit_products]:checked')).map(el => parseInt(el.value))
function updateProducts() {
allProducts.value =
document.querySelector<HTMLInputElement>("#id_all_products")?.checked ??
false;
limitProducts.value = Array.from(
document.querySelectorAll<HTMLInputElement>(
"input[name=limit_products]:checked",
),
).map((el) => parseInt(el.value));
}
// listen to change events for products
document.querySelectorAll('#id_all_products, input[name=limit_products]').forEach(el => el.addEventListener('change', updateProducts))
updateProducts()
document
.querySelectorAll("#id_all_products, input[name=limit_products]")
.forEach((el) => el.addEventListener("change", updateProducts));
updateProducts();
export const rules = ref<any>({})
export const rules = ref<any>({});
// grab rules from hidden input
const rulesInput = document.querySelector<HTMLInputElement>('#id_rules')
const rulesInput = document.querySelector<HTMLInputElement>("#id_rules");
if (rulesInput?.value) {
rules.value = JSON.parse(rulesInput.value) ?? {}
rules.value = JSON.parse(rulesInput.value) ?? {};
}
// sync back to hidden input
watch(rules, (newVal) => {
if (!rulesInput) return
rulesInput.value = JSON.stringify(newVal)
}, { deep: true })
watch(
rules,
(newVal) => {
if (!rulesInput) return;
rulesInput.value = JSON.stringify(newVal);
},
{ deep: true },
);
export const activeItems = ref<any[]>([])
export const allItems = ref<any[]>([])
export const activeItems = ref<any[]>([]);
export const allItems = ref<any[]>([]);
const itemsEl = document.querySelector('#items')
const itemsEl = document.querySelector("#items");
if (itemsEl?.textContent) {
allItems.value = JSON.parse(itemsEl.textContent || '[]')
activeItems.value = allItems.value.filter(item => item.active)
allItems.value = JSON.parse(itemsEl.textContent || "[]");
activeItems.value = allItems.value.filter((item) => item.active);
function checkForInvalidIds (validProducts: Record<string, string>, validVariations: Record<string, string>, rule: any) {
if (rule['and']) {
for (const child of rule['and'])
checkForInvalidIds(validProducts, validVariations, child)
} else if (rule['or']) {
for (const child of rule['or'])
checkForInvalidIds(validProducts, validVariations, child)
} else if (rule['inList'] && rule['inList'][0]['var'] === 'product') {
for (const item of rule['inList'][1]['objectList']) {
if (!validProducts[item['lookup'][1]])
item['lookup'][2] = '[' + gettext('Error: Product not found!') + ']'
else
item['lookup'][2] = validProducts[item['lookup'][1]]
function checkForInvalidIds(
validProducts: Record<string, string>,
validVariations: Record<string, string>,
rule: any,
) {
if (rule["and"]) {
for (const child of rule["and"])
checkForInvalidIds(validProducts, validVariations, child);
} else if (rule["or"]) {
for (const child of rule["or"])
checkForInvalidIds(validProducts, validVariations, child);
} else if (rule["inList"] && rule["inList"][0]["var"] === "product") {
for (const item of rule["inList"][1]["objectList"]) {
if (!validProducts[item["lookup"][1]])
item["lookup"][2] = "[" + gettext("Error: Product not found!") + "]";
else item["lookup"][2] = validProducts[item["lookup"][1]];
}
} else if (rule['inList'] && rule['inList'][0]['var'] === 'variation') {
for (const item of rule['inList'][1]['objectList']) {
if (!validVariations[item['lookup'][1]])
item['lookup'][2] = '[' + gettext('Error: Variation not found!') + ']'
else
item['lookup'][2] = validVariations[item['lookup'][1]]
} else if (rule["inList"] && rule["inList"][0]["var"] === "variation") {
for (const item of rule["inList"][1]["objectList"]) {
if (!validVariations[item["lookup"][1]])
item["lookup"][2] =
"[" + gettext("Error: Variation not found!") + "]";
else item["lookup"][2] = validVariations[item["lookup"][1]];
}
}
}
checkForInvalidIds(
Object.fromEntries(allItems.value.map(p => [p.id, p.name])),
Object.fromEntries(allItems.value.flatMap(p => p.variations?.map(v => [v.id, p.name + ' – ' + v.name]) ?? [])),
rules.value
)
Object.fromEntries(allItems.value.map((p) => [p.id, p.name])),
Object.fromEntries(
allItems.value.flatMap(
(p) => p.variations?.map((v) => [v.id, p.name + " – " + v.name]) ?? [],
),
),
rules.value,
);
}
export const productSelectURL = ref(document.querySelector('#product-select2')?.textContent)
export const variationSelectURL = ref(document.querySelector('#variations-select2')?.textContent)
export const gateSelectURL = ref(document.querySelector('#gates-select2')?.textContent)
export const productSelectURL = ref(
document.querySelector("#product-select2")?.textContent,
);
export const variationSelectURL = ref(
document.querySelector("#variations-select2")?.textContent,
);
export const gateSelectURL = ref(
document.querySelector("#gates-select2")?.textContent,
);
@@ -1,12 +1,12 @@
import { createApp } from 'vue'
import App from './App.vue'
import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App)
app.mount('#rules-editor')
const app = createApp(App);
app.mount("#rules-editor");
app.config.errorHandler = (error, _vm, info) => {
// vue fatals on errors by default, which is a weird choice
// https://github.com/vuejs/core/issues/3525
// https://github.com/vuejs/router/discussions/2435
console.error('[VUE]', info, error)
}
console.error("[VUE]", info, error);
};
@@ -1,93 +1,110 @@
export function convertToDNF (rules) {
export function convertToDNF(rules) {
// Converts a set of rules to disjunctive normal form, i.e. returns something of the form
// `(a AND b AND c) OR (a AND d AND f)`
// without further nesting.
if (typeof rules !== 'object' || Array.isArray(rules) || rules === null) {
return rules
if (typeof rules !== "object" || Array.isArray(rules) || rules === null) {
return rules;
}
function _distribute_or_over_and (r) {
let operator = Object.keys(r)[0]
let values = r[operator]
if (operator === 'and') {
let arg_to_distribute = null
let other_args = []
function _distribute_or_over_and(r) {
let operator = Object.keys(r)[0];
let values = r[operator];
if (operator === "and") {
let arg_to_distribute = null;
let other_args = [];
for (let arg of values) {
if (typeof arg === 'object' && !Array.isArray(arg) && typeof arg['or'] !== 'undefined' && arg_to_distribute === null) {
arg_to_distribute = arg
if (
typeof arg === "object" &&
!Array.isArray(arg) &&
typeof arg["or"] !== "undefined" &&
arg_to_distribute === null
) {
arg_to_distribute = arg;
} else {
other_args.push(arg)
other_args.push(arg);
}
}
if (arg_to_distribute === null) {
return r
return r;
}
let or_operands = []
for (let dval of arg_to_distribute['or']) {
or_operands.push({ and: other_args.concat([dval]) })
let or_operands = [];
for (let dval of arg_to_distribute["or"]) {
or_operands.push({ and: other_args.concat([dval]) });
}
return {
or: or_operands
}
or: or_operands,
};
} else if (!operator) {
return r
} else if (operator === '!' || operator === '!!' || operator === '?:' || operator === 'if') {
console.warn('Operator ' + operator + ' currently unsupported by convert_to_dnf')
return r
return r;
} else if (
operator === "!" ||
operator === "!!" ||
operator === "?:" ||
operator === "if"
) {
console.warn(
"Operator " + operator + " currently unsupported by convert_to_dnf",
);
return r;
} else {
return r
return r;
}
}
function _simplify_chained_operators (r) {
function _simplify_chained_operators(r) {
// Simplify `(a OR b) OR (c or d)` to `a OR b OR c OR d` and the same with `AND`
if (typeof r !== 'object' || Array.isArray(r)) {
return r
if (typeof r !== "object" || Array.isArray(r)) {
return r;
}
let operator = Object.keys(r)[0]
let values = r[operator]
if (operator !== 'or' && operator !== 'and') {
return r
let operator = Object.keys(r)[0];
let values = r[operator];
if (operator !== "or" && operator !== "and") {
return r;
}
let new_values = []
let new_values = [];
for (let v of values) {
if (typeof v !== 'object' || Array.isArray(v) || typeof v[operator] === 'undefined') {
new_values.push(v)
if (
typeof v !== "object" ||
Array.isArray(v) ||
typeof v[operator] === "undefined"
) {
new_values.push(v);
} else {
new_values.push(...v[operator])
new_values.push(...v[operator]);
}
}
let result = {}
result[operator] = new_values
return result
let result = {};
result[operator] = new_values;
return result;
}
// Run _distribute_or_over_and on until it no longer changes anything. Do so recursively
// for the full expression tree.
let old_rules = rules
let old_rules = rules;
while (true) {
rules = _distribute_or_over_and(rules)
let operator = Object.keys(rules)[0]
let values = rules[operator]
let no_list = false
rules = _distribute_or_over_and(rules);
let operator = Object.keys(rules)[0];
let values = rules[operator];
let no_list = false;
if (!Array.isArray(values)) {
values = [values]
no_list = true
values = [values];
no_list = true;
}
rules = {}
rules = {};
if (!no_list) {
rules[operator] = []
rules[operator] = [];
for (let v of values) {
rules[operator].push(convertToDNF(v))
rules[operator].push(convertToDNF(v));
}
} else {
rules[operator] = convertToDNF(values[0])
rules[operator] = convertToDNF(values[0]);
}
if (JSON.stringify(old_rules) === JSON.stringify(rules)) { // Let's hope this is good enough...
break
if (JSON.stringify(old_rules) === JSON.stringify(rules)) {
// Let's hope this is good enough...
break;
}
old_rules = rules
old_rules = rules;
}
rules = _simplify_chained_operators(rules)
return rules
rules = _simplify_chained_operators(rules);
return rules;
}
@@ -1,119 +1,141 @@
<script setup lang="ts">
import { ref, watch, onMounted, onUnmounted } from 'vue'
import { ref, watch, onMounted, onUnmounted } from "vue";
declare const $: any
declare const $: any;
export interface ObjectListItem {
lookup: [string, number | string, string]
lookup: [string, number | string, string];
}
export interface ObjectList {
objectList: ObjectListItem[]
objectList: ObjectListItem[];
}
const props = defineProps<{
required?: boolean
value?: ObjectList
placeholder?: string
url?: string
multiple?: boolean
}>()
required?: boolean;
value?: ObjectList;
placeholder?: string;
url?: string;
multiple?: boolean;
}>();
const emit = defineEmits<{
input: [value: any[]]
}>()
input: [value: any[]];
}>();
const select = ref<HTMLSelectElement | null>(null)
const select = ref<HTMLSelectElement | null>(null);
function opts () {
function opts() {
return {
theme: 'bootstrap',
theme: "bootstrap",
delay: 100,
width: '100%',
width: "100%",
multiple: true,
allowClear: props.required,
language: $('body').attr('data-select2-locale'),
language: $("body").attr("data-select2-locale"),
ajax: {
url: props.url,
data: function (params: { term: string; page?: number }) {
return {
query: params.term,
page: params.page || 1
}
}
page: params.page || 1,
};
},
},
templateResult: function (res: { id?: string; text: string }) {
if (!res.id) {
return res.text
return res.text;
}
const $ret = $('<span>').append(
$('<span>').addClass('primary').append($('<div>').text(res.text).html())
)
return $ret
const $ret = $("<span>").append(
$("<span>")
.addClass("primary")
.append($("<div>").text(res.text).html()),
);
return $ret;
},
}
};
}
function build () {
function build() {
$(select.value)
.empty()
.select2(opts())
.val(props.value || '')
.trigger('change')
.on('change', function (this: HTMLElement) {
emit('input', $(this).select2('data'))
})
.val(props.value || "")
.trigger("change")
.on("change", function (this: HTMLElement) {
emit("input", $(this).select2("data"));
});
if (props.value) {
for (let i = 0; i < props.value.objectList.length; i++) {
const option = new Option(props.value.objectList[i].lookup[2], String(props.value.objectList[i].lookup[1]), true, true)
$(select.value).append(option)
const option = new Option(
props.value.objectList[i].lookup[2],
String(props.value.objectList[i].lookup[1]),
true,
true,
);
$(select.value).append(option);
}
}
$(select.value).trigger('change')
$(select.value).trigger("change");
}
watch(() => props.placeholder, () => {
$(select.value).select2('destroy')
build()
})
watch(
() => props.placeholder,
() => {
$(select.value).select2("destroy");
build();
},
);
watch(() => props.required, () => {
$(select.value).select2('destroy')
build()
})
watch(
() => props.required,
() => {
$(select.value).select2("destroy");
build();
},
);
watch(() => props.url, () => {
$(select.value).select2('destroy')
build()
})
watch(
() => props.url,
() => {
$(select.value).select2("destroy");
build();
},
);
watch(() => props.value, (newval, oldval) => {
if (JSON.stringify(newval) !== JSON.stringify(oldval)) {
$(select.value).empty()
if (newval) {
for (let i = 0; i < newval.objectList.length; i++) {
const option = new Option(newval.objectList[i].lookup[2], String(newval.objectList[i].lookup[1]), true, true)
$(select.value).append(option)
watch(
() => props.value,
(newval, oldval) => {
if (JSON.stringify(newval) !== JSON.stringify(oldval)) {
$(select.value).empty();
if (newval) {
for (let i = 0; i < newval.objectList.length; i++) {
const option = new Option(
newval.objectList[i].lookup[2],
String(newval.objectList[i].lookup[1]),
true,
true,
);
$(select.value).append(option);
}
}
$(select.value).trigger("change");
}
$(select.value).trigger('change')
}
})
},
);
let rawSelectEl: HTMLSelectElement | null = null
let rawSelectEl: HTMLSelectElement | null = null;
onMounted(() => {
rawSelectEl = select.value
build()
})
rawSelectEl = select.value;
build();
});
onUnmounted(() => {
if (!rawSelectEl) return
$(rawSelectEl)
.off()
.select2('destroy')
rawSelectEl = null
})
if (!rawSelectEl) return;
$(rawSelectEl).off().select2("destroy");
rawSelectEl = null;
});
</script>
<template lang="pug">
select(ref="select")
@@ -1,21 +1,24 @@
<script setup lang="ts">
import { ref, watch, onMounted, onUnmounted } from 'vue'
import { TIME_OPTIONS } from './constants'
import { ref, watch, onMounted, onUnmounted } from "vue";
import { TIME_OPTIONS } from "./constants";
const props = defineProps<{
required?: boolean
value?: string
}>()
required?: boolean;
value?: string;
}>();
const emit = defineEmits<{
input: [value: string]
}>()
input: [value: string];
}>();
const input = ref<HTMLInputElement | null>(null)
const input = ref<HTMLInputElement | null>(null);
watch(() => props.value, (val) => {
$(input.value).data('DateTimePicker').date(val)
})
watch(
() => props.value,
(val) => {
$(input.value).data("DateTimePicker").date(val);
},
);
onMounted(() => {
$(input.value)
@@ -23,22 +26,22 @@ onMounted(() => {
...TIME_OPTIONS,
showClear: props.required,
})
.trigger('change')
.on('dp.change', function (this: HTMLElement) {
emit('input', $(this).data('DateTimePicker').date().format('HH:mm:ss'))
})
.trigger("change")
.on("dp.change", function (this: HTMLElement) {
emit("input", $(this).data("DateTimePicker").date().format("HH:mm:ss"));
});
if (!props.value) {
$(input.value).data('DateTimePicker').viewDate(moment().hour(0).minute(0).second(0).millisecond(0))
$(input.value)
.data("DateTimePicker")
.viewDate(moment().hour(0).minute(0).second(0).millisecond(0));
} else {
$(input.value).data('DateTimePicker').date(props.value)
$(input.value).data("DateTimePicker").date(props.value);
}
})
});
onUnmounted(() => {
$(input.value)
.off()
.datetimepicker('destroy')
})
$(input.value).off().datetimepicker("destroy");
});
</script>
<template lang="pug">
input.form-control(ref="input")
@@ -1,43 +1,54 @@
<script setup lang="ts">
import { computed } from 'vue'
import { TEXTS, VARS, TYPEOPS } from './constants'
import { computed } from "vue";
import { TEXTS, VARS, TYPEOPS } from "./constants";
declare const $: any
declare const moment: any
declare const $: any;
declare const moment: any;
interface GraphNode {
rule: any
column: number
children: string[]
y: number
parent?: GraphNode
rule: any;
column: number;
children: string[];
y: number;
parent?: GraphNode;
}
const props = defineProps<{
node: GraphNode
nodeid: string
children: GraphNode[]
boxWidth: number
boxHeight: number
marginX: number
marginY: number
paddingX: number
}>()
node: GraphNode;
nodeid: string;
children: GraphNode[];
boxWidth: number;
boxHeight: number;
marginX: number;
marginY: number;
paddingX: number;
}>();
const x = computed(() => {
return props.node.column * (props.boxWidth + props.marginX) + props.marginX / 2 + props.paddingX
})
return (
props.node.column * (props.boxWidth + props.marginX) +
props.marginX / 2 +
props.paddingX
);
});
const y = computed(() => {
return props.node.y * (props.boxHeight + props.marginY) + props.marginY / 2
})
return props.node.y * (props.boxHeight + props.marginY) + props.marginY / 2;
});
const edges = computed(() => {
const startX = x.value + props.boxWidth + 1
const startY = y.value + props.boxHeight / 2
const startX = x.value + props.boxWidth + 1;
const startY = y.value + props.boxHeight / 2;
return props.children.map((c) => {
const endX = (c.column * (props.boxWidth + props.marginX) + props.marginX / 2 + props.paddingX) - 1
const endY = (c.y * (props.boxHeight + props.marginY) + props.marginY / 2) + props.boxHeight / 2
const endX =
c.column * (props.boxWidth + props.marginX) +
props.marginX / 2 +
props.paddingX -
1;
const endY =
c.y * (props.boxHeight + props.marginY) +
props.marginY / 2 +
props.boxHeight / 2;
return `
M ${startX} ${startY}
@@ -45,25 +56,25 @@ const edges = computed(() => {
C ${endX - 25} ${startY} ${endX - 25} ${startY} ${endX - 25} ${startY + 25 * Math.sign(endY - startY)}
L ${endX - 25} ${endY - 25 * Math.sign(endY - startY)}
C ${endX - 25} ${endY} ${endX - 25} ${endY} ${endX} ${endY}
`
})
})
`;
});
});
const checkEdge = computed(() => {
const startX = x.value + props.boxWidth + 1
const startY = y.value + props.boxHeight / 2
const startX = x.value + props.boxWidth + 1;
const startY = y.value + props.boxHeight / 2;
return `M ${startX} ${startY} L ${startX + 25} ${startY}`
})
return `M ${startX} ${startY} L ${startX + 25} ${startY}`;
});
const rootEdge = computed(() => {
if (props.node.column > 0) {
return
return;
}
const startX = 0
const startY = props.boxHeight / 2 + props.marginY / 2
const endX = x.value - 1
const endY = y.value + props.boxHeight / 2
const startX = 0;
const startY = props.boxHeight / 2 + props.marginY / 2;
const endX = x.value - 1;
const endY = y.value + props.boxHeight / 2;
return `
M ${startX} ${startY}
@@ -71,98 +82,101 @@ const rootEdge = computed(() => {
C ${endX - 25} ${startY} ${endX - 25} ${startY} ${endX - 25} ${startY + 25 * Math.sign(endY - startY)}
L ${endX - 25} ${endY - 25 * Math.sign(endY - startY)}
C ${endX - 25} ${endY} ${endX - 25} ${endY} ${endX} ${endY}
`
})
`;
});
const operator = computed(() => {
return Object.keys(props.node.rule).filter((k) => !k.startsWith('__'))[0]
})
return Object.keys(props.node.rule).filter((k) => !k.startsWith("__"))[0];
});
const variable = computed(() => {
const op = operator.value
const op = operator.value;
if (props.node.rule[op] && props.node.rule[op][0]) {
if (props.node.rule[op][0]['entries_since']) {
return 'entries_since'
if (props.node.rule[op][0]["entries_since"]) {
return "entries_since";
}
if (props.node.rule[op][0]['entries_before']) {
return 'entries_before'
if (props.node.rule[op][0]["entries_before"]) {
return "entries_before";
}
if (props.node.rule[op][0]['entries_days_since']) {
return 'entries_days_since'
if (props.node.rule[op][0]["entries_days_since"]) {
return "entries_days_since";
}
if (props.node.rule[op][0]['entries_days_before']) {
return 'entries_days_before'
if (props.node.rule[op][0]["entries_days_before"]) {
return "entries_days_before";
}
return props.node.rule[op][0]['var']
return props.node.rule[op][0]["var"];
} else {
return ''
return "";
}
})
});
const vardata = computed(() => {
return VARS[variable.value as keyof typeof VARS]
})
return VARS[variable.value as keyof typeof VARS];
});
const varresult = computed(() => {
const op = operator.value
const op = operator.value;
if (props.node.rule[op] && props.node.rule[op][0]) {
if (typeof props.node.rule[op][0]['__result'] === 'undefined')
return null
return props.node.rule[op][0]['__result']
if (typeof props.node.rule[op][0]["__result"] === "undefined") return null;
return props.node.rule[op][0]["__result"];
} else {
return ''
return "";
}
})
});
const rightoperand = computed(() => {
const op = operator.value
if (props.node.rule[op] && typeof props.node.rule[op][1] !== 'undefined') {
return props.node.rule[op][1]
const op = operator.value;
if (props.node.rule[op] && typeof props.node.rule[op][1] !== "undefined") {
return props.node.rule[op][1];
} else {
return null
return null;
}
})
});
const op = computed(() => {
return TYPEOPS[vardata.value.type as keyof typeof TYPEOPS]?.[operator.value as any]
})
return TYPEOPS[vardata.value.type as keyof typeof TYPEOPS]?.[
operator.value as any
];
});
const operands = computed(() => {
return props.node.rule[operator.value]
})
return props.node.rule[operator.value];
});
const result = computed(() => {
return typeof props.node.rule.__result === 'undefined' ? null : !!props.node.rule.__result
})
return typeof props.node.rule.__result === "undefined"
? null
: !!props.node.rule.__result;
});
const resultInclParents = computed(() => {
if (typeof props.node.rule.__result === 'undefined') return null
if (typeof props.node.rule.__result === "undefined") return null;
function _p (node: GraphNode): boolean {
function _p(node: GraphNode): boolean {
if (node.parent) {
return node.rule.__result && _p(node.parent)
return node.rule.__result && _p(node.parent);
}
return node.rule.__result
return node.rule.__result;
}
return _p(props.node)
})
return _p(props.node);
});
const nodeClass = computed(() => {
return {
node: true,
'node-true': result.value === true,
'node-false': result.value === false,
}
})
"node-true": result.value === true,
"node-false": result.value === false,
};
});
function df (val: string) {
const format = $('body').attr('data-datetimeformat')
return moment(val).format(format)
function df(val: string) {
const format = $("body").attr("data-datetimeformat");
return moment(val).format(format);
}
function tf (val: string) {
const format = $('body').attr('data-timeformat')
return moment(val, 'HH:mm:ss').format(format)
function tf(val: string) {
const format = $("body").attr("data-timeformat");
return moment(val, "HH:mm:ss").format(format);
}
</script>
<template lang="pug">
@@ -618,18 +618,21 @@ var editor = {
$('#loading-initial').remove()
})
}
editor._update_version_notice()
editor._update_notices()
},
_update_version_notice: function () {
_update_notices: function () {
$('#version-notice').toggle(
editor._other_page_objects.some((o) => o.type === 'textcontainer')
|| editor.fabric.getObjects().some((o) => o.type === 'textcontainer')
)
$('#nocheckin-notice').toggle(
!editor.fabric.getObjects().some((o) => o.type === 'barcodearea' && (!o.content || o.content === 'secret'))
)
},
_update_toolbox_values: function () {
editor._update_version_notice()
editor._update_notices()
let o = editor.fabric.getActiveObject()
if (!o) {
return
@@ -898,6 +901,7 @@ var editor = {
o.setCoords()
editor.fabric.renderAll()
editor._update_notices()
},
_update_toolbox: function () {
@@ -103,3 +103,6 @@ body {
.panel-pdf-editor .panel-heading form > .btn-group {
margin-right: 1em;
}
#nocheckin-notice {
display: none;
}
@@ -1,95 +1,95 @@
import type { Category, DayEntry, EventEntry, MetaFilterField } from '~/types'
import type { Category, DayEntry, EventEntry, MetaFilterField } from "~/types";
export class ApiError extends Error {
status: number
responseUrl: string
status: number;
responseUrl: string;
constructor (status: number, responseUrl: string) {
super(`HTTP ${status}`)
this.status = status
this.responseUrl = responseUrl
constructor(status: number, responseUrl: string) {
super(`HTTP ${status}`);
this.status = status;
this.responseUrl = responseUrl;
}
}
// --- Product list ---
export interface ProductListResponse {
target_url?: string
subevent?: string | number
name?: string
frontpage_text?: string
date_range?: string
location?: string
items_by_category?: Category[]
currency?: string
currency_places?: number
display_net_prices?: boolean
voucher_explanation_text?: string
error?: string
display_add_to_cart?: boolean
waiting_list_enabled?: boolean
show_variations_expanded?: boolean
cart_exists?: boolean
vouchers_exist?: boolean
has_seating_plan?: boolean
has_seating_plan_waitinglist?: boolean
itemnum?: number
poweredby?: string
events?: EventEntry[]
has_more_events?: boolean
meta_filter_fields?: MetaFilterField[]
empty_text?: string
weeks?: DayEntry[][]
date?: string
days?: DayEntry[]
week?: [number, number]
target_url?: string;
subevent?: string | number;
name?: string;
frontpage_text?: string;
date_range?: string;
location?: string;
items_by_category?: Category[];
currency?: string;
currency_places?: number;
display_net_prices?: boolean;
voucher_explanation_text?: string;
error?: string;
display_add_to_cart?: boolean;
waiting_list_enabled?: boolean;
show_variations_expanded?: boolean;
cart_exists?: boolean;
vouchers_exist?: boolean;
has_seating_plan?: boolean;
has_seating_plan_waitinglist?: boolean;
itemnum?: number;
poweredby?: string;
events?: EventEntry[];
has_more_events?: boolean;
meta_filter_fields?: MetaFilterField[];
empty_text?: string;
weeks?: DayEntry[][];
date?: string;
days?: DayEntry[];
week?: [number, number];
}
export async function fetchProductList (url: string) {
const response = await fetch(url)
export async function fetchProductList(url: string) {
const response = await fetch(url);
if (!response.ok) {
throw new ApiError(response.status, response.url)
throw new ApiError(response.status, response.url);
}
return {
data: await response.json() as ProductListResponse,
data: (await response.json()) as ProductListResponse,
responseUrl: response.url,
}
};
}
export interface CartResponse {
redirect?: string
cart_id?: string
success?: boolean
message?: string
has_cart?: boolean
async_id?: string
check_url?: string
redirect?: string;
cart_id?: string;
success?: boolean;
message?: string;
has_cart?: boolean;
async_id?: string;
check_url?: string;
}
export async function submitCart (endpoint: string, formData: FormData) {
export async function submitCart(endpoint: string, formData: FormData) {
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams(formData as any).toString(),
})
});
if (!response.ok) {
throw new ApiError(response.status, response.url)
throw new ApiError(response.status, response.url);
}
return await response.json() as CartResponse
return (await response.json()) as CartResponse;
}
export async function checkAsyncTask (url: string) {
const response = await fetch(url)
export async function checkAsyncTask(url: string) {
const response = await fetch(url);
if (!response.ok) {
throw new ApiError(response.status, response.url)
throw new ApiError(response.status, response.url);
}
return await response.json() as CartResponse
return (await response.json()) as CartResponse;
}
export async function createCart (url: string) {
const response = await fetch(url)
export async function createCart(url: string) {
const response = await fetch(url);
if (!response.ok) {
throw new ApiError(response.status, response.url)
throw new ApiError(response.status, response.url);
}
return await response.json() as CartResponse
return (await response.json()) as CartResponse;
}
@@ -1,31 +1,33 @@
import { createApp, type App } from 'vue'
import ButtonComponent from '~/components/Button.vue'
import { createWidgetStore, StoreKey } from '~/sharedStore'
import { makeid } from '~/utils'
import type { WidgetData } from '~/types'
import { createApp, type App } from "vue";
import ButtonComponent from "~/components/Button.vue";
import { createWidgetStore, StoreKey } from "~/sharedStore";
import { makeid } from "~/utils";
import type { WidgetData } from "~/types";
export function createButtonInstance (element: Element, htmlId?: string): App {
let targetUrl = element.attributes.event.value
export function createButtonInstance(element: Element, htmlId?: string): App {
let targetUrl = element.attributes.event.value;
if (!targetUrl.match(/\/$/)) {
targetUrl += '/'
targetUrl += "/";
}
const widgetData: WidgetData = JSON.parse(JSON.stringify(window.PretixWidget.widget_data))
const widgetData: WidgetData = JSON.parse(
JSON.stringify(window.PretixWidget.widget_data),
);
for (const attr of Array.from(element.attributes)) {
if (attr.name.match(/^data-.*$/)) {
widgetData[attr.name.replace(/^data-/, '')] = attr.value
widgetData[attr.name.replace(/^data-/, "")] = attr.value;
}
}
const rawItems = element.attributes.items?.value || ''
const rawItems = element.attributes.items?.value || "";
// Parse items string (format: "item_1=2,item_3=1")
const buttonItems: { item: string; count: string }[] = []
for (const itemStr of rawItems.split(',')) {
if (itemStr.includes('=')) {
const [item, count] = itemStr.split('=')
buttonItems.push({ item, count })
const buttonItems: { item: string; count: string }[] = [];
for (const itemStr of rawItems.split(",")) {
if (itemStr.includes("=")) {
const [item, count] = itemStr.split("=");
buttonItems.push({ item, count });
}
}
@@ -33,45 +35,50 @@ export function createButtonInstance (element: Element, htmlId?: string): App {
targetUrl,
voucher: element.attributes.voucher?.value || null,
subevent: element.attributes.subevent?.value || null,
skipSsl: 'skip-ssl-check' in element.attributes,
disableIframe: 'disable-iframe' in element.attributes,
skipSsl: "skip-ssl-check" in element.attributes,
disableIframe: "disable-iframe" in element.attributes,
widgetData,
htmlId: htmlId || element.id || makeid(16),
isButton: true,
buttonItems,
buttonText: element.innerHTML,
keepCart: 'keep-cart' in element.attributes || buttonItems.length > 0,
})
keepCart: "keep-cart" in element.attributes || buttonItems.length > 0,
});
const observer = new MutationObserver((mutationList) => {
for (const mutation of mutationList) {
if (mutation.type === 'attributes' && mutation.attributeName?.startsWith('data-')) {
const attrName = mutation.attributeName.substring(5)
const attrValue = (mutation.target as Element).getAttribute(mutation.attributeName)
if (
mutation.type === "attributes" &&
mutation.attributeName?.startsWith("data-")
) {
const attrName = mutation.attributeName.substring(5);
const attrValue = (mutation.target as Element).getAttribute(
mutation.attributeName,
);
if (attrValue !== null) {
store.widgetData[attrName] = attrValue
store.widgetData[attrName] = attrValue;
}
}
}
})
});
const app = createApp(ButtonComponent)
app.provide(StoreKey, store)
const app = createApp(ButtonComponent);
app.provide(StoreKey, store);
app.config.errorHandler = (error, _vm, info) => {
console.error('[pretix-button]', info, error)
}
console.error("[pretix-button]", info, error);
};
// Instead of mounting to the element directly, replicate vue2.7 behaviour, where
// the HTML-element gets replaced instead of vue3’s behaviour to replace innerHTML.
// The latter can cause issues with custom CSS as HTML structure would change.
// app.mount(element)
// observer.observe(element, { attributes: true })
const fragment = document.createDocumentFragment()
app.mount(fragment)
const fragment = document.createDocumentFragment();
app.mount(fragment);
for (const attr of element.attributes) {
fragment.firstChild.setAttribute(attr.name, attr.value)
fragment.firstChild.setAttribute(attr.name, attr.value);
}
observer.observe(fragment.firstChild, { attributes: true })
element.parentNode.replaceChild(fragment, element)
observer.observe(fragment.firstChild, { attributes: true });
element.parentNode.replaceChild(fragment, element);
return app
return app;
}
@@ -1,100 +1,120 @@
<script setup lang="ts">
import { computed, ref, inject, onMounted } from 'vue'
import type { Item, Variation } from '~/types'
import { StoreKey, globalWidgetId } from '~/sharedStore'
import { STRINGS } from '~/i18n'
import { computed, ref, inject, onMounted } from "vue";
import type { Item, Variation } from "~/types";
import { StoreKey, globalWidgetId } from "~/sharedStore";
import { STRINGS } from "~/i18n";
const props = defineProps<{
item: Item
variation?: Variation
}>()
item: Item;
variation?: Variation;
}>();
const store = inject(StoreKey)!
const quantity = ref<HTMLInputElement>()
const store = inject(StoreKey)!;
const quantity = ref<HTMLInputElement>();
const avail = computed(() => props.item.has_variations ? props.variation.avail : props.item.avail)
const avail = computed(() =>
props.item.has_variations ? props.variation.avail : props.item.avail,
);
const orderMax = computed(() => props.item.has_variations ? props.variation.order_max : props.item.order_max)
const orderMax = computed(() =>
props.item.has_variations ? props.variation.order_max : props.item.order_max,
);
const inputName = computed(() => {
if (props.item.has_variations) {
return `variation_${props.item.id}_${props.variation.id}`
return `variation_${props.item.id}_${props.variation.id}`;
}
return `item_${props.item.id}`
})
return `item_${props.item.id}`;
});
const unavailabilityReasonMessage = computed(() => {
const reason = props.item.current_unavailability_reason || props.variation?.current_unavailability_reason
const reason =
props.item.current_unavailability_reason ||
props.variation?.current_unavailability_reason;
if (reason) {
return STRINGS[`unavailable_${reason}`] || reason
return STRINGS[`unavailable_${reason}`] || reason;
}
return ''
})
return "";
});
const voucherJumpLink = computed(() => `#${store.htmlId}-voucher-input`)
const voucherJumpLink = computed(() => `#${store.htmlId}-voucher-input`);
const ariaLabelledby = computed(() => `${store.htmlId}-item-label-${props.item.id}`)
const ariaLabelledby = computed(
() => `${store.htmlId}-item-label-${props.item.id}`,
);
const decLabel = computed(() => {
// TODO
const name = props.item.has_variations ? props.variation.value : props.item.name
return `- ${name}: ${STRINGS.quantity_dec}`
})
const name = props.item.has_variations
? props.variation.value
: props.item.name;
return `- ${name}: ${STRINGS.quantity_dec}`;
});
const incLabel = computed(() => {
const name = props.item.has_variations ? props.variation.value : props.item.name
return `+ ${name}: ${STRINGS.quantity_inc}`
})
const name = props.item.has_variations
? props.variation.value
: props.item.name;
return `+ ${name}: ${STRINGS.quantity_inc}`;
});
const labelSelectItem = computed(() => {
if (props.item.has_variations) return STRINGS.select_variant.replace('%s', props.variation.value)
return STRINGS.select_item.replace('%s', props.item.name)
})
if (props.item.has_variations)
return STRINGS.select_variant.replace("%s", props.variation.value);
return STRINGS.select_item.replace("%s", props.item.name);
});
const waitingListShow = computed(() => avail.value[0] < 100 && store.waitingListEnabled && props.item.allow_waitinglist)
const waitingListShow = computed(
() =>
avail.value[0] < 100 &&
store.waitingListEnabled &&
props.item.allow_waitinglist,
);
const waitingListUrl = computed(() => {
let u = `${store.targetUrl}w/${globalWidgetId}/waitinglist/?locale=${LANG}&item=${props.item.id}`
let u = `${store.targetUrl}w/${globalWidgetId}/waitinglist/?locale=${LANG}&item=${props.item.id}`;
if (props.item.has_variations && props.variation) {
u += `&var=${props.variation.id}`
u += `&var=${props.variation.id}`;
}
if (store.subevent) {
u += `&subevent=${store.subevent}`
u += `&subevent=${store.subevent}`;
}
u += `&widget_data=${encodeURIComponent(store.widgetDataJson)}`
u += store.consentParameter
return u
})
u += `&widget_data=${encodeURIComponent(store.widgetDataJson)}`;
u += store.consentParameter;
return u;
});
function onStep (e: Event) {
const target = e.target as HTMLElement
const button = target.tagName === 'BUTTON' ? target : target.closest('button')
if (!button || !quantity.value) return
function onStep(e: Event) {
const target = e.target as HTMLElement;
const button =
target.tagName === "BUTTON" ? target : target.closest("button");
if (!button || !quantity.value) return;
const step = parseFloat(button.getAttribute('data-step') || '0')
const input = quantity.value
const min = parseFloat(input.min) || 0
const max = parseFloat(input.max) || Number.MAX_SAFE_INTEGER
const currentValue = parseInt(input.value || '0')
input.value = String(Math.max(min, Math.min(max, currentValue + step)))
input.dispatchEvent(new CustomEvent('change', { bubbles: true }))
const step = parseFloat(button.getAttribute("data-step") || "0");
const input = quantity.value;
const min = parseFloat(input.min) || 0;
const max = parseFloat(input.max) || Number.MAX_SAFE_INTEGER;
const currentValue = parseInt(input.value || "0");
input.value = String(Math.max(min, Math.min(max, currentValue + step)));
input.dispatchEvent(new CustomEvent("change", { bubbles: true }));
}
onMounted(() => {
// Auto-select first item if single item with no variations
if (
!store.cartExists
&& store.itemnum === 1
&& (!store.categories[0]?.items[0]?.has_variations || store.categories[0]?.items[0]?.variations.length < 2)
&& !store.hasSeatingPlan
&& quantity.value
!store.cartExists &&
store.itemnum === 1 &&
(!store.categories[0]?.items[0]?.has_variations ||
store.categories[0]?.items[0]?.variations.length < 2) &&
!store.hasSeatingPlan &&
quantity.value
) {
quantity.value.value = '1'
if (orderMax.value === 1 && quantity.value.type === 'checkbox') {
;(quantity.value as HTMLInputElement).checked = true
quantity.value.value = "1";
if (orderMax.value === 1 && quantity.value.type === "checkbox") {
(quantity.value as HTMLInputElement).checked = true;
}
}
})
});
</script>
<template lang="pug">
.pretix-widget-availability-box
@@ -149,5 +169,4 @@ onMounted(() => {
)
span +
</template>
<style lang="sass">
</style>
<style lang="sass"></style>
@@ -1,32 +1,32 @@
<script setup lang="ts">
import { computed, inject, ref } from 'vue'
import { StoreKey } from '~/sharedStore'
import Overlay from './Overlay.vue'
import { computed, inject, ref } from "vue";
import { StoreKey } from "~/sharedStore";
import Overlay from "./Overlay.vue";
const lang = LANG // we need this so the template sees the variable
const lang = LANG; // we need this so the template sees the variable
const store = inject(StoreKey)!
const store = inject(StoreKey)!;
const form = ref<HTMLFormElement>()
const form = ref<HTMLFormElement>();
const formMethod = computed(() => {
if (!store.useIframe && store.isButton && store.items.length === 0) {
return 'get'
return "get";
}
return 'post'
})
return "post";
});
function handleBuy (event: Event) {
function handleBuy(event: Event) {
if (form.value) {
const formData = new FormData(form.value)
store.buy(formData, event)
const formData = new FormData(form.value);
store.buy(formData, event);
}
}
defineExpose({
form,
buy: handleBuy,
})
});
</script>
<template lang="pug">
.pretix-widget-wrapper
@@ -50,5 +50,4 @@ defineExpose({
Overlay
</template>
<style lang="sass">
</style>
<style lang="sass"></style>
@@ -1,10 +1,10 @@
<script setup lang="ts">
import type { Category } from '~/types'
import Item from './Item.vue'
import type { Category } from "~/types";
import Item from "./Item.vue";
defineProps<{
category: Category
}>()
category: Category;
}>();
</script>
<template lang="pug">
@@ -19,5 +19,4 @@ defineProps<{
:category="category"
)
</template>
<style lang="sass">
</style>
<style lang="sass"></style>
@@ -1,66 +1,72 @@
<script setup lang="ts">
import { computed, inject, ref } from 'vue'
import { StoreKey } from '~/sharedStore'
import { STRINGS } from '~/i18n'
import { padNumber } from '~/utils'
import EventCalendarRow from './EventCalendarRow.vue'
import EventListFilterForm from './EventListFilterForm.vue'
import { computed, inject, ref } from "vue";
import { StoreKey } from "~/sharedStore";
import { STRINGS } from "~/i18n";
import { padNumber } from "~/utils";
import EventCalendarRow from "./EventCalendarRow.vue";
import EventListFilterForm from "./EventListFilterForm.vue";
defineProps<{
mobile: boolean
}>()
mobile: boolean;
}>();
const store = inject(StoreKey)!
const calendar = ref<HTMLDivElement>()
const store = inject(StoreKey)!;
const calendar = ref<HTMLDivElement>();
const displayEventInfo = computed(() => store.displayEventInfo || (store.displayEventInfo === null && store.parentStack.length > 0))
const displayEventInfo = computed(
() =>
store.displayEventInfo ||
(store.displayEventInfo === null && store.parentStack.length > 0),
);
const monthname = computed(() => {
// TODO proper date formatting?
if (!store.date) return ''
const monthNum = store.date.substr(5, 2)
const year = store.date.substr(0, 4)
return `${STRINGS.months[monthNum]} ${year}`
})
if (!store.date) return "";
const monthNum = store.date.substr(5, 2);
const year = store.date.substr(0, 4);
return `${STRINGS.months[monthNum]} ${year}`;
});
const id = computed(() => `${store.htmlId}-event-calendar-table`)
const id = computed(() => `${store.htmlId}-event-calendar-table`);
const ariaLabelledby = computed(() => `${store.htmlId}-event-calendar-table-label`)
const ariaLabelledby = computed(
() => `${store.htmlId}-event-calendar-table-label`,
);
function backToList () {
function backToList() {
// TODO should be in store
store.weeks = null
store.view = 'events'
store.name = null
store.frontpageText = null
store.weeks = null;
store.view = "events";
store.name = null;
store.frontpageText = null;
}
function prevmonth () {
if (!store.date) return
let curMonth = parseInt(store.date.substr(5, 2))
let curYear = parseInt(store.date.substr(0, 4))
curMonth--
function prevmonth() {
if (!store.date) return;
let curMonth = parseInt(store.date.substr(5, 2));
let curYear = parseInt(store.date.substr(0, 4));
curMonth--;
if (curMonth < 1) {
curMonth = 12
curYear--
curMonth = 12;
curYear--;
}
store.date = `${curYear}-${padNumber(curMonth, 2)}-01`
store.loading++
store.reload({ focus: `#${id.value}` })
store.date = `${curYear}-${padNumber(curMonth, 2)}-01`;
store.loading++;
store.reload({ focus: `#${id.value}` });
}
function nextmonth () {
if (!store.date) return
let curMonth = parseInt(store.date.substr(5, 2))
let curYear = parseInt(store.date.substr(0, 4))
curMonth++
function nextmonth() {
if (!store.date) return;
let curMonth = parseInt(store.date.substr(5, 2));
let curYear = parseInt(store.date.substr(0, 4));
curMonth++;
if (curMonth > 12) {
curMonth = 1
curYear++
curMonth = 1;
curYear++;
}
store.date = `${curYear}-${padNumber(curMonth, 2)}-01`
store.loading++
store.reload({ focus: `#${id.value}` })
store.date = `${curYear}-${padNumber(curMonth, 2)}-01`;
store.loading++;
store.reload({ focus: `#${id.value}` });
}
</script>
<template lang="pug">
@@ -116,5 +122,4 @@ function nextmonth () {
:mobile="mobile"
)
</template>
<style lang="sass">
</style>
<style lang="sass"></style>
@@ -1,108 +1,117 @@
<script setup lang="ts">
import { computed, inject, ref, onMounted, watch } from 'vue'
import type { DayEntry } from '~/types'
import { StoreKey } from '~/sharedStore'
import EventCalendarEvent from './EventCalendarEvent.vue'
import { computed, inject, ref, onMounted, watch } from "vue";
import type { DayEntry } from "~/types";
import { StoreKey } from "~/sharedStore";
import EventCalendarEvent from "./EventCalendarEvent.vue";
const props = defineProps<{
day: DayEntry | null
mobile: boolean
}>()
day: DayEntry | null;
mobile: boolean;
}>();
const store = inject(StoreKey)!
const cellEl = ref<HTMLTableCellElement>()
const store = inject(StoreKey)!;
const cellEl = ref<HTMLTableCellElement>();
const daynum = computed(() => {
if (!props.day) return ''
return props.day.date.substr(8)
})
if (!props.day) return "";
return props.day.date.substr(8);
});
const dateStr = computed(() => props.day ? new Date(props.day.date).toLocaleDateString() : '')
const dateStr = computed(() =>
props.day ? new Date(props.day.date).toLocaleDateString() : "",
);
const role = computed(() => !props.day || !props.day.events.length || !props.mobile ? 'cell' : 'button')
const role = computed(() =>
!props.day || !props.day.events.length || !props.mobile ? "cell" : "button",
);
const tabindex = computed(() => role.value === 'button' ? '0' : '-1')
const tabindex = computed(() => (role.value === "button" ? "0" : "-1"));
const classObject = computed(() => {
const o: Record<string, boolean> = {}
const o: Record<string, boolean> = {};
if (props.day && props.day.events.length > 0) {
o['pretix-widget-has-events'] = true // TODO static
let best = 'red'
let allLow = true
o["pretix-widget-has-events"] = true; // TODO static
let best = "red";
let allLow = true;
// TODO decopypasta
for (const ev of props.day.events) {
if (ev.availability.color === 'green') {
best = 'green'
if (ev.availability.reason !== 'low') {
allLow = false
if (ev.availability.color === "green") {
best = "green";
if (ev.availability.reason !== "low") {
allLow = false;
}
} else if (ev.availability.color === 'orange' && best !== 'green') {
best = 'orange'
} else if (ev.availability.color === "orange" && best !== "green") {
best = "orange";
}
}
o[`pretix-widget-day-availability-${best}`] = true
if (best === 'green' && allLow) {
o['pretix-widget-day-availability-low'] = true
o[`pretix-widget-day-availability-${best}`] = true;
if (best === "green" && allLow) {
o["pretix-widget-day-availability-low"] = true;
}
}
return o
})
return o;
});
function selectDay (e: Event) {
if (!props.day || !props.day.events.length || !props.mobile) return
e.preventDefault()
e.stopPropagation()
function selectDay(e: Event) {
if (!props.day || !props.day.events.length || !props.mobile) return;
e.preventDefault();
e.stopPropagation();
// TODO decopypasta
if (props.day.events.length === 1) {
const ev = props.day.events[0]
store.parentStack.push(store.targetUrl)
store.targetUrl = ev.event_url
store.error = null
store.subevent = ev.subevent ?? null
store.loading++
store.reload()
const ev = props.day.events[0];
store.parentStack.push(store.targetUrl);
store.targetUrl = ev.event_url;
store.error = null;
store.subevent = ev.subevent ?? null;
store.loading++;
store.reload();
} else {
store.events = props.day.events
store.view = 'events'
store.events = props.day.events;
store.view = "events";
}
}
function onKeyDown (e: KeyboardEvent) {
const keyDown = e.key ?? e.keyCode
if (keyDown === 'Enter' || keyDown === 13 || ['Spacebar', ' '].includes(keyDown as string) || keyDown === 32) {
function onKeyDown(e: KeyboardEvent) {
const keyDown = e.key ?? e.keyCode;
if (
keyDown === "Enter" ||
keyDown === 13 ||
["Spacebar", " "].includes(keyDown as string) ||
keyDown === 32
) {
// (prevent default so the page doesn't scroll when pressing space)
e.preventDefault()
selectDay(e)
e.preventDefault();
selectDay(e);
}
}
function attachListeners () {
if (role.value === 'button' && cellEl.value) {
cellEl.value.addEventListener('click', selectDay)
cellEl.value.addEventListener('keydown', onKeyDown)
function attachListeners() {
if (role.value === "button" && cellEl.value) {
cellEl.value.addEventListener("click", selectDay);
cellEl.value.addEventListener("keydown", onKeyDown);
}
}
function detachListeners () {
function detachListeners() {
if (cellEl.value) {
cellEl.value.removeEventListener('click', selectDay)
cellEl.value.removeEventListener('keydown', onKeyDown)
cellEl.value.removeEventListener("click", selectDay);
cellEl.value.removeEventListener("keydown", onKeyDown);
}
}
onMounted(() => {
attachListeners()
})
attachListeners();
});
// TODO why different from old version?
watch(role, (newValue, oldValue) => {
if (newValue === 'button' && oldValue !== 'button') {
attachListeners()
} else if (newValue !== 'button' && oldValue === 'button') {
detachListeners()
if (newValue === "button" && oldValue !== "button") {
attachListeners();
} else if (newValue !== "button" && oldValue === "button") {
detachListeners();
}
})
});
</script>
<template lang="pug">
td(
@@ -116,5 +125,4 @@ td(
.pretix-widget-event-calendar-events(v-if="day")
EventCalendarEvent(v-for="e in day.events", :key="e.event_url+'-'+(e.subevent||'')", :event="e")
</template>
<style lang="sass">
</style>
<style lang="sass"></style>
@@ -1,33 +1,35 @@
<script setup lang="ts">
import { computed, inject } from 'vue'
import type { EventEntry } from '~/types'
import { StoreKey } from '~/sharedStore'
import { computed, inject } from "vue";
import type { EventEntry } from "~/types";
import { StoreKey } from "~/sharedStore";
const props = defineProps<{
event: EventEntry
describedby?: string
}>()
event: EventEntry;
describedby?: string;
}>();
const store = inject(StoreKey)!
const store = inject(StoreKey)!;
const classObject = computed(() => {
const o: Record<string, boolean> = {
'pretix-widget-event-calendar-event': true,
}
o[`pretix-widget-event-availability-${props.event.availability.color}`] = true
"pretix-widget-event-calendar-event": true,
};
o[`pretix-widget-event-availability-${props.event.availability.color}`] =
true;
if (props.event.availability.reason) {
o[`pretix-widget-event-availability-${props.event.availability.reason}`] = true
o[`pretix-widget-event-availability-${props.event.availability.reason}`] =
true;
}
return o
})
return o;
});
function select () {
store.parentStack.push(store.targetUrl)
store.targetUrl = props.event.event_url
store.error = null
store.subevent = props.event.subevent ?? null
store.loading++
store.reload()
function select() {
store.parentStack.push(store.targetUrl);
store.targetUrl = props.event.event_url;
store.error = null;
store.subevent = props.event.subevent ?? null;
store.loading++;
store.reload();
}
</script>
<template lang="pug">
@@ -41,5 +43,4 @@ a.pretix-widget-event-calendar-event(
.pretix-widget-event-calendar-event-date(v-if="!event.continued && event.time") {{ event.time }}
.pretix-widget-event-calendar-event-availability(v-if="!event.continued && event.availability.text") {{ event.availability.text }}
</template>
<style lang="sass">
</style>
<style lang="sass"></style>
@@ -1,15 +1,14 @@
<script setup lang="ts">
import type { DayEntry } from '~/types'
import EventCalendarCell from './EventCalendarCell.vue'
import type { DayEntry } from "~/types";
import EventCalendarCell from "./EventCalendarCell.vue";
defineProps<{
week: (DayEntry | null)[]
mobile: boolean
}>()
week: (DayEntry | null)[];
mobile: boolean;
}>();
</script>
<template lang="pug">
tr
EventCalendarCell(v-for="(d, idx) in week", :key="idx", :day="d", :mobile="mobile")
</template>
<style lang="sass">
</style>
<style lang="sass"></style>
@@ -1,119 +1,148 @@
<script setup lang="ts">
import { computed, inject, ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { StoreKey } from '~/sharedStore'
import { STRINGS } from '~/i18n'
import Category from './Category.vue'
import {
computed,
inject,
ref,
watch,
onMounted,
onBeforeUnmount,
nextTick,
} from "vue";
import { StoreKey } from "~/sharedStore";
import { STRINGS } from "~/i18n";
import Category from "./Category.vue";
const store = inject(StoreKey)!
const store = inject(StoreKey)!;
const form = ref<HTMLFormElement>()
const voucherinput = ref<HTMLInputElement>()
const isItemsSelected = ref(false)
const localVoucher = ref('')
const form = ref<HTMLFormElement>();
const voucherinput = ref<HTMLInputElement>();
const isItemsSelected = ref(false);
const localVoucher = ref("");
const idVoucherInput = computed(() => `${store.htmlId}-voucher-input`)
const ariaLabelledby = computed(() => `${store.htmlId}-voucher-headline`)
const displayEventInfo = computed(() => store.displayEventInfo || (store.displayEventInfo === null && (store.events || store.weeks || store.days)))
const idCartExistsMsg = computed(() => `${store.htmlId}-cart-exists`)
const idVoucherInput = computed(() => `${store.htmlId}-voucher-input`);
const ariaLabelledby = computed(() => `${store.htmlId}-voucher-headline`);
const displayEventInfo = computed(
() =>
store.displayEventInfo ||
(store.displayEventInfo === null &&
(store.events || store.weeks || store.days)),
);
const idCartExistsMsg = computed(() => `${store.htmlId}-cart-exists`);
const buyLabel = computed(() => {
let allFree = true
let allFree = true;
for (const cat of store.categories) {
for (const item of cat.items) {
for (const v of item.variations) {
if (v.price.gross !== '0.00') {
allFree = false
break
if (v.price.gross !== "0.00") {
allFree = false;
break;
}
}
if ((item.variations.length === 0 && item.price.gross !== '0.00') || item.mandatory_priced_addons) {
allFree = false
break
if (
(item.variations.length === 0 && item.price.gross !== "0.00") ||
item.mandatory_priced_addons
) {
allFree = false;
break;
}
}
if (!allFree) break
if (!allFree) break;
}
return allFree ? STRINGS.register : STRINGS.buy
})
return allFree ? STRINGS.register : STRINGS.buy;
});
const hiddenParams = computed(() => {
const params = new URL(store.getVoucherFormTarget()).searchParams
params.delete('iframe')
params.delete('take_cart_id')
return [...params.entries()]
})
const params = new URL(store.getVoucherFormTarget()).searchParams;
params.delete("iframe");
params.delete("take_cart_id");
return [...params.entries()];
});
const showVoucherForm = computed(() => store.vouchersExist && !store.disableVouchers && !store.voucherCode)
const showVoucherForm = computed(
() => store.vouchersExist && !store.disableVouchers && !store.voucherCode,
);
async function backToList () {
store.targetUrl = store.parentStack.pop() || store.targetUrl
store.error = null
async function backToList() {
store.targetUrl = store.parentStack.pop() || store.targetUrl;
store.error = null;
if (!store.subevent) {
// reset if we are not in a series
store.name = null
store.frontpageText = null
store.name = null;
store.frontpageText = null;
}
store.subevent = null
store.offset = 0
store.appendEvents = false
store.triggerLoadCallback()
store.subevent = null;
store.offset = 0;
store.appendEvents = false;
store.triggerLoadCallback();
if (store.events !== undefined && store.events !== null) {
store.view = 'events'
store.view = "events";
} else if (store.days !== undefined && store.days !== null) {
store.view = 'days'
store.view = "days";
} else {
store.view = 'weeks'
store.view = "weeks";
}
// wait for redraw, then focus content element for better a11y
const rootEl = form.value?.closest('.pretix-widget-wrapper') as HTMLElement | null
await nextTick()
rootEl?.focus()
const rootEl = form.value?.closest(
".pretix-widget-wrapper",
) as HTMLElement | null;
await nextTick();
rootEl?.focus();
}
function calcItemsSelected () {
if (!form.value) return
const checkboxes = form.value.querySelectorAll<HTMLInputElement>('input[type=checkbox], input[type=radio]')
const hasChecked = Array.from(checkboxes).some((el) => el.checked)
const numberInputs = form.value.querySelectorAll<HTMLInputElement>('.pretix-widget-item-count-group input')
const hasQuantity = Array.from(numberInputs).some((el) => parseInt(el.value || '0') > 0)
isItemsSelected.value = hasChecked || hasQuantity
function calcItemsSelected() {
if (!form.value) return;
const checkboxes = form.value.querySelectorAll<HTMLInputElement>(
"input[type=checkbox], input[type=radio]",
);
const hasChecked = Array.from(checkboxes).some((el) => el.checked);
const numberInputs = form.value.querySelectorAll<HTMLInputElement>(
".pretix-widget-item-count-group input",
);
const hasQuantity = Array.from(numberInputs).some(
(el) => parseInt(el.value || "0") > 0,
);
isItemsSelected.value = hasChecked || hasQuantity;
}
function _focusVoucherField () {
voucherinput.value?.focus()
function _focusVoucherField() {
voucherinput.value?.focus();
}
function handleBuy (event: Event) {
function handleBuy(event: Event) {
if (form.value) {
const formData = new FormData(form.value)
store.buy(formData, event)
const formData = new FormData(form.value);
store.buy(formData, event);
}
}
function handleRedeem (event: Event) {
store.redeem(localVoucher.value, event)
function handleRedeem(event: Event) {
store.redeem(localVoucher.value, event);
}
onMounted(() => {
if (form.value) {
form.value.addEventListener('change', calcItemsSelected)
form.value.addEventListener("change", calcItemsSelected);
}
})
});
onBeforeUnmount(() => {
if (form.value) {
form.value.removeEventListener('change', calcItemsSelected)
form.value.removeEventListener("change", calcItemsSelected);
}
})
});
watch(() => store.overlay?.frameShown, (newValue) => {
if (!newValue && form.value) {
form.value.reset()
calcItemsSelected()
}
})
watch(
() => store.overlay?.frameShown,
(newValue) => {
if (!newValue && form.value) {
form.value.reset();
calcItemsSelected();
}
},
);
</script>
<template lang="pug">
@@ -233,5 +262,4 @@ watch(() => store.overlay?.frameShown, (newValue) => {
button(@click="handleRedeem") {{ STRINGS.redeem }}
.pretix-widget-clear
</template>
<style lang="sass">
</style>
<style lang="sass"></style>
@@ -1,44 +1,50 @@
<script setup lang="ts">
import { computed, inject, nextTick } from 'vue'
import { StoreKey } from '~/sharedStore'
import { STRINGS } from '~/i18n'
import EventListEntry from './EventListEntry.vue'
import EventListFilterForm from './EventListFilterForm.vue'
import { computed, inject, nextTick } from "vue";
import { StoreKey } from "~/sharedStore";
import { STRINGS } from "~/i18n";
import EventListEntry from "./EventListEntry.vue";
import EventListFilterForm from "./EventListFilterForm.vue";
const store = inject(StoreKey)!
const store = inject(StoreKey)!;
const displayEventInfo = computed(() => store.displayEventInfo || (store.displayEventInfo === null && store.parentStack.length > 0))
const displayEventInfo = computed(
() =>
store.displayEventInfo ||
(store.displayEventInfo === null && store.parentStack.length > 0),
);
async function backToCalendar (event: MouseEvent) {
async function backToCalendar(event: MouseEvent) {
// make sure to always focus content element
await nextTick()
const rootEl = (event.target as HTMLElement).closest('.pretix-widget-wrapper') as HTMLElement | null
rootEl?.focus()
await nextTick();
const rootEl = (event.target as HTMLElement).closest(
".pretix-widget-wrapper",
) as HTMLElement | null;
rootEl?.focus();
store.offset = 0
store.appendEvents = false
store.offset = 0;
store.appendEvents = false;
if (store.weeks) {
store.events = null
store.view = 'weeks'
store.name = null
store.frontpageText = null
store.events = null;
store.view = "weeks";
store.name = null;
store.frontpageText = null;
} else {
store.loading++
store.targetUrl = store.parentStack.pop() || store.targetUrl
store.error = null
store.reload()
store.loading++;
store.targetUrl = store.parentStack.pop() || store.targetUrl;
store.error = null;
store.reload();
}
}
function loadMore () {
store.appendEvents = true
store.offset += 50
store.loading++
store.reload()
function loadMore() {
store.appendEvents = true;
store.offset += 50;
store.loading++;
store.reload();
}
console.log(store)
console.log(store);
</script>
<template lang="pug">
.pretix-widget-event-list
@@ -67,5 +73,4 @@ console.log(store)
p.pretix-widget-event-list-load-more(v-if="store.hasMoreEvents")
button(@click.prevent.stop="loadMore") {{ STRINGS.load_more }}
</template>
<style lang="sass">
</style>
<style lang="sass"></style>
@@ -1,34 +1,38 @@
<script setup lang="ts">
import { computed, inject } from 'vue'
import type { EventEntry } from '~/types'
import { StoreKey } from '~/sharedStore'
import { computed, inject } from "vue";
import type { EventEntry } from "~/types";
import { StoreKey } from "~/sharedStore";
const props = defineProps<{
event: EventEntry
}>()
event: EventEntry;
}>();
const store = inject(StoreKey)!
const store = inject(StoreKey)!;
const classObject = computed(() => {
const o: Record<string, boolean> = {
'pretix-widget-event-list-entry': true,
}
o[`pretix-widget-event-availability-${props.event.availability.color}`] = true
"pretix-widget-event-list-entry": true,
};
o[`pretix-widget-event-availability-${props.event.availability.color}`] =
true;
if (props.event.availability.reason) {
o[`pretix-widget-event-availability-${props.event.availability.reason}`] = true
o[`pretix-widget-event-availability-${props.event.availability.reason}`] =
true;
}
return o
})
return o;
});
const location = computed(() => props.event.location.replace(/\s*\n\s*/g, ', '))
const location = computed(() =>
props.event.location.replace(/\s*\n\s*/g, ", "),
);
function select () {
store.parentStack.push(store.targetUrl)
store.targetUrl = props.event.event_url
store.error = null
store.subevent = props.event.subevent ?? null
store.loading++
store.reload()
function select() {
store.parentStack.push(store.targetUrl);
store.targetUrl = props.event.event_url;
store.error = null;
store.subevent = props.event.subevent ?? null;
store.loading++;
store.reload();
}
</script>
<template lang="pug">
@@ -40,5 +44,4 @@ a.pretix-widget-event-list-entry(href="#", :class="classObject", @click.prevent.
.pretix-widget-event-list-entry-availability
span {{ event.availability.text }}
</template>
<style lang="sass">
</style>
<style lang="sass"></style>
Loaded 100 of 127 files, more files were not shown because too many files have changed in this diff. Show more