mirror of
https://github.com/pretix/pretix.git
synced 2026-11-15 02:07:08 +00:00
Compare commits
52
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9520c411db | ||
|
|
749211c206 | ||
|
|
f346acb0d2 | ||
|
|
0478559fd7 | ||
|
|
137059dd4e | ||
|
|
bd0b55bdd3 | ||
|
|
ceff9de5ee | ||
|
|
38b467cef7 | ||
|
|
ae95df7ea2 | ||
|
|
d362f16417 | ||
|
|
7f1a0fd58f | ||
|
|
6d05cfea8b | ||
|
|
c1b8259075 | ||
|
|
e394054ae3 | ||
|
|
dda2a18fdf | ||
|
|
68bd182f79 | ||
|
|
2ee6ea7692 | ||
|
|
a37305f8f4 | ||
|
|
bd4b049c16 | ||
|
|
cb852a8fa7 | ||
|
|
86dd5619e0 | ||
|
|
b1a1d66156 | ||
|
|
e229697d2a | ||
|
|
f9c7378da7 | ||
|
|
86152a182a | ||
|
|
76899600b0 | ||
|
|
04a0b009a6 | ||
|
|
4e9d60d238 | ||
|
|
fe0d0ceb2e | ||
|
|
8457603555 | ||
|
|
ae35af596f | ||
|
|
47c3493372 | ||
|
|
85d65d17b4 | ||
|
|
e50c845e09 | ||
|
|
c84d9061da | ||
|
|
25c9659655 | ||
|
|
b6bce961c3 | ||
|
|
1de0455ebc | ||
|
|
9fdafa57de | ||
|
|
50bcfc8c74 | ||
|
|
fa552fe7d1 | ||
|
|
cbfc24dd64 | ||
|
|
6e0c2d69a5 | ||
|
|
34ed175abe | ||
|
|
a9aaf8638e | ||
|
|
a613affa05 | ||
|
|
cd0ed78b76 | ||
|
|
4889abd75c | ||
|
|
0a73faa8bd | ||
|
|
def5042661 | ||
|
|
e6f8f178af | ||
|
|
5f5380e8b1 |
No files matched your search
@@ -1 +0,0 @@
|
||||
/*
|
||||
@@ -56,7 +56,7 @@ calculated automatically.
|
||||
},
|
||||
"answers": [
|
||||
{
|
||||
"question": 1,
|
||||
"datafield": "A1B2C3D4",
|
||||
"answer": "23",
|
||||
"options": []
|
||||
}
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
|
||||
@@ -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": []
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
-----
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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,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,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
|
||||
|
||||
@@ -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,
|
||||
])
|
||||
Generated
+127
@@ -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",
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -264,6 +264,8 @@ def oidc_validate_authorization(provider, code, redirect_uri, pkce_code_verifier
|
||||
)
|
||||
)
|
||||
|
||||
profile['oidc_claims'] = userinfo
|
||||
|
||||
return profile
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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."),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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': []})
|
||||
|
||||
|
||||
@@ -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!"))
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
+247
-209
@@ -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
|
||||
|
||||
+8
-8
@@ -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)")
|
||||
|
||||
+45
-45
@@ -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
|
||||
|
||||
+41
-35
@@ -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")
|
||||
|
||||
+41
-35
@@ -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")
|
||||
|
||||
+26
-18
@@ -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)")
|
||||
|
||||
+41
-35
@@ -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);
|
||||
};
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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
@@ -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)
|
||||
@@ -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">
|
||||
|
||||
+137
-114
@@ -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
Reference in new issue
Block a user