mirror of
https://github.com/pretix/pretix.git
synced 2026-08-23 12:51:59 +00:00
Compare commits
198
Commits
v4.7.1
...
aws_ses_454
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c4173a2ab | ||
|
|
e9c440ceed | ||
|
|
5cd79ee2b0 | ||
|
|
15c6e22414 | ||
|
|
8d04a0183a | ||
|
|
2f6881934e | ||
|
|
8f7bc59214 | ||
|
|
9bf3b54a83 | ||
|
|
82cd6e320d | ||
|
|
e308b38d6f | ||
|
|
6b7a2e1981 | ||
|
|
d19cb14dc1 | ||
|
|
3e8e454e92 | ||
|
|
f46de92303 | ||
|
|
aeba9542be | ||
|
|
a380044639 | ||
|
|
4cbe50f3a2 | ||
|
|
278d54e780 | ||
|
|
9634598952 | ||
|
|
b60583168b | ||
|
|
ea8630d3d7 | ||
|
|
3cdf578c14 | ||
|
|
f7c0921f18 | ||
|
|
a7ae556478 | ||
|
|
a755bfd22c | ||
|
|
22920a7318 | ||
|
|
cf6a8c333a | ||
|
|
2623bfd2db | ||
|
|
dcc1a93b72 | ||
|
|
3aeea82d2e | ||
|
|
732621f121 | ||
|
|
24e7be4142 | ||
|
|
20c6f0b327 | ||
|
|
e4817518d8 | ||
|
|
35c443f90f | ||
|
|
de669156cd | ||
|
|
d8e3b49b04 | ||
|
|
567f68965d | ||
|
|
65f6892896 | ||
|
|
ec9800b215 | ||
|
|
9010d8f6a1 | ||
|
|
f260945fdf | ||
|
|
b7241825c3 | ||
|
|
c9ed155870 | ||
|
|
69d0a20674 | ||
|
|
b699e8977a | ||
|
|
4f25d8ba89 | ||
|
|
71f5303a5e | ||
|
|
69f91e54e6 | ||
|
|
bcee2c231a | ||
|
|
d1745bb703 | ||
|
|
f468b393c0 | ||
|
|
4b0c38e4ee | ||
|
|
6768bbb486 | ||
|
|
f2c9b46d3e | ||
|
|
b7a3db2ac0 | ||
|
|
307a6654f2 | ||
|
|
f2cc8c77e8 | ||
|
|
830d48255e | ||
|
|
df2b428aa1 | ||
|
|
7d1d05de02 | ||
|
|
921d8b6057 | ||
|
|
0b13ec49f1 | ||
|
|
62ef89f87c | ||
|
|
d0920caf32 | ||
|
|
d0474afdfe | ||
|
|
4ab298dd10 | ||
|
|
db39b89ae4 | ||
|
|
162ae3ead7 | ||
|
|
65a7e8516e | ||
|
|
cccd4af6dd | ||
|
|
751cfdf203 | ||
|
|
898776b617 | ||
|
|
b4db81d6c3 | ||
|
|
e96fdf2a2c | ||
|
|
9052b39f9c | ||
|
|
725725223e | ||
|
|
71b4c3117f | ||
|
|
5e1dc5ac5b | ||
|
|
0b403b7b3a | ||
|
|
be46a00d38 | ||
|
|
22f3412ad0 | ||
|
|
c23a3fcfcd | ||
|
|
1412b0afdb | ||
|
|
60f14b6a68 | ||
|
|
dc440d6cc5 | ||
|
|
7cef7b4d5d | ||
|
|
659a587cdf | ||
|
|
ee72009e73 | ||
|
|
69375f4092 | ||
|
|
76475039b5 | ||
|
|
8a8524a346 | ||
|
|
e7996c693a | ||
|
|
551bd3e284 | ||
|
|
645a7489df | ||
|
|
dff4bf2841 | ||
|
|
fc1e2ca3b0 | ||
|
|
2a0748a008 | ||
|
|
41d3f39fc7 | ||
|
|
8e481905e2 | ||
|
|
552e130fed | ||
|
|
f8b879c2e6 | ||
|
|
9e5403333c | ||
|
|
2486c3d205 | ||
|
|
ee9bf25ae1 | ||
|
|
c4d3c48837 | ||
|
|
d97b7b4fb6 | ||
|
|
fd83e91174 | ||
|
|
6e3b62696e | ||
|
|
a0b8fedaa7 | ||
|
|
a4961d9dfb | ||
|
|
889f972768 | ||
|
|
478ab633e0 | ||
|
|
0e4047018e | ||
|
|
cbeaffa415 | ||
|
|
09972b62c9 | ||
|
|
3b2eec659d | ||
|
|
4e6c61316c | ||
|
|
c308939f3c | ||
|
|
41604ebdd9 | ||
|
|
e2adf1fdb3 | ||
|
|
90d9ddebb5 | ||
|
|
684b8e4102 | ||
|
|
a83e963ea5 | ||
|
|
c9392236f5 | ||
|
|
57d68eaddb | ||
|
|
ab1e063a93 | ||
|
|
4b76dcedc4 | ||
|
|
55752a4319 | ||
|
|
5bde98e349 | ||
|
|
1b2ca87f2d | ||
|
|
8562c2f103 | ||
|
|
ea0283266f | ||
|
|
cc912b9ea7 | ||
|
|
ebae275a2d | ||
|
|
02f8bcbe23 | ||
|
|
4ae7cc9f50 | ||
|
|
c2f2e157d7 | ||
|
|
ebf0320c2c | ||
|
|
b05fa89010 | ||
|
|
ab0f76c7bb | ||
|
|
e4c1f30b9d | ||
|
|
9ffeafa6a5 | ||
|
|
cb7e014966 | ||
|
|
7b7a8e655e | ||
|
|
ac46172f6b | ||
|
|
31eda01464 | ||
|
|
0adddb3084 | ||
|
|
0b91f9da31 | ||
|
|
0a7ba606c6 | ||
|
|
959b076b52 | ||
|
|
06b0ef906a | ||
|
|
4f1607040e | ||
|
|
38a3fa23ed | ||
|
|
baae16d9e7 | ||
|
|
e2d9cc10e8 | ||
|
|
f0f8bbb2b0 | ||
|
|
8d57544ba3 | ||
|
|
065533ee4d | ||
|
|
fb2ad2711f | ||
|
|
52004e5c46 | ||
|
|
9fcd099531 | ||
|
|
d8cf3552ba | ||
|
|
cc04f66a48 | ||
|
|
86d28b3f21 | ||
|
|
07b401b7af | ||
|
|
8ff4f778bf | ||
|
|
5174d38017 | ||
|
|
5681ea121d | ||
|
|
4e2a3b45da | ||
|
|
d796c399a6 | ||
|
|
bee5b5f1af | ||
|
|
028be3a5c3 | ||
|
|
990fd9a40c | ||
|
|
746c9af531 | ||
|
|
47208afcec | ||
|
|
b215a0bd04 | ||
|
|
6c94b62bb1 | ||
|
|
d7a32d985b | ||
|
|
fac3e7f7bd | ||
|
|
8e8dce7ccf | ||
|
|
465e11c765 | ||
|
|
7e6c512a76 | ||
|
|
5d0b8c5084 | ||
|
|
6008e3d11c | ||
|
|
aeeebc0b4b | ||
|
|
a568a37f4c | ||
|
|
b74f5508b7 | ||
|
|
a9b0651345 | ||
|
|
c126445fe0 | ||
|
|
f8b8d92f2c | ||
|
|
a66fdc5084 | ||
|
|
6d6883b343 | ||
|
|
482968175b | ||
|
|
5d6302d5fd | ||
|
|
92fbe76327 | ||
|
|
2fd2462ef1 | ||
|
|
19881dff44 |
@@ -55,7 +55,7 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pip-
|
${{ runner.os }}-pip-
|
||||||
- name: Install system dependencies
|
- name: Install system dependencies
|
||||||
run: sudo apt update && sudo apt install gettext mariadb-client
|
run: sudo apt update && sudo apt install gettext mariadb-client-10.3
|
||||||
- name: Install Python dependencies
|
- name: Install Python dependencies
|
||||||
run: pip3 install -e ".[dev]" mysqlclient psycopg2-binary
|
run: pip3 install -e ".[dev]" mysqlclient psycopg2-binary
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ Example::
|
|||||||
A comma-separated list of plugins that are not available even though they are installed.
|
A comma-separated list of plugins that are not available even though they are installed.
|
||||||
Defaults to an empty string.
|
Defaults to an empty string.
|
||||||
|
|
||||||
|
``plugins_show_meta``
|
||||||
|
Whether to show authors and versions of plugins, defaults to ``on``.
|
||||||
|
|
||||||
``auth_backends``
|
``auth_backends``
|
||||||
A comma-separated list of available auth backends. Defaults to ``pretix.base.auth.NativeAuthBackend``.
|
A comma-separated list of available auth backends. Defaults to ``pretix.base.auth.NativeAuthBackend``.
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,8 @@ following endpoint:
|
|||||||
"hardware_brand": "Samsung",
|
"hardware_brand": "Samsung",
|
||||||
"hardware_model": "Galaxy S",
|
"hardware_model": "Galaxy S",
|
||||||
"software_brand": "pretixdroid",
|
"software_brand": "pretixdroid",
|
||||||
"software_version": "4.1.0"
|
"software_version": "4.1.0",
|
||||||
|
"info": {"arbitrary": "data"}
|
||||||
}
|
}
|
||||||
|
|
||||||
You will receive a response equivalent to the response of your initialization request.
|
You will receive a response equivalent to the response of your initialization request.
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ Possible permissions are:
|
|||||||
* Can view vouchers
|
* Can view vouchers
|
||||||
* Can change vouchers
|
* Can change vouchers
|
||||||
|
|
||||||
|
.. _`rest-compat`:
|
||||||
|
|
||||||
Compatibility
|
Compatibility
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
@@ -58,6 +60,7 @@ that your clients can deal with them properly:
|
|||||||
* Support of new HTTP methods for a given API endpoint
|
* Support of new HTTP methods for a given API endpoint
|
||||||
* Support of new query parameters for a given API endpoint
|
* Support of new query parameters for a given API endpoint
|
||||||
* New fields contained in API responses
|
* New fields contained in API responses
|
||||||
|
* Response body structure or message texts on failed requests (``4xx``, ``5xx`` response codes)
|
||||||
|
|
||||||
We treat the following types of changes as *backwards-incompatible*:
|
We treat the following types of changes as *backwards-incompatible*:
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
Resources and endpoints
|
Resources and endpoints
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
|
With a few exceptions, this only lists resources bundled in the pretix core modules.
|
||||||
|
Additional endpoints are provided by pretix plugins. Some of them are documented
|
||||||
|
at :ref:`plugin-docs`.
|
||||||
|
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
@@ -33,4 +38,4 @@ Resources and endpoints
|
|||||||
exporters
|
exporters
|
||||||
sendmail_rules
|
sendmail_rules
|
||||||
billing_invoices
|
billing_invoices
|
||||||
billing_var
|
billing_var
|
||||||
@@ -68,6 +68,7 @@ positions list of objects List of order p
|
|||||||
non-canceled positions are included.
|
non-canceled positions are included.
|
||||||
fees list of objects List of fees included in the order total. By default, only
|
fees list of objects List of fees included in the order total. By default, only
|
||||||
non-canceled fees are included.
|
non-canceled fees are included.
|
||||||
|
├ id integer Internal ID of the fee record
|
||||||
├ fee_type string Type of fee (currently ``payment``, ``passbook``,
|
├ fee_type string Type of fee (currently ``payment``, ``passbook``,
|
||||||
``other``)
|
``other``)
|
||||||
├ value money (string) Fee amount
|
├ value money (string) Fee amount
|
||||||
@@ -136,6 +137,10 @@ last_modified datetime Last modificati
|
|||||||
|
|
||||||
The ``subevent`` query parameters has been added.
|
The ``subevent`` query parameters has been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.8
|
||||||
|
|
||||||
|
The ``order.fees.id`` attribute has been added.
|
||||||
|
|
||||||
|
|
||||||
.. _order-position-resource:
|
.. _order-position-resource:
|
||||||
|
|
||||||
@@ -735,6 +740,37 @@ Generating new secrets
|
|||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/regenerate_secrets/
|
||||||
|
|
||||||
|
Triggers generation of a new ``secret`` attribute for a single order position.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/orderpositions/23/regenerate_secrets/ 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
|
||||||
|
|
||||||
|
(Full order position resource, see above.)
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer of the event
|
||||||
|
:param event: The ``slug`` field of the event
|
||||||
|
:param code: The ``id`` field of the order position to update
|
||||||
|
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The order position could not be updated due to invalid submitted data.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order position.
|
||||||
|
|
||||||
Deleting orders
|
Deleting orders
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
@@ -1046,6 +1082,9 @@ Order state operations
|
|||||||
will instead stay paid, but all positions will be removed (or marked as canceled) and replaced by the cancellation
|
will instead stay paid, but all positions will be removed (or marked as canceled) and replaced by the cancellation
|
||||||
fee as the only component of the order.
|
fee as the only component of the order.
|
||||||
|
|
||||||
|
You can control whether the customer is notified through ``send_email`` (defaults to ``true``).
|
||||||
|
You can pass a ``comment`` that can be visible to the user if it is used in the email template.
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@@ -1057,6 +1096,7 @@ Order state operations
|
|||||||
|
|
||||||
{
|
{
|
||||||
"send_email": true,
|
"send_email": true,
|
||||||
|
"comment": "Event was canceled.",
|
||||||
"cancellation_fee": null
|
"cancellation_fee": null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1642,6 +1682,8 @@ Order position ticket download
|
|||||||
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
|
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
|
||||||
seconds.
|
seconds.
|
||||||
|
|
||||||
|
.. _rest-orderpositions-manipulate:
|
||||||
|
|
||||||
Manipulating individual positions
|
Manipulating individual positions
|
||||||
---------------------------------
|
---------------------------------
|
||||||
|
|
||||||
@@ -1649,6 +1691,11 @@ Manipulating individual positions
|
|||||||
|
|
||||||
The ``PATCH`` method has been added for individual positions.
|
The ``PATCH`` method has been added for individual positions.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.8
|
||||||
|
|
||||||
|
The ``PATCH`` method now supports changing items, variations, subevents, seats, prices, and tax rules.
|
||||||
|
The ``POST`` endpoint to add individual positions has been added.
|
||||||
|
|
||||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/
|
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/
|
||||||
|
|
||||||
Updates specific fields on an order position. Currently, only the following fields are supported:
|
Updates specific fields on an order position. Currently, only the following fields are supported:
|
||||||
@@ -1675,6 +1722,21 @@ Manipulating individual positions
|
|||||||
and ``option_identifiers`` will be ignored. As a special case, you can submit the magic value
|
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.
|
``"file:keep"`` as the answer to a file question to keep the current value without re-uploading it.
|
||||||
|
|
||||||
|
* ``item``
|
||||||
|
|
||||||
|
* ``variation``
|
||||||
|
|
||||||
|
* ``subevent``
|
||||||
|
|
||||||
|
* ``seat`` (specified as a string mapping to a ``string_guid``)
|
||||||
|
|
||||||
|
* ``price``
|
||||||
|
|
||||||
|
* ``tax_rule``
|
||||||
|
|
||||||
|
Changing parameters such as ``item`` or ``price`` will **not** automatically trigger creation of a new invoice,
|
||||||
|
you need to take care of that yourself.
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@@ -1696,7 +1758,7 @@ Manipulating individual positions
|
|||||||
Vary: Accept
|
Vary: Accept
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
(Full order resource, see above.)
|
(Full order position resource, see above.)
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer of the event
|
:param organizer: The ``slug`` field of the organizer of the event
|
||||||
:param event: The ``slug`` field of the event
|
:param event: The ``slug`` field of the event
|
||||||
@@ -1707,9 +1769,83 @@ Manipulating individual positions
|
|||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/
|
||||||
|
|
||||||
|
Adds a new position to an order. Currently, only the following fields are supported:
|
||||||
|
|
||||||
|
* ``order`` (mandatory, specified as a string mapping to a ``code``)
|
||||||
|
|
||||||
|
* ``addon_to`` (optional, specified as an integer mapping to the ``positionid`` of the parent position)
|
||||||
|
|
||||||
|
* ``item`` (mandatory)
|
||||||
|
|
||||||
|
* ``variation`` (mandatory depending on item)
|
||||||
|
|
||||||
|
* ``subevent`` (mandatory depending on event)
|
||||||
|
|
||||||
|
* ``seat`` (specified as a string mapping to a ``string_guid``, mandatory depending on event and item)
|
||||||
|
|
||||||
|
* ``price`` (default price will be used if unset)
|
||||||
|
|
||||||
|
* ``attendee_email``
|
||||||
|
|
||||||
|
* ``attendee_name_parts`` or ``attendee_name``
|
||||||
|
|
||||||
|
* ``company``
|
||||||
|
|
||||||
|
* ``street``
|
||||||
|
|
||||||
|
* ``zipcode``
|
||||||
|
|
||||||
|
* ``city``
|
||||||
|
|
||||||
|
* ``country``
|
||||||
|
|
||||||
|
* ``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``
|
||||||
|
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.
|
||||||
|
|
||||||
|
This will **not** automatically trigger creation of a new invoice, you need to take care of that yourself.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/orderpositions/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"order": "ABC12",
|
||||||
|
"item": 5,
|
||||||
|
"addon_to": 1
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
(Full order position resource, see above.)
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer of the event
|
||||||
|
:param event: The ``slug`` field of the event
|
||||||
|
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The position 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 position.
|
||||||
|
|
||||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/
|
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/
|
||||||
|
|
||||||
Deletes an order position, identified by its internal ID.
|
Cancels an order position, identified by its internal ID.
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
@@ -1735,6 +1871,128 @@ Manipulating individual positions
|
|||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||||
:statuscode 404: The requested order position does not exist.
|
:statuscode 404: The requested order position does not exist.
|
||||||
|
|
||||||
|
Changing order contents
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
While you can :ref:`change positions individually <rest-orderpositions-manipulate>` sometimes it is necessary to make
|
||||||
|
multiple changes to an order at once within one transaction. This makes it possible to e.g. swap the seats of two
|
||||||
|
attendees in an order without running into conflicts. This interface also offers some possibilities not available
|
||||||
|
otherwise, such as splitting an order or changing fees.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.8
|
||||||
|
|
||||||
|
This endpoint has been added to the system.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/change/
|
||||||
|
|
||||||
|
Performs a change operation on an order. You can supply the following fields:
|
||||||
|
|
||||||
|
* ``patch_positions``: A list of objects with the two keys ``position`` specifying an order position ID and
|
||||||
|
``body`` specifying the desired changed values of the position (``item``, ``variation``, ``subevent``, ``seat``,
|
||||||
|
``price``, ``tax_rule``).
|
||||||
|
|
||||||
|
* ``cancel_positions``: A list of objects with the single key ``position`` specifying an order position ID.
|
||||||
|
|
||||||
|
* ``split_positions``: A list of objects with the single key ``position`` specifying an order position ID.
|
||||||
|
|
||||||
|
* ``create_positions``: A list of objects describing new order positions with the same fields supported as when
|
||||||
|
creating them individually through the ``POST …/orderpositions/`` endpoint.
|
||||||
|
|
||||||
|
* ``patch_fees``: A list of objects with the two keys ``fee`` specifying an order fee ID and
|
||||||
|
``body`` specifying the desired changed values of the position (``value``).
|
||||||
|
|
||||||
|
* ``cancel_fees``: A list of objects with the single key ``fee`` specifying an order fee ID.
|
||||||
|
|
||||||
|
* ``recalculate_taxes``: If set to ``"keep_net"``, all taxes will be recalculated based on the tax rule and invoice
|
||||||
|
address, the net price will be kept. If set to ``"keep_gross"``, the gross price will be kept. If set to ``null``
|
||||||
|
(the default) the taxes are not recalculated.
|
||||||
|
|
||||||
|
* ``send_email``: If set to ``true``, the customer will be notified about the change. Defaults to ``false``.
|
||||||
|
|
||||||
|
* ``reissue_invoice``: If set to ``true`` and an invoice exists for the order, it will be canceled and a new invoice
|
||||||
|
will be issued. Defaults to ``true``.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"cancel_positions": [
|
||||||
|
{
|
||||||
|
"position": 12373
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"patch_positions": [
|
||||||
|
{
|
||||||
|
"position": 12374,
|
||||||
|
"body": {
|
||||||
|
"item": 12,
|
||||||
|
"variation": None,
|
||||||
|
"subevent": 562,
|
||||||
|
"seat": "seat-guid-2",
|
||||||
|
"price": "99.99",
|
||||||
|
"tax_rule": 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"split_positions": [
|
||||||
|
{
|
||||||
|
"position": 12375
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"create_positions": [
|
||||||
|
{
|
||||||
|
"item": 12,
|
||||||
|
"variation": None,
|
||||||
|
"subevent": 562,
|
||||||
|
"seat": "seat-guid-2",
|
||||||
|
"price": "99.99",
|
||||||
|
"addon_to": 12374,
|
||||||
|
"attendee_name": "Peter",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cancel_fees": [
|
||||||
|
{
|
||||||
|
"fee": 49
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"change_fees": [
|
||||||
|
{
|
||||||
|
"fee": 51,
|
||||||
|
"body": {
|
||||||
|
"value": "12.00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"reissue_invoice": true,
|
||||||
|
"send_email": true,
|
||||||
|
"recalculate_taxes": "keep_gross"
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
(Full order position resource, see above.)
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer of the event
|
||||||
|
:param event: The ``slug`` field of the event
|
||||||
|
:param code: The ``code`` field of the order to update
|
||||||
|
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The order could not be updated due to invalid submitted data.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order.
|
||||||
|
|
||||||
|
|
||||||
Order payment endpoints
|
Order payment endpoints
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|||||||
@@ -45,13 +45,17 @@ Attribute Type Description
|
|||||||
name string The human-readable name of your plugin
|
name string The human-readable name of your plugin
|
||||||
author string Your name
|
author string Your name
|
||||||
version string A human-readable version code of your plugin
|
version string A human-readable version code of your plugin
|
||||||
description string A more verbose description of what your plugin does.
|
description string A more verbose description of what your plugin does. May contain HTML.
|
||||||
category string Category of a plugin. Either one of ``"FEATURE"``, ``"PAYMENT"``,
|
category string Category of a plugin. Either one of ``"FEATURE"``, ``"PAYMENT"``,
|
||||||
``"INTEGRATION"``, ``"CUSTOMIZATION"``, ``"FORMAT"``, or ``"API"``,
|
``"INTEGRATION"``, ``"CUSTOMIZATION"``, ``"FORMAT"``, or ``"API"``,
|
||||||
or any other string.
|
or any other string.
|
||||||
|
picture string (optional) Path to a picture resolvable through the static file system.
|
||||||
|
featured boolean (optional) ``False`` by default, can promote a plugin if it's something many users will want, use carefully.
|
||||||
visible boolean (optional) ``True`` by default, can hide a plugin so it cannot be normally activated.
|
visible boolean (optional) ``True`` by default, can hide a plugin so it cannot be normally activated.
|
||||||
restricted boolean (optional) ``False`` by default, restricts a plugin such that it can only be enabled
|
restricted boolean (optional) ``False`` by default, restricts a plugin such that it can only be enabled
|
||||||
for an event by system administrators / superusers.
|
for an event by system administrators / superusers.
|
||||||
|
experimental boolean (optional) ``False`` by default, marks a plugin as an experimental feature in the plugins list.
|
||||||
|
picture string (optional) Path to a picture resolvable through the static file system.
|
||||||
compatibility string Specifier for compatible pretix versions.
|
compatibility string Specifier for compatible pretix versions.
|
||||||
================== ==================== ===========================================================
|
================== ==================== ===========================================================
|
||||||
|
|
||||||
@@ -74,8 +78,10 @@ A working example would be:
|
|||||||
name = _("PayPal")
|
name = _("PayPal")
|
||||||
author = _("the pretix team")
|
author = _("the pretix team")
|
||||||
version = '1.0.0'
|
version = '1.0.0'
|
||||||
category = 'PAYMENT
|
category = 'PAYMENT'
|
||||||
|
picture = 'pretix_paypal/paypal_logo.svg'
|
||||||
visible = True
|
visible = True
|
||||||
|
featured = False
|
||||||
restricted = False
|
restricted = False
|
||||||
description = _("This plugin allows you to receive payments via PayPal")
|
description = _("This plugin allows you to receive payments via PayPal")
|
||||||
compatibility = "pretix>=2.7.0"
|
compatibility = "pretix>=2.7.0"
|
||||||
|
|||||||
@@ -0,0 +1,630 @@
|
|||||||
|
Exhibitors
|
||||||
|
==========
|
||||||
|
|
||||||
|
The exhibitors plugin allows to manage exhibitors at your trade show or conference. After signing up your exhibitors
|
||||||
|
in the system, you can assign vouchers to exhibitors and give them access to the data of these vouchers. The exhibitors
|
||||||
|
module is also the basis of the pretixLEAD lead scanning application.
|
||||||
|
|
||||||
|
.. note:: On pretix Hosted, using the lead scanning feature of the exhibitors plugin can add additional costs
|
||||||
|
depending on your contract.
|
||||||
|
|
||||||
|
The plugin exposes two APIs. One (REST API) is intended for bulk-data operations from the admin side, and one
|
||||||
|
(App API) that is used by the pretixLEAD app.
|
||||||
|
|
||||||
|
REST API
|
||||||
|
---------
|
||||||
|
|
||||||
|
The REST API for exhibitors requires the usual :ref:`rest-auth`.
|
||||||
|
|
||||||
|
Resources
|
||||||
|
"""""""""
|
||||||
|
|
||||||
|
The exhibitors plugin provides a HTTP API that allows you to create new exhibitors.
|
||||||
|
|
||||||
|
The exhibitors resource contains the following public fields:
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
id integer Internal exhibitor ID in pretix
|
||||||
|
name string Exhibitor name
|
||||||
|
internal_id string Can be used for the ID in your exhibition system, your customer ID, etc. Can be ``null``. Maximum 255 characters.
|
||||||
|
contact_name string Contact person (or ``null``)
|
||||||
|
contact_name_parts object of strings Decomposition of contact name (i.e. given name, family name)
|
||||||
|
contact_email string Contact person email address (or ``null``)
|
||||||
|
booth string Booth number (or ``null``). Maximum 100 characters.
|
||||||
|
locale string Locale for communication with the exhibitor (or ``null``).
|
||||||
|
access_code string Access code for the exhibitor to access their data or use the lead scanning app (read-only).
|
||||||
|
allow_lead_scanning boolean Enables lead scanning app
|
||||||
|
allow_lead_access boolean Enables access to data gathered by the lead scanning app
|
||||||
|
allow_voucher_access boolean Enables access to data gathered by exhibitor vouchers
|
||||||
|
comment string Internal comment, not shown to exhibitor
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
You can also access the scanned leads through the API which contains the following public fields:
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
attendee_order string Order code of the order the scanned attendee belongs to
|
||||||
|
attendee_positionid integer ``positionid`` if the attendee within the order specified by ``attendee_order``
|
||||||
|
rating integer A rating of 0 to 5 stars (or ``null``)
|
||||||
|
notes string A note taken by the exhibitor after scanning
|
||||||
|
tags list of strings Additional tags selected by the exhibitor
|
||||||
|
first_upload datetime Date and time of the first upload of this lead
|
||||||
|
data list of objects Attendee data set that may be shown to the exhibitor based o
|
||||||
|
the event's configuration. Each entry contains the fields ``id``,
|
||||||
|
``label``, ``value``, and ``details``. ``details`` is usually empty
|
||||||
|
except in a few cases where it contains an additional list of objects
|
||||||
|
with ``value`` and ``label`` keys (e.g. splitting of names).
|
||||||
|
device_name string User-defined name for the device used for scanning (or ``null``).
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
Endpoints
|
||||||
|
"""""""""
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/
|
||||||
|
|
||||||
|
Returns a list of all exhibitors configured for an event.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/exhibitors/ 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,
|
||||||
|
"name": "Aperture Science",
|
||||||
|
"internal_id": null,
|
||||||
|
"contact_name": "Dr Cave Johnson",
|
||||||
|
"contact_name_parts": {
|
||||||
|
"_scheme": "salutation_title_given_family",
|
||||||
|
"family_name": "Johnson",
|
||||||
|
"given_name": "Cave",
|
||||||
|
"salutation": "",
|
||||||
|
"title": "Dr"
|
||||||
|
},
|
||||||
|
"contact_email": "johnson@as.example.org",
|
||||||
|
"booth": "A2",
|
||||||
|
"locale": "de",
|
||||||
|
"access_code": "VKHZ2FU8",
|
||||||
|
"allow_lead_scanning": true,
|
||||||
|
"allow_lead_access": true,
|
||||||
|
"allow_voucher_access": true,
|
||||||
|
"comment": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:query page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:param organizer: The ``slug`` field of a valid organizer
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/(id)/
|
||||||
|
|
||||||
|
Returns information on one exhibitor, identified by its ID.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/exhibitors/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,
|
||||||
|
"name": "Aperture Science",
|
||||||
|
"internal_id": null,
|
||||||
|
"contact_name": "Dr Cave Johnson",
|
||||||
|
"contact_name_parts": {
|
||||||
|
"_scheme": "salutation_title_given_family",
|
||||||
|
"family_name": "Johnson",
|
||||||
|
"given_name": "Cave",
|
||||||
|
"salutation": "",
|
||||||
|
"title": "Dr"
|
||||||
|
},
|
||||||
|
"contact_email": "johnson@as.example.org",
|
||||||
|
"booth": "A2",
|
||||||
|
"locale": "de",
|
||||||
|
"access_code": "VKHZ2FU8",
|
||||||
|
"allow_lead_scanning": true,
|
||||||
|
"allow_lead_access": true,
|
||||||
|
"allow_voucher_access": true,
|
||||||
|
"comment": ""
|
||||||
|
}
|
||||||
|
|
||||||
|
: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 exhibitor to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event/exhibitor does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/(id)/leads/
|
||||||
|
|
||||||
|
Returns a list of all scanned leads of an exhibitor.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/exhibitors/1/leads/ 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": [
|
||||||
|
{
|
||||||
|
"attendee_order": "T0E7E",
|
||||||
|
"attendee_positionid": 1,
|
||||||
|
"rating": 1,
|
||||||
|
"notes": "",
|
||||||
|
"tags": [],
|
||||||
|
"first_upload": "2021-07-06T11:03:31.414491+01:00",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "attendee_name",
|
||||||
|
"label": "Attendee name",
|
||||||
|
"value": "Peter Miller",
|
||||||
|
"details": [
|
||||||
|
{"label": "Given name", "value": "Peter"},
|
||||||
|
{"label": "Family name", "value": "Miller"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:query page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:param organizer: The ``slug`` field of a valid organizer
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:param id: The ``id`` field of the exhibitor to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer or event or exhibitor does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/
|
||||||
|
|
||||||
|
Create a new exhibitor.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/exhibitors/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 166
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Aperture Science",
|
||||||
|
"internal_id": null,
|
||||||
|
"contact_name_parts": {
|
||||||
|
"_scheme": "salutation_title_given_family",
|
||||||
|
"family_name": "Johnson",
|
||||||
|
"given_name": "Cave",
|
||||||
|
"salutation": "",
|
||||||
|
"title": "Dr"
|
||||||
|
},
|
||||||
|
"contact_email": "johnson@as.example.org",
|
||||||
|
"booth": "A2",
|
||||||
|
"locale": "de",
|
||||||
|
"access_code": "VKHZ2FU8",
|
||||||
|
"allow_lead_scanning": true,
|
||||||
|
"allow_lead_access": true,
|
||||||
|
"allow_voucher_access": true,
|
||||||
|
"comment": ""
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Aperture Science",
|
||||||
|
"internal_id": null,
|
||||||
|
"contact_name": "Dr Cave Johnson",
|
||||||
|
"contact_name_parts": {
|
||||||
|
"_scheme": "salutation_title_given_family",
|
||||||
|
"family_name": "Johnson",
|
||||||
|
"given_name": "Cave",
|
||||||
|
"salutation": "",
|
||||||
|
"title": "Dr"
|
||||||
|
},
|
||||||
|
"contact_email": "johnson@as.example.org",
|
||||||
|
"booth": "A2",
|
||||||
|
"locale": "de",
|
||||||
|
"access_code": "VKHZ2FU8",
|
||||||
|
"allow_lead_scanning": true,
|
||||||
|
"allow_lead_access": true,
|
||||||
|
"allow_voucher_access": true,
|
||||||
|
"comment": ""
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to create new exhibitor for
|
||||||
|
:param event: The ``slug`` field of the event to create new exhibitor for
|
||||||
|
:statuscode 201: no error
|
||||||
|
:statuscode 400: The exhibitor 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 exhibitors.
|
||||||
|
|
||||||
|
|
||||||
|
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/(id)/
|
||||||
|
|
||||||
|
Update an exhibitor. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||||
|
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||||
|
want to change.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PATCH /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 34
|
||||||
|
|
||||||
|
{
|
||||||
|
"internal_id": "ABC"
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Aperture Science",
|
||||||
|
"internal_id": "ABC",
|
||||||
|
"contact_name": "Dr Cave Johnson",
|
||||||
|
"contact_name_parts": {
|
||||||
|
"_scheme": "salutation_title_given_family",
|
||||||
|
"family_name": "Johnson",
|
||||||
|
"given_name": "Cave",
|
||||||
|
"salutation": "",
|
||||||
|
"title": "Dr"
|
||||||
|
},
|
||||||
|
"contact_email": "johnson@as.example.org",
|
||||||
|
"booth": "A2",
|
||||||
|
"locale": "de",
|
||||||
|
"access_code": "VKHZ2FU8",
|
||||||
|
"allow_lead_scanning": true,
|
||||||
|
"allow_lead_access": true,
|
||||||
|
"allow_voucher_access": true,
|
||||||
|
"comment": ""
|
||||||
|
}
|
||||||
|
|
||||||
|
: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 exhibitor to modify
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The exhibitor could not be modified due to invalid submitted data.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event/exhibitor does not exist **or** you have no permission to change it.
|
||||||
|
|
||||||
|
|
||||||
|
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/(id)/
|
||||||
|
|
||||||
|
Delete an exhibitor.
|
||||||
|
|
||||||
|
.. warning:: This deletes all lead scan data and removes all connections to vouchers (the vouchers are not deleted).
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
DELETE /api/v1/organizers/bigevents/events/sampleconf/exhibitors/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 exhibitor to delete
|
||||||
|
:statuscode 204: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event/exhibitor does not exist **or** you have no permission to change it
|
||||||
|
|
||||||
|
|
||||||
|
App API
|
||||||
|
-------
|
||||||
|
|
||||||
|
The App API is used for communication between the pretixLEAD app and the pretix server.
|
||||||
|
|
||||||
|
.. warning:: We consider this an internal API, it is not intended for external use. You may still use it, but
|
||||||
|
our :ref:`compatibility commitment <rest-compat>` does not apply.
|
||||||
|
|
||||||
|
Authentication
|
||||||
|
""""""""""""""
|
||||||
|
|
||||||
|
Every exhibitor has an "access code", usually consisting of 8 alphanumeric uppercase characters.
|
||||||
|
This access code is communicated to event exhibitors by the event organizers, so this is also what
|
||||||
|
exhibitors should enter into a login screen.
|
||||||
|
|
||||||
|
All API requests need to contain this access code as a header like this::
|
||||||
|
|
||||||
|
Authorization: Exhibitor ABCDE123
|
||||||
|
|
||||||
|
Exhibitor profile
|
||||||
|
"""""""""""""""""
|
||||||
|
|
||||||
|
Upon login and in regular intervals after that, the API should fetch the exhibitors profile.
|
||||||
|
This serves two purposes:
|
||||||
|
|
||||||
|
* Checking if the authorization code is actually valid
|
||||||
|
|
||||||
|
* Obtaining information that can be shown in the app
|
||||||
|
|
||||||
|
The resource consists of the following fields:
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
name string Exhibitor name
|
||||||
|
booth string Booth number (or ``null``)
|
||||||
|
event object Object describing the event
|
||||||
|
├ name multi-lingual string Event name
|
||||||
|
├ imprint_url string URL to legal notice page. If not ``null``, a button in the app should link to this page.
|
||||||
|
├ privacy_url string URL to privacy notice page. If not ``null``, a button in the app should link to this page.
|
||||||
|
├ help_url string URL to help page. If not ``null``, a button in the app should link to this page.
|
||||||
|
├ logo_url string URL to event logo. If not ``null``, this logo may be shown in the app.
|
||||||
|
├ slug string Event short form
|
||||||
|
└ organizer string Organizer short form
|
||||||
|
notes boolean Specifies whether the exhibitor is allowed to take notes on leads
|
||||||
|
tags list of strings List of tags the exhibitor can assign to their leads
|
||||||
|
scan_types list of objects Only used for a special case, fixed value that external API consumers should ignore
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
.. http:get:: /exhibitors/api/v1/profile
|
||||||
|
|
||||||
|
**Example request:**
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /exhibitors/api/v1/profile HTTP/1.1
|
||||||
|
Authorization: Exhibitor ABCDE123
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response:**
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Aperture Science",
|
||||||
|
"booth": "A2",
|
||||||
|
"event": {
|
||||||
|
"name": {"en": "Sample conference", "de": "Beispielkonferenz"},
|
||||||
|
"slug": "bigevents",
|
||||||
|
"imprint_url": null,
|
||||||
|
"privacy_url": null,
|
||||||
|
"help_url": null,
|
||||||
|
"logo_url": null,
|
||||||
|
"organizer": "sampleconf"
|
||||||
|
},
|
||||||
|
"notes": true,
|
||||||
|
"tags": ["foo", "bar"],
|
||||||
|
"scan_types": [
|
||||||
|
{
|
||||||
|
"key": "lead",
|
||||||
|
"label": "Lead Scanning"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Invalid authentication code
|
||||||
|
|
||||||
|
Submitting a lead
|
||||||
|
"""""""""""""""""
|
||||||
|
|
||||||
|
After a ticket/badge is scanned, it should immediately be submitted to the server
|
||||||
|
so the scan is stored and information about the person can be shown in the app. The same
|
||||||
|
code can be submitted multiple times, so it's no problem to just submit it again after the
|
||||||
|
exhibitor set a note or a rating (0-5) inside the app.
|
||||||
|
|
||||||
|
On the request, you should set the following properties:
|
||||||
|
|
||||||
|
* ``code`` with the scanned barcode
|
||||||
|
* ``notes`` with the exhibitor's notes
|
||||||
|
* ``scanned`` with the date and time of the actual scan (not the time of the upload)
|
||||||
|
* ``scan_type`` set to ``lead`` statically
|
||||||
|
* ``tags`` with the list of selected tags
|
||||||
|
* ``rating`` with the rating assigned by the exhibitor
|
||||||
|
* ``device_name`` with a user-specified name of the device used for scanning (max. 190 characters), or ``null``
|
||||||
|
|
||||||
|
If you submit ``tags`` and ``rating`` to be ``null`` and ``notes`` to be ``""``, the server
|
||||||
|
responds with the previously saved information and will not delete that information. If you
|
||||||
|
supply other values, the information saved on the server will be overridden.
|
||||||
|
|
||||||
|
The response will also contain ``tags``, ``rating``, and ``notes``. Additionally,
|
||||||
|
it will include ``attendee`` with a list of ``fields`` that can be shown to the
|
||||||
|
user. Each field has an internal ``id``, a human-readable ``label``, and a ``value`` (all strings).
|
||||||
|
|
||||||
|
Note that the ``fields`` array can contain any number of dynamic keys!
|
||||||
|
Depending on the exhibitors permission and event configuration this might be empty,
|
||||||
|
or contain lots of details. The app should dynamically show these values (read-only)
|
||||||
|
with the labels sent by the server.
|
||||||
|
|
||||||
|
The request for this looks like this:
|
||||||
|
|
||||||
|
.. http:post:: /exhibitors/api/v1/leads/
|
||||||
|
|
||||||
|
**Example request:**
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /exhibitors/api/v1/leads/ HTTP/1.1
|
||||||
|
Authorization: Exhibitor ABCDE123
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"code": "qrcodecontent",
|
||||||
|
"notes": "Great customer, wants our newsletter",
|
||||||
|
"scanned": "2020-10-18T12:24:23.000+00:00",
|
||||||
|
"scan_type": "lead",
|
||||||
|
"tags": ["foo"],
|
||||||
|
"rating": 4,
|
||||||
|
"device_name": "DEV1"
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response:**
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"attendee": {
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"id": "attendee_name",
|
||||||
|
"label": "Name",
|
||||||
|
"value": "Jon Doe",
|
||||||
|
"details": [
|
||||||
|
{"label": "Given name", "value": "John"},
|
||||||
|
{"label": "Family name", "value": "Doe"},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "attendee_email",
|
||||||
|
"label": "Email",
|
||||||
|
"value": "test@example.com",
|
||||||
|
"details": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"rating": 4,
|
||||||
|
"tags": ["foo"],
|
||||||
|
"notes": "Great customer, wants our newsletter"
|
||||||
|
}
|
||||||
|
|
||||||
|
:statuscode 200: No error, leads was not scanned for the first time
|
||||||
|
:statuscode 201: No error, leads was scanned for the first time
|
||||||
|
:statuscode 400: Invalid data submitted
|
||||||
|
:statuscode 401: Invalid authentication code
|
||||||
|
|
||||||
|
You can also fetch existing leads (if you are authorized to do so):
|
||||||
|
|
||||||
|
.. http:get:: /exhibitors/api/v1/leads/
|
||||||
|
|
||||||
|
**Example request:**
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /exhibitors/api/v1/leads/ HTTP/1.1
|
||||||
|
Authorization: Exhibitor ABCDE123
|
||||||
|
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": [
|
||||||
|
{
|
||||||
|
"attendee": {
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"id": "attendee_name",
|
||||||
|
"label": "Name",
|
||||||
|
"value": "Jon Doe",
|
||||||
|
"details": [
|
||||||
|
{"label": "Given name", "value": "John"},
|
||||||
|
{"label": "Family name", "value": "Doe"},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "attendee_email",
|
||||||
|
"label": "Email",
|
||||||
|
"value": "test@example.com",
|
||||||
|
"details": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"rating": 4,
|
||||||
|
"tags": ["foo"],
|
||||||
|
"notes": "Great customer, wants our newsletter"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:statuscode 200: No error
|
||||||
|
:statuscode 401: Invalid authentication code
|
||||||
|
:statuscode 403: Not permitted to access bulk data
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
.. _`plugin-docs`:
|
||||||
|
|
||||||
Plugin documentation
|
Plugin documentation
|
||||||
====================
|
====================
|
||||||
|
|
||||||
@@ -10,13 +12,13 @@ If you want to **create** a plugin, please go to the
|
|||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
list
|
list
|
||||||
pretixdroid
|
|
||||||
banktransfer
|
banktransfer
|
||||||
ticketoutputpdf
|
ticketoutputpdf
|
||||||
badges
|
badges
|
||||||
campaigns
|
campaigns
|
||||||
certificates
|
certificates
|
||||||
digital
|
digital
|
||||||
|
exhibitors
|
||||||
imported_secrets
|
imported_secrets
|
||||||
webinar
|
webinar
|
||||||
presale-saml
|
presale-saml
|
||||||
|
|||||||
@@ -5,6 +5,6 @@ List of plugins
|
|||||||
===============
|
===============
|
||||||
|
|
||||||
A detailed list of plugins that are available for pretix can be found on the
|
A detailed list of plugins that are available for pretix can be found on the
|
||||||
`project website`_.
|
`pretix Marketplace`_.
|
||||||
|
|
||||||
.. _project website: https://pretix.eu/about/en/plugins
|
.. _pretix Marketplace: https://marketplace.pretix.eu
|
||||||
|
|||||||
@@ -1,368 +0,0 @@
|
|||||||
pretixdroid HTTP API
|
|
||||||
====================
|
|
||||||
|
|
||||||
The pretixdroid plugin provides a HTTP API that the `pretixdroid Android app`_
|
|
||||||
uses to communicate with the pretix server.
|
|
||||||
|
|
||||||
.. warning:: This API is **DEPRECATED** and will probably go away soon. It is used **only** to serve the pretixdroid
|
|
||||||
Android app. There are no backwards compatibility guarantees on this API. We will not add features that
|
|
||||||
are not required for the Android App. There is a general-purpose :ref:`rest-api` that provides all
|
|
||||||
features that you need to check in.
|
|
||||||
|
|
||||||
.. versionchanged:: 1.12
|
|
||||||
|
|
||||||
Support for check-in-time questions has been added. The new API features are fully backwards-compatible and
|
|
||||||
negotiated live, so clients which do not need this feature can ignore the change. For this reason, the API version
|
|
||||||
has not been increased and is still set to 3.
|
|
||||||
|
|
||||||
.. versionchanged:: 1.13
|
|
||||||
|
|
||||||
Support for checking in unpaid tickets has been added.
|
|
||||||
|
|
||||||
|
|
||||||
.. http:post:: /pretixdroid/api/(organizer)/(event)/redeem/
|
|
||||||
|
|
||||||
Redeems a ticket, i.e. checks the user in.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
POST /pretixdroid/api/demoorga/democon/redeem/?key=ABCDEF HTTP/1.1
|
|
||||||
Host: demo.pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
Content-Type: application/x-www-form-urlencoded
|
|
||||||
|
|
||||||
secret=az9u4mymhqktrbupmwkvv6xmgds5dk3&questions_supported=true
|
|
||||||
|
|
||||||
You **must** set the parameter secret.
|
|
||||||
|
|
||||||
You **must** set the parameter ``questions_supported`` to ``true`` **if** you support asking questions
|
|
||||||
back to the app operator. You **must not** set it if you do not support this feature. In that case, questions
|
|
||||||
will just be ignored.
|
|
||||||
|
|
||||||
You **may** set the additional parameter ``datetime`` in the body containing an ISO8601-encoded
|
|
||||||
datetime of the entry attempt. If you don"t, the current date and time will be used.
|
|
||||||
|
|
||||||
You **may** set the additional parameter ``force`` to indicate that the request should be logged
|
|
||||||
regardless of previous check-ins for the same ticket. This might be useful if you made the entry decision offline.
|
|
||||||
Questions will also always be ignored in this case (i.e. supplied answers will be saved, but no error will be
|
|
||||||
thrown if they are missing or invalid).
|
|
||||||
|
|
||||||
You **may** set the additional parameter ``nonce`` with a globally unique random value to identify this
|
|
||||||
check-in. This is meant to be used to prevent duplicate check-ins when you are just retrying after a connection
|
|
||||||
failure.
|
|
||||||
|
|
||||||
You **may** set the additional parameter ``ignore_unpaid`` to indicate that the check-in should be performed even
|
|
||||||
if the order is in pending state.
|
|
||||||
|
|
||||||
If questions are supported and required, you will receive a dictionary ``questions`` containing details on the
|
|
||||||
particular questions to ask. To answer them, just re-send your redemption request with additional parameters of
|
|
||||||
the form ``answer_<question>=<answer>``, e.g. ``answer_12=24``.
|
|
||||||
|
|
||||||
**Example successful response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Content-Type: text/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"status": "ok"
|
|
||||||
"version": 3,
|
|
||||||
"data": {
|
|
||||||
"secret": "az9u4mymhqktrbupmwkvv6xmgds5dk3",
|
|
||||||
"order": "ABCDE",
|
|
||||||
"item": "Standard ticket",
|
|
||||||
"item_id": 1,
|
|
||||||
"variation": null,
|
|
||||||
"variation_id": null,
|
|
||||||
"attendee_name": "Peter Higgs",
|
|
||||||
"attention": false,
|
|
||||||
"redeemed": true,
|
|
||||||
"checkin_allowed": true,
|
|
||||||
"addons_text": "Parking spot",
|
|
||||||
"paid": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
**Example response with required questions**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Content-Type: text/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"status": "incomplete"
|
|
||||||
"version": 3
|
|
||||||
"data": {
|
|
||||||
"secret": "az9u4mymhqktrbupmwkvv6xmgds5dk3",
|
|
||||||
"order": "ABCDE",
|
|
||||||
"item": "Standard ticket",
|
|
||||||
"item_id": 1,
|
|
||||||
"variation": null,
|
|
||||||
"variation_id": null,
|
|
||||||
"attendee_name": "Peter Higgs",
|
|
||||||
"attention": false,
|
|
||||||
"redeemed": true,
|
|
||||||
"checkin_allowed": true,
|
|
||||||
"addons_text": "Parking spot",
|
|
||||||
"paid": true
|
|
||||||
},
|
|
||||||
"questions": [
|
|
||||||
{
|
|
||||||
"id": 12,
|
|
||||||
"type": "C",
|
|
||||||
"question": "Choose a shirt size",
|
|
||||||
"required": true,
|
|
||||||
"position": 2,
|
|
||||||
"items": [1],
|
|
||||||
"options": [
|
|
||||||
{
|
|
||||||
"id": 24,
|
|
||||||
"answer": "M"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 25,
|
|
||||||
"answer": "L"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
**Example error response with data**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Content-Type: text/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"status": "error",
|
|
||||||
"reason": "already_redeemed",
|
|
||||||
"version": 3,
|
|
||||||
"data": {
|
|
||||||
"secret": "az9u4mymhqktrbupmwkvv6xmgds5dk3",
|
|
||||||
"order": "ABCDE",
|
|
||||||
"item": "Standard ticket",
|
|
||||||
"item_id": 1,
|
|
||||||
"variation": null,
|
|
||||||
"variation_id": null,
|
|
||||||
"attendee_name": "Peter Higgs",
|
|
||||||
"attention": false,
|
|
||||||
"redeemed": true,
|
|
||||||
"checkin_allowed": true,
|
|
||||||
"addons_text": "Parking spot",
|
|
||||||
"paid": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
**Example error response without data**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Content-Type: text/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"status": "error",
|
|
||||||
"reason": "unkown_ticket",
|
|
||||||
"version": 3
|
|
||||||
}
|
|
||||||
|
|
||||||
Possible error reasons:
|
|
||||||
|
|
||||||
* ``unpaid`` - Ticket is not paid for or has been refunded
|
|
||||||
* ``already_redeemed`` - Ticket already has been redeemed
|
|
||||||
* ``product`` - Tickets with this product may not be scanned at this device
|
|
||||||
* ``unknown_ticket`` - Secret does not match a ticket in the database
|
|
||||||
|
|
||||||
:query key: Secret API key
|
|
||||||
:statuscode 200: Valid request
|
|
||||||
:statuscode 404: Unknown organizer or event
|
|
||||||
:statuscode 403: Invalid authorization key
|
|
||||||
|
|
||||||
.. http:get:: /pretixdroid/api/(organizer)/(event)/search/
|
|
||||||
|
|
||||||
Searches for a ticket.
|
|
||||||
At most 25 results will be returned. **Queries with less than 4 characters will always return an empty result set.**
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /pretixdroid/api/demoorga/democon/search/?key=ABCDEF&query=Peter HTTP/1.1
|
|
||||||
Host: demo.pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Content-Type: text/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"results": [
|
|
||||||
{
|
|
||||||
"secret": "az9u4mymhqktrbupmwkvv6xmgds5dk3",
|
|
||||||
"order": "ABCE6",
|
|
||||||
"item": "Standard ticket",
|
|
||||||
"variation": null,
|
|
||||||
"attendee_name": "Peter Higgs",
|
|
||||||
"redeemed": false,
|
|
||||||
"attention": false,
|
|
||||||
"checkin_allowed": true,
|
|
||||||
"addons_text": "Parking spot",
|
|
||||||
"paid": true
|
|
||||||
},
|
|
||||||
...
|
|
||||||
],
|
|
||||||
"version": 3
|
|
||||||
}
|
|
||||||
|
|
||||||
:query query: Search query
|
|
||||||
:query key: Secret API key
|
|
||||||
:statuscode 200: Valid request
|
|
||||||
:statuscode 404: Unknown organizer or event
|
|
||||||
:statuscode 403: Invalid authorization key
|
|
||||||
|
|
||||||
.. http:get:: /pretixdroid/api/(organizer)/(event)/download/
|
|
||||||
|
|
||||||
Download data for all tickets.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /pretixdroid/api/demoorga/democon/download/?key=ABCDEF HTTP/1.1
|
|
||||||
Host: demo.pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Content-Type: text/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"version": 3,
|
|
||||||
"results": [
|
|
||||||
{
|
|
||||||
"secret": "az9u4mymhqktrbupmwkvv6xmgds5dk3",
|
|
||||||
"order": "ABCE6",
|
|
||||||
"item": "Standard ticket",
|
|
||||||
"variation": null,
|
|
||||||
"attendee_name": "Peter Higgs",
|
|
||||||
"redeemed": false,
|
|
||||||
"attention": false,
|
|
||||||
"checkin_allowed": true,
|
|
||||||
"paid": true
|
|
||||||
},
|
|
||||||
...
|
|
||||||
],
|
|
||||||
"questions": [
|
|
||||||
{
|
|
||||||
"id": 12,
|
|
||||||
"type": "C",
|
|
||||||
"question": "Choose a shirt size",
|
|
||||||
"required": true,
|
|
||||||
"position": 2,
|
|
||||||
"items": [1],
|
|
||||||
"options": [
|
|
||||||
{
|
|
||||||
"id": 24,
|
|
||||||
"answer": "M"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 25,
|
|
||||||
"answer": "L"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
:query key: Secret API key
|
|
||||||
:statuscode 200: Valid request
|
|
||||||
:statuscode 404: Unknown organizer or event
|
|
||||||
:statuscode 403: Invalid authorization key
|
|
||||||
|
|
||||||
.. http:get:: /pretixdroid/api/(organizer)/(event)/status/
|
|
||||||
|
|
||||||
Returns status information, such as the total number of tickets and the
|
|
||||||
number of performed check-ins.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /pretixdroid/api/demoorga/democon/status/?key=ABCDEF HTTP/1.1
|
|
||||||
Host: demo.pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Content-Type: text/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"checkins": 17,
|
|
||||||
"total": 42,
|
|
||||||
"version": 3,
|
|
||||||
"event": {
|
|
||||||
"name": "Demo Conference",
|
|
||||||
"slug": "democon",
|
|
||||||
"date_from": "2016-12-27T17:00:00Z",
|
|
||||||
"date_to": "2016-12-30T18:00:00Z",
|
|
||||||
"timezone": "UTC",
|
|
||||||
"url": "https://demo.pretix.eu/demoorga/democon/",
|
|
||||||
"organizer": {
|
|
||||||
"name": "Demo Organizer",
|
|
||||||
"slug": "demoorga"
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"name": "T-Shirt",
|
|
||||||
"id": 1,
|
|
||||||
"checkins": 1,
|
|
||||||
"admission": False,
|
|
||||||
"total": 1,
|
|
||||||
"variations": [
|
|
||||||
{
|
|
||||||
"name": "Red",
|
|
||||||
"id": 1,
|
|
||||||
"checkins": 1,
|
|
||||||
"total": 12
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Blue",
|
|
||||||
"id": 2,
|
|
||||||
"checkins": 4,
|
|
||||||
"total": 8
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Ticket",
|
|
||||||
"id": 2,
|
|
||||||
"checkins": 15,
|
|
||||||
"admission": True,
|
|
||||||
"total": 22,
|
|
||||||
"variations": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
:query key: Secret API key
|
|
||||||
:statuscode 200: Valid request
|
|
||||||
:statuscode 404: Unknown organizer or event
|
|
||||||
:statuscode 403: Invalid authorization key
|
|
||||||
|
|
||||||
.. _pretixdroid Android app: https://github.com/pretix/pretixdroid
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
-e ../src/
|
-e ../src/
|
||||||
sphinx==2.3.*
|
sphinx==2.3.*
|
||||||
|
jinja2==3.0.*
|
||||||
sphinx-rtd-theme
|
sphinx-rtd-theme
|
||||||
sphinxcontrib-httpdomain
|
sphinxcontrib-httpdomain
|
||||||
sphinxcontrib-images
|
sphinxcontrib-images
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ prepending
|
|||||||
preprocessor
|
preprocessor
|
||||||
presale
|
presale
|
||||||
pretix
|
pretix
|
||||||
|
pretixLEAD
|
||||||
pretixSCAN
|
pretixSCAN
|
||||||
pretixdroid
|
pretixdroid
|
||||||
pretixPOS
|
pretixPOS
|
||||||
|
|||||||
@@ -253,18 +253,21 @@ If you want, you can suppress us loading the widget and/or modify the user data
|
|||||||
|
|
||||||
If you then later want to trigger loading the widgets, just call ``window.PretixWidget.buildWidgets()``.
|
If you then later want to trigger loading the widgets, just call ``window.PretixWidget.buildWidgets()``.
|
||||||
|
|
||||||
Waiting for the widget to load
|
Waiting for the widget to load or close
|
||||||
------------------------------
|
---------------------------------------
|
||||||
|
|
||||||
If you want to run custom JavaScript once the widget is fully loaded, you can register a callback function. Note that
|
If you want to run custom JavaScript once the widget is fully loaded or when it is closed, you can register callback
|
||||||
this function might be run multiple times, for example if you have multiple widgets on a page or if the user switches
|
functions. Note that these function might be run multiple times, for example if you have multiple widgets on a page
|
||||||
e.g. from an event list to an event detail view::
|
or if the user switches e.g. from an event list to an event detail view::
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
window.pretixWidgetCallback = function () {
|
window.pretixWidgetCallback = function () {
|
||||||
window.PretixWidget.addLoadListener(function () {
|
window.PretixWidget.addLoadListener(function () {
|
||||||
console.log("Widget has loaded!");
|
console.log("Widget has loaded!");
|
||||||
});
|
});
|
||||||
|
window.PretixWidget.addCloseListener(function () {
|
||||||
|
console.log("Widget has been closed!");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ recursive-include pretix/plugins/banktransfer/static *
|
|||||||
recursive-include pretix/plugins/manualpayment/templates *
|
recursive-include pretix/plugins/manualpayment/templates *
|
||||||
recursive-include pretix/plugins/manualpayment/static *
|
recursive-include pretix/plugins/manualpayment/static *
|
||||||
recursive-include pretix/plugins/paypal/templates *
|
recursive-include pretix/plugins/paypal/templates *
|
||||||
|
recursive-include pretix/plugins/paypal/static *
|
||||||
recursive-include pretix/plugins/pretixdroid/templates *
|
recursive-include pretix/plugins/pretixdroid/templates *
|
||||||
recursive-include pretix/plugins/pretixdroid/static *
|
recursive-include pretix/plugins/pretixdroid/static *
|
||||||
recursive-include pretix/plugins/sendmail/templates *
|
recursive-include pretix/plugins/sendmail/templates *
|
||||||
|
|||||||
@@ -19,4 +19,4 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
__version__ = "4.7.0"
|
__version__ = "4.9.0.dev0"
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
|
|||||||
('POST', 'plugins:pretix_posbackend:posdebuglogentry-bulk-create'),
|
('POST', 'plugins:pretix_posbackend:posdebuglogentry-bulk-create'),
|
||||||
('GET', 'plugins:pretix_posbackend:poscashier-list'),
|
('GET', 'plugins:pretix_posbackend:poscashier-list'),
|
||||||
('POST', 'plugins:pretix_posbackend:stripeterminal.token'),
|
('POST', 'plugins:pretix_posbackend:stripeterminal.token'),
|
||||||
|
('PUT', 'plugins:pretix_posbackend:file.upload'),
|
||||||
('GET', 'api-v1:revokedsecrets-list'),
|
('GET', 'api-v1:revokedsecrets-list'),
|
||||||
('GET', 'api-v1:event.settings'),
|
('GET', 'api-v1:event.settings'),
|
||||||
('GET', 'plugins:pretix_seating:event.event'),
|
('GET', 'plugins:pretix_seating:event.event'),
|
||||||
|
|||||||
@@ -424,88 +424,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
|||||||
self.fields.pop('pdf_data', None)
|
self.fields.pop('pdf_data', None)
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
if data.get('attendee_name') and data.get('attendee_name_parts'):
|
raise TypeError("this serializer is readonly")
|
||||||
raise ValidationError(
|
|
||||||
{'attendee_name': ['Do not specify attendee_name if you specified attendee_name_parts.']}
|
|
||||||
)
|
|
||||||
if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'):
|
|
||||||
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
|
|
||||||
|
|
||||||
if data.get('country'):
|
|
||||||
if not pycountry.countries.get(alpha_2=data.get('country').code):
|
|
||||||
raise ValidationError(
|
|
||||||
{'country': ['Invalid country code.']}
|
|
||||||
)
|
|
||||||
|
|
||||||
if data.get('state'):
|
|
||||||
cc = str(data.get('country') or self.instance.country or '')
|
|
||||||
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
|
||||||
raise ValidationError(
|
|
||||||
{'state': ['States are not supported in country "{}".'.format(cc)]}
|
|
||||||
)
|
|
||||||
if not pycountry.subdivisions.get(code=cc + '-' + data.get('state')):
|
|
||||||
raise ValidationError(
|
|
||||||
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
|
|
||||||
)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
|
||||||
# Even though all fields that shouldn't be edited are marked as read_only in the serializer
|
|
||||||
# (hopefully), we'll be extra careful here and be explicit about the model fields we update.
|
|
||||||
update_fields = [
|
|
||||||
'attendee_name_parts', 'company', 'street', 'zipcode', 'city', 'country',
|
|
||||||
'state', 'attendee_email',
|
|
||||||
]
|
|
||||||
answers_data = validated_data.pop('answers', None)
|
|
||||||
|
|
||||||
name = validated_data.pop('attendee_name', '')
|
|
||||||
if name and not validated_data.get('attendee_name_parts'):
|
|
||||||
validated_data['attendee_name_parts'] = {
|
|
||||||
'_legacy': name
|
|
||||||
}
|
|
||||||
|
|
||||||
for attr, value in validated_data.items():
|
|
||||||
if attr in update_fields:
|
|
||||||
setattr(instance, attr, value)
|
|
||||||
|
|
||||||
instance.save(update_fields=update_fields)
|
|
||||||
|
|
||||||
if answers_data is not None:
|
|
||||||
qs_seen = set()
|
|
||||||
answercache = {
|
|
||||||
a.question_id: a for a in instance.answers.all()
|
|
||||||
}
|
|
||||||
for answ_data in answers_data:
|
|
||||||
options = answ_data.pop('options', [])
|
|
||||||
if answ_data['question'].pk in qs_seen:
|
|
||||||
raise ValidationError(f'Question {answ_data["question"]} was sent twice.')
|
|
||||||
if answ_data['question'].pk in answercache:
|
|
||||||
a = answercache[answ_data['question'].pk]
|
|
||||||
if isinstance(answ_data['answer'], File):
|
|
||||||
a.file.save(answ_data['answer'].name, answ_data['answer'], save=False)
|
|
||||||
a.answer = 'file://' + a.file.name
|
|
||||||
elif a.answer.startswith('file://') and answ_data['answer'] == "file:keep":
|
|
||||||
pass # keep current file
|
|
||||||
else:
|
|
||||||
for attr, value in answ_data.items():
|
|
||||||
setattr(a, attr, value)
|
|
||||||
a.save()
|
|
||||||
else:
|
|
||||||
if isinstance(answ_data['answer'], File):
|
|
||||||
an = answ_data.pop('answer')
|
|
||||||
a = instance.answers.create(**answ_data, answer='')
|
|
||||||
a.file.save(os.path.basename(an.name), an, save=False)
|
|
||||||
a.answer = 'file://' + a.file.name
|
|
||||||
a.save()
|
|
||||||
else:
|
|
||||||
a = instance.answers.create(**answ_data)
|
|
||||||
a.options.set(options)
|
|
||||||
qs_seen.add(a.question_id)
|
|
||||||
for qid, a in answercache.items():
|
|
||||||
if qid not in qs_seen:
|
|
||||||
a.delete()
|
|
||||||
|
|
||||||
return instance
|
|
||||||
|
|
||||||
|
|
||||||
class RequireAttentionField(serializers.Field):
|
class RequireAttentionField(serializers.Field):
|
||||||
@@ -593,7 +512,7 @@ class OrderPaymentDateField(serializers.DateField):
|
|||||||
class OrderFeeSerializer(I18nAwareModelSerializer):
|
class OrderFeeSerializer(I18nAwareModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = OrderFee
|
model = OrderFee
|
||||||
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule', 'canceled')
|
fields = ('id', 'fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule', 'canceled')
|
||||||
|
|
||||||
|
|
||||||
class PaymentURLField(serializers.URLField):
|
class PaymentURLField(serializers.URLField):
|
||||||
@@ -1361,14 +1280,18 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
f.order = order._wrapped if simulate else order
|
f.order = order._wrapped if simulate else order
|
||||||
f._calculate_tax()
|
f._calculate_tax()
|
||||||
fees.append(f)
|
fees.append(f)
|
||||||
if not simulate:
|
if simulate:
|
||||||
|
f.id = 0
|
||||||
|
else:
|
||||||
f.save()
|
f.save()
|
||||||
else:
|
else:
|
||||||
f = OrderFee(**fee_data)
|
f = OrderFee(**fee_data)
|
||||||
f.order = order._wrapped if simulate else order
|
f.order = order._wrapped if simulate else order
|
||||||
f._calculate_tax()
|
f._calculate_tax()
|
||||||
fees.append(f)
|
fees.append(f)
|
||||||
if not simulate:
|
if simulate:
|
||||||
|
f.id = 0
|
||||||
|
else:
|
||||||
f.save()
|
f.save()
|
||||||
|
|
||||||
order.total += sum([f.value for f in fees])
|
order.total += sum([f.value for f in fees])
|
||||||
|
|||||||
@@ -0,0 +1,424 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
import pycountry
|
||||||
|
from django.core.files import File
|
||||||
|
from rest_framework import serializers
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
|
from pretix.api.serializers.order import (
|
||||||
|
AnswerCreateSerializer, AnswerSerializer, CompatibleCountryField,
|
||||||
|
OrderPositionCreateSerializer,
|
||||||
|
)
|
||||||
|
from pretix.base.models import ItemVariation, Order, OrderFee, OrderPosition
|
||||||
|
from pretix.base.services.orders import OrderError
|
||||||
|
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerializer):
|
||||||
|
order = serializers.SlugRelatedField(slug_field='code', queryset=Order.objects.none(), required=True, allow_null=False)
|
||||||
|
answers = AnswerCreateSerializer(many=True, required=False)
|
||||||
|
addon_to = serializers.IntegerField(required=False, allow_null=True)
|
||||||
|
secret = serializers.CharField(required=False)
|
||||||
|
attendee_name = serializers.CharField(required=False, allow_null=True)
|
||||||
|
seat = serializers.CharField(required=False, allow_null=True)
|
||||||
|
price = serializers.DecimalField(required=False, allow_null=True, decimal_places=2,
|
||||||
|
max_digits=10)
|
||||||
|
country = CompatibleCountryField(source='*')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = OrderPosition
|
||||||
|
fields = ('order', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||||
|
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||||
|
'secret', 'addon_to', 'subevent', 'answers', 'seat')
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if not self.context:
|
||||||
|
return
|
||||||
|
self.fields['order'].queryset = self.context['event'].orders.all()
|
||||||
|
self.fields['item'].queryset = self.context['event'].items.all()
|
||||||
|
self.fields['subevent'].queryset = self.context['event'].subevents.all()
|
||||||
|
self.fields['seat'].queryset = self.context['event'].seats.all()
|
||||||
|
self.fields['variation'].queryset = ItemVariation.objects.filter(item__event=self.context['event'])
|
||||||
|
if 'order' in self.context:
|
||||||
|
del self.fields['order']
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
data = super().validate(data)
|
||||||
|
if data.get('addon_to'):
|
||||||
|
try:
|
||||||
|
data['addon_to'] = data['order'].positions.get(positionid=data['addon_to'])
|
||||||
|
except OrderPosition.DoesNotExist:
|
||||||
|
raise ValidationError({
|
||||||
|
'addon_to': ['addon_to refers to an unknown position ID for this order.']
|
||||||
|
})
|
||||||
|
return data
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
ocm = self.context['ocm']
|
||||||
|
|
||||||
|
try:
|
||||||
|
ocm.add_position(
|
||||||
|
item=validated_data['item'],
|
||||||
|
variation=validated_data.get('variation'),
|
||||||
|
price=validated_data.get('price'),
|
||||||
|
addon_to=validated_data.get('addon_to'),
|
||||||
|
subevent=validated_data.get('subevent'),
|
||||||
|
seat=validated_data.get('seat'),
|
||||||
|
)
|
||||||
|
if self.context.get('commit', True):
|
||||||
|
ocm.commit()
|
||||||
|
return validated_data['order'].positions.order_by('-positionid').first()
|
||||||
|
else:
|
||||||
|
return OrderPosition() # fake to appease DRF
|
||||||
|
except OrderError as e:
|
||||||
|
raise ValidationError(str(e))
|
||||||
|
|
||||||
|
|
||||||
|
class OrderPositionInfoPatchSerializer(serializers.ModelSerializer):
|
||||||
|
answers = AnswerSerializer(many=True)
|
||||||
|
country = CompatibleCountryField(source='*')
|
||||||
|
attendee_name = serializers.CharField(required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = OrderPosition
|
||||||
|
fields = (
|
||||||
|
'attendee_name', 'attendee_name_parts', 'company', 'street', 'zipcode', 'city', 'country',
|
||||||
|
'state', 'attendee_email', 'answers',
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
if data.get('attendee_name') and data.get('attendee_name_parts'):
|
||||||
|
raise ValidationError(
|
||||||
|
{'attendee_name': ['Do not specify attendee_name if you specified attendee_name_parts.']}
|
||||||
|
)
|
||||||
|
if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'):
|
||||||
|
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
|
||||||
|
|
||||||
|
if data.get('country'):
|
||||||
|
if not pycountry.countries.get(alpha_2=data.get('country').code):
|
||||||
|
raise ValidationError(
|
||||||
|
{'country': ['Invalid country code.']}
|
||||||
|
)
|
||||||
|
|
||||||
|
if data.get('state'):
|
||||||
|
cc = str(data.get('country') or self.instance.country or '')
|
||||||
|
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||||
|
raise ValidationError(
|
||||||
|
{'state': ['States are not supported in country "{}".'.format(cc)]}
|
||||||
|
)
|
||||||
|
if not pycountry.subdivisions.get(code=cc + '-' + data.get('state')):
|
||||||
|
raise ValidationError(
|
||||||
|
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
answers_data = validated_data.pop('answers', None)
|
||||||
|
|
||||||
|
name = validated_data.pop('attendee_name', '')
|
||||||
|
if name and not validated_data.get('attendee_name_parts'):
|
||||||
|
validated_data['attendee_name_parts'] = {
|
||||||
|
'_legacy': name
|
||||||
|
}
|
||||||
|
|
||||||
|
for attr, value in validated_data.items():
|
||||||
|
if attr in self.fields:
|
||||||
|
setattr(instance, attr, value)
|
||||||
|
|
||||||
|
instance.save(update_fields=list(validated_data.keys()))
|
||||||
|
|
||||||
|
if answers_data is not None:
|
||||||
|
qs_seen = set()
|
||||||
|
answercache = {
|
||||||
|
a.question_id: a for a in instance.answers.all()
|
||||||
|
}
|
||||||
|
for answ_data in answers_data:
|
||||||
|
options = answ_data.pop('options', [])
|
||||||
|
if answ_data['question'].pk in qs_seen:
|
||||||
|
raise ValidationError(f'Question {answ_data["question"]} was sent twice.')
|
||||||
|
if answ_data['question'].pk in answercache:
|
||||||
|
a = answercache[answ_data['question'].pk]
|
||||||
|
if isinstance(answ_data['answer'], File):
|
||||||
|
a.file.save(answ_data['answer'].name, answ_data['answer'], save=False)
|
||||||
|
a.answer = 'file://' + a.file.name
|
||||||
|
elif a.answer.startswith('file://') and answ_data['answer'] == "file:keep":
|
||||||
|
pass # keep current file
|
||||||
|
else:
|
||||||
|
for attr, value in answ_data.items():
|
||||||
|
setattr(a, attr, value)
|
||||||
|
a.save()
|
||||||
|
else:
|
||||||
|
if isinstance(answ_data['answer'], File):
|
||||||
|
an = answ_data.pop('answer')
|
||||||
|
a = instance.answers.create(**answ_data, answer='')
|
||||||
|
a.file.save(os.path.basename(an.name), an, save=False)
|
||||||
|
a.answer = 'file://' + a.file.name
|
||||||
|
a.save()
|
||||||
|
else:
|
||||||
|
a = instance.answers.create(**answ_data)
|
||||||
|
a.options.set(options)
|
||||||
|
qs_seen.add(a.question_id)
|
||||||
|
for qid, a in answercache.items():
|
||||||
|
if qid not in qs_seen:
|
||||||
|
a.delete()
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class OrderPositionChangeSerializer(serializers.ModelSerializer):
|
||||||
|
seat = serializers.CharField(source='seat.seat_guid', allow_null=True, required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = OrderPosition
|
||||||
|
fields = (
|
||||||
|
'item', 'variation', 'subevent', 'seat', 'price', 'tax_rule',
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if not self.context:
|
||||||
|
return
|
||||||
|
self.fields['item'].queryset = self.context['event'].items.all()
|
||||||
|
self.fields['subevent'].queryset = self.context['event'].subevents.all()
|
||||||
|
self.fields['tax_rule'].queryset = self.context['event'].tax_rules.all()
|
||||||
|
if kwargs.get('partial'):
|
||||||
|
for k, v in self.fields.items():
|
||||||
|
self.fields[k].required = False
|
||||||
|
|
||||||
|
def validate_item(self, item):
|
||||||
|
if item.event != self.context['event']:
|
||||||
|
raise ValidationError(
|
||||||
|
'The specified item does not belong to this event.'
|
||||||
|
)
|
||||||
|
return item
|
||||||
|
|
||||||
|
def validate_subevent(self, subevent):
|
||||||
|
if self.context['event'].has_subevents:
|
||||||
|
if not subevent:
|
||||||
|
raise ValidationError(
|
||||||
|
'You need to set a subevent.'
|
||||||
|
)
|
||||||
|
if subevent.event != self.context['event']:
|
||||||
|
raise ValidationError(
|
||||||
|
'The specified subevent does not belong to this event.'
|
||||||
|
)
|
||||||
|
elif subevent:
|
||||||
|
raise ValidationError(
|
||||||
|
'You cannot set a subevent for this event.'
|
||||||
|
)
|
||||||
|
return subevent
|
||||||
|
|
||||||
|
def validate(self, data, instance=None):
|
||||||
|
instance = instance or self.instance
|
||||||
|
if instance is None:
|
||||||
|
return data # needs to be done later
|
||||||
|
if data.get('item', instance.item):
|
||||||
|
if data.get('item', instance.item).has_variations:
|
||||||
|
if not data.get('variation', instance.variation):
|
||||||
|
raise ValidationError({'variation': ['You should specify a variation for this item.']})
|
||||||
|
else:
|
||||||
|
if data.get('variation', instance.variation).item != data.get('item', instance.item):
|
||||||
|
raise ValidationError(
|
||||||
|
{'variation': ['The specified variation does not belong to the specified item.']}
|
||||||
|
)
|
||||||
|
elif data.get('variation', instance.variation):
|
||||||
|
raise ValidationError(
|
||||||
|
{'variation': ['You cannot specify a variation for this item.']}
|
||||||
|
)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
ocm = self.context['ocm']
|
||||||
|
current_seat = {'seat_guid': instance.seat.seat_guid} if instance.seat else None
|
||||||
|
item = validated_data.get('item', instance.item)
|
||||||
|
variation = validated_data.get('variation', instance.variation)
|
||||||
|
subevent = validated_data.get('subevent', instance.subevent)
|
||||||
|
price = validated_data.get('price', instance.price)
|
||||||
|
seat = validated_data.get('seat', current_seat)
|
||||||
|
tax_rule = validated_data.get('tax_rule', instance.tax_rule)
|
||||||
|
|
||||||
|
change_item = None
|
||||||
|
if item != instance.item or variation != instance.variation:
|
||||||
|
change_item = (item, variation)
|
||||||
|
|
||||||
|
change_subevent = None
|
||||||
|
if self.context['event'].has_subevents and subevent != instance.subevent:
|
||||||
|
change_subevent = (subevent,)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if change_item is not None and change_subevent is not None:
|
||||||
|
ocm.change_item_and_subevent(instance, *change_item, *change_subevent)
|
||||||
|
elif change_item is not None:
|
||||||
|
ocm.change_item(instance, *change_item)
|
||||||
|
elif change_subevent is not None:
|
||||||
|
ocm.change_subevent(instance, *change_subevent)
|
||||||
|
|
||||||
|
if seat != current_seat or change_subevent:
|
||||||
|
ocm.change_seat(instance, seat['seat_guid'] if seat else None)
|
||||||
|
|
||||||
|
if price != instance.price:
|
||||||
|
ocm.change_price(instance, price)
|
||||||
|
|
||||||
|
if tax_rule != instance.tax_rule:
|
||||||
|
ocm.change_tax_rule(instance, tax_rule)
|
||||||
|
|
||||||
|
if self.context.get('commit', True):
|
||||||
|
ocm.commit()
|
||||||
|
instance.refresh_from_db()
|
||||||
|
except OrderError as e:
|
||||||
|
raise ValidationError(str(e))
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class PatchPositionSerializer(serializers.Serializer):
|
||||||
|
position = serializers.PrimaryKeyRelatedField(queryset=OrderPosition.all.none())
|
||||||
|
|
||||||
|
def validate_position(self, value):
|
||||||
|
self.fields['body'].instance = value # hack around DRFs validation order
|
||||||
|
return value
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
OrderPositionChangeSerializer(context=self.context, partial=True).validate(data['body'], data['position'])
|
||||||
|
return data
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['position'].queryset = self.context['order'].positions.all()
|
||||||
|
self.fields['body'] = OrderPositionChangeSerializer(context=self.context, partial=True)
|
||||||
|
|
||||||
|
|
||||||
|
class SelectPositionSerializer(serializers.Serializer):
|
||||||
|
position = serializers.PrimaryKeyRelatedField(queryset=OrderPosition.all.none())
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['position'].queryset = self.context['order'].positions.all()
|
||||||
|
|
||||||
|
|
||||||
|
class OrderFeeChangeSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = OrderFee
|
||||||
|
fields = (
|
||||||
|
'value',
|
||||||
|
)
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
ocm = self.context['ocm']
|
||||||
|
value = validated_data.get('value', instance.value)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if value != instance.value:
|
||||||
|
ocm.change_fee(instance, value)
|
||||||
|
|
||||||
|
if self.context.get('commit', True):
|
||||||
|
ocm.commit()
|
||||||
|
instance.refresh_from_db()
|
||||||
|
except OrderError as e:
|
||||||
|
raise ValidationError(str(e))
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class PatchFeeSerializer(serializers.Serializer):
|
||||||
|
fee = serializers.PrimaryKeyRelatedField(queryset=OrderFee.all.none())
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['fee'].queryset = self.context['order'].fees.all()
|
||||||
|
self.fields['body'] = OrderFeeChangeSerializer(context=self.context)
|
||||||
|
|
||||||
|
|
||||||
|
class SelectFeeSerializer(serializers.Serializer):
|
||||||
|
fee = serializers.PrimaryKeyRelatedField(queryset=OrderFee.all.none())
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if not self.context:
|
||||||
|
return
|
||||||
|
self.fields['fee'].queryset = self.context['order'].fees.all()
|
||||||
|
|
||||||
|
|
||||||
|
class OrderChangeOperationSerializer(serializers.Serializer):
|
||||||
|
send_email = serializers.BooleanField(default=False, required=False)
|
||||||
|
reissue_invoice = serializers.BooleanField(default=True, required=False)
|
||||||
|
recalculate_taxes = serializers.ChoiceField(default=None, allow_null=True, required=False, choices=[
|
||||||
|
('keep_net', 'keep_net'),
|
||||||
|
('keep_gross', 'keep_gross'),
|
||||||
|
])
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(self, *args, **kwargs)
|
||||||
|
self.fields['patch_positions'] = PatchPositionSerializer(
|
||||||
|
many=True, required=False, context=self.context
|
||||||
|
)
|
||||||
|
self.fields['cancel_positions'] = SelectPositionSerializer(
|
||||||
|
many=True, required=False, context=self.context
|
||||||
|
)
|
||||||
|
self.fields['create_positions'] = OrderPositionCreateForExistingOrderSerializer(
|
||||||
|
many=True, required=False, context=self.context
|
||||||
|
)
|
||||||
|
self.fields['split_positions'] = SelectPositionSerializer(
|
||||||
|
many=True, required=False, context=self.context
|
||||||
|
)
|
||||||
|
self.fields['patch_fees'] = PatchFeeSerializer(
|
||||||
|
many=True, required=False, context=self.context
|
||||||
|
)
|
||||||
|
self.fields['cancel_fees'] = SelectFeeSerializer(
|
||||||
|
many=True, required=False, context=self.context
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
seen_positions = set()
|
||||||
|
for d in data.get('patch_positions', []):
|
||||||
|
print(d, seen_positions)
|
||||||
|
if d['position'] in seen_positions:
|
||||||
|
raise ValidationError({'patch_positions': ['You have specified the same object twice.']})
|
||||||
|
seen_positions.add(d['position'])
|
||||||
|
seen_positions = set()
|
||||||
|
for d in data.get('cancel_positions', []):
|
||||||
|
if d['position'] in seen_positions:
|
||||||
|
raise ValidationError({'cancel_positions': ['You have specified the same object twice.']})
|
||||||
|
seen_positions.add(d['position'])
|
||||||
|
seen_positions = set()
|
||||||
|
for d in data.get('split_positions', []):
|
||||||
|
if d['position'] in seen_positions:
|
||||||
|
raise ValidationError({'split_positions': ['You have specified the same object twice.']})
|
||||||
|
seen_positions.add(d['position'])
|
||||||
|
seen_fees = set()
|
||||||
|
for d in data.get('patch_fees', []):
|
||||||
|
if d['fee'] in seen_fees:
|
||||||
|
raise ValidationError({'patch_fees': ['You have specified the same object twice.']})
|
||||||
|
seen_positions.add(d['fee'])
|
||||||
|
seen_fees = set()
|
||||||
|
for d in data.get('cancel_fees', []):
|
||||||
|
if d['fee'] in seen_fees:
|
||||||
|
raise ValidationError({'cancel_fees': ['You have specified the same object twice.']})
|
||||||
|
seen_positions.add(d['fee'])
|
||||||
|
|
||||||
|
return data
|
||||||
@@ -42,6 +42,7 @@ class InitializationRequestSerializer(serializers.Serializer):
|
|||||||
hardware_model = serializers.CharField(max_length=190)
|
hardware_model = serializers.CharField(max_length=190)
|
||||||
software_brand = serializers.CharField(max_length=190)
|
software_brand = serializers.CharField(max_length=190)
|
||||||
software_version = serializers.CharField(max_length=190)
|
software_version = serializers.CharField(max_length=190)
|
||||||
|
info = serializers.JSONField(required=False, allow_null=True)
|
||||||
|
|
||||||
|
|
||||||
class UpdateRequestSerializer(serializers.Serializer):
|
class UpdateRequestSerializer(serializers.Serializer):
|
||||||
@@ -49,6 +50,7 @@ class UpdateRequestSerializer(serializers.Serializer):
|
|||||||
hardware_model = serializers.CharField(max_length=190)
|
hardware_model = serializers.CharField(max_length=190)
|
||||||
software_brand = serializers.CharField(max_length=190)
|
software_brand = serializers.CharField(max_length=190)
|
||||||
software_version = serializers.CharField(max_length=190)
|
software_version = serializers.CharField(max_length=190)
|
||||||
|
info = serializers.JSONField(required=False, allow_null=True)
|
||||||
|
|
||||||
|
|
||||||
class GateSerializer(serializers.ModelSerializer):
|
class GateSerializer(serializers.ModelSerializer):
|
||||||
@@ -94,6 +96,7 @@ class InitializeView(APIView):
|
|||||||
device.hardware_model = serializer.validated_data.get('hardware_model')
|
device.hardware_model = serializer.validated_data.get('hardware_model')
|
||||||
device.software_brand = serializer.validated_data.get('software_brand')
|
device.software_brand = serializer.validated_data.get('software_brand')
|
||||||
device.software_version = serializer.validated_data.get('software_version')
|
device.software_version = serializer.validated_data.get('software_version')
|
||||||
|
device.info = serializer.validated_data.get('info')
|
||||||
device.api_token = generate_api_token()
|
device.api_token = generate_api_token()
|
||||||
device.save()
|
device.save()
|
||||||
|
|
||||||
@@ -114,6 +117,7 @@ class UpdateView(APIView):
|
|||||||
device.hardware_model = serializer.validated_data.get('hardware_model')
|
device.hardware_model = serializer.validated_data.get('hardware_model')
|
||||||
device.software_brand = serializer.validated_data.get('software_brand')
|
device.software_brand = serializer.validated_data.get('software_brand')
|
||||||
device.software_version = serializer.validated_data.get('software_version')
|
device.software_version = serializer.validated_data.get('software_version')
|
||||||
|
device.info = serializer.validated_data.get('info')
|
||||||
device.save()
|
device.save()
|
||||||
device.log_action('pretix.device.updated', data=serializer.validated_data, auth=device)
|
device.log_action('pretix.device.updated', data=serializer.validated_data, auth=device)
|
||||||
|
|
||||||
|
|||||||
@@ -147,7 +147,11 @@ class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
|
|||||||
@cached_property
|
@cached_property
|
||||||
def exporters(self):
|
def exporters(self):
|
||||||
exporters = []
|
exporters = []
|
||||||
events = (self.request.auth or self.request.user).get_events_with_permission('can_view_orders', request=self.request).filter(
|
if isinstance(self.request.auth, (Device, TeamAPIToken)):
|
||||||
|
perm_holder = self.request.auth
|
||||||
|
else:
|
||||||
|
perm_holder = self.request.user
|
||||||
|
events = perm_holder.get_events_with_permission('can_view_orders', request=self.request).filter(
|
||||||
organizer=self.request.organizer
|
organizer=self.request.organizer
|
||||||
)
|
)
|
||||||
responses = register_multievent_data_exporters.send(self.request.organizer)
|
responses = register_multievent_data_exporters.send(self.request.organizer)
|
||||||
@@ -157,8 +161,12 @@ class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
|
|||||||
return exporters
|
return exporters
|
||||||
|
|
||||||
def get_serializer_kwargs(self):
|
def get_serializer_kwargs(self):
|
||||||
|
if isinstance(self.request.auth, (Device, TeamAPIToken)):
|
||||||
|
perm_holder = self.request.auth
|
||||||
|
else:
|
||||||
|
perm_holder = self.request.user
|
||||||
return {
|
return {
|
||||||
'events': self.request.auth.get_events_with_permission('can_view_orders', request=self.request).filter(
|
'events': perm_holder.get_events_with_permission('can_view_orders', request=self.request).filter(
|
||||||
organizer=self.request.organizer
|
organizer=self.request.organizer
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+201
-17
@@ -36,7 +36,7 @@ from django.utils.translation import gettext as _
|
|||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from rest_framework import mixins, serializers, status, viewsets
|
from rest_framework import serializers, status, viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import (
|
from rest_framework.exceptions import (
|
||||||
APIException, NotFound, PermissionDenied, ValidationError,
|
APIException, NotFound, PermissionDenied, ValidationError,
|
||||||
@@ -53,6 +53,12 @@ from pretix.api.serializers.order import (
|
|||||||
PriceCalcSerializer, RevokedTicketSecretSerializer,
|
PriceCalcSerializer, RevokedTicketSecretSerializer,
|
||||||
SimulatedOrderSerializer,
|
SimulatedOrderSerializer,
|
||||||
)
|
)
|
||||||
|
from pretix.api.serializers.orderchange import (
|
||||||
|
OrderChangeOperationSerializer, OrderFeeChangeSerializer,
|
||||||
|
OrderPositionChangeSerializer,
|
||||||
|
OrderPositionCreateForExistingOrderSerializer,
|
||||||
|
OrderPositionInfoPatchSerializer,
|
||||||
|
)
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CachedCombinedTicket, CachedTicket, Checkin, Device, Event, Invoice,
|
CachedCombinedTicket, CachedTicket, Checkin, Device, Event, Invoice,
|
||||||
@@ -144,7 +150,8 @@ with scopes_disabled():
|
|||||||
matching_positions = OrderPosition.objects.filter(
|
matching_positions = OrderPosition.objects.filter(
|
||||||
Q(order=OuterRef('pk')) & Q(
|
Q(order=OuterRef('pk')) & Q(
|
||||||
Q(attendee_name_cached__icontains=u) | Q(attendee_email__icontains=u)
|
Q(attendee_name_cached__icontains=u) | Q(attendee_email__icontains=u)
|
||||||
| Q(secret__istartswith=u) | Q(voucher__code__icontains=u)
|
| Q(secret__istartswith=u)
|
||||||
|
# | Q(voucher__code__icontains=u) # temporarily removed since it caused bad query performance on postgres
|
||||||
)
|
)
|
||||||
).values('id')
|
).values('id')
|
||||||
|
|
||||||
@@ -338,6 +345,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
|||||||
@action(detail=True, methods=['POST'])
|
@action(detail=True, methods=['POST'])
|
||||||
def mark_canceled(self, request, **kwargs):
|
def mark_canceled(self, request, **kwargs):
|
||||||
send_mail = request.data.get('send_email', True)
|
send_mail = request.data.get('send_email', True)
|
||||||
|
comment = request.data.get('comment', None)
|
||||||
cancellation_fee = request.data.get('cancellation_fee', None)
|
cancellation_fee = request.data.get('cancellation_fee', None)
|
||||||
if cancellation_fee:
|
if cancellation_fee:
|
||||||
try:
|
try:
|
||||||
@@ -360,6 +368,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
|||||||
device=request.auth if isinstance(request.auth, Device) else None,
|
device=request.auth if isinstance(request.auth, Device) else None,
|
||||||
oauth_application=request.auth.application if isinstance(request.auth, OAuthAccessToken) else None,
|
oauth_application=request.auth.application if isinstance(request.auth, OAuthAccessToken) else None,
|
||||||
send_mail=send_mail,
|
send_mail=send_mail,
|
||||||
|
email_comment=comment,
|
||||||
cancellation_fee=cancellation_fee
|
cancellation_fee=cancellation_fee
|
||||||
)
|
)
|
||||||
except OrderError as e:
|
except OrderError as e:
|
||||||
@@ -644,7 +653,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
|||||||
if send_mail:
|
if send_mail:
|
||||||
free_flow = (
|
free_flow = (
|
||||||
payment and order.total == Decimal('0.00') and order.status == Order.STATUS_PAID and
|
payment and order.total == Decimal('0.00') and order.status == Order.STATUS_PAID and
|
||||||
not order.require_approval and payment.provider == "free"
|
not order.require_approval and payment.provider in ("free", "boxoffice")
|
||||||
)
|
)
|
||||||
if order.require_approval:
|
if order.require_approval:
|
||||||
email_template = request.event.settings.mail_text_order_placed_require_approval
|
email_template = request.event.settings.mail_text_order_placed_require_approval
|
||||||
@@ -782,6 +791,79 @@ class OrderViewSet(viewsets.ModelViewSet):
|
|||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
self.get_object().gracefully_delete(user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
|
self.get_object().gracefully_delete(user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
|
||||||
|
|
||||||
|
@action(detail=True, methods=['POST'])
|
||||||
|
def change(self, request, **kwargs):
|
||||||
|
order = self.get_object()
|
||||||
|
|
||||||
|
serializer = OrderChangeOperationSerializer(
|
||||||
|
context={'order': order, **self.get_serializer_context()},
|
||||||
|
data=request.data,
|
||||||
|
)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ocm = OrderChangeManager(
|
||||||
|
order=order,
|
||||||
|
user=self.request.user if self.request.user.is_authenticated else None,
|
||||||
|
auth=request.auth,
|
||||||
|
notify=serializer.validated_data.get('send_email', False),
|
||||||
|
reissue_invoice=serializer.validated_data.get('reissue_invoice', True),
|
||||||
|
)
|
||||||
|
|
||||||
|
canceled_positions = set()
|
||||||
|
for r in serializer.validated_data.get('cancel_positions', []):
|
||||||
|
ocm.cancel(r['position'])
|
||||||
|
canceled_positions.add(r['position'])
|
||||||
|
|
||||||
|
for r in serializer.validated_data.get('patch_positions', []):
|
||||||
|
if r['position'] in canceled_positions:
|
||||||
|
continue
|
||||||
|
pos_serializer = OrderPositionChangeSerializer(
|
||||||
|
context={'ocm': ocm, 'commit': False, 'event': request.event, **self.get_serializer_context()},
|
||||||
|
partial=True,
|
||||||
|
)
|
||||||
|
pos_serializer.update(r['position'], r['body'])
|
||||||
|
|
||||||
|
for r in serializer.validated_data.get('split_positions', []):
|
||||||
|
if r['position'] in canceled_positions:
|
||||||
|
continue
|
||||||
|
ocm.split(r['position'])
|
||||||
|
|
||||||
|
for r in serializer.validated_data.get('create_positions', []):
|
||||||
|
pos_serializer = OrderPositionCreateForExistingOrderSerializer(
|
||||||
|
context={'ocm': ocm, 'commit': False, 'event': request.event, **self.get_serializer_context()},
|
||||||
|
)
|
||||||
|
pos_serializer.create(r)
|
||||||
|
|
||||||
|
canceled_fees = set()
|
||||||
|
for r in serializer.validated_data.get('cancel_fees', []):
|
||||||
|
ocm.cancel_fee(r['fee'])
|
||||||
|
canceled_fees.add(r['fee'])
|
||||||
|
|
||||||
|
for r in serializer.validated_data.get('patch_fees', []):
|
||||||
|
if r['fee'] in canceled_fees:
|
||||||
|
continue
|
||||||
|
pos_serializer = OrderFeeChangeSerializer(
|
||||||
|
context={'ocm': ocm, 'commit': False, 'event': request.event, **self.get_serializer_context()},
|
||||||
|
)
|
||||||
|
pos_serializer.update(r['fee'], r['body'])
|
||||||
|
|
||||||
|
if serializer.validated_data.get('recalculate_taxes') == 'keep_net':
|
||||||
|
ocm.recalculate_taxes(keep='net')
|
||||||
|
elif serializer.validated_data.get('recalculate_taxes') == 'keep_gross':
|
||||||
|
ocm.recalculate_taxes(keep='gross')
|
||||||
|
|
||||||
|
ocm.commit()
|
||||||
|
except OrderError as e:
|
||||||
|
raise ValidationError(str(e))
|
||||||
|
|
||||||
|
order.refresh_from_db()
|
||||||
|
serializer = OrderSerializer(
|
||||||
|
instance=order,
|
||||||
|
context=self.get_serializer_context(),
|
||||||
|
)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
class OrderPositionFilter(FilterSet):
|
class OrderPositionFilter(FilterSet):
|
||||||
@@ -823,7 +905,7 @@ with scopes_disabled():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class OrderPositionViewSet(mixins.DestroyModelMixin, mixins.UpdateModelMixin, viewsets.ReadOnlyModelViewSet):
|
class OrderPositionViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = OrderPositionSerializer
|
serializer_class = OrderPositionSerializer
|
||||||
queryset = OrderPosition.all.none()
|
queryset = OrderPosition.all.none()
|
||||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||||
@@ -1060,6 +1142,25 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, mixins.UpdateModelMixin, vi
|
|||||||
)
|
)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
@action(detail=True, methods=['POST'])
|
||||||
|
def regenerate_secrets(self, request, **kwargs):
|
||||||
|
instance = self.get_object()
|
||||||
|
try:
|
||||||
|
ocm = OrderChangeManager(
|
||||||
|
instance.order,
|
||||||
|
user=self.request.user if self.request.user.is_authenticated else None,
|
||||||
|
auth=self.request.auth,
|
||||||
|
notify=False,
|
||||||
|
reissue_invoice=False,
|
||||||
|
)
|
||||||
|
ocm.regenerate_secret(instance)
|
||||||
|
ocm.commit()
|
||||||
|
except OrderError as e:
|
||||||
|
raise ValidationError(str(e))
|
||||||
|
except Quota.QuotaExceededException as e:
|
||||||
|
raise ValidationError(str(e))
|
||||||
|
return self.retrieve(request, [], **kwargs)
|
||||||
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
try:
|
try:
|
||||||
ocm = OrderChangeManager(
|
ocm = OrderChangeManager(
|
||||||
@@ -1075,18 +1176,33 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, mixins.UpdateModelMixin, vi
|
|||||||
except Quota.QuotaExceededException as e:
|
except Quota.QuotaExceededException as e:
|
||||||
raise ValidationError(str(e))
|
raise ValidationError(str(e))
|
||||||
|
|
||||||
def update(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
partial = kwargs.get('partial', False)
|
|
||||||
if not partial:
|
|
||||||
return Response(
|
|
||||||
{"detail": "Method \"PUT\" not allowed."},
|
|
||||||
status=status.HTTP_405_METHOD_NOT_ALLOWED,
|
|
||||||
)
|
|
||||||
return super().update(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
old_data = self.get_serializer_class()(instance=serializer.instance, context=self.get_serializer_context()).data
|
serializer = OrderPositionCreateForExistingOrderSerializer(
|
||||||
|
data=request.data,
|
||||||
|
context=self.get_serializer_context(),
|
||||||
|
)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
order = serializer.validated_data['order']
|
||||||
|
ocm = OrderChangeManager(
|
||||||
|
order=order,
|
||||||
|
user=self.request.user if self.request.user.is_authenticated else None,
|
||||||
|
auth=request.auth,
|
||||||
|
notify=False,
|
||||||
|
reissue_invoice=False,
|
||||||
|
)
|
||||||
|
serializer.context['ocm'] = ocm
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
|
# Fields that can be easily patched after the position was added
|
||||||
|
old_data = OrderPositionInfoPatchSerializer(instance=serializer.instance, context=self.get_serializer_context()).data
|
||||||
|
serializer = OrderPositionInfoPatchSerializer(
|
||||||
|
instance=serializer.instance,
|
||||||
|
context=self.get_serializer_context(),
|
||||||
|
partial=True,
|
||||||
|
data=request.data
|
||||||
|
)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
new_data = serializer.data
|
new_data = serializer.data
|
||||||
|
|
||||||
@@ -1109,9 +1225,77 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, mixins.UpdateModelMixin, vi
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
tickets.invalidate_cache.apply_async(
|
||||||
|
kwargs={'event': serializer.instance.order.event.pk, 'order': serializer.instance.order.pk})
|
||||||
|
order_modified.send(sender=serializer.instance.order.event, order=serializer.instance.order)
|
||||||
|
return Response(
|
||||||
|
OrderPositionSerializer(serializer.instance, context=self.get_serializer_context()).data,
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
|
||||||
tickets.invalidate_cache.apply_async(kwargs={'event': serializer.instance.order.event.pk, 'order': serializer.instance.order.pk})
|
def update(self, request, *args, **kwargs):
|
||||||
order_modified.send(sender=serializer.instance.order.event, order=serializer.instance.order)
|
partial = kwargs.get('partial', False)
|
||||||
|
if not partial:
|
||||||
|
return Response(
|
||||||
|
{"detail": "Method \"PUT\" not allowed."},
|
||||||
|
status=status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||||
|
)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
instance = self.get_object()
|
||||||
|
ocm = OrderChangeManager(
|
||||||
|
order=instance.order,
|
||||||
|
user=self.request.user if self.request.user.is_authenticated else None,
|
||||||
|
auth=request.auth,
|
||||||
|
notify=False,
|
||||||
|
reissue_invoice=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Field that need to go through OrderChangeManager
|
||||||
|
serializer = OrderPositionChangeSerializer(
|
||||||
|
instance=instance,
|
||||||
|
context={'ocm': ocm, **self.get_serializer_context()},
|
||||||
|
partial=True,
|
||||||
|
data=request.data
|
||||||
|
)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
|
# Fields that can be easily patched
|
||||||
|
old_data = OrderPositionInfoPatchSerializer(instance=instance, context=self.get_serializer_context()).data
|
||||||
|
serializer = OrderPositionInfoPatchSerializer(
|
||||||
|
instance=instance,
|
||||||
|
context=self.get_serializer_context(),
|
||||||
|
partial=True,
|
||||||
|
data=request.data
|
||||||
|
)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
new_data = serializer.data
|
||||||
|
|
||||||
|
if old_data != new_data:
|
||||||
|
log_data = self.request.data
|
||||||
|
if 'answers' in log_data:
|
||||||
|
for a in new_data['answers']:
|
||||||
|
log_data[f'question_{a["question"]}'] = a["answer"]
|
||||||
|
log_data.pop('answers', None)
|
||||||
|
serializer.instance.order.log_action(
|
||||||
|
'pretix.event.order.modified',
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data={
|
||||||
|
'data': [
|
||||||
|
dict(
|
||||||
|
position=serializer.instance.pk,
|
||||||
|
**log_data
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
tickets.invalidate_cache.apply_async(kwargs={'event': serializer.instance.order.event.pk, 'order': serializer.instance.order.pk})
|
||||||
|
order_modified.send(sender=serializer.instance.order.event, order=serializer.instance.order)
|
||||||
|
|
||||||
|
return Response(self.get_serializer_class()(instance=serializer.instance, context=self.get_serializer_context()).data)
|
||||||
|
|
||||||
|
|
||||||
class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
|||||||
op.variation,
|
op.variation,
|
||||||
op.subevent,
|
op.subevent,
|
||||||
op.attendee_name,
|
op.attendee_name,
|
||||||
(op.pk if op.addon_to_id else None),
|
op.addon_to_id,
|
||||||
(op.pk if op.has_addons else None)
|
(op.pk if op.has_addons else None)
|
||||||
)
|
)
|
||||||
)]
|
)]
|
||||||
@@ -310,7 +310,11 @@ def get_email_context(**kwargs):
|
|||||||
val = [val]
|
val = [val]
|
||||||
for v in val:
|
for v in val:
|
||||||
if all(rp in kwargs for rp in v.required_context):
|
if all(rp in kwargs for rp in v.required_context):
|
||||||
ctx[v.identifier] = v.render(kwargs)
|
try:
|
||||||
|
ctx[v.identifier] = v.render(kwargs)
|
||||||
|
except:
|
||||||
|
ctx[v.identifier] = '(error)'
|
||||||
|
logger.exception(f'Failed to process email placeholder {v.identifier}.')
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,6 @@
|
|||||||
# License for the specific language governing permissions and limitations under the License.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import re
|
|
||||||
import tempfile
|
import tempfile
|
||||||
from collections import OrderedDict, namedtuple
|
from collections import OrderedDict, namedtuple
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
@@ -46,26 +45,13 @@ from django.conf import settings
|
|||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
from django.utils.formats import localize
|
from django.utils.formats import localize
|
||||||
from django.utils.translation import gettext, gettext_lazy as _
|
from django.utils.translation import gettext, gettext_lazy as _
|
||||||
from openpyxl import Workbook
|
|
||||||
from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE, KNOWN_TYPES, Cell
|
|
||||||
|
|
||||||
from pretix.base.models import Event
|
from pretix.base.models import Event
|
||||||
|
from pretix.helpers.safe_openpyxl import ( # NOQA: backwards compatibility for plugins using excel_safe
|
||||||
|
SafeWorkbook, remove_invalid_excel_chars as excel_safe,
|
||||||
|
)
|
||||||
|
|
||||||
|
__ = excel_safe # just so the compatbility import above is "used" and doesn't get removed by linter
|
||||||
def excel_safe(val):
|
|
||||||
if isinstance(val, Cell):
|
|
||||||
return val
|
|
||||||
|
|
||||||
if not isinstance(val, KNOWN_TYPES):
|
|
||||||
val = str(val)
|
|
||||||
|
|
||||||
if isinstance(val, bytes):
|
|
||||||
val = val.decode("utf-8", errors="ignore")
|
|
||||||
|
|
||||||
if isinstance(val, str):
|
|
||||||
val = re.sub(ILLEGAL_CHARACTERS_RE, '', val)
|
|
||||||
|
|
||||||
return val
|
|
||||||
|
|
||||||
|
|
||||||
class BaseExporter:
|
class BaseExporter:
|
||||||
@@ -228,7 +214,7 @@ class ListExporter(BaseExporter):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def _render_xlsx(self, form_data, output_file=None):
|
def _render_xlsx(self, form_data, output_file=None):
|
||||||
wb = Workbook(write_only=True)
|
wb = SafeWorkbook(write_only=True)
|
||||||
ws = wb.create_sheet()
|
ws = wb.create_sheet()
|
||||||
self.prepare_xlsx_sheet(ws)
|
self.prepare_xlsx_sheet(ws)
|
||||||
try:
|
try:
|
||||||
@@ -242,7 +228,7 @@ class ListExporter(BaseExporter):
|
|||||||
total = line.total
|
total = line.total
|
||||||
continue
|
continue
|
||||||
ws.append([
|
ws.append([
|
||||||
excel_safe(val) for val in line
|
val for val in line
|
||||||
])
|
])
|
||||||
if total:
|
if total:
|
||||||
counter += 1
|
counter += 1
|
||||||
@@ -347,7 +333,7 @@ class MultiSheetListExporter(ListExporter):
|
|||||||
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
|
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
|
||||||
|
|
||||||
def _render_xlsx(self, form_data, output_file=None):
|
def _render_xlsx(self, form_data, output_file=None):
|
||||||
wb = Workbook(write_only=True)
|
wb = SafeWorkbook(write_only=True)
|
||||||
n_sheets = len(self.sheets)
|
n_sheets = len(self.sheets)
|
||||||
for i_sheet, (s, l) in enumerate(self.sheets):
|
for i_sheet, (s, l) in enumerate(self.sheets):
|
||||||
ws = wb.create_sheet(str(l))
|
ws = wb.create_sheet(str(l))
|
||||||
@@ -361,8 +347,7 @@ class MultiSheetListExporter(ListExporter):
|
|||||||
total = line.total
|
total = line.total
|
||||||
continue
|
continue
|
||||||
ws.append([
|
ws.append([
|
||||||
excel_safe(val)
|
val for val in line
|
||||||
for val in line
|
|
||||||
])
|
])
|
||||||
if total:
|
if total:
|
||||||
counter += 1
|
counter += 1
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
#
|
#
|
||||||
from .answers import * # noqa
|
from .answers import * # noqa
|
||||||
from .dekodi import * # noqa
|
from .dekodi import * # noqa
|
||||||
|
from .events import * # noqa
|
||||||
from .invoices import * # noqa
|
from .invoices import * # noqa
|
||||||
from .json import * # noqa
|
from .json import * # noqa
|
||||||
from .mail import * # noqa
|
from .mail import * # noqa
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
||||||
|
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||||
|
#
|
||||||
|
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
|
||||||
|
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
|
||||||
|
#
|
||||||
|
# This file contains Apache-licensed contributions copyrighted by: Tobias Kunze
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.utils.formats import date_format
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from ...control.forms.filter import get_all_payment_providers
|
||||||
|
from ..exporter import ListExporter
|
||||||
|
from ..signals import register_multievent_data_exporters
|
||||||
|
|
||||||
|
|
||||||
|
class EventDataExporter(ListExporter):
|
||||||
|
identifier = 'eventdata'
|
||||||
|
verbose_name = _('Event data')
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def providers(self):
|
||||||
|
return dict(get_all_payment_providers())
|
||||||
|
|
||||||
|
def iterate_list(self, form_data):
|
||||||
|
header = [
|
||||||
|
_("Event name"),
|
||||||
|
_("Short form"),
|
||||||
|
_("Shop is live"),
|
||||||
|
_("Event currency"),
|
||||||
|
_("Event start time"),
|
||||||
|
_("Event end time"),
|
||||||
|
_("Admission time"),
|
||||||
|
_("Start of presale"),
|
||||||
|
_("End of presale"),
|
||||||
|
_("Location"),
|
||||||
|
_("Latitude"),
|
||||||
|
_("Longitude"),
|
||||||
|
_("Internal comment"),
|
||||||
|
]
|
||||||
|
props = list(self.organizer.meta_properties.all())
|
||||||
|
for p in props:
|
||||||
|
header.append(p.name)
|
||||||
|
yield header
|
||||||
|
|
||||||
|
for e in self.events.all():
|
||||||
|
m = e.meta_data
|
||||||
|
yield [
|
||||||
|
str(e.name),
|
||||||
|
e.slug,
|
||||||
|
_('Yes') if e.live else _('No'),
|
||||||
|
e.currency,
|
||||||
|
date_format(e.date_from, 'SHORT_DATETIME_FORMAT'),
|
||||||
|
date_format(e.date_to, 'SHORT_DATETIME_FORMAT') if e.date_to else '',
|
||||||
|
date_format(e.date_admission, 'SHORT_DATETIME_FORMAT') if e.date_admission else '',
|
||||||
|
date_format(e.presale_start, 'SHORT_DATETIME_FORMAT') if e.presale_start else '',
|
||||||
|
date_format(e.presale_end, 'SHORT_DATETIME_FORMAT') if e.presale_end else '',
|
||||||
|
str(e.location),
|
||||||
|
e.geo_lat or '',
|
||||||
|
e.geo_lon or '',
|
||||||
|
e.comment,
|
||||||
|
] + [
|
||||||
|
m.get(p.name, '') for p in props
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_filename(self):
|
||||||
|
return '{}_events'.format(self.events.first().organizer.slug)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_eventdata")
|
||||||
|
def register_multievent_eventdata_exporter(sender, **kwargs):
|
||||||
|
return EventDataExporter
|
||||||
@@ -113,10 +113,13 @@ class SettingsForm(i18nfield.forms.I18nFormMixin, HierarkeyForm):
|
|||||||
if isinstance(f, (RelativeDateTimeField, RelativeDateField)):
|
if isinstance(f, (RelativeDateTimeField, RelativeDateField)):
|
||||||
f.set_event(self.obj)
|
f.set_event(self.obj)
|
||||||
|
|
||||||
def save(self):
|
def _unmask_secret_fields(self):
|
||||||
for k, v in self.cleaned_data.items():
|
for k, v in self.cleaned_data.items():
|
||||||
if isinstance(self.fields.get(k), SecretKeySettingsField) and self.cleaned_data.get(k) == SECRET_REDACTED:
|
if isinstance(self.fields.get(k), SecretKeySettingsField) and self.cleaned_data.get(k) == SECRET_REDACTED:
|
||||||
self.cleaned_data[k] = self.initial[k]
|
self.cleaned_data[k] = self.initial[k]
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
self._unmask_secret_fields()
|
||||||
return super().save()
|
return super().save()
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ from io import BytesIO
|
|||||||
import dateutil.parser
|
import dateutil.parser
|
||||||
import pycountry
|
import pycountry
|
||||||
import pytz
|
import pytz
|
||||||
from babel import Locale
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
@@ -52,7 +51,6 @@ from django.core.validators import (
|
|||||||
)
|
)
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
from django.forms import Select, widgets
|
from django.forms import Select, widgets
|
||||||
from django.utils import translation
|
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
@@ -87,7 +85,9 @@ from pretix.base.templatetags.rich_text import rich_text
|
|||||||
from pretix.control.forms import (
|
from pretix.control.forms import (
|
||||||
ExtFileField, ExtValidationMixin, SizeValidationMixin, SplitDateTimeField,
|
ExtFileField, ExtValidationMixin, SizeValidationMixin, SplitDateTimeField,
|
||||||
)
|
)
|
||||||
from pretix.helpers.countries import CachedCountries
|
from pretix.helpers.countries import (
|
||||||
|
CachedCountries, get_phone_prefixes_sorted_and_localized,
|
||||||
|
)
|
||||||
from pretix.helpers.escapejson import escapejson_attr
|
from pretix.helpers.escapejson import escapejson_attr
|
||||||
from pretix.helpers.i18n import get_format_without_seconds
|
from pretix.helpers.i18n import get_format_without_seconds
|
||||||
from pretix.presale.signals import question_form_fields
|
from pretix.presale.signals import question_form_fields
|
||||||
@@ -264,17 +264,14 @@ class WrappedPhonePrefixSelect(Select):
|
|||||||
|
|
||||||
def __init__(self, initial=None):
|
def __init__(self, initial=None):
|
||||||
choices = [("", "---------")]
|
choices = [("", "---------")]
|
||||||
language = get_babel_locale() # changed from default implementation that used the django locale
|
|
||||||
locale = Locale(translation.to_locale(language))
|
if initial:
|
||||||
for prefix, values in _COUNTRY_CODE_TO_REGION_CODE.items():
|
for prefix, values in _COUNTRY_CODE_TO_REGION_CODE.items():
|
||||||
prefix = "+%d" % prefix
|
if initial in values:
|
||||||
if initial and initial in values:
|
self.initial = "+%d" % prefix
|
||||||
self.initial = prefix
|
break
|
||||||
for country_code in values:
|
choices += get_phone_prefixes_sorted_and_localized()
|
||||||
country_name = locale.territories.get(country_code)
|
super().__init__(choices=choices, attrs={'aria-label': pgettext_lazy('phonenumber', 'International area code')})
|
||||||
if country_name:
|
|
||||||
choices.append((prefix, "{} {}".format(country_name, prefix)))
|
|
||||||
super().__init__(choices=sorted(choices, key=lambda item: item[1]), attrs={'aria-label': pgettext_lazy('phonenumber', 'International area code')})
|
|
||||||
|
|
||||||
def render(self, name, value, *args, **kwargs):
|
def render(self, name, value, *args, **kwargs):
|
||||||
return super().render(name, value or self.initial, *args, **kwargs)
|
return super().render(name, value or self.initial, *args, **kwargs)
|
||||||
@@ -318,7 +315,12 @@ class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
|
|||||||
silently deleting data.
|
silently deleting data.
|
||||||
"""
|
"""
|
||||||
if value:
|
if value:
|
||||||
if type(value) == PhoneNumber:
|
if isinstance(value, str):
|
||||||
|
try:
|
||||||
|
value = PhoneNumber.from_string(value)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if isinstance(value, PhoneNumber):
|
||||||
if value.country_code and value.national_number:
|
if value.country_code and value.national_number:
|
||||||
return [
|
return [
|
||||||
"+%d" % value.country_code,
|
"+%d" % value.country_code,
|
||||||
@@ -705,7 +707,7 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
label=label, required=required,
|
label=label, required=required,
|
||||||
min_value=q.valid_number_min or Decimal('0.00'),
|
min_value=q.valid_number_min or Decimal('0.00'),
|
||||||
max_value=q.valid_number_max,
|
max_value=q.valid_number_max,
|
||||||
help_text=q.help_text,
|
help_text=help_text,
|
||||||
initial=initial.answer if initial else None,
|
initial=initial.answer if initial else None,
|
||||||
)
|
)
|
||||||
elif q.type == Question.TYPE_STRING:
|
elif q.type == Question.TYPE_STRING:
|
||||||
|
|||||||
@@ -42,6 +42,24 @@ from django.utils.timezone import get_current_timezone, now
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
def replace_arabic_numbers(inp):
|
||||||
|
if not isinstance(inp, str):
|
||||||
|
return inp
|
||||||
|
table = {
|
||||||
|
1632: 48, # 0
|
||||||
|
1633: 49, # 1
|
||||||
|
1634: 50, # 2
|
||||||
|
1635: 51, # 3
|
||||||
|
1636: 52, # 4
|
||||||
|
1637: 53, # 5
|
||||||
|
1638: 54, # 6
|
||||||
|
1639: 55, # 7
|
||||||
|
1640: 56, # 8
|
||||||
|
1641: 57, # 9
|
||||||
|
}
|
||||||
|
return inp.translate(table)
|
||||||
|
|
||||||
|
|
||||||
class DatePickerWidget(forms.DateInput):
|
class DatePickerWidget(forms.DateInput):
|
||||||
def __init__(self, attrs=None, date_format=None):
|
def __init__(self, attrs=None, date_format=None):
|
||||||
attrs = attrs or {}
|
attrs = attrs or {}
|
||||||
@@ -62,6 +80,10 @@ class DatePickerWidget(forms.DateInput):
|
|||||||
|
|
||||||
forms.DateInput.__init__(self, date_attrs, date_format)
|
forms.DateInput.__init__(self, date_attrs, date_format)
|
||||||
|
|
||||||
|
def value_from_datadict(self, data, files, name):
|
||||||
|
v = super().value_from_datadict(data, files, name)
|
||||||
|
return replace_arabic_numbers(v)
|
||||||
|
|
||||||
|
|
||||||
class TimePickerWidget(forms.TimeInput):
|
class TimePickerWidget(forms.TimeInput):
|
||||||
def __init__(self, attrs=None, time_format=None):
|
def __init__(self, attrs=None, time_format=None):
|
||||||
@@ -83,6 +105,10 @@ class TimePickerWidget(forms.TimeInput):
|
|||||||
|
|
||||||
forms.TimeInput.__init__(self, time_attrs, time_format)
|
forms.TimeInput.__init__(self, time_attrs, time_format)
|
||||||
|
|
||||||
|
def value_from_datadict(self, data, files, name):
|
||||||
|
v = super().value_from_datadict(data, files, name)
|
||||||
|
return replace_arabic_numbers(v)
|
||||||
|
|
||||||
|
|
||||||
class UploadedFileWidget(forms.ClearableFileInput):
|
class UploadedFileWidget(forms.ClearableFileInput):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -179,6 +205,10 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
|
|||||||
# Skip one hierarchy level
|
# Skip one hierarchy level
|
||||||
forms.MultiWidget.__init__(self, widgets, attrs)
|
forms.MultiWidget.__init__(self, widgets, attrs)
|
||||||
|
|
||||||
|
def value_from_datadict(self, data, files, name):
|
||||||
|
v = super().value_from_datadict(data, files, name)
|
||||||
|
return [replace_arabic_numbers(i) for i in v]
|
||||||
|
|
||||||
|
|
||||||
class BusinessBooleanRadio(forms.RadioSelect):
|
class BusinessBooleanRadio(forms.RadioSelect):
|
||||||
def __init__(self, require_business=False, attrs=None):
|
def __init__(self, require_business=False, attrs=None):
|
||||||
|
|||||||
@@ -19,11 +19,13 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.db import connection
|
||||||
from django_scopes import scope, scopes_disabled
|
from django_scopes import scope, scopes_disabled
|
||||||
|
|
||||||
|
|
||||||
@@ -33,6 +35,13 @@ class Command(BaseCommand):
|
|||||||
parser.parse_args = lambda x: parser.parse_known_args(x)[0]
|
parser.parse_args = lambda x: parser.parse_known_args(x)[0]
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
'--print-sql',
|
||||||
|
action='store_true',
|
||||||
|
help='Print all SQL queries.',
|
||||||
|
)
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
try:
|
try:
|
||||||
from django_extensions.management.commands import shell_plus # noqa
|
from django_extensions.management.commands import shell_plus # noqa
|
||||||
@@ -41,6 +50,11 @@ class Command(BaseCommand):
|
|||||||
cmd = 'shell'
|
cmd = 'shell'
|
||||||
del options['skip_checks']
|
del options['skip_checks']
|
||||||
|
|
||||||
|
if options['print_sql']:
|
||||||
|
connection.force_debug_cursor = True
|
||||||
|
logger = logging.getLogger("django.db.backends")
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
parser = self.create_parser(sys.argv[0], sys.argv[1])
|
parser = self.create_parser(sys.argv[0], sys.argv[1])
|
||||||
flags = parser.parse_known_args(sys.argv[2:])[1]
|
flags = parser.parse_known_args(sys.argv[2:])[1]
|
||||||
if "--override" in flags:
|
if "--override" in flags:
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.12 on 2022-03-22 11:57
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0208_auto_20220214_1632'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='device',
|
||||||
|
name='info',
|
||||||
|
field=models.JSONField(null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -498,6 +498,23 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
| Q(id__in=self.teams.filter(**kwargs).values_list('limit_events__id', flat=True))
|
| Q(id__in=self.teams.filter(**kwargs).values_list('limit_events__id', flat=True))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@scopes_disabled()
|
||||||
|
def get_organizers_with_any_permission(self, request=None):
|
||||||
|
"""
|
||||||
|
Returns a queryset of organizers the user has any permissions to.
|
||||||
|
|
||||||
|
:param request: The current request (optional). Required to detect staff sessions properly.
|
||||||
|
:return: Iterable of Organizers
|
||||||
|
"""
|
||||||
|
from .event import Organizer
|
||||||
|
|
||||||
|
if request and self.has_active_staff_session(request.session.session_key):
|
||||||
|
return Organizer.objects.all()
|
||||||
|
|
||||||
|
return Organizer.objects.filter(
|
||||||
|
id__in=self.teams.values_list('organizer', flat=True)
|
||||||
|
)
|
||||||
|
|
||||||
@scopes_disabled()
|
@scopes_disabled()
|
||||||
def get_organizers_with_permission(self, permission, request=None):
|
def get_organizers_with_permission(self, permission, request=None):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -156,6 +156,9 @@ class Device(LoggedModel):
|
|||||||
null=True,
|
null=True,
|
||||||
blank=False
|
blank=False
|
||||||
)
|
)
|
||||||
|
info = models.JSONField(
|
||||||
|
null=True, blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
objects = ScopedManager(organizer='organizer')
|
objects = ScopedManager(organizer='organizer')
|
||||||
|
|
||||||
|
|||||||
@@ -638,12 +638,13 @@ class Order(LockModel, LoggedModel):
|
|||||||
return False
|
return False
|
||||||
if self.user_cancel_deadline and now() > self.user_cancel_deadline:
|
if self.user_cancel_deadline and now() > self.user_cancel_deadline:
|
||||||
return False
|
return False
|
||||||
if self.status == Order.STATUS_PENDING:
|
|
||||||
return self.event.settings.cancel_allow_user
|
if self.status == Order.STATUS_PAID or self.payment_refund_sum > Decimal('0.00'):
|
||||||
elif self.status == Order.STATUS_PAID:
|
|
||||||
if self.total == Decimal('0.00'):
|
if self.total == Decimal('0.00'):
|
||||||
return self.event.settings.cancel_allow_user
|
return self.event.settings.cancel_allow_user
|
||||||
return self.event.settings.cancel_allow_user_paid
|
return self.event.settings.cancel_allow_user_paid
|
||||||
|
elif self.status == Order.STATUS_PENDING:
|
||||||
|
return self.event.settings.cancel_allow_user
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def propose_auto_refunds(self, amount: Decimal, payments: list=None):
|
def propose_auto_refunds(self, amount: Decimal, payments: list=None):
|
||||||
@@ -1330,6 +1331,10 @@ class AbstractPosition(models.Model):
|
|||||||
else:
|
else:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def item_and_variation(self):
|
||||||
|
return self.item, self.variation
|
||||||
|
|
||||||
@meta_info_data.setter
|
@meta_info_data.setter
|
||||||
def meta_info_data(self, d):
|
def meta_info_data(self, d):
|
||||||
self.meta_info = json.dumps(d)
|
self.meta_info = json.dumps(d)
|
||||||
|
|||||||
@@ -955,6 +955,8 @@ class BoxOfficeProvider(BasePaymentProvider):
|
|||||||
return {
|
return {
|
||||||
"pos_id": payment.info_data.get('pos_id', None),
|
"pos_id": payment.info_data.get('pos_id', None),
|
||||||
"receipt_id": payment.info_data.get('receipt_id', None),
|
"receipt_id": payment.info_data.get('receipt_id', None),
|
||||||
|
"payment_type": payment.info_data.get('payment_type', None),
|
||||||
|
"payment_data": payment.info_data.get('payment_data', {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
def payment_control_render(self, request, payment) -> str:
|
def payment_control_render(self, request, payment) -> str:
|
||||||
|
|||||||
+122
-27
@@ -37,6 +37,7 @@ import hashlib
|
|||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
import uuid
|
import uuid
|
||||||
@@ -48,12 +49,14 @@ from arabic_reshaper import ArabicReshaper
|
|||||||
from bidi.algorithm import get_display
|
from bidi.algorithm import get_display
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.staticfiles import finders
|
from django.contrib.staticfiles import finders
|
||||||
|
from django.db.models import Max, Min
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.functional import SimpleLazyObject
|
from django.utils.functional import SimpleLazyObject
|
||||||
from django.utils.html import conditional_escape
|
from django.utils.html import conditional_escape
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from i18nfield.strings import LazyI18nString
|
||||||
from PyPDF2 import PdfFileReader
|
from PyPDF2 import PdfFileReader
|
||||||
from pytz import timezone
|
from pytz import timezone
|
||||||
from reportlab.graphics import renderPDF
|
from reportlab.graphics import renderPDF
|
||||||
@@ -202,6 +205,11 @@ DEFAULT_VARIABLES = OrderedDict((
|
|||||||
"editor_sample": 'foo@bar.com',
|
"editor_sample": 'foo@bar.com',
|
||||||
"evaluate": lambda op, order, ev: op.attendee_email or (op.addon_to.attendee_email if op.addon_to else '')
|
"evaluate": lambda op, order, ev: op.attendee_email or (op.addon_to.attendee_email if op.addon_to else '')
|
||||||
}),
|
}),
|
||||||
|
("pseudonymization_id", {
|
||||||
|
"label": _("Pseudonymization ID (lead scanning)"),
|
||||||
|
"editor_sample": "GG89JUJDTA",
|
||||||
|
"evaluate": lambda orderposition, order, event: orderposition.pseudonymization_id,
|
||||||
|
}),
|
||||||
("event_name", {
|
("event_name", {
|
||||||
"label": _("Event name"),
|
"label": _("Event name"),
|
||||||
"editor_sample": _("Sample event name"),
|
"editor_sample": _("Sample event name"),
|
||||||
@@ -387,30 +395,41 @@ DEFAULT_VARIABLES = OrderedDict((
|
|||||||
("seat", {
|
("seat", {
|
||||||
"label": _("Seat: Full name"),
|
"label": _("Seat: Full name"),
|
||||||
"editor_sample": _("Ground floor, Row 3, Seat 4"),
|
"editor_sample": _("Ground floor, Row 3, Seat 4"),
|
||||||
"evaluate": lambda op, order, ev: str(op.seat if op.seat else
|
"evaluate": lambda op, order, ev: str(get_seat(op) if get_seat(op) else
|
||||||
_('General admission') if ev.seating_plan_id is not None else "")
|
_('General admission') if ev.seating_plan_id is not None else "")
|
||||||
}),
|
}),
|
||||||
("seat_zone", {
|
("seat_zone", {
|
||||||
"label": _("Seat: zone"),
|
"label": _("Seat: zone"),
|
||||||
"editor_sample": _("Ground floor"),
|
"editor_sample": _("Ground floor"),
|
||||||
"evaluate": lambda op, order, ev: str(op.seat.zone_name if op.seat else
|
"evaluate": lambda op, order, ev: str(get_seat(op).zone_name if get_seat(op) else
|
||||||
_('General admission') if ev.seating_plan_id is not None else "")
|
_('General admission') if ev.seating_plan_id is not None else "")
|
||||||
}),
|
}),
|
||||||
("seat_row", {
|
("seat_row", {
|
||||||
"label": _("Seat: row"),
|
"label": _("Seat: row"),
|
||||||
"editor_sample": "3",
|
"editor_sample": "3",
|
||||||
"evaluate": lambda op, order, ev: str(op.seat.row_name if op.seat else "")
|
"evaluate": lambda op, order, ev: str(get_seat(op).row_name if get_seat(op) else "")
|
||||||
}),
|
}),
|
||||||
("seat_number", {
|
("seat_number", {
|
||||||
"label": _("Seat: seat number"),
|
"label": _("Seat: seat number"),
|
||||||
"editor_sample": 4,
|
"editor_sample": 4,
|
||||||
"evaluate": lambda op, order, ev: str(op.seat.seat_number if op.seat else "")
|
"evaluate": lambda op, order, ev: str(get_seat(op).seat_number if get_seat(op) else "")
|
||||||
}),
|
}),
|
||||||
("first_scan", {
|
("first_scan", {
|
||||||
"label": _("Date and time of first scan"),
|
"label": _("Date and time of first scan"),
|
||||||
"editor_sample": _("2017-05-31 19:00"),
|
"editor_sample": _("2017-05-31 19:00"),
|
||||||
"evaluate": lambda op, order, ev: get_first_scan(op)
|
"evaluate": lambda op, order, ev: get_first_scan(op)
|
||||||
}),
|
}),
|
||||||
|
("giftcard_issuance_date", {
|
||||||
|
|
||||||
|
"label": _("Gift card: Issuance date"),
|
||||||
|
"editor_sample": _("2017-05-31"),
|
||||||
|
"evaluate": lambda op, order, ev: get_giftcard_issuance(op, ev)
|
||||||
|
}),
|
||||||
|
("giftcard_expiry_date", {
|
||||||
|
"label": _("Gift card: Expiration date"),
|
||||||
|
"editor_sample": _("2017-05-31"),
|
||||||
|
"evaluate": lambda op, order, ev: get_giftcard_expiry(op, ev)
|
||||||
|
}),
|
||||||
))
|
))
|
||||||
DEFAULT_IMAGES = OrderedDict([])
|
DEFAULT_IMAGES = OrderedDict([])
|
||||||
|
|
||||||
@@ -485,10 +504,17 @@ def variables_from_questions(sender, *args, **kwargs):
|
|||||||
for q in sender.questions.all():
|
for q in sender.questions.all():
|
||||||
if q.type == Question.TYPE_FILE:
|
if q.type == Question.TYPE_FILE:
|
||||||
continue
|
continue
|
||||||
|
d['question_{}'.format(q.identifier)] = {
|
||||||
|
'label': _('Question: {question}').format(question=q.question),
|
||||||
|
'editor_sample': _('<Answer: {question}>').format(question=q.question),
|
||||||
|
'evaluate': partial(get_answer, question_id=q.pk),
|
||||||
|
'migrate_from': 'question_{}'.format(q.pk)
|
||||||
|
}
|
||||||
d['question_{}'.format(q.pk)] = {
|
d['question_{}'.format(q.pk)] = {
|
||||||
'label': _('Question: {question}').format(question=q.question),
|
'label': _('Question: {question}').format(question=q.question),
|
||||||
'editor_sample': _('<Answer: {question}>').format(question=q.question),
|
'editor_sample': _('<Answer: {question}>').format(question=q.question),
|
||||||
'evaluate': partial(get_answer, question_id=q.pk)
|
'evaluate': partial(get_answer, question_id=q.pk),
|
||||||
|
'hidden': True,
|
||||||
}
|
}
|
||||||
return d
|
return d
|
||||||
|
|
||||||
@@ -546,6 +572,24 @@ def get_variables(event):
|
|||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def get_giftcard_expiry(op: OrderPosition, ev):
|
||||||
|
if not op.item.issue_giftcard:
|
||||||
|
return "" # performance optimization
|
||||||
|
m = op.issued_gift_cards.aggregate(m=Min('expires'))['m']
|
||||||
|
if not m:
|
||||||
|
return ""
|
||||||
|
return date_format(m.astimezone(ev.timezone), "SHORT_DATE_FORMAT")
|
||||||
|
|
||||||
|
|
||||||
|
def get_giftcard_issuance(op: OrderPosition, ev):
|
||||||
|
if not op.item.issue_giftcard:
|
||||||
|
return "" # performance optimization
|
||||||
|
m = op.issued_gift_cards.aggregate(m=Max('issuance'))['m']
|
||||||
|
if not m:
|
||||||
|
return ""
|
||||||
|
return date_format(m.astimezone(ev.timezone), "SHORT_DATE_FORMAT")
|
||||||
|
|
||||||
|
|
||||||
def get_first_scan(op: OrderPosition):
|
def get_first_scan(op: OrderPosition):
|
||||||
scans = list(op.checkins.all())
|
scans = list(op.checkins.all())
|
||||||
|
|
||||||
@@ -557,6 +601,14 @@ def get_first_scan(op: OrderPosition):
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def get_seat(op: OrderPosition):
|
||||||
|
if op.seat_id:
|
||||||
|
return op.seat
|
||||||
|
if op.addon_to_id:
|
||||||
|
return op.addon_to.seat
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={
|
reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={
|
||||||
'delete_harakat': True,
|
'delete_harakat': True,
|
||||||
'support_ligatures': False,
|
'support_ligatures': False,
|
||||||
@@ -616,12 +668,14 @@ class Renderer:
|
|||||||
preserveAspectRatio=True, anchor='n',
|
preserveAspectRatio=True, anchor='n',
|
||||||
mask='auto')
|
mask='auto')
|
||||||
|
|
||||||
def _draw_barcodearea(self, canvas: Canvas, op: OrderPosition, o: dict):
|
def _draw_barcodearea(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
|
||||||
content = o.get('content', 'secret')
|
content = o.get('content', 'secret')
|
||||||
if content == 'secret':
|
if content == 'secret':
|
||||||
|
# do not use get_text_content because it uses a shortened version of secret
|
||||||
|
# and does not deal with our default value here properly
|
||||||
content = op.secret
|
content = op.secret
|
||||||
elif content == 'pseudonymization_id':
|
else:
|
||||||
content = op.pseudonymization_id
|
content = self._get_text_content(op, order, o)
|
||||||
|
|
||||||
level = 'H'
|
level = 'H'
|
||||||
if len(content) > 32:
|
if len(content) > 32:
|
||||||
@@ -648,20 +702,51 @@ class Renderer:
|
|||||||
return self._get_text_content(op, order, o, True)
|
return self._get_text_content(op, order, o, True)
|
||||||
|
|
||||||
ev = self._get_ev(op, order)
|
ev = self._get_ev(op, order)
|
||||||
|
|
||||||
if not o['content']:
|
if not o['content']:
|
||||||
return '(error)'
|
return '(error)'
|
||||||
if o['content'] == 'other':
|
|
||||||
return o['text']
|
if o['content'] == 'other' or o['content'] == 'other_i18n':
|
||||||
|
if o['content'] == 'other_i18n':
|
||||||
|
text = str(LazyI18nString(o['text_i18n']))
|
||||||
|
else:
|
||||||
|
text = o['text']
|
||||||
|
|
||||||
|
def replace(x):
|
||||||
|
print(x.group(1))
|
||||||
|
if x.group(1).startswith('itemmeta:'):
|
||||||
|
return op.item.meta_data.get(x.group(1)[9:]) or ''
|
||||||
|
elif x.group(1).startswith('meta:'):
|
||||||
|
return ev.meta_data.get(x.group(1)[5:]) or ''
|
||||||
|
elif x.group(1) not in self.variables:
|
||||||
|
return x.group(0)
|
||||||
|
if x.group(1) == 'secret':
|
||||||
|
# Do not use shortened version
|
||||||
|
return op.secret
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.variables[x.group(1)]['evaluate'](op, order, ev)
|
||||||
|
except:
|
||||||
|
logger.exception('Failed to process variable.')
|
||||||
|
return '(error)'
|
||||||
|
|
||||||
|
# We do not use str.format like in emails so we (a) can evaluate lazily and (b) can re-implement this
|
||||||
|
# 1:1 on other platforms that render PDFs through our API (libpretixprint)
|
||||||
|
return re.sub(r'\{([a-zA-Z0-9:_]+)\}', replace, text)
|
||||||
|
|
||||||
elif o['content'].startswith('itemmeta:'):
|
elif o['content'].startswith('itemmeta:'):
|
||||||
return op.item.meta_data.get(o['content'][9:]) or ''
|
return op.item.meta_data.get(o['content'][9:]) or ''
|
||||||
|
|
||||||
elif o['content'].startswith('meta:'):
|
elif o['content'].startswith('meta:'):
|
||||||
return ev.meta_data.get(o['content'][5:]) or ''
|
return ev.meta_data.get(o['content'][5:]) or ''
|
||||||
|
|
||||||
elif o['content'] in self.variables:
|
elif o['content'] in self.variables:
|
||||||
try:
|
try:
|
||||||
return self.variables[o['content']]['evaluate'](op, order, ev)
|
return self.variables[o['content']]['evaluate'](op, order, ev)
|
||||||
except:
|
except:
|
||||||
logger.exception('Failed to process variable.')
|
logger.exception('Failed to process variable.')
|
||||||
return '(error)'
|
return '(error)'
|
||||||
|
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
def _draw_imagearea(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
|
def _draw_imagearea(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
|
||||||
@@ -754,20 +839,30 @@ class Renderer:
|
|||||||
p.drawOn(canvas, 0, -h - ad[1])
|
p.drawOn(canvas, 0, -h - ad[1])
|
||||||
canvas.restoreState()
|
canvas.restoreState()
|
||||||
|
|
||||||
def draw_page(self, canvas: Canvas, order: Order, op: OrderPosition, show_page=True):
|
def draw_page(self, canvas: Canvas, order: Order, op: OrderPosition, show_page=True, only_page=None):
|
||||||
for o in self.layout:
|
page_count = self.bg_pdf.getNumPages()
|
||||||
if o['type'] == "barcodearea":
|
|
||||||
self._draw_barcodearea(canvas, op, o)
|
if not only_page and not show_page:
|
||||||
elif o['type'] == "imagearea":
|
raise ValueError("only_page=None and show_page=False cannot be combined")
|
||||||
self._draw_imagearea(canvas, op, order, o)
|
|
||||||
elif o['type'] == "textarea":
|
for page in range(page_count):
|
||||||
self._draw_textarea(canvas, op, order, o)
|
if only_page and only_page != page + 1:
|
||||||
elif o['type'] == "poweredby":
|
continue
|
||||||
self._draw_poweredby(canvas, op, o)
|
for o in self.layout:
|
||||||
if self.bg_pdf:
|
if o.get('page', 1) != page + 1:
|
||||||
canvas.setPageSize((self.bg_pdf.getPage(0).mediaBox[2], self.bg_pdf.getPage(0).mediaBox[3]))
|
continue
|
||||||
if show_page:
|
if o['type'] == "barcodearea":
|
||||||
canvas.showPage()
|
self._draw_barcodearea(canvas, op, order, o)
|
||||||
|
elif o['type'] == "imagearea":
|
||||||
|
self._draw_imagearea(canvas, op, order, o)
|
||||||
|
elif o['type'] == "textarea":
|
||||||
|
self._draw_textarea(canvas, op, order, o)
|
||||||
|
elif o['type'] == "poweredby":
|
||||||
|
self._draw_poweredby(canvas, op, o)
|
||||||
|
if self.bg_pdf:
|
||||||
|
canvas.setPageSize((self.bg_pdf.getPage(page).mediaBox[2], self.bg_pdf.getPage(page).mediaBox[3]))
|
||||||
|
if show_page:
|
||||||
|
canvas.showPage()
|
||||||
|
|
||||||
def render_background(self, buffer, title=_('Ticket')):
|
def render_background(self, buffer, title=_('Ticket')):
|
||||||
if settings.PDFTK:
|
if settings.PDFTK:
|
||||||
@@ -780,7 +875,7 @@ class Renderer:
|
|||||||
subprocess.run([
|
subprocess.run([
|
||||||
settings.PDFTK,
|
settings.PDFTK,
|
||||||
os.path.join(d, 'front.pdf'),
|
os.path.join(d, 'front.pdf'),
|
||||||
'background',
|
'multibackground',
|
||||||
os.path.join(d, 'back.pdf'),
|
os.path.join(d, 'back.pdf'),
|
||||||
'output',
|
'output',
|
||||||
os.path.join(d, 'out.pdf'),
|
os.path.join(d, 'out.pdf'),
|
||||||
@@ -794,8 +889,8 @@ class Renderer:
|
|||||||
new_pdf = PdfFileReader(buffer)
|
new_pdf = PdfFileReader(buffer)
|
||||||
output = PdfFileWriter()
|
output = PdfFileWriter()
|
||||||
|
|
||||||
for page in new_pdf.pages:
|
for i, page in enumerate(new_pdf.pages):
|
||||||
bg_page = copy.copy(self.bg_pdf.getPage(0))
|
bg_page = copy.copy(self.bg_pdf.getPage(i))
|
||||||
bg_page.mergePage(page)
|
bg_page.mergePage(page)
|
||||||
output.addPage(bg_page)
|
output.addPage(bg_page)
|
||||||
|
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ def assign_ticket_secret(event, position, force_invalidate_if_revokation_list_us
|
|||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
changed = position.secret != secret
|
changed = position.secret != secret
|
||||||
if position.secret and changed and gen.use_revocation_list:
|
if position.secret and changed and gen.use_revocation_list and position.pk:
|
||||||
position.revoked_secrets.create(event=event, secret=position.secret)
|
position.revoked_secrets.create(event=event, secret=position.secret)
|
||||||
position.secret = secret
|
position.secret = secret
|
||||||
if save and changed:
|
if save and changed:
|
||||||
|
|||||||
@@ -214,8 +214,8 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
|
|||||||
refund_amount = o.payment_refund_sum
|
refund_amount = o.payment_refund_sum
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if auto_refund:
|
if auto_refund or manual_refund:
|
||||||
_try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True,
|
_try_auto_refund(o.pk, auto_refund=auto_refund, manual_refund=manual_refund, allow_partial=True,
|
||||||
source=OrderRefund.REFUND_SOURCE_ADMIN, refund_as_giftcard=refund_as_giftcard,
|
source=OrderRefund.REFUND_SOURCE_ADMIN, refund_as_giftcard=refund_as_giftcard,
|
||||||
giftcard_expires=giftcard_expires, giftcard_conditions=giftcard_conditions,
|
giftcard_expires=giftcard_expires, giftcard_conditions=giftcard_conditions,
|
||||||
comment=gettext('Event canceled'))
|
comment=gettext('Event canceled'))
|
||||||
@@ -272,8 +272,8 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
|
|||||||
ocm.commit()
|
ocm.commit()
|
||||||
refund_amount = o.payment_refund_sum - o.total
|
refund_amount = o.payment_refund_sum - o.total
|
||||||
|
|
||||||
if auto_refund:
|
if auto_refund or manual_refund:
|
||||||
_try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True,
|
_try_auto_refund(o.pk, auto_refund=auto_refund, manual_refund=manual_refund, allow_partial=True,
|
||||||
source=OrderRefund.REFUND_SOURCE_ADMIN, refund_as_giftcard=refund_as_giftcard,
|
source=OrderRefund.REFUND_SOURCE_ADMIN, refund_as_giftcard=refund_as_giftcard,
|
||||||
giftcard_expires=giftcard_expires, giftcard_conditions=giftcard_conditions,
|
giftcard_expires=giftcard_expires, giftcard_conditions=giftcard_conditions,
|
||||||
comment=gettext('Event canceled'))
|
comment=gettext('Event canceled'))
|
||||||
|
|||||||
@@ -485,14 +485,17 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
|||||||
try:
|
try:
|
||||||
backend.send_messages([email])
|
backend.send_messages([email])
|
||||||
except (smtplib.SMTPResponseException, smtplib.SMTPSenderRefused) as e:
|
except (smtplib.SMTPResponseException, smtplib.SMTPSenderRefused) as e:
|
||||||
if e.smtp_code in (101, 111, 421, 422, 431, 432, 442, 447, 452):
|
if e.smtp_code in (101, 111, 421, 422, 431, 432, 442, 447, 452, 454):
|
||||||
if e.smtp_code == 432 and settings.HAS_REDIS:
|
if e.smtp_code in (432, 454) and settings.HAS_REDIS:
|
||||||
# This is likely Microsoft Exchange Online which has a pretty bad rate limit of max. 3 concurrent
|
# This is likely Microsoft Exchange Online which has a pretty bad rate limit of max. 3 concurrent
|
||||||
# SMTP connections which is *easily* exceeded with many celery threads. Just retrying with exponential
|
# SMTP connections which is *easily* exceeded with many celery threads. Just retrying with exponential
|
||||||
# backoff won't be good enough if we have a lot of emails, instead we'll need to make sure our retry
|
# backoff won't be good enough if we have a lot of emails, instead we'll need to make sure our retry
|
||||||
# intervals scatter such that the email won't all be retried at the same time again and cause the
|
# intervals scatter such that the email won't all be retried at the same time again and cause the
|
||||||
# same problem.
|
# same problem.
|
||||||
# See also https://docs.microsoft.com/en-us/exchange/troubleshoot/send-emails/smtp-submission-improvements
|
# See also https://docs.microsoft.com/en-us/exchange/troubleshoot/send-emails/smtp-submission-improvements
|
||||||
|
#
|
||||||
|
# Also observed on AWS SES with 454:
|
||||||
|
# https://aws.amazon.com/blogs/messaging-and-targeting/how-to-handle-a-throttling-maximum-sending-rate-exceeded-error/
|
||||||
from django_redis import get_redis_connection
|
from django_redis import get_redis_connection
|
||||||
|
|
||||||
redis_key = "pretix_mail_retry_" + hashlib.sha1(f"{getattr(backend, 'username', '_')}@{getattr(backend, 'host', '_')}".encode()).hexdigest()
|
redis_key = "pretix_mail_retry_" + hashlib.sha1(f"{getattr(backend, 'username', '_')}@{getattr(backend, 'host', '_')}".encode()).hexdigest()
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ from django.db.transaction import get_connection
|
|||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.timezone import make_aware, now
|
from django.utils.timezone import make_aware, now
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _, gettext_lazy
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.api.models import OAuthApplication
|
from pretix.api.models import OAuthApplication
|
||||||
@@ -384,7 +384,7 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
|
|||||||
|
|
||||||
|
|
||||||
def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device=None, oauth_application=None,
|
def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device=None, oauth_application=None,
|
||||||
cancellation_fee=None, keep_fees=None, cancel_invoice=True):
|
cancellation_fee=None, keep_fees=None, cancel_invoice=True, comment=None):
|
||||||
"""
|
"""
|
||||||
Mark this order as canceled
|
Mark this order as canceled
|
||||||
:param order: The order to change
|
:param order: The order to change
|
||||||
@@ -481,7 +481,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
|
|||||||
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
|
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
|
||||||
|
|
||||||
order.log_action('pretix.event.order.canceled', user=user, auth=api_token or oauth_application or device,
|
order.log_action('pretix.event.order.canceled', user=user, auth=api_token or oauth_application or device,
|
||||||
data={'cancellation_fee': cancellation_fee})
|
data={'cancellation_fee': cancellation_fee, 'comment': comment})
|
||||||
order.cancellation_requests.all().delete()
|
order.cancellation_requests.all().delete()
|
||||||
|
|
||||||
order.create_transactions()
|
order.create_transactions()
|
||||||
@@ -489,7 +489,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
|
|||||||
if send_mail:
|
if send_mail:
|
||||||
email_template = order.event.settings.mail_text_order_canceled
|
email_template = order.event.settings.mail_text_order_canceled
|
||||||
with language(order.locale, order.event.settings.region):
|
with language(order.locale, order.event.settings.region):
|
||||||
email_context = get_email_context(event=order.event, order=order)
|
email_context = get_email_context(event=order.event, order=order, comment=comment or "")
|
||||||
email_subject = _('Order canceled: %(code)s') % {'code': order.code}
|
email_subject = _('Order canceled: %(code)s') % {'code': order.code}
|
||||||
try:
|
try:
|
||||||
order.send_mail(
|
order.send_mail(
|
||||||
@@ -934,7 +934,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
|
|||||||
def _order_placed_email(event: Event, order: Order, pprov: BasePaymentProvider, email_template, log_entry: str,
|
def _order_placed_email(event: Event, order: Order, pprov: BasePaymentProvider, email_template, log_entry: str,
|
||||||
invoice, payment: OrderPayment, is_free=False):
|
invoice, payment: OrderPayment, is_free=False):
|
||||||
email_context = get_email_context(event=event, order=order, payment=payment if pprov else None)
|
email_context = get_email_context(event=event, order=order, payment=payment if pprov else None)
|
||||||
email_subject = _('Your order: %(code)s') % {'code': order.code}
|
email_subject = gettext_lazy('Your order: {code}')
|
||||||
try:
|
try:
|
||||||
order.send_mail(
|
order.send_mail(
|
||||||
email_subject, email_template, email_context,
|
email_subject, email_template, email_context,
|
||||||
@@ -952,7 +952,7 @@ def _order_placed_email(event: Event, order: Order, pprov: BasePaymentProvider,
|
|||||||
|
|
||||||
def _order_placed_email_attendee(event: Event, order: Order, position: OrderPosition, email_template, log_entry: str, is_free=False):
|
def _order_placed_email_attendee(event: Event, order: Order, position: OrderPosition, email_template, log_entry: str, is_free=False):
|
||||||
email_context = get_email_context(event=event, order=order, position=position)
|
email_context = get_email_context(event=event, order=order, position=position)
|
||||||
email_subject = _('Your event registration: %(code)s') % {'code': order.code}
|
email_subject = gettext_lazy('Your event registration: {code}')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
position.send_mail(
|
position.send_mail(
|
||||||
@@ -1467,7 +1467,7 @@ class OrderChangeManager:
|
|||||||
if self.order.event.settings.invoice_include_free or position.price != Decimal('0.00'):
|
if self.order.event.settings.invoice_include_free or position.price != Decimal('0.00'):
|
||||||
self._invoice_dirty = True
|
self._invoice_dirty = True
|
||||||
|
|
||||||
def add_position(self, item: Item, variation: ItemVariation, price: Decimal, addon_to: Order = None,
|
def add_position(self, item: Item, variation: ItemVariation, price: Decimal, addon_to: OrderPosition = None,
|
||||||
subevent: SubEvent = None, seat: Seat = None, membership: Membership = None):
|
subevent: SubEvent = None, seat: Seat = None, membership: Membership = None):
|
||||||
if isinstance(seat, str):
|
if isinstance(seat, str):
|
||||||
if not seat:
|
if not seat:
|
||||||
@@ -1492,6 +1492,8 @@ class OrderChangeManager:
|
|||||||
|
|
||||||
if price is None:
|
if price is None:
|
||||||
raise OrderError(self.error_messages['product_invalid'])
|
raise OrderError(self.error_messages['product_invalid'])
|
||||||
|
if item.variations.exists() and not variation:
|
||||||
|
raise OrderError(self.error_messages['product_without_variation'])
|
||||||
if not addon_to and item.category and item.category.is_addon:
|
if not addon_to and item.category and item.category.is_addon:
|
||||||
raise OrderError(self.error_messages['addon_to_required'])
|
raise OrderError(self.error_messages['addon_to_required'])
|
||||||
if addon_to:
|
if addon_to:
|
||||||
@@ -1527,6 +1529,8 @@ class OrderChangeManager:
|
|||||||
self._invoice_dirty = True
|
self._invoice_dirty = True
|
||||||
|
|
||||||
self._operations.append(self.SplitOperation(position))
|
self._operations.append(self.SplitOperation(position))
|
||||||
|
for a in position.addons.all():
|
||||||
|
self._operations.append(self.SplitOperation(a))
|
||||||
|
|
||||||
def set_addons(self, addons):
|
def set_addons(self, addons):
|
||||||
if self._operations:
|
if self._operations:
|
||||||
@@ -2383,7 +2387,8 @@ def perform_order(self, event: Event, payment_provider: str, positions: List[str
|
|||||||
_unset = object()
|
_unset = object()
|
||||||
|
|
||||||
|
|
||||||
def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=OrderRefund.REFUND_SOURCE_BUYER,
|
def _try_auto_refund(order, auto_refund=True, manual_refund=False, allow_partial=False,
|
||||||
|
source=OrderRefund.REFUND_SOURCE_BUYER,
|
||||||
refund_as_giftcard=False, giftcard_expires=_unset, giftcard_conditions=None, comment=None):
|
refund_as_giftcard=False, giftcard_expires=_unset, giftcard_conditions=None, comment=None):
|
||||||
notify_admin = False
|
notify_admin = False
|
||||||
error = False
|
error = False
|
||||||
@@ -2393,9 +2398,9 @@ def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=Ord
|
|||||||
if refund_amount <= Decimal('0.00'):
|
if refund_amount <= Decimal('0.00'):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
can_auto_refund_sum = 0
|
||||||
|
|
||||||
if refund_as_giftcard:
|
if refund_as_giftcard:
|
||||||
proposals = {}
|
|
||||||
can_auto_refund = True
|
|
||||||
can_auto_refund_sum = refund_amount
|
can_auto_refund_sum = refund_amount
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
giftcard = order.event.organizer.issued_gift_cards.create(
|
giftcard = order.event.organizer.issued_gift_cards.create(
|
||||||
@@ -2435,42 +2440,41 @@ def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=Ord
|
|||||||
if r.state != OrderRefund.REFUND_STATE_DONE:
|
if r.state != OrderRefund.REFUND_STATE_DONE:
|
||||||
notify_admin = True
|
notify_admin = True
|
||||||
|
|
||||||
else:
|
elif auto_refund:
|
||||||
proposals = order.propose_auto_refunds(refund_amount)
|
proposals = order.propose_auto_refunds(refund_amount)
|
||||||
can_auto_refund_sum = sum(proposals.values())
|
can_auto_refund_sum = sum(proposals.values())
|
||||||
can_auto_refund = (allow_partial and can_auto_refund_sum) or can_auto_refund_sum == refund_amount
|
if (allow_partial and can_auto_refund_sum) or can_auto_refund_sum == refund_amount:
|
||||||
if can_auto_refund:
|
for p, value in proposals.items():
|
||||||
for p, value in proposals.items():
|
|
||||||
with transaction.atomic():
|
|
||||||
r = order.refunds.create(
|
|
||||||
payment=p,
|
|
||||||
source=source,
|
|
||||||
state=OrderRefund.REFUND_STATE_CREATED,
|
|
||||||
amount=value,
|
|
||||||
comment=comment,
|
|
||||||
provider=p.provider
|
|
||||||
)
|
|
||||||
order.log_action('pretix.event.order.refund.created', {
|
|
||||||
'local_id': r.local_id,
|
|
||||||
'provider': r.provider,
|
|
||||||
})
|
|
||||||
|
|
||||||
try:
|
|
||||||
r.payment_provider.execute_refund(r)
|
|
||||||
except PaymentException as e:
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
r.state = OrderRefund.REFUND_STATE_FAILED
|
r = order.refunds.create(
|
||||||
r.save()
|
payment=p,
|
||||||
order.log_action('pretix.event.order.refund.failed', {
|
source=source,
|
||||||
|
state=OrderRefund.REFUND_STATE_CREATED,
|
||||||
|
amount=value,
|
||||||
|
comment=comment,
|
||||||
|
provider=p.provider
|
||||||
|
)
|
||||||
|
order.log_action('pretix.event.order.refund.created', {
|
||||||
'local_id': r.local_id,
|
'local_id': r.local_id,
|
||||||
'provider': r.provider,
|
'provider': r.provider,
|
||||||
'error': str(e)
|
|
||||||
})
|
})
|
||||||
error = True
|
|
||||||
notify_admin = True
|
try:
|
||||||
else:
|
r.payment_provider.execute_refund(r)
|
||||||
if r.state not in (OrderRefund.REFUND_STATE_TRANSIT, OrderRefund.REFUND_STATE_DONE):
|
except PaymentException as e:
|
||||||
|
with transaction.atomic():
|
||||||
|
r.state = OrderRefund.REFUND_STATE_FAILED
|
||||||
|
r.save()
|
||||||
|
order.log_action('pretix.event.order.refund.failed', {
|
||||||
|
'local_id': r.local_id,
|
||||||
|
'provider': r.provider,
|
||||||
|
'error': str(e)
|
||||||
|
})
|
||||||
|
error = True
|
||||||
notify_admin = True
|
notify_admin = True
|
||||||
|
else:
|
||||||
|
if r.state not in (OrderRefund.REFUND_STATE_TRANSIT, OrderRefund.REFUND_STATE_DONE):
|
||||||
|
notify_admin = True
|
||||||
|
|
||||||
if refund_amount - can_auto_refund_sum > Decimal('0.00'):
|
if refund_amount - can_auto_refund_sum > Decimal('0.00'):
|
||||||
if manual_refund:
|
if manual_refund:
|
||||||
@@ -2502,15 +2506,15 @@ def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=Ord
|
|||||||
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
||||||
@scopes_disabled()
|
@scopes_disabled()
|
||||||
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None,
|
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None,
|
||||||
device=None, cancellation_fee=None, try_auto_refund=False, refund_as_giftcard=False, comment=None,
|
device=None, cancellation_fee=None, try_auto_refund=False, refund_as_giftcard=False,
|
||||||
cancel_invoice=True):
|
email_comment=None, refund_comment=None, cancel_invoice=True):
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
ret = _cancel_order(order, user, send_mail, api_token, device, oauth_application,
|
ret = _cancel_order(order, user, send_mail, api_token, device, oauth_application,
|
||||||
cancellation_fee, cancel_invoice=cancel_invoice)
|
cancellation_fee, cancel_invoice=cancel_invoice, comment=email_comment)
|
||||||
if try_auto_refund:
|
if try_auto_refund:
|
||||||
_try_auto_refund(order, refund_as_giftcard=refund_as_giftcard,
|
_try_auto_refund(order, refund_as_giftcard=refund_as_giftcard,
|
||||||
comment=comment)
|
comment=refund_comment)
|
||||||
return ret
|
return ret
|
||||||
except LockTimeoutException:
|
except LockTimeoutException:
|
||||||
self.retry()
|
self.retry()
|
||||||
|
|||||||
@@ -86,9 +86,9 @@ def primary_font_kwargs():
|
|||||||
from pretix.presale.style import get_fonts
|
from pretix.presale.style import get_fonts
|
||||||
|
|
||||||
choices = [('Open Sans', 'Open Sans')]
|
choices = [('Open Sans', 'Open Sans')]
|
||||||
choices += [
|
choices += sorted([
|
||||||
(a, {"title": a, "data": v}) for a, v in get_fonts().items()
|
(a, {"title": a, "data": v}) for a, v in get_fonts().items() if not v.get('pdf_only', False)
|
||||||
]
|
], key=lambda a: a[0])
|
||||||
return {
|
return {
|
||||||
'choices': choices,
|
'choices': choices,
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,6 @@ def restricted_plugin_kwargs():
|
|||||||
(p.module, p.name) for p in get_all_plugins(None)
|
(p.module, p.name) for p in get_all_plugins(None)
|
||||||
if (
|
if (
|
||||||
not p.name.startswith('.') and
|
not p.name.startswith('.') and
|
||||||
getattr(p, 'visible', True) and
|
|
||||||
getattr(p, 'restricted', False) and
|
getattr(p, 'restricted', False) and
|
||||||
not hasattr(p, 'is_available') # this means you should not really use restricted and is_available
|
not hasattr(p, 'is_available') # this means you should not really use restricted and is_available
|
||||||
)
|
)
|
||||||
@@ -1196,7 +1195,20 @@ DEFAULTS = {
|
|||||||
help_text=_("If you ask for a phone number, explain why you do so and what you will use the phone number for.")
|
help_text=_("If you ask for a phone number, explain why you do so and what you will use the phone number for.")
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
'show_checkin_number_user': {
|
||||||
|
'default': 'False',
|
||||||
|
'type': bool,
|
||||||
|
'serializer_class': serializers.BooleanField,
|
||||||
|
'form_class': forms.BooleanField,
|
||||||
|
'form_kwargs': dict(
|
||||||
|
label=_("Show number of check-ins to customer"),
|
||||||
|
help_text=_('With this option enabled, your customers will be able how many times they entered '
|
||||||
|
'the event. This is usually not necessary, but might be useful in combination with tickets '
|
||||||
|
'that are usable a specific number of times, so customers can see how many times they have '
|
||||||
|
'already been used. Exits or failed scans will not be counted, and the user will not see '
|
||||||
|
'the different check-in lists.'),
|
||||||
|
)
|
||||||
|
},
|
||||||
'ticket_download': {
|
'ticket_download': {
|
||||||
'default': 'False',
|
'default': 'False',
|
||||||
'type': bool,
|
'type': bool,
|
||||||
@@ -1902,6 +1914,8 @@ Your {event} team"""))
|
|||||||
|
|
||||||
your order {code} for {event} has been canceled.
|
your order {code} for {event} has been canceled.
|
||||||
|
|
||||||
|
{comment}
|
||||||
|
|
||||||
You can view the details of your order at
|
You can view the details of your order at
|
||||||
{url}
|
{url}
|
||||||
|
|
||||||
|
|||||||
@@ -90,13 +90,16 @@
|
|||||||
{% for groupkey, positions in cart %}
|
{% for groupkey, positions in cart %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{% if not groupkey.4 %} {# is addon #}
|
{% if not groupkey.4 %} {# is not addon #}
|
||||||
{{ positions|length }}x
|
{{ positions|length }}x
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if groupkey.4 %} {# is addon #}
|
{% if groupkey.4 %} {# is addon #}
|
||||||
+
|
+
|
||||||
|
{% if positions|length > 1 %}
|
||||||
|
{{ positions|length }}x
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ groupkey.0.name }}{% if groupkey.1 %} – {{ groupkey.1.value }}{% endif %}
|
{{ groupkey.0.name }}{% if groupkey.1 %} – {{ groupkey.1.value }}{% endif %}
|
||||||
{% if groupkey.2 %} {# subevent #}
|
{% if groupkey.2 %} {# subevent #}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class BaseQuestionsViewMixin:
|
|||||||
def _positions_for_questions(self):
|
def _positions_for_questions(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def get_question_override_sets(self, position):
|
def get_question_override_sets(self, position, index):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def question_form_kwargs(self, cr):
|
def question_form_kwargs(self, cr):
|
||||||
@@ -72,7 +72,7 @@ class BaseQuestionsViewMixin:
|
|||||||
submitted at once.
|
submitted at once.
|
||||||
"""
|
"""
|
||||||
formlist = []
|
formlist = []
|
||||||
for cr in self._positions_for_questions:
|
for idx, cr in enumerate(self._positions_for_questions):
|
||||||
cartpos = cr if isinstance(cr, CartPosition) else None
|
cartpos = cr if isinstance(cr, CartPosition) else None
|
||||||
orderpos = cr if isinstance(cr, OrderPosition) else None
|
orderpos = cr if isinstance(cr, OrderPosition) else None
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ class BaseQuestionsViewMixin:
|
|||||||
))
|
))
|
||||||
)
|
)
|
||||||
|
|
||||||
override_sets = self.get_question_override_sets(cr)
|
override_sets = self.get_question_override_sets(cr, idx)
|
||||||
for overrides in override_sets:
|
for overrides in override_sets:
|
||||||
for question_name, question_field in form.fields.items():
|
for question_name, question_field in form.fields.items():
|
||||||
if hasattr(question_field, 'question'):
|
if hasattr(question_field, 'question'):
|
||||||
|
|||||||
@@ -523,6 +523,7 @@ class EventSettingsForm(SettingsForm):
|
|||||||
'last_order_modification_date',
|
'last_order_modification_date',
|
||||||
'allow_modifications_after_checkin',
|
'allow_modifications_after_checkin',
|
||||||
'checkout_show_copy_answers_button',
|
'checkout_show_copy_answers_button',
|
||||||
|
'show_checkin_number_user',
|
||||||
'primary_color',
|
'primary_color',
|
||||||
'theme_color_success',
|
'theme_color_success',
|
||||||
'theme_color_danger',
|
'theme_color_danger',
|
||||||
@@ -1074,7 +1075,7 @@ class MailSettingsForm(SettingsForm):
|
|||||||
'mail_text_order_free': ['event', 'order'],
|
'mail_text_order_free': ['event', 'order'],
|
||||||
'mail_text_order_free_attendee': ['event', 'order', 'position'],
|
'mail_text_order_free_attendee': ['event', 'order', 'position'],
|
||||||
'mail_text_order_changed': ['event', 'order'],
|
'mail_text_order_changed': ['event', 'order'],
|
||||||
'mail_text_order_canceled': ['event', 'order'],
|
'mail_text_order_canceled': ['event', 'order', 'comment'],
|
||||||
'mail_text_order_expire_warning': ['event', 'order'],
|
'mail_text_order_expire_warning': ['event', 'order'],
|
||||||
'mail_text_order_custom_mail': ['event', 'order'],
|
'mail_text_order_custom_mail': ['event', 'order'],
|
||||||
'mail_text_download_reminder': ['event', 'order'],
|
'mail_text_download_reminder': ['event', 'order'],
|
||||||
|
|||||||
@@ -255,7 +255,6 @@ class OrderFilterForm(FilterForm):
|
|||||||
| Q(pk__in=matching_invoices)
|
| Q(pk__in=matching_invoices)
|
||||||
| Q(pk__in=matching_positions)
|
| Q(pk__in=matching_positions)
|
||||||
| Q(pk__in=matching_invoice_addresses)
|
| Q(pk__in=matching_invoice_addresses)
|
||||||
| Q(pk__in=matching_invoices)
|
|
||||||
)
|
)
|
||||||
for recv, q in order_search_filter_q.send(sender=getattr(self, 'event', None), query=u):
|
for recv, q in order_search_filter_q.send(sender=getattr(self, 'event', None), query=u):
|
||||||
mainq = mainq | q
|
mainq = mainq | q
|
||||||
@@ -1860,11 +1859,11 @@ class VoucherFilterForm(FilterForm):
|
|||||||
for i in self.event.items.prefetch_related('variations').all():
|
for i in self.event.items.prefetch_related('variations').all():
|
||||||
variations = list(i.variations.all())
|
variations = list(i.variations.all())
|
||||||
if variations:
|
if variations:
|
||||||
choices.append((str(i.pk), _('{product} – Any variation').format(product=i.name)))
|
choices.append((str(i.pk), _('{product} – Any variation').format(product=str(i))))
|
||||||
for v in variations:
|
for v in variations:
|
||||||
choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (i.name, v.value)))
|
choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (str(i), v.value)))
|
||||||
else:
|
else:
|
||||||
choices.append((str(i.pk), i.name))
|
choices.append((str(i.pk), str(i)))
|
||||||
for q in self.event.quotas.all():
|
for q in self.event.quotas.all():
|
||||||
choices.append(('q-%d' % q.pk, _('Any product in quota "{quota}"').format(quota=q)))
|
choices.append(('q-%d' % q.pk, _('Any product in quota "{quota}"').format(quota=q)))
|
||||||
self.fields['itemvar'].choices = choices
|
self.fields['itemvar'].choices = choices
|
||||||
@@ -2121,7 +2120,7 @@ class CheckinFilterForm(FilterForm):
|
|||||||
self.event = kwargs.pop('event')
|
self.event = kwargs.pop('event')
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.fields['device'].queryset = self.event.organizer.devices.all()
|
self.fields['device'].queryset = self.event.organizer.devices.all().order_by('device_id')
|
||||||
self.fields['gate'].queryset = self.event.organizer.gates.all()
|
self.fields['gate'].queryset = self.event.organizer.gates.all()
|
||||||
|
|
||||||
self.fields['checkin_list'].queryset = self.event.checkin_lists.all()
|
self.fields['checkin_list'].queryset = self.event.checkin_lists.all()
|
||||||
@@ -2142,11 +2141,11 @@ class CheckinFilterForm(FilterForm):
|
|||||||
for i in self.event.items.prefetch_related('variations').all():
|
for i in self.event.items.prefetch_related('variations').all():
|
||||||
variations = list(i.variations.all())
|
variations = list(i.variations.all())
|
||||||
if variations:
|
if variations:
|
||||||
choices.append((str(i.pk), _('{product} – Any variation').format(product=i.name)))
|
choices.append((str(i.pk), _('{product} – Any variation').format(product=str(i))))
|
||||||
for v in variations:
|
for v in variations:
|
||||||
choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (i.name, v.value)))
|
choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (str(i), v.value)))
|
||||||
else:
|
else:
|
||||||
choices.append((str(i.pk), i.name))
|
choices.append((str(i.pk), str(i)))
|
||||||
self.fields['itemvar'].choices = choices
|
self.fields['itemvar'].choices = choices
|
||||||
|
|
||||||
def filter_qs(self, qs):
|
def filter_qs(self, qs):
|
||||||
|
|||||||
@@ -434,6 +434,9 @@ class ItemCreateForm(I18nModelForm):
|
|||||||
if self.cleaned_data.get('copy_from'):
|
if self.cleaned_data.get('copy_from'):
|
||||||
for question in self.cleaned_data['copy_from'].questions.all():
|
for question in self.cleaned_data['copy_from'].questions.all():
|
||||||
question.items.add(instance)
|
question.items.add(instance)
|
||||||
|
question.log_action('pretix.event.question.changed', user=self.user, data={
|
||||||
|
'item_added': self.instance.pk
|
||||||
|
})
|
||||||
for a in self.cleaned_data['copy_from'].addons.all():
|
for a in self.cleaned_data['copy_from'].addons.all():
|
||||||
instance.addons.create(addon_category=a.addon_category, min_count=a.min_count, max_count=a.max_count,
|
instance.addons.create(addon_category=a.addon_category, min_count=a.min_count, max_count=a.max_count,
|
||||||
price_included=a.price_included, position=a.position,
|
price_included=a.price_included, position=a.position,
|
||||||
@@ -563,7 +566,7 @@ class ItemUpdateForm(I18nModelForm):
|
|||||||
if d['tax_rule'] and d['tax_rule'].rate > 0:
|
if d['tax_rule'] and d['tax_rule'].rate > 0:
|
||||||
self.add_error(
|
self.add_error(
|
||||||
'tax_rule',
|
'tax_rule',
|
||||||
_("Gift card products should not be associated with non-zero tax rates since sales tax will be applied when the gift card is redeemed.")
|
_("Gift card products should use a tax rule with a rate of 0 percent since sales tax will be applied when the gift card is redeemed.")
|
||||||
)
|
)
|
||||||
if d['admission']:
|
if d['admission']:
|
||||||
self.add_error(
|
self.add_error(
|
||||||
|
|||||||
@@ -167,6 +167,12 @@ class CancelForm(ForceQuotaConfirmationForm):
|
|||||||
initial=True,
|
initial=True,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
comment = forms.CharField(
|
||||||
|
label=_('Comment (will be sent to the user)'),
|
||||||
|
help_text=_('Will be included in the notification email when the respective placeholder is present in the '
|
||||||
|
'configured email text.'),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -482,6 +488,9 @@ class OrderPositionChangeForm(forms.Form):
|
|||||||
self.fields['tax_rule'].queryset = instance.event.tax_rules.all()
|
self.fields['tax_rule'].queryset = instance.event.tax_rules.all()
|
||||||
self.fields['tax_rule'].label_from_instance = self.taxrule_label_from_instance
|
self.fields['tax_rule'].label_from_instance = self.taxrule_label_from_instance
|
||||||
|
|
||||||
|
if instance.addon_to_id:
|
||||||
|
del self.fields['operation_split']
|
||||||
|
|
||||||
if not instance.seat and not (
|
if not instance.seat and not (
|
||||||
instance.item.seat_category_mappings.filter(subevent=instance.subevent).exists()
|
instance.item.seat_category_mappings.filter(subevent=instance.subevent).exists()
|
||||||
):
|
):
|
||||||
@@ -746,16 +755,17 @@ class EventCancelForm(forms.Form):
|
|||||||
auto_refund = forms.BooleanField(
|
auto_refund = forms.BooleanField(
|
||||||
label=_('Automatically refund money if possible'),
|
label=_('Automatically refund money if possible'),
|
||||||
initial=True,
|
initial=True,
|
||||||
required=False
|
required=False,
|
||||||
|
help_text=_('Only available for payment method that support automatic refunds.')
|
||||||
)
|
)
|
||||||
manual_refund = forms.BooleanField(
|
manual_refund = forms.BooleanField(
|
||||||
label=_('Create manual refund if the payment method does not support automatic refunds'),
|
label=_('Create refund in the manual refund to-do list'),
|
||||||
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_auto_refund'}),
|
|
||||||
initial=True,
|
initial=True,
|
||||||
required=False,
|
required=False,
|
||||||
help_text=_('If checked, all payments with a payment method not supporting automatic refunds will be on your '
|
help_text=_('Manual refunds will be created which will be listed in the manual refund to-do list. '
|
||||||
'manual refund to-do list. Do not check if you want to refund some of the orders by offsetting '
|
'When combined with the automatic refund functionally, only payments with a payment method not '
|
||||||
'with different orders or issuing gift cards.')
|
'supporting automatic refunds will be on your manual refund to-do list. Do not check if you want '
|
||||||
|
'to refund some of the orders by offsetting with different orders or issuing gift cards.')
|
||||||
)
|
)
|
||||||
refund_as_giftcard = forms.BooleanField(
|
refund_as_giftcard = forms.BooleanField(
|
||||||
label=_('Refund order value to a gift card instead instead of the original payment method'),
|
label=_('Refund order value to a gift card instead instead of the original payment method'),
|
||||||
@@ -840,7 +850,7 @@ class EventCancelForm(forms.Form):
|
|||||||
label=_("Subject"),
|
label=_("Subject"),
|
||||||
required=True,
|
required=True,
|
||||||
widget_kwargs={'attrs': {'data-display-dependency': '#id_send'}},
|
widget_kwargs={'attrs': {'data-display-dependency': '#id_send'}},
|
||||||
initial=_('Canceled: {event}'),
|
initial=LazyI18nString.from_gettext(gettext_noop('Canceled: {event}')),
|
||||||
widget=I18nTextInput,
|
widget=I18nTextInput,
|
||||||
locales=self.event.settings.get('locales'),
|
locales=self.event.settings.get('locales'),
|
||||||
)
|
)
|
||||||
@@ -866,7 +876,7 @@ class EventCancelForm(forms.Form):
|
|||||||
self.fields['send_waitinglist_subject'] = I18nFormField(
|
self.fields['send_waitinglist_subject'] = I18nFormField(
|
||||||
label=_("Subject"),
|
label=_("Subject"),
|
||||||
required=True,
|
required=True,
|
||||||
initial=_('Canceled: {event}'),
|
initial=LazyI18nString.from_gettext(gettext_noop('Canceled: {event}')),
|
||||||
widget=I18nTextInput,
|
widget=I18nTextInput,
|
||||||
widget_kwargs={'attrs': {'data-display-dependency': '#id_send_waitinglist'}},
|
widget_kwargs={'attrs': {'data-display-dependency': '#id_send_waitinglist'}},
|
||||||
locales=self.event.settings.get('locales'),
|
locales=self.event.settings.get('locales'),
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ from django import forms
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.forms.utils import ErrorDict
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
@@ -268,6 +269,69 @@ class DeviceForm(forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceBulkEditForm(forms.ModelForm):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
organizer = kwargs.pop('organizer')
|
||||||
|
self.mixed_values = kwargs.pop('mixed_values')
|
||||||
|
self.queryset = kwargs.pop('queryset')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['limit_events'].queryset = organizer.events.all().order_by(
|
||||||
|
'-has_subevents', '-date_from'
|
||||||
|
)
|
||||||
|
self.fields['gate'].queryset = organizer.gates.all()
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
d = super().clean()
|
||||||
|
if self.prefix + '__events' in self.data.getlist('_bulk') and not d['all_events'] and not d['limit_events']:
|
||||||
|
raise ValidationError(_('Your device will not have access to anything, please select some events.'))
|
||||||
|
|
||||||
|
return d
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Device
|
||||||
|
fields = ['all_events', 'limit_events', 'security_profile', 'gate']
|
||||||
|
widgets = {
|
||||||
|
'limit_events': forms.CheckboxSelectMultiple(attrs={
|
||||||
|
'data-inverse-dependency': '#id_all_events',
|
||||||
|
'class': 'scrolling-multiple-choice scrolling-multiple-choice-large',
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
field_classes = {
|
||||||
|
'limit_events': SafeEventMultipleChoiceField
|
||||||
|
}
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
objs = list(self.queryset)
|
||||||
|
fields = set()
|
||||||
|
|
||||||
|
check_map = {
|
||||||
|
'all_events': '__events',
|
||||||
|
'limit_events': '__events',
|
||||||
|
}
|
||||||
|
for k in self.fields:
|
||||||
|
cb_val = self.prefix + check_map.get(k, k)
|
||||||
|
if cb_val not in self.data.getlist('_bulk'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
fields.add(k)
|
||||||
|
for obj in objs:
|
||||||
|
if k == 'limit_events':
|
||||||
|
getattr(obj, k).set(self.cleaned_data[k])
|
||||||
|
else:
|
||||||
|
setattr(obj, k, self.cleaned_data[k])
|
||||||
|
|
||||||
|
if fields:
|
||||||
|
Device.objects.bulk_update(objs, [f for f in fields if f != 'limit_events'], 200)
|
||||||
|
|
||||||
|
def full_clean(self):
|
||||||
|
if len(self.data) == 0:
|
||||||
|
# form wasn't submitted
|
||||||
|
self._errors = ErrorDict()
|
||||||
|
return
|
||||||
|
super().full_clean()
|
||||||
|
|
||||||
|
|
||||||
class OrganizerSettingsForm(SettingsForm):
|
class OrganizerSettingsForm(SettingsForm):
|
||||||
timezone = forms.ChoiceField(
|
timezone = forms.ChoiceField(
|
||||||
choices=((a, a) for a in common_timezones),
|
choices=((a, a) for a in common_timezones),
|
||||||
|
|||||||
@@ -385,6 +385,12 @@ class VoucherBulkForm(VoucherForm):
|
|||||||
if vouchers.exists():
|
if vouchers.exists():
|
||||||
raise ValidationError(_('A voucher with one of these codes already exists.'))
|
raise ValidationError(_('A voucher with one of these codes already exists.'))
|
||||||
|
|
||||||
|
codes_seen = set()
|
||||||
|
for c in data['codes']:
|
||||||
|
if c in codes_seen:
|
||||||
|
raise ValidationError(_('The voucher code {code} appears in your list twice.').format(code=c))
|
||||||
|
codes_seen.add(c)
|
||||||
|
|
||||||
if data.get('send') and not all([data.get('send_subject'), data.get('send_message'), data.get('send_recipients')]):
|
if data.get('send') and not all([data.get('send_subject'), data.get('send_message'), data.get('send_recipients')]):
|
||||||
raise ValidationError(_('If vouchers should be sent by email, subject, message and recipients need to be specified.'))
|
raise ValidationError(_('If vouchers should be sent by email, subject, message and recipients need to be specified.'))
|
||||||
|
|
||||||
|
|||||||
@@ -341,13 +341,12 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
|||||||
'pretix.event.order.paid': _('The order has been marked as paid.'),
|
'pretix.event.order.paid': _('The order has been marked as paid.'),
|
||||||
'pretix.event.order.cancellationrequest.deleted': _('The cancellation request has been deleted.'),
|
'pretix.event.order.cancellationrequest.deleted': _('The cancellation request has been deleted.'),
|
||||||
'pretix.event.order.refunded': _('The order has been refunded.'),
|
'pretix.event.order.refunded': _('The order has been refunded.'),
|
||||||
'pretix.event.order.canceled': _('The order has been canceled.'),
|
|
||||||
'pretix.event.order.reactivated': _('The order has been reactivated.'),
|
'pretix.event.order.reactivated': _('The order has been reactivated.'),
|
||||||
'pretix.event.order.deleted': _('The test mode order {code} has been deleted.'),
|
'pretix.event.order.deleted': _('The test mode order {code} has been deleted.'),
|
||||||
'pretix.event.order.placed': _('The order has been created.'),
|
'pretix.event.order.placed': _('The order has been created.'),
|
||||||
'pretix.event.order.placed.require_approval': _('The order requires approval before it can continue to be processed.'),
|
'pretix.event.order.placed.require_approval': _('The order requires approval before it can continue to be processed.'),
|
||||||
'pretix.event.order.approved': _('The order has been approved.'),
|
'pretix.event.order.approved': _('The order has been approved.'),
|
||||||
'pretix.event.order.denied': _('The order has been denied.'),
|
'pretix.event.order.denied': _('The order has been denied (comment: "{comment}").'),
|
||||||
'pretix.event.order.contact.changed': _('The email address has been changed from "{old_email}" '
|
'pretix.event.order.contact.changed': _('The email address has been changed from "{old_email}" '
|
||||||
'to "{new_email}".'),
|
'to "{new_email}".'),
|
||||||
'pretix.event.order.contact.confirmed': _('The email address has been confirmed to be working (the user clicked on a link '
|
'pretix.event.order.contact.confirmed': _('The email address has been confirmed to be working (the user clicked on a link '
|
||||||
@@ -423,6 +422,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
|||||||
'pretix.voucher.added': _('The voucher has been created.'),
|
'pretix.voucher.added': _('The voucher has been created.'),
|
||||||
'pretix.voucher.sent': _('The voucher has been sent to {recipient}.'),
|
'pretix.voucher.sent': _('The voucher has been sent to {recipient}.'),
|
||||||
'pretix.voucher.added.waitinglist': _('The voucher has been created and sent to a person on the waiting list.'),
|
'pretix.voucher.added.waitinglist': _('The voucher has been created and sent to a person on the waiting list.'),
|
||||||
|
'pretix.voucher.expired.waitinglist': _('The voucher has been set to expire because the recipient removed themselves from the waiting list.'),
|
||||||
'pretix.voucher.changed': _('The voucher has been changed.'),
|
'pretix.voucher.changed': _('The voucher has been changed.'),
|
||||||
'pretix.voucher.deleted': _('The voucher has been deleted.'),
|
'pretix.voucher.deleted': _('The voucher has been deleted.'),
|
||||||
'pretix.voucher.redeemed': _('The voucher has been redeemed in order {order_code}.'),
|
'pretix.voucher.redeemed': _('The voucher has been redeemed in order {order_code}.'),
|
||||||
@@ -531,6 +531,27 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
|||||||
bleach.clean(logentry.parsed_data.get('msg'), tags=[], strip=True)
|
bleach.clean(logentry.parsed_data.get('msg'), tags=[], strip=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if logentry.action_type == 'pretix.event.order.canceled':
|
||||||
|
comment = logentry.parsed_data.get('comment')
|
||||||
|
if comment:
|
||||||
|
return _('The order has been canceled (comment: "{comment}").').format(comment=comment)
|
||||||
|
else:
|
||||||
|
return _('The order has been canceled.')
|
||||||
|
|
||||||
|
if logentry.action_type in ('pretix.control.views.checkin.reverted', 'pretix.event.checkin.reverted'):
|
||||||
|
if 'list' in data:
|
||||||
|
try:
|
||||||
|
checkin_list = sender.checkin_lists.get(pk=data.get('list')).name
|
||||||
|
except CheckinList.DoesNotExist:
|
||||||
|
checkin_list = _("(unknown)")
|
||||||
|
else:
|
||||||
|
checkin_list = _("(unknown)")
|
||||||
|
|
||||||
|
return _('The check-in of position #{posid} on list "{list}" has been reverted.').format(
|
||||||
|
posid=data.get('positionid'),
|
||||||
|
list=checkin_list,
|
||||||
|
)
|
||||||
|
|
||||||
if sender and logentry.action_type.startswith('pretix.event.checkin'):
|
if sender and logentry.action_type.startswith('pretix.event.checkin'):
|
||||||
return _display_checkin(sender, logentry)
|
return _display_checkin(sender, logentry)
|
||||||
|
|
||||||
@@ -559,20 +580,6 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
|||||||
list=checkin_list
|
list=checkin_list
|
||||||
)
|
)
|
||||||
|
|
||||||
if logentry.action_type in ('pretix.control.views.checkin.reverted', 'pretix.event.checkin.reverted'):
|
|
||||||
if 'list' in data:
|
|
||||||
try:
|
|
||||||
checkin_list = sender.checkin_lists.get(pk=data.get('list')).name
|
|
||||||
except CheckinList.DoesNotExist:
|
|
||||||
checkin_list = _("(unknown)")
|
|
||||||
else:
|
|
||||||
checkin_list = _("(unknown)")
|
|
||||||
|
|
||||||
return _('The check-in of position #{posid} on list "{list}" has been reverted.').format(
|
|
||||||
posid=data.get('positionid'),
|
|
||||||
list=checkin_list,
|
|
||||||
)
|
|
||||||
|
|
||||||
if logentry.action_type == 'pretix.team.member.added':
|
if logentry.action_type == 'pretix.team.member.added':
|
||||||
return _('{user} has been added to the team.').format(user=data.get('email'))
|
return _('{user} has been added to the team.').format(user=data.get('email'))
|
||||||
|
|
||||||
|
|||||||
@@ -142,6 +142,18 @@ class PermissionMiddleware:
|
|||||||
return redirect(reverse('control:user.settings.2fa'))
|
return redirect(reverse('control:user.settings.2fa'))
|
||||||
|
|
||||||
if 'event' in url.kwargs and 'organizer' in url.kwargs:
|
if 'event' in url.kwargs and 'organizer' in url.kwargs:
|
||||||
|
if url.kwargs['organizer'] == '-' and url.kwargs['event'] == '-':
|
||||||
|
# This is a hack that just takes the user to ANY event. It's useful to link to features in support
|
||||||
|
# or documentation.
|
||||||
|
ev = request.user.get_events_with_any_permission().order_by('-date_from').first()
|
||||||
|
if not ev:
|
||||||
|
raise Http404(_("The selected event was not found or you "
|
||||||
|
"have no permission to administrate it."))
|
||||||
|
k = dict(url.kwargs)
|
||||||
|
k['organizer'] = ev.organizer.slug
|
||||||
|
k['event'] = ev.slug
|
||||||
|
return redirect(reverse(url.view_name, kwargs=k, args=url.args))
|
||||||
|
|
||||||
with scope(organizer=None):
|
with scope(organizer=None):
|
||||||
request.event = Event.objects.filter(
|
request.event = Event.objects.filter(
|
||||||
slug=url.kwargs['event'],
|
slug=url.kwargs['event'],
|
||||||
@@ -157,6 +169,17 @@ class PermissionMiddleware:
|
|||||||
else:
|
else:
|
||||||
request.eventpermset = request.user.get_event_permission_set(request.organizer, request.event)
|
request.eventpermset = request.user.get_event_permission_set(request.organizer, request.event)
|
||||||
elif 'organizer' in url.kwargs:
|
elif 'organizer' in url.kwargs:
|
||||||
|
if url.kwargs['organizer'] == '-':
|
||||||
|
# This is a hack that just takes the user to ANY organizer. It's useful to link to features in support
|
||||||
|
# or documentation.
|
||||||
|
org = request.user.get_organizers_with_any_permission().first()
|
||||||
|
if not org:
|
||||||
|
raise Http404(_("The selected organizer was not found or you "
|
||||||
|
"have no permission to administrate it."))
|
||||||
|
k = dict(url.kwargs)
|
||||||
|
k['organizer'] = org.slug
|
||||||
|
return redirect(reverse(url.view_name, kwargs=k, args=url.args))
|
||||||
|
|
||||||
request.organizer = Organizer.objects.filter(
|
request.organizer = Organizer.objects.filter(
|
||||||
slug=url.kwargs['organizer'],
|
slug=url.kwargs['organizer'],
|
||||||
).first()
|
).first()
|
||||||
|
|||||||
@@ -57,7 +57,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif payment_info.payment_type == "izettle" %}
|
{% elif payment_info.payment_type == "izettle" %}
|
||||||
<dt>{% trans "Payment provider" %}</dt>
|
<dt>{% trans "Payment provider" %}</dt>
|
||||||
<dd>iZettle</dd>
|
<dd>Zettle</dd>
|
||||||
|
{% if payment_info.payment_data.reference %}
|
||||||
|
<dt>{% trans "Paymeant reference" %}</dt>
|
||||||
|
<dd>{{ payment_info.payment_data.reference }}</dd>
|
||||||
|
{% endif %}
|
||||||
<dt>{% trans "Payment Application" %}</dt>
|
<dt>{% trans "Payment Application" %}</dt>
|
||||||
<dd>{{ payment_info.payment_data.applicationName }}</dd>
|
<dd>{{ payment_info.payment_data.applicationName }}</dd>
|
||||||
<dt>{% trans "Card Entry Mode" %}</dt>
|
<dt>{% trans "Card Entry Mode" %}</dt>
|
||||||
|
|||||||
@@ -93,6 +93,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="alert alert-info" v-if="missingItems.length">
|
||||||
|
<p>
|
||||||
|
{% trans "Your rule always filters by product or variation, but the following products or variations are not contained in any of your rule parts so people with these tickets will not get in:" %}
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li v-for="h in missingItems">{{ "{" }}{h}{{ "}" }}</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
{% trans "Please double-check if this was intentional." %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="disabled-withoutjs sr-only">
|
<div class="disabled-withoutjs sr-only">
|
||||||
{{ form.rules }}
|
{{ form.rules }}
|
||||||
@@ -105,6 +116,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
{{ items|json_script:"items" }}
|
||||||
|
|
||||||
{% compress js %}
|
{% compress js %}
|
||||||
<script type="text/javascript" src="{% static "vuejs/vue.js" %}"></script>
|
<script type="text/javascript" src="{% static "vuejs/vue.js" %}"></script>
|
||||||
@@ -118,6 +130,7 @@
|
|||||||
<script type="text/javascript" src="{% static "d3/d3-transition.v2.js" %}"></script>
|
<script type="text/javascript" src="{% static "d3/d3-transition.v2.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "d3/d3-drag.v2.js" %}"></script>
|
<script type="text/javascript" src="{% static "d3/d3-drag.v2.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "d3/d3-zoom.v2.js" %}"></script>
|
<script type="text/javascript" src="{% static "d3/d3-zoom.v2.js" %}"></script>
|
||||||
|
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/checkinrules/jsonlogic-boolalg.js" %}"></script>
|
||||||
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/datetimefield.vue' %}"></script>
|
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/datetimefield.vue' %}"></script>
|
||||||
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/timefield.vue' %}"></script>
|
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/timefield.vue' %}"></script>
|
||||||
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/lookup-select2.vue' %}"></script>
|
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/lookup-select2.vue' %}"></script>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<p class="">
|
<p class="">
|
||||||
<a href="{% url "control:events" %}?ordering=date_from&status=-date_to" class="">
|
<a href="{% url "control:events" %}?ordering=date_from&status=date_past" class="">
|
||||||
{% trans "View all recent events" %}
|
{% trans "View all recent events" %}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
{% if show_meta %}
|
||||||
|
{% if plugin.author %}
|
||||||
|
<p class="meta text-muted">
|
||||||
|
{% blocktrans trimmed with a=plugin.author %}
|
||||||
|
by <em>{{ a }}</em>
|
||||||
|
{% endblocktrans %}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
<p>{{ plugin.description|safe }}</p>
|
||||||
|
{% if plugin.restricted and plugin.module not in request.event.settings.allowed_restricted_plugins %}
|
||||||
|
<p class="text-muted">
|
||||||
|
<span class="fa fa-info-circle" aria-hidden="true"></span>
|
||||||
|
{% trans "This plugin needs to be enabled by a system administrator for your account." %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if plugin.app.compatibility_errors %}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
{% trans "This plugin cannot be enabled for the following reasons:" %}
|
||||||
|
<ul>
|
||||||
|
{% for e in plugin.app.compatibility_errors %}
|
||||||
|
<li>{{ e }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if plugin.app.compatibility_warnings %}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
{% trans "This plugin reports the following problems:" %}
|
||||||
|
<ul>
|
||||||
|
{% for e in plugin.app.compatibility_warnings %}
|
||||||
|
<li>{{ e }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
@@ -1,8 +1,15 @@
|
|||||||
{% extends "pretixcontrol/event/settings_base.html" %}
|
{% extends "pretixcontrol/event/settings_base.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% block inside %}
|
{% block inside %}
|
||||||
<h1>{% trans "Installed plugins" %}</h1>
|
<h1>{% trans "Available plugins" %}</h1>
|
||||||
|
<p>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
On this page, you can choose plugins you want to enable for your event. Plugins might bring additional
|
||||||
|
software functionality, connect your event to third-party services, or apply other forms of customizations.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
<form action="" method="post" class="form-horizontal form-plugins">
|
<form action="" method="post" class="form-horizontal form-plugins">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% if "success" in request.GET %}
|
{% if "success" in request.GET %}
|
||||||
@@ -11,71 +18,71 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="tabbed-form">
|
<div class="tabbed-form">
|
||||||
{% for cat, catlabel, plist in plugins %}
|
{% for cat, catlabel, plist, has_pictures in plugins %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{{ catlabel }}</legend>
|
<legend>{{ catlabel }}</legend>
|
||||||
<div class="table-responsive">
|
<div class="plugin-list">
|
||||||
<table class="table">
|
{% for plugin in plist %}
|
||||||
{% for plugin in plist %}
|
<div class="plugin-container {% if plugin.featured %}featured-plugin{% endif %}">
|
||||||
<tr class="{% if plugin.app.compatibility_errors %}warning{% elif plugin.module in plugins_active %}success{% else %}default{% endif %}">
|
{% if plugin.featured %}
|
||||||
<td>
|
<div class="panel panel-default">
|
||||||
<strong>{{ plugin.name }}</strong>
|
<div class="panel-body">
|
||||||
{% if plugin.author %}
|
{% endif %}
|
||||||
<p class="meta text-muted">
|
<div class="plugin-text">
|
||||||
{% blocktrans trimmed with v=plugin.version a=plugin.author %}
|
{% if plugin.featured or plugin.experimental %}
|
||||||
Version {{ v }} by <em>{{ a }}</em>
|
<p class="text-muted">
|
||||||
{% endblocktrans %}</p>
|
{% if plugin.featured %}
|
||||||
{% else %}
|
<span class="fa fa-thumbs-up" aria-hidden="true"></span>
|
||||||
<p class="meta text-muted">
|
{% trans "Top recommendation" %}
|
||||||
{% blocktrans trimmed with v=plugin.version a=plugin.author %}
|
{% endif %}
|
||||||
Version {{ v }}
|
{% if plugin.experimental %}
|
||||||
{% endblocktrans %}</p>
|
<span class="fa fa-flask" aria-hidden="true"></span>
|
||||||
|
{% trans "Experimental feature" %}
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p>{{ plugin.description }}</p>
|
{% if plugin.picture %}
|
||||||
{% if plugin.restricted and plugin.module not in request.event.settings.allowed_restricted_plugins %}
|
<p><img src="{% static plugin.picture %}" class="plugin-picture"></p>
|
||||||
<span class="text-muted">
|
|
||||||
{% trans "This plugin needs to be enabled by a system administrator for your account." %}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<h4>
|
||||||
|
{{ plugin.name }}
|
||||||
|
{% if show_meta %}
|
||||||
|
<span class="text-muted text-sm">{{ plugin.version }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if plugin.module in plugins_active %}
|
||||||
|
<span class="label label-success">
|
||||||
|
<span class="fa fa-check" aria-hidden="true"></span>
|
||||||
|
{% trans "Active" %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</h4>
|
||||||
|
{% include "pretixcontrol/event/fragment_plugin_description.html" with plugin=plugin %}
|
||||||
|
</div>
|
||||||
{% if plugin.app.compatibility_errors %}
|
{% if plugin.app.compatibility_errors %}
|
||||||
<div class="alert alert-warning">
|
<div class="plugin-action">
|
||||||
{% trans "This plugin cannot be enabled for the following reasons:" %}
|
<span class="text-muted">{% trans "Incompatible" %}</span>
|
||||||
<ul>
|
</div>
|
||||||
{% for e in plugin.app.compatibility_errors %}
|
{% elif plugin.restricted and plugin.module not in request.event.settings.allowed_restricted_plugins %}
|
||||||
<li>{{ e }}</li>
|
<div class="plugin-action">
|
||||||
{% endfor %}
|
<span class="text-muted">{% trans "Not available" %}</span>
|
||||||
</ul>
|
</div>
|
||||||
|
{% elif plugin.module in plugins_active %}
|
||||||
|
<div class="plugin-action flip">
|
||||||
|
<button class="btn btn-default{% if plugin.featured %} btn-lg{% endif %}" name="plugin:{{ plugin.module }}"
|
||||||
|
value="disable">{% trans "Disable" %}</button>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="plugin-action flip">
|
||||||
|
<button class="btn btn-primary{% if plugin.featured %} btn-lg{% endif %}" name="plugin:{{ plugin.module }}"
|
||||||
|
value="enable">{% trans "Enable" %}</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if plugin.app.compatibility_warnings %}
|
{% if plugin.featured %}
|
||||||
<div class="alert alert-warning">
|
</div>
|
||||||
{% trans "This plugin reports the following problems:" %}
|
</div>
|
||||||
<ul>
|
{% endif %}
|
||||||
{% for e in plugin.app.compatibility_warnings %}
|
</div>
|
||||||
<li>{{ e }}</li>
|
{% endfor %}
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="text-right flip" width="20%">
|
|
||||||
{% if plugin.app.compatibility_errors %}
|
|
||||||
<button class="btn disabled btn-block btn-default"
|
|
||||||
disabled="disabled">{% trans "Incompatible" %}</button>
|
|
||||||
{% elif plugin.restricted and plugin.module not in request.event.settings.allowed_restricted_plugins %}
|
|
||||||
<button class="btn disabled btn-block btn-default"
|
|
||||||
disabled="disabled">{% trans "Not available" %}</button>
|
|
||||||
{% elif plugin.module in plugins_active %}
|
|
||||||
<button class="btn btn-default btn-block" name="plugin:{{ plugin.module }}"
|
|
||||||
value="disable">{% trans "Disable" %}</button>
|
|
||||||
{% else %}
|
|
||||||
<button class="btn btn-default btn-block" name="plugin:{{ plugin.module }}"
|
|
||||||
value="enable">{% trans "Enable" %}</button>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -221,6 +221,7 @@
|
|||||||
{% bootstrap_field sform.show_items_outside_presale_period layout="control" %}
|
{% bootstrap_field sform.show_items_outside_presale_period layout="control" %}
|
||||||
{% bootstrap_field sform.last_order_modification_date layout="control" %}
|
{% bootstrap_field sform.last_order_modification_date layout="control" %}
|
||||||
{% bootstrap_field sform.allow_modifications_after_checkin layout="control" %}
|
{% bootstrap_field sform.allow_modifications_after_checkin layout="control" %}
|
||||||
|
{% bootstrap_field sform.show_checkin_number_user layout="control" %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "Display" %}</legend>
|
<legend>{% trans "Display" %}</legend>
|
||||||
|
|||||||
@@ -20,6 +20,11 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-md-3 control-label">{% trans "Product type" %}</label>
|
<label class="col-md-3 control-label">{% trans "Product type" %}</label>
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
|
{% for e in form.errors.admission %}
|
||||||
|
<div class="alert alert-danger has-error">
|
||||||
|
{{ e }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
<div class="big-radio radio">
|
<div class="big-radio radio">
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" value="on" name="{{ form.admission.html_name }}" {% if form.admission.value %}checked{% endif %}>
|
<input type="radio" value="on" name="{{ form.admission.html_name }}" {% if form.admission.value %}checked{% endif %}>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{% extends "pretixcontrol/event/base.html" %}
|
{% extends "pretixcontrol/event/base.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
|
{% load money %}
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans "Cancel order" %}
|
{% trans "Cancel order" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -22,13 +23,22 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="status" value="c"/>
|
<input type="hidden" name="status" value="c"/>
|
||||||
{% bootstrap_form_errors form %}
|
{% bootstrap_form_errors form %}
|
||||||
|
{% if form.cancellation_fee %}
|
||||||
|
{% if fee %}
|
||||||
|
{% with fee|money:request.event.currency as f %}
|
||||||
|
<p>{% blocktrans trimmed with fee="<strong>"|add:f|add:"</strong>"|safe %}
|
||||||
|
The configured cancellation fee for a self-service cancellation would be {{ fee }} for this
|
||||||
|
order, but for a cancellation performed by you, you need to set the cancellation fee here:
|
||||||
|
{% endblocktrans %}</p>
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
{% bootstrap_field form.cancellation_fee layout='' %}
|
||||||
|
{% endif %}
|
||||||
{% bootstrap_field form.send_email layout='' %}
|
{% bootstrap_field form.send_email layout='' %}
|
||||||
|
{% bootstrap_field form.comment layout='' %}
|
||||||
{% if form.cancel_invoice %}
|
{% if form.cancel_invoice %}
|
||||||
{% bootstrap_field form.cancel_invoice layout='' %}
|
{% bootstrap_field form.cancel_invoice layout='' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if form.cancellation_fee %}
|
|
||||||
{% bootstrap_field form.cancellation_fee layout='' %}
|
|
||||||
{% endif %}
|
|
||||||
<div class="row checkout-button-row">
|
<div class="row checkout-button-row">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<a class="btn btn-block btn-default btn-lg"
|
<a class="btn btn-block btn-default btn-lg"
|
||||||
|
|||||||
@@ -202,10 +202,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% bootstrap_field position.form.operation_cancel layout='inline' %}
|
{% bootstrap_field position.form.operation_cancel layout='inline' %}
|
||||||
{% bootstrap_field position.form.operation_split layout='inline' %}
|
{% if position.form.operation_split %}
|
||||||
|
{% bootstrap_field position.form.operation_split layout='inline' %}
|
||||||
|
{% endif %}
|
||||||
{% if position.addons.exists %}
|
{% if position.addons.exists %}
|
||||||
<em class="text-danger">
|
<em class="text-danger">
|
||||||
{% trans "Removing this position will also remove all add-ons to this position." %}
|
{% trans "Removing or splitting this position will also remove or split all add-ons to this position." %}
|
||||||
</em>
|
</em>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>
|
<h1>
|
||||||
{% trans "Refund order" %}
|
{% trans "Refund order" %}
|
||||||
|
<small class="text-muted">{{ full_refund|money:request.event.currency }}</small>
|
||||||
<a class="btn btn-link btn-lg"
|
<a class="btn btn-link btn-lg"
|
||||||
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
||||||
{% blocktrans trimmed with order=order.code %}
|
{% blocktrans trimmed with order=order.code %}
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</h1>
|
</h1>
|
||||||
{% for e in exporters %}
|
{% for e in exporters %}
|
||||||
<details class="panel panel-default" {% if "identifier" in request.GET or "exporter" in request.POST %}open{% endif %}>
|
<details class="panel panel-default"
|
||||||
|
{% if request.GET.identifier == e.identifier or request.POST.exporter == e.identifier %}open{% endif %}>
|
||||||
<summary class="panel-heading">
|
<summary class="panel-heading">
|
||||||
<h3 class="panel-title">
|
<h3 class="panel-title">
|
||||||
{{ e.verbose_name }}
|
{{ e.verbose_name }}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{% extends "pretixcontrol/organizers/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% block inner %}
|
||||||
|
<h1>
|
||||||
|
{% trans "Change multiple devices" %}
|
||||||
|
<small>
|
||||||
|
{% blocktrans trimmed with number=devices.count %}
|
||||||
|
{{ number }} selected
|
||||||
|
{% endblocktrans %}
|
||||||
|
</small>
|
||||||
|
</h1>
|
||||||
|
<form class="form-horizontal" action="" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form_errors form %}
|
||||||
|
<div class="hidden">
|
||||||
|
{% for d in devices %}
|
||||||
|
<input type="hidden" name="device" value="{{ d.pk }}">
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<fieldset>
|
||||||
|
<legend>{% trans "General" %}</legend>
|
||||||
|
<div class="bulk-edit-field-group">
|
||||||
|
<label class="field-toggle">
|
||||||
|
<input type="checkbox" name="_bulk" value="{{ form.prefix }}__events" {% if form.prefix|add:"__events" in bulk_selected %}checked{% endif %}>
|
||||||
|
{% trans "change" context "form_bulk" %}
|
||||||
|
</label>
|
||||||
|
<div class="field-content">
|
||||||
|
{% bootstrap_field form.all_events layout="control" %}
|
||||||
|
{% bootstrap_field form.limit_events layout="control" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<p> </p>
|
||||||
|
<fieldset>
|
||||||
|
<legend>{% trans "Advanced settings" %}</legend>
|
||||||
|
{% bootstrap_field form.security_profile layout="bulkedit" %}
|
||||||
|
{% bootstrap_field form.gate layout="bulkedit" %}
|
||||||
|
</fieldset>
|
||||||
|
<div class="form-group submit-group">
|
||||||
|
<button type="submit" class="btn btn-primary btn-save">
|
||||||
|
{% trans "Save" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<a href="{% url "control:organizer.device.add" organizer=request.organizer.slug %}"
|
<a href="{% url "control:organizer.device.add" organizer=request.organizer.slug %}"
|
||||||
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Connect a device" %}</a>
|
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Connect a device" %}</a>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
@@ -53,101 +53,139 @@
|
|||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
<a href="{% url "control:organizer.device.add" organizer=request.organizer.slug %}"
|
<a href="{% url "control:organizer.device.add" organizer=request.organizer.slug %}"
|
||||||
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Connect a device" %}</a>
|
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Connect a device" %}</a>
|
||||||
</p>
|
</p>
|
||||||
<div class="table-responsive">
|
<form action="{% url "control:organizer.device.bulk_edit" organizer=request.organizer.slug %}" method="post">
|
||||||
<table class="table table-condensed table-hover">
|
{% csrf_token %}
|
||||||
<thead>
|
<div class="hidden">
|
||||||
<tr>
|
{{ filter_form.as_p }}
|
||||||
<th>{% trans "Device ID" %}
|
</div>
|
||||||
<a href="?{% url_replace request 'ordering' '-device_id' %}"><i class="fa fa-caret-down"></i></a>
|
<div class="table-responsive">
|
||||||
<a href="?{% url_replace request 'ordering' 'device_id' %}"><i class="fa fa-caret-up"></i></a></th>
|
<table class="table table-condensed table-hover table-quotas">
|
||||||
</th>
|
<thead>
|
||||||
<th>{% trans "Name" %}
|
<tr>
|
||||||
<a href="?{% url_replace request 'ordering' '-name' %}"><i class="fa fa-caret-down"></i></a>
|
<th>
|
||||||
<a href="?{% url_replace request 'ordering' 'name' %}"><i class="fa fa-caret-up"></i></a></th>
|
<label aria-label="{% trans "select all rows for batch-operation" %}"
|
||||||
</th>
|
class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
|
||||||
<th>{% trans "Hardware model" %}</th>
|
</th>
|
||||||
<th>{% trans "Software" %}</th>
|
<th>{% trans "Device ID" %}
|
||||||
<th>{% trans "Setup date" %}
|
<a href="?{% url_replace request 'ordering' '-device_id' %}"><i
|
||||||
<a href="?{% url_replace request 'ordering' '-initialized' %}"><i class="fa fa-caret-down"></i></a>
|
class="fa fa-caret-down"></i></a>
|
||||||
<a href="?{% url_replace request 'ordering' 'initialized' %}"><i class="fa fa-caret-up"></i></a></th>
|
<a href="?{% url_replace request 'ordering' 'device_id' %}"><i
|
||||||
</th>
|
class="fa fa-caret-up"></i></a>
|
||||||
<th>{% trans "Events" %}</th>
|
</th>
|
||||||
<th></th>
|
<th>{% trans "Name" %}
|
||||||
</tr>
|
<a href="?{% url_replace request 'ordering' '-name' %}"><i class="fa fa-caret-down"></i></a>
|
||||||
</thead>
|
<a href="?{% url_replace request 'ordering' 'name' %}"><i class="fa fa-caret-up"></i></a>
|
||||||
<tbody>
|
</th>
|
||||||
{% for d in devices %}
|
<th>{% trans "Hardware model" %}</th>
|
||||||
<tr {% if d.revoked %}class="text-muted"{% endif %}>
|
<th>{% trans "Software" %}</th>
|
||||||
<td>
|
<th>{% trans "Setup date" %}
|
||||||
{{ d.device_id }}
|
<a href="?{% url_replace request 'ordering' '-initialized' %}"><i
|
||||||
</td>
|
class="fa fa-caret-down"></i></a>
|
||||||
<td>
|
<a href="?{% url_replace request 'ordering' 'initialized' %}"><i class="fa fa-caret-up"></i></a>
|
||||||
{% if d.revoked %}<del>{% endif %}
|
</th>
|
||||||
{{ d.name }}
|
<th>{% trans "Events" %}</th>
|
||||||
{% if d.revoked %}</del>{% endif %}
|
<th></th>
|
||||||
{% if d.gate %}
|
|
||||||
<br>
|
|
||||||
<small class="text-muted">{{ d.gate.name }}</small>
|
|
||||||
{% endif %}
|
|
||||||
<br>
|
|
||||||
<small class="text-muted">{{ d.unique_serial }}</small>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ d.hardware_brand|default_if_none:"" }} {{ d.hardware_model|default_if_none:"" }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ d.software_brand|default_if_none:"" }} {{ d.software_version|default_if_none:"" }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if d.initialized %}
|
|
||||||
{{ d.initialized|date:"SHORT_DATETIME_FORMAT" }}
|
|
||||||
{% else %}
|
|
||||||
<em>{% trans "Not yet initialized" %}</em>
|
|
||||||
{% endif %}
|
|
||||||
{% if d.revoked %}
|
|
||||||
<span class="label label-danger">{% trans "Revoked" %}</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if d.all_events %}
|
|
||||||
{% trans "All" %}
|
|
||||||
{% else %}
|
|
||||||
<ul>
|
|
||||||
{% for e in d.limit_events.all %}
|
|
||||||
<li>
|
|
||||||
<a href="{% url "control:event.index" organizer=request.organizer.slug event=e.slug %}">
|
|
||||||
{{ e }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="text-right flip">
|
|
||||||
{% if not d.initialized %}
|
|
||||||
<a href="{% url "control:organizer.device.connect" organizer=request.organizer.slug device=d.id %}"
|
|
||||||
class="btn btn-primary btn-sm"><i class="fa fa-link"></i>
|
|
||||||
{% trans "Connect" %}</a>
|
|
||||||
{% elif d.api_token %}
|
|
||||||
<a href="{% url "control:organizer.device.revoke" organizer=request.organizer.slug device=d.id %}"
|
|
||||||
class="btn btn-default btn-sm">
|
|
||||||
{% trans "Revoke access" %}</a>
|
|
||||||
{% endif %}
|
|
||||||
<a href="{% url "control:organizer.device.logs" organizer=request.organizer.slug device=d.id %}"
|
|
||||||
class="btn btn-default btn-sm">
|
|
||||||
<span class="fa fa-list-alt"></span>
|
|
||||||
{% trans "Logs" %}
|
|
||||||
</a>
|
|
||||||
<a href="{% url "control:organizer.device.edit" organizer=request.organizer.slug device=d.id %}"
|
|
||||||
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% if page_obj.paginator.num_pages > 1 %}
|
||||||
</tbody>
|
<tr class="table-select-all warning hidden">
|
||||||
</table>
|
<td>
|
||||||
</div>
|
<input type="checkbox" name="__ALL" id="__all"
|
||||||
|
data-results-total="{{ page_obj.paginator.count }}">
|
||||||
|
</td>
|
||||||
|
<td colspan="7">
|
||||||
|
<label for="__all">
|
||||||
|
{% trans "Select all results on other pages as well" %}
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for d in devices %}
|
||||||
|
<tr {% if d.revoked %}class="text-muted"{% endif %}>
|
||||||
|
<td>
|
||||||
|
<label aria-label="{% trans "select row for batch-operation" %}"
|
||||||
|
class="batch-select-label"><input type="checkbox" name="device"
|
||||||
|
class="batch-select-checkbox"
|
||||||
|
value="{{ d.pk }}"/></label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ d.device_id }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if d.revoked %}
|
||||||
|
<del>{% endif %}
|
||||||
|
{{ d.name }}
|
||||||
|
{% if d.revoked %}</del>{% endif %}
|
||||||
|
{% if d.gate %}
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">{{ d.gate.name }}</small>
|
||||||
|
{% endif %}
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">{{ d.unique_serial }}</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ d.hardware_brand|default_if_none:"" }} {{ d.hardware_model|default_if_none:"" }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ d.software_brand|default_if_none:"" }} {{ d.software_version|default_if_none:"" }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if d.initialized %}
|
||||||
|
{{ d.initialized|date:"SHORT_DATETIME_FORMAT" }}
|
||||||
|
{% else %}
|
||||||
|
<em>{% trans "Not yet initialized" %}</em>
|
||||||
|
{% endif %}
|
||||||
|
{% if d.revoked %}
|
||||||
|
<span class="label label-danger">{% trans "Revoked" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if d.all_events %}
|
||||||
|
{% trans "All" %}
|
||||||
|
{% else %}
|
||||||
|
<ul>
|
||||||
|
{% for e in d.limit_events.all %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url "control:event.index" organizer=request.organizer.slug event=e.slug %}">
|
||||||
|
{{ e }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-right flip">
|
||||||
|
{% if not d.initialized %}
|
||||||
|
<a href="{% url "control:organizer.device.connect" organizer=request.organizer.slug device=d.id %}"
|
||||||
|
class="btn btn-primary btn-sm"><i class="fa fa-link"></i>
|
||||||
|
{% trans "Connect" %}</a>
|
||||||
|
{% elif d.api_token %}
|
||||||
|
<a href="{% url "control:organizer.device.revoke" organizer=request.organizer.slug device=d.id %}"
|
||||||
|
class="btn btn-default btn-sm">
|
||||||
|
{% trans "Revoke access" %}</a>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{% url "control:organizer.device.logs" organizer=request.organizer.slug device=d.id %}"
|
||||||
|
class="btn btn-default btn-sm">
|
||||||
|
<span class="fa fa-list-alt"></span>
|
||||||
|
{% trans "Logs" %}
|
||||||
|
</a>
|
||||||
|
<a href="{% url "control:organizer.device.edit" organizer=request.organizer.slug device=d.id %}"
|
||||||
|
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="batch-select-actions">
|
||||||
|
<button type="submit" class="btn btn-primary btn-save" name="action" value="edit">
|
||||||
|
<i class="fa fa-edit"></i>{% trans "Edit selected" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
{% include "pretixcontrol/pagination.html" %}
|
{% include "pretixcontrol/pagination.html" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
</script>
|
</script>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default panel-pdf-editor">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<div class="pull-right flip">
|
<div class="pull-right flip">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
@@ -48,6 +48,8 @@
|
|||||||
{% trans "Editor" %}
|
{% trans "Editor" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
<ul class="nav nav-pills" id="page_nav">
|
||||||
|
</ul>
|
||||||
<div id="editor-canvas-area">
|
<div id="editor-canvas-area">
|
||||||
<canvas id="pdf-canvas"
|
<canvas id="pdf-canvas"
|
||||||
data-pdf-url="{{ pdf }}"
|
data-pdf-url="{{ pdf }}"
|
||||||
@@ -193,7 +195,7 @@
|
|||||||
<span class="btn btn-default fileinput-button background-button">
|
<span class="btn btn-default fileinput-button background-button">
|
||||||
<i class="fa fa-upload"></i>
|
<i class="fa fa-upload"></i>
|
||||||
<span>{% trans "Upload custom background" %}</span>
|
<span>{% trans "Upload custom background" %}</span>
|
||||||
<input id="fileupload" type="file" name="background">
|
<input id="fileupload" type="file" name="background" accept="application/pdf">
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-12 help-inline">
|
<div class="col-sm-12 help-inline">
|
||||||
@@ -204,6 +206,14 @@
|
|||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<p>
|
||||||
|
<a class="btn btn-default background-download-button" href="{{ pdf }}" target="_blank">
|
||||||
|
<i class="fa fa-download"></i>
|
||||||
|
<span>{% trans "Download current background" %}</span>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row control-group pdf-info">
|
<div class="row control-group pdf-info">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
@@ -357,12 +367,14 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row control-group text">
|
<div class="row control-group text textcontent">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<label>{% trans "Text content" %}</label><br>
|
<label>{% trans "Content" %}</label><br>
|
||||||
<select class="input-block-level form-control" id="toolbox-content">
|
<select class="input-block-level form-control" id="toolbox-content">
|
||||||
{% for varname, var in variables.items %}
|
{% for varname, var in variables.items %}
|
||||||
<option data-sample="{{ var.editor_sample }}" value="{{ varname }}">{{ var.label }}</option>
|
{% if not var.hidden %}
|
||||||
|
<option data-sample="{{ var.editor_sample }}" {% if var.migrate_from %}data-old-value="{{ var.migrate_from }}"{% endif %} value="{{ varname }}">{{ var.label }}</option>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for p in request.organizer.meta_properties.all %}
|
{% for p in request.organizer.meta_properties.all %}
|
||||||
<option value="meta:{{ p.name }}">
|
<option value="meta:{{ p.name }}">
|
||||||
@@ -374,10 +386,19 @@
|
|||||||
{% trans "Item attribute:" %} {{ p.name }}
|
{% trans "Item attribute:" %} {{ p.name }}
|
||||||
</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
<option value="other_i18n">{% trans "Other… (multilingual)" %}</option>
|
||||||
<option value="other">{% trans "Other…" %}</option>
|
<option value="other">{% trans "Other…" %}</option>
|
||||||
</select>
|
</select>
|
||||||
<textarea type="text" value="" class="input-block-level form-control"
|
<textarea type="text" value="" class="input-block-level form-control"
|
||||||
id="toolbox-content-other"></textarea>
|
id="toolbox-content-other"></textarea>
|
||||||
|
<div class="i18n-form-group" id="toolbox-content-other-i18n">
|
||||||
|
{% for l in request.event.settings.locales %}
|
||||||
|
<textarea id="toolbox-content-other-{{ l }}" rows="3" class="input-block-level form-control" title="{{ l }}" lang="{{ l }}"></textarea>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<p class="help-block" id="toolbox-content-other-help">
|
||||||
|
<a href="?placeholders=true" target="_blank">{% trans "Show available placeholders" %}</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -401,13 +422,20 @@
|
|||||||
<span class="fa fa-qrcode"></span>
|
<span class="fa fa-qrcode"></span>
|
||||||
{% trans "QR code for Lead Scanning" %}
|
{% trans "QR code for Lead Scanning" %}
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-default btn-block" id="editor-add-qrcode-other"
|
||||||
|
data-content="secret"
|
||||||
|
disabled>
|
||||||
|
<span class="fa fa-qrcode"></span>
|
||||||
|
{% trans "Other QR code" %}
|
||||||
|
</button>
|
||||||
<button class="btn btn-default btn-block" id="editor-add-poweredby"
|
<button class="btn btn-default btn-block" id="editor-add-poweredby"
|
||||||
data-content="dark"
|
data-content="dark"
|
||||||
disabled>
|
disabled>
|
||||||
<span class="fa fa-image"></span>
|
<span class="fa fa-image"></span>
|
||||||
{% trans "pretix Logo" %}
|
{% trans "pretix Logo" %}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-default btn-block" id="editor-add-image" disabled>
|
<button class="btn btn-default btn-block" id="editor-add-image" disabled
|
||||||
|
data-toggle="tooltip" title="{% trans "You can use this to add user-uploaded pictures from questions or pictures generated by plugins. If you want to embed a logo or other images, use a custom background instead." %}">
|
||||||
<span class="fa fa-image"></span>
|
<span class="fa fa-image"></span>
|
||||||
{% trans "Dynamic image" %}
|
{% trans "Dynamic image" %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
{% extends "pretixcontrol/event/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
{% load compress %}
|
||||||
|
{% block title %}{% trans "PDF Editor" %}{% endblock %}
|
||||||
|
{% block custom_header %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% compress css %}
|
||||||
|
<link type="text/css" rel="stylesheet" href="{% static "pretixcontrol/scss/pdfeditor.css" %}">
|
||||||
|
{% endcompress %}
|
||||||
|
<link type="text/css" rel="stylesheet" href="{% url "control:pdf.css" %}">
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>
|
||||||
|
{% trans "PDF Editor" %}
|
||||||
|
<small>{% trans "Available placeholders" %}</small>
|
||||||
|
</h1>
|
||||||
|
<p>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
You can use placeholders in custom texts on tickets to enrich your text with individual data. Which
|
||||||
|
placeholders are available depends on your event settings, activated plugins, the selected product,
|
||||||
|
as well as user input.
|
||||||
|
This page lists all placeholders technically available for your event, however most of them can also
|
||||||
|
be empty in some cases depending on configuration.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover table-condensed">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Placeholder" %}</th>
|
||||||
|
<th>{% trans "Description" %}</th>
|
||||||
|
<th>{% trans "Formatting example" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for varname, var in variables.items %}
|
||||||
|
{% if not var.hidden %}
|
||||||
|
<tr>
|
||||||
|
<td><code>{{ "{" }}{{ varname }}{{ "}" }}</code></td>
|
||||||
|
<td>{{ var.label }}</td>
|
||||||
|
<td>{{ var.editor_sample }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% for p in request.organizer.meta_properties.all %}
|
||||||
|
<tr>
|
||||||
|
<td><code>{{ "{" }}meta:{{ p.name }}{{ "}" }}</code></td>
|
||||||
|
<td>
|
||||||
|
{% trans "Event attribute:" %} {{ p.name }}
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% for p in request.event.item_meta_properties.all %}
|
||||||
|
<tr>
|
||||||
|
<td><code>{{ "{" }}itemmeta:{{ p.name }}{{ "}" }}</code></td>
|
||||||
|
<td>
|
||||||
|
{% trans "Item attribute:" %} {{ p.name }}
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
@@ -188,7 +188,7 @@
|
|||||||
<button type="submit" class="btn btn-danger btn-save" name="action" value="delete">
|
<button type="submit" class="btn btn-danger btn-save" name="action" value="delete">
|
||||||
<i class="fa fa-trash"></i>{% trans "Delete selected" %}
|
<i class="fa fa-trash"></i>{% trans "Delete selected" %}
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" class="btn btn-primary btn-save" name="action" value="disable"
|
<button type="submit" class="btn btn-primary btn-save" name="action" value="edit"
|
||||||
formaction="{% url "control:event.subevents.bulkedit" organizer=request.event.organizer.slug event=request.event.slug %}">
|
formaction="{% url "control:event.subevents.bulkedit" organizer=request.event.organizer.slug event=request.event.slug %}">
|
||||||
<i class="fa fa-edit"></i>{% trans "Edit selected" %}
|
<i class="fa fa-edit"></i>{% trans "Edit selected" %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -164,6 +164,8 @@ urlpatterns = [
|
|||||||
re_path(r'^organizer/(?P<organizer>[^/]+)/devices$', organizer.DeviceListView.as_view(), name='organizer.devices'),
|
re_path(r'^organizer/(?P<organizer>[^/]+)/devices$', organizer.DeviceListView.as_view(), name='organizer.devices'),
|
||||||
re_path(r'^organizer/(?P<organizer>[^/]+)/device/add$', organizer.DeviceCreateView.as_view(),
|
re_path(r'^organizer/(?P<organizer>[^/]+)/device/add$', organizer.DeviceCreateView.as_view(),
|
||||||
name='organizer.device.add'),
|
name='organizer.device.add'),
|
||||||
|
re_path(r'^organizer/(?P<organizer>[^/]+)/device/bulk_edit$', organizer.DeviceBulkUpdateView.as_view(),
|
||||||
|
name='organizer.device.bulk_edit'),
|
||||||
re_path(r'^organizer/(?P<organizer>[^/]+)/device/(?P<device>[^/]+)/edit$', organizer.DeviceUpdateView.as_view(),
|
re_path(r'^organizer/(?P<organizer>[^/]+)/device/(?P<device>[^/]+)/edit$', organizer.DeviceUpdateView.as_view(),
|
||||||
name='organizer.device.edit'),
|
name='organizer.device.edit'),
|
||||||
re_path(r'^organizer/(?P<organizer>[^/]+)/device/(?P<device>[^/]+)/connect$', organizer.DeviceConnectView.as_view(),
|
re_path(r'^organizer/(?P<organizer>[^/]+)/device/(?P<device>[^/]+)/connect$', organizer.DeviceConnectView.as_view(),
|
||||||
|
|||||||
@@ -313,6 +313,23 @@ class CheckinListUpdate(EventPermissionRequiredMixin, UpdateView):
|
|||||||
r['Content-Security-Policy'] = 'script-src \'unsafe-eval\''
|
r['Content-Security-Policy'] = 'script-src \'unsafe-eval\''
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
return {
|
||||||
|
'items': [
|
||||||
|
{
|
||||||
|
'id': i.pk,
|
||||||
|
'name': str(i),
|
||||||
|
'variations': [
|
||||||
|
{
|
||||||
|
'id': v.pk,
|
||||||
|
'name': str(v.value)
|
||||||
|
} for v in i.variations.all()
|
||||||
|
]
|
||||||
|
} for i in self.request.event.items.filter(active=True).prefetch_related('variations')
|
||||||
|
],
|
||||||
|
**super().get_context_data(),
|
||||||
|
}
|
||||||
|
|
||||||
def get_object(self, queryset=None) -> CheckinList:
|
def get_object(self, queryset=None) -> CheckinList:
|
||||||
try:
|
try:
|
||||||
return self.request.event.checkin_lists.get(
|
return self.request.event.checkin_lists.get(
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ from ...base.i18n import language
|
|||||||
from ...base.models.items import (
|
from ...base.models.items import (
|
||||||
Item, ItemCategory, ItemMetaProperty, Question, Quota,
|
Item, ItemCategory, ItemMetaProperty, Question, Quota,
|
||||||
)
|
)
|
||||||
|
from ...base.services.mail import TolerantDict
|
||||||
from ...base.settings import SETTINGS_AFFECTING_CSS, LazyI18nStringList
|
from ...base.settings import SETTINGS_AFFECTING_CSS, LazyI18nStringList
|
||||||
from ..logdisplay import OVERVIEW_BANLIST
|
from ..logdisplay import OVERVIEW_BANLIST
|
||||||
from . import CreateView, PaginationMixin, UpdateView
|
from . import CreateView, PaginationMixin, UpdateView
|
||||||
@@ -327,15 +328,27 @@ class EventPlugins(EventSettingsViewMixin, EventPermissionRequiredMixin, Templat
|
|||||||
'FORMAT': _('Output and export formats'),
|
'FORMAT': _('Output and export formats'),
|
||||||
'API': _('API features'),
|
'API': _('API features'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
plugins_grouped = groupby(
|
||||||
|
sorted(
|
||||||
|
plugins,
|
||||||
|
key=lambda p: (
|
||||||
|
str(getattr(p, 'category', _('Other'))),
|
||||||
|
(0 if getattr(p, 'featured', False) else 1),
|
||||||
|
str(p.name).lower().replace('pretix ', '')
|
||||||
|
),
|
||||||
|
),
|
||||||
|
lambda p: str(getattr(p, 'category', _('Other')))
|
||||||
|
)
|
||||||
|
plugins_grouped = [(c, list(plist)) for c, plist in plugins_grouped]
|
||||||
|
|
||||||
context['plugins'] = sorted([
|
context['plugins'] = sorted([
|
||||||
(c, labels.get(c, c), list(plist))
|
(c, labels.get(c, c), plist, any(getattr(p, 'picture', None) for p in plist))
|
||||||
for c, plist
|
for c, plist
|
||||||
in groupby(
|
in plugins_grouped
|
||||||
sorted(plugins, key=lambda p: str(getattr(p, 'category', _('Other')))),
|
|
||||||
lambda p: str(getattr(p, 'category', _('Other')))
|
|
||||||
)
|
|
||||||
], key=lambda c: (order.index(c[0]), c[1]) if c[0] in order else (999, str(c[1])))
|
], key=lambda c: (order.index(c[0]), c[1]) if c[0] in order else (999, str(c[1])))
|
||||||
context['plugins_active'] = self.object.get_plugins()
|
context['plugins_active'] = self.object.get_plugins()
|
||||||
|
context['show_meta'] = settings.PRETIX_PLUGINS_SHOW_META
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
@@ -729,7 +742,7 @@ class MailSettingsRendererPreview(MailSettingsPreview):
|
|||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
v = str(request.event.settings.mail_text_order_placed)
|
v = str(request.event.settings.mail_text_order_placed)
|
||||||
v = v.format_map(self.placeholders('mail_text_order_placed'))
|
v = v.format_map(TolerantDict(self.placeholders('mail_text_order_placed')))
|
||||||
renderers = request.event.get_html_mail_renderers()
|
renderers = request.event.get_html_mail_renderers()
|
||||||
if request.GET.get('renderer') in renderers:
|
if request.GET.get('renderer') in renderers:
|
||||||
with rolledback_transaction():
|
with rolledback_transaction():
|
||||||
@@ -1042,6 +1055,9 @@ class EventLog(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
|||||||
elif self.request.GET.get('user'):
|
elif self.request.GET.get('user'):
|
||||||
qs = qs.filter(user_id=self.request.GET.get('user'))
|
qs = qs.filter(user_id=self.request.GET.get('user'))
|
||||||
|
|
||||||
|
if self.request.GET.get('action_type'):
|
||||||
|
qs = qs.filter(action_type=self.request.GET['action_type'])
|
||||||
|
|
||||||
if self.request.GET.get('content_type'):
|
if self.request.GET.get('content_type'):
|
||||||
qs = qs.filter(content_type=get_object_or_404(ContentType, pk=self.request.GET.get('content_type')))
|
qs = qs.filter(content_type=get_object_or_404(ContentType, pk=self.request.GET.get('content_type')))
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ class ItemList(ListView):
|
|||||||
).annotate(
|
).annotate(
|
||||||
var_count=Count('variations')
|
var_count=Count('variations')
|
||||||
).prefetch_related("category").order_by(
|
).prefetch_related("category").order_by(
|
||||||
F('category__position').desc(nulls_first=True),
|
F('category__position').asc(nulls_first=True),
|
||||||
'category', 'position'
|
'category', 'position'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -243,6 +243,8 @@ class MailSettingsSetupView(TemplateView):
|
|||||||
messages.success(request, _('Your changes have been saved.'))
|
messages.success(request, _('Your changes have been saved.'))
|
||||||
return redirect(self.get_success_url())
|
return redirect(self.get_success_url())
|
||||||
else:
|
else:
|
||||||
|
self.smtp_form._unmask_secret_fields()
|
||||||
|
|
||||||
backend = get_connection(
|
backend = get_connection(
|
||||||
backend=settings.EMAIL_CUSTOM_SMTP_BACKEND,
|
backend=settings.EMAIL_CUSTOM_SMTP_BACKEND,
|
||||||
host=self.smtp_form.cleaned_data['smtp_host'],
|
host=self.smtp_form.cleaned_data['smtp_host'],
|
||||||
|
|||||||
@@ -265,8 +265,6 @@ class EventWizard(SafeSessionWizardView):
|
|||||||
event.has_subevents = foundation_data['has_subevents']
|
event.has_subevents = foundation_data['has_subevents']
|
||||||
event.testmode = True
|
event.testmode = True
|
||||||
form_dict['basics'].save()
|
form_dict['basics'].save()
|
||||||
event.set_active_plugins(settings.PRETIX_PLUGINS_DEFAULT.split(","), allow_restricted=settings.PRETIX_PLUGINS_DEFAULT.split(","))
|
|
||||||
event.save(update_fields=['plugins'])
|
|
||||||
event.log_action(
|
event.log_action(
|
||||||
'pretix.event.added',
|
'pretix.event.added',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
@@ -299,6 +297,9 @@ class EventWizard(SafeSessionWizardView):
|
|||||||
elif self.clone_from:
|
elif self.clone_from:
|
||||||
event.copy_data_from(self.clone_from)
|
event.copy_data_from(self.clone_from)
|
||||||
else:
|
else:
|
||||||
|
event.set_active_plugins(settings.PRETIX_PLUGINS_DEFAULT.split(","),
|
||||||
|
allow_restricted=settings.PRETIX_PLUGINS_DEFAULT.split(","))
|
||||||
|
event.save(update_fields=['plugins'])
|
||||||
event.checkin_lists.create(
|
event.checkin_lists.create(
|
||||||
name=_('Default'),
|
name=_('Default'),
|
||||||
all_products=True
|
all_products=True
|
||||||
|
|||||||
@@ -1123,6 +1123,7 @@ class OrderRefundView(OrderView):
|
|||||||
return render(self.request, 'pretixcontrol/order/refund_choose.html', {
|
return render(self.request, 'pretixcontrol/order/refund_choose.html', {
|
||||||
'payments': payments,
|
'payments': payments,
|
||||||
'new_refunds': new_refunds,
|
'new_refunds': new_refunds,
|
||||||
|
'full_refund': full_refund,
|
||||||
'remainder': to_refund,
|
'remainder': to_refund,
|
||||||
'order': self.order,
|
'order': self.order,
|
||||||
'comment': comment,
|
'comment': comment,
|
||||||
@@ -1265,6 +1266,7 @@ class OrderTransition(OrderView):
|
|||||||
elif self.order.cancel_allowed() and to == 'c' and self.mark_canceled_form.is_valid():
|
elif self.order.cancel_allowed() and to == 'c' and self.mark_canceled_form.is_valid():
|
||||||
try:
|
try:
|
||||||
cancel_order(self.order.pk, user=self.request.user,
|
cancel_order(self.order.pk, user=self.request.user,
|
||||||
|
email_comment=self.mark_canceled_form.cleaned_data['comment'],
|
||||||
send_mail=self.mark_canceled_form.cleaned_data['send_email'],
|
send_mail=self.mark_canceled_form.cleaned_data['send_email'],
|
||||||
cancel_invoice=self.mark_canceled_form.cleaned_data.get('cancel_invoice', True),
|
cancel_invoice=self.mark_canceled_form.cleaned_data.get('cancel_invoice', True),
|
||||||
cancellation_fee=self.mark_canceled_form.cleaned_data.get('cancellation_fee'))
|
cancellation_fee=self.mark_canceled_form.cleaned_data.get('cancellation_fee'))
|
||||||
@@ -1302,6 +1304,7 @@ class OrderTransition(OrderView):
|
|||||||
elif self.order.cancel_allowed() and to == 'c':
|
elif self.order.cancel_allowed() and to == 'c':
|
||||||
return render(self.request, 'pretixcontrol/order/cancel.html', {
|
return render(self.request, 'pretixcontrol/order/cancel.html', {
|
||||||
'form': self.mark_canceled_form,
|
'form': self.mark_canceled_form,
|
||||||
|
'fee': self.order.user_cancel_fee,
|
||||||
'order': self.order,
|
'order': self.order,
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
@@ -1760,7 +1763,7 @@ class OrderChange(OrderView):
|
|||||||
if p.form.cleaned_data['tax_rule'] and p.form.cleaned_data['tax_rule'] != p.tax_rule:
|
if p.form.cleaned_data['tax_rule'] and p.form.cleaned_data['tax_rule'] != p.tax_rule:
|
||||||
ocm.change_tax_rule(p, p.form.cleaned_data['tax_rule'])
|
ocm.change_tax_rule(p, p.form.cleaned_data['tax_rule'])
|
||||||
|
|
||||||
if p.form.cleaned_data['operation_split']:
|
if p.form.cleaned_data.get('operation_split'):
|
||||||
ocm.split(p)
|
ocm.split(p)
|
||||||
|
|
||||||
if p.form.cleaned_data['operation_secret']:
|
if p.form.cleaned_data['operation_secret']:
|
||||||
@@ -2229,8 +2232,9 @@ class ExportMixin:
|
|||||||
def exporters(self):
|
def exporters(self):
|
||||||
exporters = []
|
exporters = []
|
||||||
responses = register_data_exporters.send(self.request.event)
|
responses = register_data_exporters.send(self.request.event)
|
||||||
|
id = self.request.GET.get("identifier") or self.request.POST.get("exporter")
|
||||||
for ex in sorted([response(self.request.event, self.request.organizer) for r, response in responses if response], key=lambda ex: str(ex.verbose_name)):
|
for ex in sorted([response(self.request.event, self.request.organizer) for r, response in responses if response], key=lambda ex: str(ex.verbose_name)):
|
||||||
if self.request.GET.get("identifier") and ex.identifier != self.request.GET.get("identifier"):
|
if id and ex.identifier != id:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Use form parse cycle to generate useful defaults
|
# Use form parse cycle to generate useful defaults
|
||||||
|
|||||||
@@ -42,14 +42,14 @@ from django.conf import settings
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.exceptions import PermissionDenied, ValidationError
|
from django.core.exceptions import PermissionDenied, ValidationError
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from django.db import transaction
|
from django.db import connections, transaction
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Count, Exists, IntegerField, Max, Min, OuterRef, Prefetch, ProtectedError,
|
Count, Exists, IntegerField, Max, Min, OuterRef, Prefetch, ProtectedError,
|
||||||
Q, Subquery, Sum,
|
Q, Subquery, Sum,
|
||||||
)
|
)
|
||||||
from django.db.models.functions import Coalesce, Greatest
|
from django.db.models.functions import Coalesce, Greatest
|
||||||
from django.forms import DecimalField
|
from django.forms import DecimalField
|
||||||
from django.http import HttpResponseBadRequest, JsonResponse
|
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
@@ -89,11 +89,11 @@ from pretix.control.forms.filter import (
|
|||||||
)
|
)
|
||||||
from pretix.control.forms.orders import ExporterForm
|
from pretix.control.forms.orders import ExporterForm
|
||||||
from pretix.control.forms.organizer import (
|
from pretix.control.forms.organizer import (
|
||||||
CustomerCreateForm, CustomerUpdateForm, DeviceForm, EventMetaPropertyForm,
|
CustomerCreateForm, CustomerUpdateForm, DeviceBulkEditForm, DeviceForm,
|
||||||
GateForm, GiftCardCreateForm, GiftCardUpdateForm, MailSettingsForm,
|
EventMetaPropertyForm, GateForm, GiftCardCreateForm, GiftCardUpdateForm,
|
||||||
MembershipTypeForm, MembershipUpdateForm, OrganizerDeleteForm,
|
MailSettingsForm, MembershipTypeForm, MembershipUpdateForm,
|
||||||
OrganizerForm, OrganizerSettingsForm, OrganizerUpdateForm, TeamForm,
|
OrganizerDeleteForm, OrganizerForm, OrganizerSettingsForm,
|
||||||
WebHookForm,
|
OrganizerUpdateForm, TeamForm, WebHookForm,
|
||||||
)
|
)
|
||||||
from pretix.control.logdisplay import OVERVIEW_BANLIST
|
from pretix.control.logdisplay import OVERVIEW_BANLIST
|
||||||
from pretix.control.permissions import (
|
from pretix.control.permissions import (
|
||||||
@@ -102,6 +102,7 @@ from pretix.control.permissions import (
|
|||||||
from pretix.control.signals import nav_organizer
|
from pretix.control.signals import nav_organizer
|
||||||
from pretix.control.views import PaginationMixin
|
from pretix.control.views import PaginationMixin
|
||||||
from pretix.control.views.mailsetup import MailSettingsSetupView
|
from pretix.control.views.mailsetup import MailSettingsSetupView
|
||||||
|
from pretix.helpers import GroupConcat
|
||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
from pretix.helpers.urls import build_absolute_uri as build_global_uri
|
from pretix.helpers.urls import build_absolute_uri as build_global_uri
|
||||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
@@ -819,11 +820,17 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class DeviceListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
class DeviceQueryMixin:
|
||||||
model = Device
|
|
||||||
template_name = 'pretixcontrol/organizers/devices.html'
|
@cached_property
|
||||||
permission = 'can_change_organizer_settings'
|
def request_data(self):
|
||||||
context_object_name = 'devices'
|
if self.request.method == "POST":
|
||||||
|
return self.request.POST
|
||||||
|
return self.request.GET
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def filter_form(self):
|
||||||
|
return DeviceFilterForm(data=self.request.GET, request=self.request)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = self.request.organizer.devices.prefetch_related(
|
qs = self.request.organizer.devices.prefetch_related(
|
||||||
@@ -831,17 +838,27 @@ class DeviceListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
|
|||||||
).order_by('revoked', '-device_id')
|
).order_by('revoked', '-device_id')
|
||||||
if self.filter_form.is_valid():
|
if self.filter_form.is_valid():
|
||||||
qs = self.filter_form.filter_qs(qs)
|
qs = self.filter_form.filter_qs(qs)
|
||||||
|
|
||||||
|
if 'device' in self.request_data and '__ALL' not in self.request_data:
|
||||||
|
qs = qs.filter(
|
||||||
|
id__in=self.request_data.getlist('device')
|
||||||
|
)
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceListView(DeviceQueryMixin, OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
||||||
|
model = Device
|
||||||
|
template_name = 'pretixcontrol/organizers/devices.html'
|
||||||
|
permission = 'can_change_organizer_settings'
|
||||||
|
context_object_name = 'devices'
|
||||||
|
paginate_by = 100
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data(**kwargs)
|
ctx = super().get_context_data(**kwargs)
|
||||||
ctx['filter_form'] = self.filter_form
|
ctx['filter_form'] = self.filter_form
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def filter_form(self):
|
|
||||||
return DeviceFilterForm(data=self.request.GET, request=self.request)
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
class DeviceCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
||||||
model = Device
|
model = Device
|
||||||
@@ -935,6 +952,125 @@ class DeviceUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
|
|||||||
return super().form_invalid(form)
|
return super().form_invalid(form)
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceBulkUpdateView(DeviceQueryMixin, OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, FormView):
|
||||||
|
template_name = 'pretixcontrol/organizers/device_bulk_edit.html'
|
||||||
|
permission = 'can_change_organizer_settings'
|
||||||
|
context_object_name = 'device'
|
||||||
|
form_class = DeviceBulkEditForm
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return super().get_queryset().prefetch_related(None).order_by()
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
return HttpResponse(status=405)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def is_submitted(self):
|
||||||
|
# Usually, django considers a form "bound" / "submitted" on every POST request. However, this view is always
|
||||||
|
# called with POST method, even if just to pass the selection of objects to work on, so we want to modify
|
||||||
|
# that behaviour
|
||||||
|
return '_bulk' in self.request.POST
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
initial = {}
|
||||||
|
mixed_values = set()
|
||||||
|
qs = self.get_queryset().annotate(
|
||||||
|
limit_events_list=Subquery(
|
||||||
|
Device.limit_events.through.objects.filter(
|
||||||
|
device_id=OuterRef('pk')
|
||||||
|
).order_by('device_id', 'event_id').values('device_id').annotate(
|
||||||
|
g=GroupConcat('event_id', separator=',')
|
||||||
|
).values('g')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
fields = {
|
||||||
|
'all_events': 'all_events',
|
||||||
|
'limit_events': 'limit_events_list',
|
||||||
|
'security_profile': 'security_profile',
|
||||||
|
'gate': 'gate',
|
||||||
|
}
|
||||||
|
for k, f in fields.items():
|
||||||
|
existing_values = list(qs.order_by(f).values(f).annotate(c=Count('*')))
|
||||||
|
if len(existing_values) == 1:
|
||||||
|
if k == 'limit_events':
|
||||||
|
if existing_values[0][f]:
|
||||||
|
initial[k] = self.request.organizer.events.filter(id__in=existing_values[0][f].split(","))
|
||||||
|
else:
|
||||||
|
initial[k] = []
|
||||||
|
else:
|
||||||
|
initial[k] = existing_values[0][f]
|
||||||
|
elif len(existing_values) > 1:
|
||||||
|
mixed_values.add(k)
|
||||||
|
initial[k] = None
|
||||||
|
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
kwargs['organizer'] = self.request.organizer
|
||||||
|
kwargs['prefix'] = 'bulkedit'
|
||||||
|
kwargs['initial'] = initial
|
||||||
|
kwargs['queryset'] = self.get_queryset()
|
||||||
|
kwargs['mixed_values'] = mixed_values
|
||||||
|
if not self.is_submitted:
|
||||||
|
kwargs['data'] = None
|
||||||
|
kwargs['files'] = None
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def get_object(self, queryset=None):
|
||||||
|
return get_object_or_404(Device, organizer=self.request.organizer, pk=self.kwargs.get('device'))
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse('control:organizer.devices', kwargs={
|
||||||
|
'organizer': self.request.organizer.slug,
|
||||||
|
})
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
|
def form_valid(self, form):
|
||||||
|
log_entries = []
|
||||||
|
|
||||||
|
# Main form
|
||||||
|
form.save()
|
||||||
|
data = {
|
||||||
|
k: (v if k != 'limit_events' else [e.id for e in v])
|
||||||
|
for k, v in form.cleaned_data.items()
|
||||||
|
if k in form.changed_data
|
||||||
|
}
|
||||||
|
data['_raw_bulk_data'] = self.request.POST.dict()
|
||||||
|
for obj in self.get_queryset():
|
||||||
|
log_entries.append(
|
||||||
|
obj.log_action('pretix.device.changed', data=data, user=self.request.user, save=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
if connections['default'].features.can_return_rows_from_bulk_insert:
|
||||||
|
LogEntry.objects.bulk_create(log_entries, batch_size=200)
|
||||||
|
LogEntry.bulk_postprocess(log_entries)
|
||||||
|
else:
|
||||||
|
for le in log_entries:
|
||||||
|
le.save()
|
||||||
|
LogEntry.bulk_postprocess(log_entries)
|
||||||
|
|
||||||
|
messages.success(self.request, _('Your changes have been saved.'))
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
ctx['devices'] = self.get_queryset()
|
||||||
|
ctx['bulk_selected'] = self.request.POST.getlist("_bulk")
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
form = self.get_form()
|
||||||
|
is_valid = (
|
||||||
|
self.is_submitted and
|
||||||
|
form.is_valid()
|
||||||
|
)
|
||||||
|
if is_valid:
|
||||||
|
return self.form_valid(form)
|
||||||
|
else:
|
||||||
|
if self.is_submitted:
|
||||||
|
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
||||||
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
|
||||||
class DeviceConnectView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
|
class DeviceConnectView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
|
||||||
model = Device
|
model = Device
|
||||||
template_name = 'pretixcontrol/organizers/device_connect.html'
|
template_name = 'pretixcontrol/organizers/device_connect.html'
|
||||||
@@ -1624,6 +1760,8 @@ class LogView(OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
|
|||||||
'user', 'content_type', 'api_token', 'oauth_application', 'device'
|
'user', 'content_type', 'api_token', 'oauth_application', 'device'
|
||||||
).order_by('-datetime')
|
).order_by('-datetime')
|
||||||
qs = qs.exclude(action_type__in=OVERVIEW_BANLIST)
|
qs = qs.exclude(action_type__in=OVERVIEW_BANLIST)
|
||||||
|
if self.request.GET.get('action_type'):
|
||||||
|
qs = qs.filter(action_type=self.request.GET['action_type'])
|
||||||
if self.request.GET.get('user'):
|
if self.request.GET.get('user'):
|
||||||
qs = qs.filter(user_id=self.request.GET.get('user'))
|
qs = qs.filter(user_id=self.request.GET.get('user'))
|
||||||
return qs
|
return qs
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import mimetypes
|
import mimetypes
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from decimal import Decimal
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -32,13 +33,14 @@ from django.core.files.storage import default_storage
|
|||||||
from django.http import (
|
from django.http import (
|
||||||
FileResponse, HttpResponse, HttpResponseBadRequest, JsonResponse,
|
FileResponse, HttpResponse, HttpResponseBadRequest, JsonResponse,
|
||||||
)
|
)
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
from PyPDF2 import PdfFileWriter
|
from PyPDF2 import PdfFileReader, PdfFileWriter
|
||||||
|
from PyPDF2.utils import PdfReadError
|
||||||
from reportlab.lib.units import mm
|
from reportlab.lib.units import mm
|
||||||
|
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
@@ -63,10 +65,17 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
|||||||
title = None
|
title = None
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
if 'placeholders' in request.GET:
|
||||||
|
return self.get_placeholders_help(request)
|
||||||
resp = super().get(request, *args, **kwargs)
|
resp = super().get(request, *args, **kwargs)
|
||||||
resp._csp_ignore = True
|
resp._csp_ignore = True
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
def get_placeholders_help(self, request):
|
||||||
|
ctx = {}
|
||||||
|
ctx['variables'] = self.get_variables()
|
||||||
|
return render(request, 'pretixcontrol/pdf/placeholders.html', ctx)
|
||||||
|
|
||||||
def process_upload(self):
|
def process_upload(self):
|
||||||
f = self.request.FILES.get('background')
|
f = self.request.FILES.get('background')
|
||||||
error = False
|
error = False
|
||||||
@@ -82,15 +91,15 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
|||||||
return None, f
|
return None, f
|
||||||
|
|
||||||
def _get_preview_position(self):
|
def _get_preview_position(self):
|
||||||
item = self.request.event.items.create(name=_("Sample product"), default_price=42.23,
|
item = self.request.event.items.create(name=_("Sample product"), default_price=Decimal('42.23'),
|
||||||
description=_("Sample product description"))
|
description=_("Sample product description"))
|
||||||
item2 = self.request.event.items.create(name=_("Sample workshop"), default_price=23.40)
|
item2 = self.request.event.items.create(name=_("Sample workshop"), default_price=Decimal('23.40'))
|
||||||
|
|
||||||
from pretix.base.models import Order
|
from pretix.base.models import Order
|
||||||
order = self.request.event.orders.create(status=Order.STATUS_PENDING, datetime=now(),
|
order = self.request.event.orders.create(status=Order.STATUS_PENDING, datetime=now(),
|
||||||
email='sample@pretix.eu',
|
email='sample@pretix.eu',
|
||||||
locale=self.request.event.settings.locale,
|
locale=self.request.event.settings.locale,
|
||||||
expires=now(), code="PREVIEW1234", total=119)
|
expires=now(), code="PREVIEW1234", total=Decimal('119.00'))
|
||||||
|
|
||||||
scheme = PERSON_NAME_SCHEMES[self.request.event.settings.name_scheme]
|
scheme = PERSON_NAME_SCHEMES[self.request.event.settings.name_scheme]
|
||||||
sample = {k: str(v) for k, v in scheme['sample'].items()}
|
sample = {k: str(v) for k, v in scheme['sample'].items()}
|
||||||
@@ -191,6 +200,17 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
|||||||
c.file = fileobj
|
c.file = fileobj
|
||||||
c.save()
|
c.save()
|
||||||
c.refresh_from_db()
|
c.refresh_from_db()
|
||||||
|
|
||||||
|
try:
|
||||||
|
bg_bytes = c.file.read()
|
||||||
|
PdfFileReader(BytesIO(bg_bytes), strict=False)
|
||||||
|
except PdfReadError as e:
|
||||||
|
return JsonResponse({
|
||||||
|
"status": "error",
|
||||||
|
"error": _('Unfortunately, we were unable to process this PDF file ({reason}).').format(
|
||||||
|
reason=str(e)
|
||||||
|
)
|
||||||
|
})
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"id": c.id,
|
"id": c.id,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ from datetime import datetime, time
|
|||||||
import pytz
|
import pytz
|
||||||
from dateutil.parser import parse
|
from dateutil.parser import parse
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.db.models import Max, Min, Q
|
from django.db.models import F, Max, Min, Q
|
||||||
from django.db.models.functions import Coalesce, Greatest
|
from django.db.models.functions import Coalesce, Greatest
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
@@ -402,7 +402,12 @@ def items_select2(request, **kwargs):
|
|||||||
|
|
||||||
qs = request.event.items.filter(
|
qs = request.event.items.filter(
|
||||||
name__icontains=i18ncomp(query)
|
name__icontains=i18ncomp(query)
|
||||||
).order_by('position')
|
).order_by(
|
||||||
|
F('category__position').asc(nulls_first=True),
|
||||||
|
'category',
|
||||||
|
'position',
|
||||||
|
'pk'
|
||||||
|
)
|
||||||
|
|
||||||
total = qs.count()
|
total = qs.count()
|
||||||
pagesize = 20
|
pagesize = 20
|
||||||
@@ -434,7 +439,14 @@ def variations_select2(request, **kwargs):
|
|||||||
for word in query.split():
|
for word in query.split():
|
||||||
q &= Q(value__icontains=i18ncomp(word)) | Q(item__name__icontains=i18ncomp(ord))
|
q &= Q(value__icontains=i18ncomp(word)) | Q(item__name__icontains=i18ncomp(ord))
|
||||||
|
|
||||||
qs = ItemVariation.objects.filter(q).order_by('item__position', 'item__name', 'position', 'value').select_related('item')
|
qs = ItemVariation.objects.filter(q).order_by(
|
||||||
|
F('item__category__position').asc(nulls_first=True),
|
||||||
|
'item__category_id',
|
||||||
|
'item__position',
|
||||||
|
'item__pk'
|
||||||
|
'position',
|
||||||
|
'value'
|
||||||
|
).select_related('item')
|
||||||
|
|
||||||
total = qs.count()
|
total = qs.count()
|
||||||
pagesize = 20
|
pagesize = 20
|
||||||
|
|||||||
@@ -399,7 +399,8 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa
|
|||||||
ukey,
|
ukey,
|
||||||
self.request.user.email,
|
self.request.user.email,
|
||||||
str(self.request.user),
|
str(self.request.user),
|
||||||
settings.SITE_URL
|
settings.SITE_URL,
|
||||||
|
attestation="none"
|
||||||
)
|
)
|
||||||
ctx['jsondata'] = json.dumps(make_credential_options.registration_dict)
|
ctx['jsondata'] = json.dumps(make_credential_options.registration_dict)
|
||||||
|
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ class UserAnonymizeView(AdministratorPermissionRequiredMixin, RecentAuthenticati
|
|||||||
self.object.fullname = ""
|
self.object.fullname = ""
|
||||||
self.object.is_active = False
|
self.object.is_active = False
|
||||||
self.object.notifications_send = False
|
self.object.notifications_send = False
|
||||||
self.object.auth_backend = None
|
self.object.auth_backend = 'anonymized'
|
||||||
self.object.auth_backend_identifier = None
|
self.object.auth_backend_identifier = None
|
||||||
self.object.save()
|
self.object.save()
|
||||||
for le in self.object.all_logentries.filter(action_type="pretix.user.settings.changed"):
|
for le in self.object.all_logentries.filter(action_type="pretix.user.settings.changed"):
|
||||||
|
|||||||
@@ -19,11 +19,17 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import pyuca
|
||||||
|
from babel.core import Locale
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
from django.utils import translation
|
||||||
from django_countries import Countries
|
from django_countries import Countries
|
||||||
from django_countries.fields import CountryField
|
from django_countries.fields import CountryField
|
||||||
|
from phonenumbers.data import _COUNTRY_CODE_TO_REGION_CODE
|
||||||
|
|
||||||
from pretix.base.i18n import get_language_without_region
|
from pretix.base.i18n import get_babel_locale, get_language_without_region
|
||||||
|
|
||||||
|
_collator = pyuca.Collator()
|
||||||
|
|
||||||
|
|
||||||
class CachedCountries(Countries):
|
class CachedCountries(Countries):
|
||||||
@@ -83,3 +89,35 @@ class FastCountryField(CountryField):
|
|||||||
*self._check_multiple(),
|
*self._check_multiple(),
|
||||||
*self._check_max_length_attribute(**kwargs),
|
*self._check_max_length_attribute(**kwargs),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
_cached_phone_prefixes = {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_phone_prefixes_sorted_and_localized():
|
||||||
|
language = get_babel_locale() # changed from default implementation that used the django locale
|
||||||
|
|
||||||
|
cache_key = "phoneprefixes:all:{}".format(language)
|
||||||
|
if cache_key in _cached_phone_prefixes:
|
||||||
|
return _cached_phone_prefixes[cache_key]
|
||||||
|
|
||||||
|
val = cache.get(cache_key)
|
||||||
|
if val:
|
||||||
|
_cached_phone_prefixes[cache_key] = val
|
||||||
|
return val
|
||||||
|
|
||||||
|
val = []
|
||||||
|
|
||||||
|
locale = Locale(translation.to_locale(language))
|
||||||
|
for prefix, values in _COUNTRY_CODE_TO_REGION_CODE.items():
|
||||||
|
prefix = "+%d" % prefix
|
||||||
|
for country_code in values:
|
||||||
|
country_name = locale.territories.get(country_code)
|
||||||
|
if country_name:
|
||||||
|
val.append((prefix, "{} {}".format(country_name, prefix)))
|
||||||
|
|
||||||
|
val = sorted(val, key=lambda item: _collator.sort_key(item[1]))
|
||||||
|
|
||||||
|
_cached_phone_prefixes[cache_key] = val
|
||||||
|
cache.set(cache_key, val, 3600 * 24 * 30)
|
||||||
|
return val
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
DATE_INPUT_FORMATS = [
|
||||||
|
"%d-%m-%Y",
|
||||||
|
"%Y-%m-%d",
|
||||||
|
"%m/%d/%Y",
|
||||||
|
"%m/%d/%y",
|
||||||
|
"%b %d %Y",
|
||||||
|
"%b %d, %Y",
|
||||||
|
"%d %b %Y",
|
||||||
|
"%d %b, %Y",
|
||||||
|
"%B %d %Y",
|
||||||
|
"%B %d, %Y",
|
||||||
|
"%d %B %Y",
|
||||||
|
"%d %B, %Y"
|
||||||
|
]
|
||||||
@@ -35,7 +35,7 @@ def convert_to_dnf(rules):
|
|||||||
|
|
||||||
def _distribute_or_over_and(r):
|
def _distribute_or_over_and(r):
|
||||||
operator = list(r.keys())[0]
|
operator = list(r.keys())[0]
|
||||||
values = rules[operator]
|
values = r[operator]
|
||||||
if operator == "and":
|
if operator == "and":
|
||||||
arg_to_distribute = [arg for arg in values if isinstance(arg, dict) and "or" in arg]
|
arg_to_distribute = [arg for arg in values if isinstance(arg, dict) and "or" in arg]
|
||||||
if not arg_to_distribute:
|
if not arg_to_distribute:
|
||||||
@@ -57,7 +57,7 @@ def convert_to_dnf(rules):
|
|||||||
if not isinstance(r, dict):
|
if not isinstance(r, dict):
|
||||||
return r
|
return r
|
||||||
operator = list(r.keys())[0]
|
operator = list(r.keys())[0]
|
||||||
values = rules[operator]
|
values = r[operator]
|
||||||
if operator not in ("or", "and"):
|
if operator not in ("or", "and"):
|
||||||
return r
|
return r
|
||||||
new_values = []
|
new_values = []
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class GreaterEqualThan(Func):
|
|||||||
|
|
||||||
|
|
||||||
class LowerEqualThan(Func):
|
class LowerEqualThan(Func):
|
||||||
arg_joiner = ' < '
|
arg_joiner = ' <= '
|
||||||
arity = 2
|
arity = 2
|
||||||
function = ''
|
function = ''
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
from PIL.Image import BICUBIC
|
from PIL.Image import Resampling
|
||||||
from reportlab.lib.utils import ImageReader
|
from reportlab.lib.utils import ImageReader
|
||||||
|
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ class ThumbnailingImageReader(ImageReader):
|
|||||||
height = width * self._image.size[1] / self._image.size[0]
|
height = width * self._image.size[1] / self._image.size[0]
|
||||||
self._image.thumbnail(
|
self._image.thumbnail(
|
||||||
size=(int(width * dpi / 72), int(height * dpi / 72)),
|
size=(int(width * dpi / 72), int(height * dpi / 72)),
|
||||||
resample=BICUBIC
|
resample=Resampling.BICUBIC
|
||||||
)
|
)
|
||||||
self._data = None
|
self._data = None
|
||||||
return width, height
|
return width, height
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import re
|
||||||
|
from inspect import isgenerator
|
||||||
|
|
||||||
|
from openpyxl import Workbook
|
||||||
|
from openpyxl.cell.cell import (
|
||||||
|
ILLEGAL_CHARACTERS_RE, KNOWN_TYPES, TIME_TYPES, TYPE_FORMULA, TYPE_STRING,
|
||||||
|
Cell,
|
||||||
|
)
|
||||||
|
from openpyxl.compat import NUMERIC_TYPES
|
||||||
|
from openpyxl.utils import column_index_from_string
|
||||||
|
from openpyxl.utils.exceptions import ReadOnlyWorkbookException
|
||||||
|
from openpyxl.worksheet._write_only import WriteOnlyWorksheet
|
||||||
|
from openpyxl.worksheet.worksheet import Worksheet
|
||||||
|
|
||||||
|
SAFE_TYPES = NUMERIC_TYPES + TIME_TYPES + (bool, type(None))
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
This module provides a safer version of openpyxl's `Workbook` class to generate XLSX files from
|
||||||
|
user-generated data using `WriteOnlyWorksheet` and `ws.append()`. We commonly use these methods
|
||||||
|
to output e.g. order data, which contains data from untrusted sources such as attendee names.
|
||||||
|
|
||||||
|
There are mainly two problems this solves:
|
||||||
|
|
||||||
|
- It makes sure strings starting with = are treated as text, not as a formula, as openpyxl will
|
||||||
|
otherwise assume, which can be used for remote code execution.
|
||||||
|
|
||||||
|
- It removes characters considered invalid by Excel to avoid exporter crashes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def remove_invalid_excel_chars(val):
|
||||||
|
if isinstance(val, Cell):
|
||||||
|
return val
|
||||||
|
|
||||||
|
if not isinstance(val, KNOWN_TYPES):
|
||||||
|
val = str(val)
|
||||||
|
|
||||||
|
if isinstance(val, bytes):
|
||||||
|
val = val.decode("utf-8", errors="ignore")
|
||||||
|
|
||||||
|
if isinstance(val, str):
|
||||||
|
val = re.sub(ILLEGAL_CHARACTERS_RE, '', val)
|
||||||
|
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
def SafeCell(*args, value=None, **kwargs):
|
||||||
|
value = remove_invalid_excel_chars(value)
|
||||||
|
c = Cell(*args, value=value, **kwargs)
|
||||||
|
if c.data_type == TYPE_FORMULA:
|
||||||
|
c.data_type = TYPE_STRING
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
class SafeAppendMixin:
|
||||||
|
def append(self, iterable):
|
||||||
|
row_idx = self._current_row + 1
|
||||||
|
|
||||||
|
if isinstance(iterable, (list, tuple, range)) or isgenerator(iterable):
|
||||||
|
for col_idx, content in enumerate(iterable, 1):
|
||||||
|
if isinstance(content, Cell):
|
||||||
|
# compatible with write-only mode
|
||||||
|
cell = content
|
||||||
|
if cell.parent and cell.parent != self:
|
||||||
|
raise ValueError("Cells cannot be copied from other worksheets")
|
||||||
|
cell.parent = self
|
||||||
|
cell.column = col_idx
|
||||||
|
cell.row = row_idx
|
||||||
|
else:
|
||||||
|
cell = SafeCell(self, row=row_idx, column=col_idx, value=remove_invalid_excel_chars(content))
|
||||||
|
self._cells[(row_idx, col_idx)] = cell
|
||||||
|
|
||||||
|
elif isinstance(iterable, dict):
|
||||||
|
for col_idx, content in iterable.items():
|
||||||
|
if isinstance(col_idx, str):
|
||||||
|
col_idx = column_index_from_string(col_idx)
|
||||||
|
cell = SafeCell(self, row=row_idx, column=col_idx, value=content)
|
||||||
|
self._cells[(row_idx, col_idx)] = cell
|
||||||
|
|
||||||
|
else:
|
||||||
|
self._invalid_row(iterable)
|
||||||
|
|
||||||
|
self._current_row = row_idx
|
||||||
|
|
||||||
|
|
||||||
|
class SafeWriteOnlyWorksheet(SafeAppendMixin, WriteOnlyWorksheet):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SafeWorksheet(SafeAppendMixin, Worksheet):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SafeWorkbook(Workbook):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if self._sheets:
|
||||||
|
# monkeypatch existing sheets
|
||||||
|
for s in self._sheets:
|
||||||
|
s.append = SafeAppendMixin.append
|
||||||
|
|
||||||
|
def create_sheet(self, title=None, index=None):
|
||||||
|
if self.read_only:
|
||||||
|
raise ReadOnlyWorkbookException('Cannot create new sheet in a read-only workbook')
|
||||||
|
|
||||||
|
if self.write_only:
|
||||||
|
new_ws = SafeWriteOnlyWorksheet(parent=self, title=title)
|
||||||
|
else:
|
||||||
|
new_ws = SafeWorksheet(parent=self, title=title)
|
||||||
|
|
||||||
|
self._add_sheet(sheet=new_ws, index=index)
|
||||||
|
return new_ws
|
||||||
@@ -26,7 +26,7 @@ from io import BytesIO
|
|||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.core.files.storage import default_storage
|
from django.core.files.storage import default_storage
|
||||||
from PIL import Image, ImageOps
|
from PIL import Image, ImageOps
|
||||||
from PIL.Image import LANCZOS
|
from PIL.Image import Resampling
|
||||||
|
|
||||||
from pretix.helpers.models import Thumbnail
|
from pretix.helpers.models import Thumbnail
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@ def resize_image(image, size):
|
|||||||
image = ImageOps.exif_transpose(image)
|
image = ImageOps.exif_transpose(image)
|
||||||
|
|
||||||
new_size, crop = get_sizes(size, image.size)
|
new_size, crop = get_sizes(size, image.size)
|
||||||
image = image.resize(new_size, resample=LANCZOS)
|
image = image.resize(new_size, resample=Resampling.LANCZOS)
|
||||||
if crop:
|
if crop:
|
||||||
image = image.crop(crop)
|
image = image.crop(crop)
|
||||||
|
|
||||||
|
|||||||
+2207
-1877
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2022-02-25 10:05+0000\n"
|
"POT-Creation-Date: 2022-04-12 08:33+0000\n"
|
||||||
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
|
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
|
||||||
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
|
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
|
||||||
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
@@ -318,96 +318,92 @@ msgstr "عدد المدخلات السابقة قبل منتصف الليل"
|
|||||||
msgid "Number of days with a previous entry"
|
msgid "Number of days with a previous entry"
|
||||||
msgstr "عدد الأيام التي تحتوي على مدخل سابق"
|
msgstr "عدد الأيام التي تحتوي على مدخل سابق"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:97
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:100
|
||||||
msgid "All of the conditions below (AND)"
|
msgid "All of the conditions below (AND)"
|
||||||
msgstr "جميع الشروط في الأسفل"
|
msgstr "جميع الشروط في الأسفل"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:98
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:101
|
||||||
msgid "At least one of the conditions below (OR)"
|
msgid "At least one of the conditions below (OR)"
|
||||||
msgstr "خيار واحد على الأقل"
|
msgstr "خيار واحد على الأقل"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:99
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:102
|
||||||
msgid "Event start"
|
msgid "Event start"
|
||||||
msgstr "بداية الفعالية"
|
msgstr "بداية الفعالية"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:100
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:103
|
||||||
msgid "Event end"
|
msgid "Event end"
|
||||||
msgstr "نهاية الفعالية"
|
msgstr "نهاية الفعالية"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:101
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:104
|
||||||
msgid "Event admission"
|
msgid "Event admission"
|
||||||
msgstr "تسجيل الفعالية"
|
msgstr "تسجيل الفعالية"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:102
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:105
|
||||||
msgid "custom date and time"
|
msgid "custom date and time"
|
||||||
msgstr "تحديد التاريخ والوقت"
|
msgstr "تحديد التاريخ والوقت"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:103
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:106
|
||||||
msgid "custom time"
|
msgid "custom time"
|
||||||
msgstr "الوقت المحدد"
|
msgstr "الوقت المحدد"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:104
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:107
|
||||||
msgid "Tolerance (minutes)"
|
msgid "Tolerance (minutes)"
|
||||||
msgstr "القدرة على التحمل (الدقائق)"
|
msgstr "القدرة على التحمل (الدقائق)"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:105
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:108
|
||||||
msgid "Add condition"
|
msgid "Add condition"
|
||||||
msgstr "أضف شرطا"
|
msgstr "أضف شرطا"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:106
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:109
|
||||||
msgid "minutes"
|
msgid "minutes"
|
||||||
msgstr "الدقائق"
|
msgstr "الدقائق"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:69
|
|
||||||
msgid "Lead Scan QR"
|
|
||||||
msgstr "قم بمسح QR"
|
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:71
|
#: pretix/static/pretixcontrol/js/ui/editor.js:71
|
||||||
msgid "Check-in QR"
|
msgid "Check-in QR"
|
||||||
msgstr "QR الدخول"
|
msgstr "QR الدخول"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:313
|
#: pretix/static/pretixcontrol/js/ui/editor.js:376
|
||||||
msgid "The PDF background file could not be loaded for the following reason:"
|
msgid "The PDF background file could not be loaded for the following reason:"
|
||||||
msgstr "لا يمكن تحميل ملف PDF الخلفية للأسباب التالية:"
|
msgstr "لا يمكن تحميل ملف PDF الخلفية للأسباب التالية:"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:521
|
#: pretix/static/pretixcontrol/js/ui/editor.js:624
|
||||||
msgid "Group of objects"
|
msgid "Group of objects"
|
||||||
msgstr "مجموعة من العناصر"
|
msgstr "مجموعة من العناصر"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:527
|
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||||
msgid "Text object"
|
msgid "Text object"
|
||||||
msgstr "عنصر نص"
|
msgstr "عنصر نص"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:529
|
#: pretix/static/pretixcontrol/js/ui/editor.js:632
|
||||||
msgid "Barcode area"
|
msgid "Barcode area"
|
||||||
msgstr "منطقة باركود"
|
msgstr "منطقة باركود"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:531
|
#: pretix/static/pretixcontrol/js/ui/editor.js:634
|
||||||
msgid "Image area"
|
msgid "Image area"
|
||||||
msgstr "منطقة صورة"
|
msgstr "منطقة صورة"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:533
|
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||||
msgid "Powered by pretix"
|
msgid "Powered by pretix"
|
||||||
msgstr "مدعوم من pretix"
|
msgstr "مدعوم من pretix"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:535
|
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||||
msgid "Object"
|
msgid "Object"
|
||||||
msgstr "عنصر"
|
msgstr "عنصر"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:539
|
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||||
msgid "Ticket design"
|
msgid "Ticket design"
|
||||||
msgstr "تصميم التذكرة"
|
msgstr "تصميم التذكرة"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:813
|
#: pretix/static/pretixcontrol/js/ui/editor.js:932
|
||||||
msgid "Saving failed."
|
msgid "Saving failed."
|
||||||
msgstr "فشلت عملية الحفظ."
|
msgstr "فشلت عملية الحفظ."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:862
|
#: pretix/static/pretixcontrol/js/ui/editor.js:982
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:901
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1021
|
||||||
msgid "Error while uploading your PDF file, please try again."
|
msgid "Error while uploading your PDF file, please try again."
|
||||||
msgstr "حصل خطأ أثناء رفع ملف PDF الخاص بك، يرجى المحاولة مرة أخرى."
|
msgstr "حصل خطأ أثناء رفع ملف PDF الخاص بك، يرجى المحاولة مرة أخرى."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:886
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
||||||
msgid "Do you really want to leave the editor without saving your changes?"
|
msgid "Do you really want to leave the editor without saving your changes?"
|
||||||
msgstr "هل تريد أن تغادر المحرر دون حفظ التعديلات؟"
|
msgstr "هل تريد أن تغادر المحرر دون حفظ التعديلات؟"
|
||||||
|
|
||||||
@@ -535,20 +531,20 @@ msgstr "ستسترد %(currency)%(amount)"
|
|||||||
msgid "Please enter the amount the organizer can keep."
|
msgid "Please enter the amount the organizer can keep."
|
||||||
msgstr "الرجاء إدخال المبلغ الذي يمكن للمنظم الاحتفاظ به."
|
msgstr "الرجاء إدخال المبلغ الذي يمكن للمنظم الاحتفاظ به."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:361
|
#: pretix/static/pretixpresale/js/ui/main.js:364
|
||||||
msgid "Please enter a quantity for one of the ticket types."
|
msgid "Please enter a quantity for one of the ticket types."
|
||||||
msgstr "الرجاء إدخال عدد لأحد أنواع التذاكر."
|
msgstr "الرجاء إدخال عدد لأحد أنواع التذاكر."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:397
|
#: pretix/static/pretixpresale/js/ui/main.js:400
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr "مطلوب"
|
msgstr "مطلوب"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:500
|
#: pretix/static/pretixpresale/js/ui/main.js:503
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:519
|
#: pretix/static/pretixpresale/js/ui/main.js:522
|
||||||
msgid "Time zone:"
|
msgid "Time zone:"
|
||||||
msgstr "المنطقة الزمنية:"
|
msgstr "المنطقة الزمنية:"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:510
|
#: pretix/static/pretixpresale/js/ui/main.js:513
|
||||||
msgid "Your local time:"
|
msgid "Your local time:"
|
||||||
msgstr "التوقيت المحلي:"
|
msgstr "التوقيت المحلي:"
|
||||||
|
|
||||||
@@ -828,6 +824,9 @@ msgstr "نوفمبر"
|
|||||||
msgid "December"
|
msgid "December"
|
||||||
msgstr "ديسمبر"
|
msgstr "ديسمبر"
|
||||||
|
|
||||||
|
#~ msgid "Lead Scan QR"
|
||||||
|
#~ msgstr "قم بمسح QR"
|
||||||
|
|
||||||
#~ msgid "Invalid product"
|
#~ msgid "Invalid product"
|
||||||
#~ msgstr "منتج غير ساري المفعول"
|
#~ msgstr "منتج غير ساري المفعول"
|
||||||
|
|
||||||
|
|||||||
+2160
-1867
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2022-02-25 10:05+0000\n"
|
"POT-Creation-Date: 2022-04-12 08:33+0000\n"
|
||||||
"PO-Revision-Date: 2020-12-19 07:00+0000\n"
|
"PO-Revision-Date: 2020-12-19 07:00+0000\n"
|
||||||
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
|
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
|
||||||
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
|
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||||
@@ -303,96 +303,92 @@ msgstr ""
|
|||||||
msgid "Number of days with a previous entry"
|
msgid "Number of days with a previous entry"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:97
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:100
|
||||||
msgid "All of the conditions below (AND)"
|
msgid "All of the conditions below (AND)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:98
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:101
|
||||||
msgid "At least one of the conditions below (OR)"
|
msgid "At least one of the conditions below (OR)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:99
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:102
|
||||||
msgid "Event start"
|
msgid "Event start"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:100
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:103
|
||||||
msgid "Event end"
|
msgid "Event end"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:101
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:104
|
||||||
msgid "Event admission"
|
msgid "Event admission"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:102
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:105
|
||||||
msgid "custom date and time"
|
msgid "custom date and time"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:103
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:106
|
||||||
msgid "custom time"
|
msgid "custom time"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:104
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:107
|
||||||
msgid "Tolerance (minutes)"
|
msgid "Tolerance (minutes)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:105
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:108
|
||||||
msgid "Add condition"
|
msgid "Add condition"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:106
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:109
|
||||||
msgid "minutes"
|
msgid "minutes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:69
|
|
||||||
msgid "Lead Scan QR"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:71
|
#: pretix/static/pretixcontrol/js/ui/editor.js:71
|
||||||
msgid "Check-in QR"
|
msgid "Check-in QR"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:313
|
#: pretix/static/pretixcontrol/js/ui/editor.js:376
|
||||||
msgid "The PDF background file could not be loaded for the following reason:"
|
msgid "The PDF background file could not be loaded for the following reason:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:521
|
#: pretix/static/pretixcontrol/js/ui/editor.js:624
|
||||||
msgid "Group of objects"
|
msgid "Group of objects"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:527
|
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||||
msgid "Text object"
|
msgid "Text object"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:529
|
#: pretix/static/pretixcontrol/js/ui/editor.js:632
|
||||||
msgid "Barcode area"
|
msgid "Barcode area"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:531
|
#: pretix/static/pretixcontrol/js/ui/editor.js:634
|
||||||
msgid "Image area"
|
msgid "Image area"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:533
|
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||||
msgid "Powered by pretix"
|
msgid "Powered by pretix"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:535
|
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||||
msgid "Object"
|
msgid "Object"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:539
|
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||||
msgid "Ticket design"
|
msgid "Ticket design"
|
||||||
msgstr "Disseny del tiquet"
|
msgstr "Disseny del tiquet"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:813
|
#: pretix/static/pretixcontrol/js/ui/editor.js:932
|
||||||
msgid "Saving failed."
|
msgid "Saving failed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:862
|
#: pretix/static/pretixcontrol/js/ui/editor.js:982
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:901
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1021
|
||||||
msgid "Error while uploading your PDF file, please try again."
|
msgid "Error while uploading your PDF file, please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:886
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
||||||
msgid "Do you really want to leave the editor without saving your changes?"
|
msgid "Do you really want to leave the editor without saving your changes?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -510,22 +506,22 @@ msgstr ""
|
|||||||
msgid "Please enter the amount the organizer can keep."
|
msgid "Please enter the amount the organizer can keep."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:361
|
#: pretix/static/pretixpresale/js/ui/main.js:364
|
||||||
msgid "Please enter a quantity for one of the ticket types."
|
msgid "Please enter a quantity for one of the ticket types."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:397
|
#: pretix/static/pretixpresale/js/ui/main.js:400
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Cart expired"
|
#| msgid "Cart expired"
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr "Cistella expirada"
|
msgstr "Cistella expirada"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:500
|
#: pretix/static/pretixpresale/js/ui/main.js:503
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:519
|
#: pretix/static/pretixpresale/js/ui/main.js:522
|
||||||
msgid "Time zone:"
|
msgid "Time zone:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:510
|
#: pretix/static/pretixpresale/js/ui/main.js:513
|
||||||
msgid "Your local time:"
|
msgid "Your local time:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
+2124
-1884
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2022-02-25 10:05+0000\n"
|
"POT-Creation-Date: 2022-04-12 08:33+0000\n"
|
||||||
"PO-Revision-Date: 2021-12-06 23:00+0000\n"
|
"PO-Revision-Date: 2021-12-06 23:00+0000\n"
|
||||||
"Last-Translator: Ondřej Sokol <osokol@treesoft.cz>\n"
|
"Last-Translator: Ondřej Sokol <osokol@treesoft.cz>\n"
|
||||||
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
@@ -315,96 +315,92 @@ msgstr "Počet záznamů od půlnoci"
|
|||||||
msgid "Number of days with a previous entry"
|
msgid "Number of days with a previous entry"
|
||||||
msgstr "Počet dní bez úprav"
|
msgstr "Počet dní bez úprav"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:97
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:100
|
||||||
msgid "All of the conditions below (AND)"
|
msgid "All of the conditions below (AND)"
|
||||||
msgstr "Všechny následující podmínky (AND)"
|
msgstr "Všechny následující podmínky (AND)"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:98
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:101
|
||||||
msgid "At least one of the conditions below (OR)"
|
msgid "At least one of the conditions below (OR)"
|
||||||
msgstr "Alespoň jedna z následujících podmínek (OR)"
|
msgstr "Alespoň jedna z následujících podmínek (OR)"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:99
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:102
|
||||||
msgid "Event start"
|
msgid "Event start"
|
||||||
msgstr "Začátek akce"
|
msgstr "Začátek akce"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:100
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:103
|
||||||
msgid "Event end"
|
msgid "Event end"
|
||||||
msgstr "Konec akce"
|
msgstr "Konec akce"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:101
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:104
|
||||||
msgid "Event admission"
|
msgid "Event admission"
|
||||||
msgstr "Vstup na akci"
|
msgstr "Vstup na akci"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:102
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:105
|
||||||
msgid "custom date and time"
|
msgid "custom date and time"
|
||||||
msgstr "Pevný termín"
|
msgstr "Pevný termín"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:103
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:106
|
||||||
msgid "custom time"
|
msgid "custom time"
|
||||||
msgstr "Pevná doba"
|
msgstr "Pevná doba"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:104
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:107
|
||||||
msgid "Tolerance (minutes)"
|
msgid "Tolerance (minutes)"
|
||||||
msgstr "Tolerance (v minutách)"
|
msgstr "Tolerance (v minutách)"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:105
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:108
|
||||||
msgid "Add condition"
|
msgid "Add condition"
|
||||||
msgstr "Přidat podmínku"
|
msgstr "Přidat podmínku"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:106
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:109
|
||||||
msgid "minutes"
|
msgid "minutes"
|
||||||
msgstr "minuty"
|
msgstr "minuty"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:69
|
|
||||||
msgid "Lead Scan QR"
|
|
||||||
msgstr "Lead Scan QR kód"
|
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:71
|
#: pretix/static/pretixcontrol/js/ui/editor.js:71
|
||||||
msgid "Check-in QR"
|
msgid "Check-in QR"
|
||||||
msgstr "Check-in QR kód"
|
msgstr "Check-in QR kód"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:313
|
#: pretix/static/pretixcontrol/js/ui/editor.js:376
|
||||||
msgid "The PDF background file could not be loaded for the following reason:"
|
msgid "The PDF background file could not be loaded for the following reason:"
|
||||||
msgstr "Pozadí PDF nemohl být načten:"
|
msgstr "Pozadí PDF nemohl být načten:"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:521
|
#: pretix/static/pretixcontrol/js/ui/editor.js:624
|
||||||
msgid "Group of objects"
|
msgid "Group of objects"
|
||||||
msgstr "Skupina objektů"
|
msgstr "Skupina objektů"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:527
|
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||||
msgid "Text object"
|
msgid "Text object"
|
||||||
msgstr "Textový objekt"
|
msgstr "Textový objekt"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:529
|
#: pretix/static/pretixcontrol/js/ui/editor.js:632
|
||||||
msgid "Barcode area"
|
msgid "Barcode area"
|
||||||
msgstr "Oblast s QR kódem"
|
msgstr "Oblast s QR kódem"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:531
|
#: pretix/static/pretixcontrol/js/ui/editor.js:634
|
||||||
msgid "Image area"
|
msgid "Image area"
|
||||||
msgstr "Oblast obrazu"
|
msgstr "Oblast obrazu"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:533
|
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||||
msgid "Powered by pretix"
|
msgid "Powered by pretix"
|
||||||
msgstr "Poháněno společností pretix"
|
msgstr "Poháněno společností pretix"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:535
|
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||||
msgid "Object"
|
msgid "Object"
|
||||||
msgstr "Objekt"
|
msgstr "Objekt"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:539
|
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||||
msgid "Ticket design"
|
msgid "Ticket design"
|
||||||
msgstr "Design vstupenky"
|
msgstr "Design vstupenky"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:813
|
#: pretix/static/pretixcontrol/js/ui/editor.js:932
|
||||||
msgid "Saving failed."
|
msgid "Saving failed."
|
||||||
msgstr "Uložení se nepodařilo."
|
msgstr "Uložení se nepodařilo."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:862
|
#: pretix/static/pretixcontrol/js/ui/editor.js:982
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:901
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1021
|
||||||
msgid "Error while uploading your PDF file, please try again."
|
msgid "Error while uploading your PDF file, please try again."
|
||||||
msgstr "Při nahrávání souboru PDF došlo k problému, zkuste to prosím znovu."
|
msgstr "Při nahrávání souboru PDF došlo k problému, zkuste to prosím znovu."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:886
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
||||||
msgid "Do you really want to leave the editor without saving your changes?"
|
msgid "Do you really want to leave the editor without saving your changes?"
|
||||||
msgstr "Opravdu chcete opustit editor bez uložení změn?"
|
msgstr "Opravdu chcete opustit editor bez uložení změn?"
|
||||||
|
|
||||||
@@ -528,20 +524,20 @@ msgstr "Dostanete %(currency)s %(amount)s zpět"
|
|||||||
msgid "Please enter the amount the organizer can keep."
|
msgid "Please enter the amount the organizer can keep."
|
||||||
msgstr "Zadejte částku, kterou si organizátor může ponechat."
|
msgstr "Zadejte částku, kterou si organizátor může ponechat."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:361
|
#: pretix/static/pretixpresale/js/ui/main.js:364
|
||||||
msgid "Please enter a quantity for one of the ticket types."
|
msgid "Please enter a quantity for one of the ticket types."
|
||||||
msgstr "Zadejte prosím množství pro jeden z typů vstupenek."
|
msgstr "Zadejte prosím množství pro jeden z typů vstupenek."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:397
|
#: pretix/static/pretixpresale/js/ui/main.js:400
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr "povinný"
|
msgstr "povinný"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:500
|
#: pretix/static/pretixpresale/js/ui/main.js:503
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:519
|
#: pretix/static/pretixpresale/js/ui/main.js:522
|
||||||
msgid "Time zone:"
|
msgid "Time zone:"
|
||||||
msgstr "Časové pásmo:"
|
msgstr "Časové pásmo:"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:510
|
#: pretix/static/pretixpresale/js/ui/main.js:513
|
||||||
msgid "Your local time:"
|
msgid "Your local time:"
|
||||||
msgstr "Místní čas:"
|
msgstr "Místní čas:"
|
||||||
|
|
||||||
@@ -821,6 +817,9 @@ msgstr "Listopad"
|
|||||||
msgid "December"
|
msgid "December"
|
||||||
msgstr "Prosinec"
|
msgstr "Prosinec"
|
||||||
|
|
||||||
|
#~ msgid "Lead Scan QR"
|
||||||
|
#~ msgstr "Lead Scan QR kód"
|
||||||
|
|
||||||
#~ msgid "Invalid product"
|
#~ msgid "Invalid product"
|
||||||
#~ msgstr "Neplatný produkt"
|
#~ msgstr "Neplatný produkt"
|
||||||
|
|
||||||
|
|||||||
+2295
-3963
File diff suppressed because it is too large
Load Diff
@@ -6,9 +6,9 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2022-02-25 10:05+0000\n"
|
"POT-Creation-Date: 2022-04-12 08:33+0000\n"
|
||||||
"PO-Revision-Date: 2021-09-13 09:48+0000\n"
|
"PO-Revision-Date: 2022-04-01 13:36+0000\n"
|
||||||
"Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n"
|
"Last-Translator: Anna-itk <abc@aarhus.dk>\n"
|
||||||
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
"da/>\n"
|
"da/>\n"
|
||||||
"Language: da\n"
|
"Language: da\n"
|
||||||
@@ -16,7 +16,7 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||||
"X-Generator: Weblate 4.8\n"
|
"X-Generator: Weblate 4.11.2\n"
|
||||||
|
|
||||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||||
@@ -343,98 +343,94 @@ msgstr ""
|
|||||||
msgid "Number of days with a previous entry"
|
msgid "Number of days with a previous entry"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:97
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:100
|
||||||
msgid "All of the conditions below (AND)"
|
msgid "All of the conditions below (AND)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:98
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:101
|
||||||
msgid "At least one of the conditions below (OR)"
|
msgid "At least one of the conditions below (OR)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:99
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:102
|
||||||
msgid "Event start"
|
msgid "Event start"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:100
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:103
|
||||||
msgid "Event end"
|
msgid "Event end"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:101
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:104
|
||||||
msgid "Event admission"
|
msgid "Event admission"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:102
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:105
|
||||||
msgid "custom date and time"
|
msgid "custom date and time"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:103
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:106
|
||||||
msgid "custom time"
|
msgid "custom time"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:104
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:107
|
||||||
msgid "Tolerance (minutes)"
|
msgid "Tolerance (minutes)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:105
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:108
|
||||||
msgid "Add condition"
|
msgid "Add condition"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:106
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:109
|
||||||
msgid "minutes"
|
msgid "minutes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:69
|
|
||||||
msgid "Lead Scan QR"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:71
|
#: pretix/static/pretixcontrol/js/ui/editor.js:71
|
||||||
msgid "Check-in QR"
|
msgid "Check-in QR"
|
||||||
msgstr "Check-in QR"
|
msgstr "Check-in QR"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:313
|
#: pretix/static/pretixcontrol/js/ui/editor.js:376
|
||||||
msgid "The PDF background file could not be loaded for the following reason:"
|
msgid "The PDF background file could not be loaded for the following reason:"
|
||||||
msgstr "Baggrunds-pdf'en kunne ikke hentes af følgende grund:"
|
msgstr "Baggrunds-pdf'en kunne ikke hentes af følgende grund:"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:521
|
#: pretix/static/pretixcontrol/js/ui/editor.js:624
|
||||||
msgid "Group of objects"
|
msgid "Group of objects"
|
||||||
msgstr "Gruppe af objekter"
|
msgstr "Gruppe af objekter"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:527
|
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||||
msgid "Text object"
|
msgid "Text object"
|
||||||
msgstr "Tekstobjekt"
|
msgstr "Tekstobjekt"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:529
|
#: pretix/static/pretixcontrol/js/ui/editor.js:632
|
||||||
msgid "Barcode area"
|
msgid "Barcode area"
|
||||||
msgstr "QR-kode-område"
|
msgstr "QR-kode-område"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:531
|
#: pretix/static/pretixcontrol/js/ui/editor.js:634
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Barcode area"
|
#| msgid "Barcode area"
|
||||||
msgid "Image area"
|
msgid "Image area"
|
||||||
msgstr "QR-kode-område"
|
msgstr "QR-kode-område"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:533
|
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||||
msgid "Powered by pretix"
|
msgid "Powered by pretix"
|
||||||
msgstr "Drevet af pretix"
|
msgstr "Drevet af pretix"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:535
|
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||||
msgid "Object"
|
msgid "Object"
|
||||||
msgstr "Objekt"
|
msgstr "Objekt"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:539
|
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||||
msgid "Ticket design"
|
msgid "Ticket design"
|
||||||
msgstr "Billetdesign"
|
msgstr "Billetdesign"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:813
|
#: pretix/static/pretixcontrol/js/ui/editor.js:932
|
||||||
msgid "Saving failed."
|
msgid "Saving failed."
|
||||||
msgstr "Gem fejlede."
|
msgstr "Gem fejlede."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:862
|
#: pretix/static/pretixcontrol/js/ui/editor.js:982
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:901
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1021
|
||||||
msgid "Error while uploading your PDF file, please try again."
|
msgid "Error while uploading your PDF file, please try again."
|
||||||
msgstr "Fejl under upload af pdf. Prøv venligt igen."
|
msgstr "Fejl under upload af pdf. Prøv venligt igen."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:886
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
||||||
msgid "Do you really want to leave the editor without saving your changes?"
|
msgid "Do you really want to leave the editor without saving your changes?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Er du sikker på at du vil forlade editoren uden at gemme dine ændringer?"
|
"Er du sikker på at du vil forlade editoren uden at gemme dine ændringer?"
|
||||||
@@ -524,12 +520,12 @@ msgstr[0] "(en dato mere)"
|
|||||||
msgstr[1] "({num} datoer mere)"
|
msgstr[1] "({num} datoer mere)"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/cart.js:43
|
#: pretix/static/pretixpresale/js/ui/cart.js:43
|
||||||
#, fuzzy
|
|
||||||
#| msgid "The items in your cart are no longer reserved for you."
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"The items in your cart are no longer reserved for you. You can still "
|
"The items in your cart are no longer reserved for you. You can still "
|
||||||
"complete your order as long as they’re available."
|
"complete your order as long as they’re available."
|
||||||
msgstr "Varerne i din kurv er ikke længere reserverede for dig."
|
msgstr ""
|
||||||
|
"Varerne i din indkøbskurv er ikke længere reserverede til dig. Du kan stadig "
|
||||||
|
"færdiggøre din ordre, så længe der er ledige billetter."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/cart.js:45
|
#: pretix/static/pretixpresale/js/ui/cart.js:45
|
||||||
msgid "Cart expired"
|
msgid "Cart expired"
|
||||||
@@ -563,22 +559,22 @@ msgstr "fra %(currency)s %(price)s"
|
|||||||
msgid "Please enter the amount the organizer can keep."
|
msgid "Please enter the amount the organizer can keep."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:361
|
#: pretix/static/pretixpresale/js/ui/main.js:364
|
||||||
msgid "Please enter a quantity for one of the ticket types."
|
msgid "Please enter a quantity for one of the ticket types."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:397
|
#: pretix/static/pretixpresale/js/ui/main.js:400
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Cart expired"
|
#| msgid "Cart expired"
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr "Kurv udløbet"
|
msgstr "Kurv udløbet"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:500
|
#: pretix/static/pretixpresale/js/ui/main.js:503
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:519
|
#: pretix/static/pretixpresale/js/ui/main.js:522
|
||||||
msgid "Time zone:"
|
msgid "Time zone:"
|
||||||
msgstr "Tidszone:"
|
msgstr "Tidszone:"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:510
|
#: pretix/static/pretixpresale/js/ui/main.js:513
|
||||||
msgid "Your local time:"
|
msgid "Your local time:"
|
||||||
msgstr "Din lokaltid:"
|
msgstr "Din lokaltid:"
|
||||||
|
|
||||||
|
|||||||
+2524
-1919
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2022-02-25 10:05+0000\n"
|
"POT-Creation-Date: 2022-04-12 08:33+0000\n"
|
||||||
"PO-Revision-Date: 2021-10-25 15:40+0000\n"
|
"PO-Revision-Date: 2021-10-25 15:40+0000\n"
|
||||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
@@ -318,97 +318,93 @@ msgstr "Anzahl bisheriger Eintritte seit Mitternacht"
|
|||||||
msgid "Number of days with a previous entry"
|
msgid "Number of days with a previous entry"
|
||||||
msgstr "Anzahl an Tagen mit vorherigem Eintritt"
|
msgstr "Anzahl an Tagen mit vorherigem Eintritt"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:97
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:100
|
||||||
msgid "All of the conditions below (AND)"
|
msgid "All of the conditions below (AND)"
|
||||||
msgstr "Alle der folgenden Bedingungen (UND)"
|
msgstr "Alle der folgenden Bedingungen (UND)"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:98
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:101
|
||||||
msgid "At least one of the conditions below (OR)"
|
msgid "At least one of the conditions below (OR)"
|
||||||
msgstr "Mindestens eine der folgenden Bedingungen (ODER)"
|
msgstr "Mindestens eine der folgenden Bedingungen (ODER)"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:99
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:102
|
||||||
msgid "Event start"
|
msgid "Event start"
|
||||||
msgstr "Veranstaltungsbeginn"
|
msgstr "Veranstaltungsbeginn"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:100
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:103
|
||||||
msgid "Event end"
|
msgid "Event end"
|
||||||
msgstr "Veranstaltungsende"
|
msgstr "Veranstaltungsende"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:101
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:104
|
||||||
msgid "Event admission"
|
msgid "Event admission"
|
||||||
msgstr "Einlass"
|
msgstr "Einlass"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:102
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:105
|
||||||
msgid "custom date and time"
|
msgid "custom date and time"
|
||||||
msgstr "Fester Zeitpunkt"
|
msgstr "Fester Zeitpunkt"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:103
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:106
|
||||||
msgid "custom time"
|
msgid "custom time"
|
||||||
msgstr "Feste Uhrzeit"
|
msgstr "Feste Uhrzeit"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:104
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:107
|
||||||
msgid "Tolerance (minutes)"
|
msgid "Tolerance (minutes)"
|
||||||
msgstr "Toleranz (Minuten)"
|
msgstr "Toleranz (Minuten)"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:105
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:108
|
||||||
msgid "Add condition"
|
msgid "Add condition"
|
||||||
msgstr "Bedingung hinzufügen"
|
msgstr "Bedingung hinzufügen"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:106
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:109
|
||||||
msgid "minutes"
|
msgid "minutes"
|
||||||
msgstr "Minuten"
|
msgstr "Minuten"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:69
|
|
||||||
msgid "Lead Scan QR"
|
|
||||||
msgstr "Lead-Scanning-QR-Code"
|
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:71
|
#: pretix/static/pretixcontrol/js/ui/editor.js:71
|
||||||
msgid "Check-in QR"
|
msgid "Check-in QR"
|
||||||
msgstr "Check-in-QR-Code"
|
msgstr "Check-in-QR-Code"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:313
|
#: pretix/static/pretixcontrol/js/ui/editor.js:376
|
||||||
msgid "The PDF background file could not be loaded for the following reason:"
|
msgid "The PDF background file could not be loaded for the following reason:"
|
||||||
msgstr "Die Hintergrund-PDF-Datei konnte nicht geladen werden:"
|
msgstr "Die Hintergrund-PDF-Datei konnte nicht geladen werden:"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:521
|
#: pretix/static/pretixcontrol/js/ui/editor.js:624
|
||||||
msgid "Group of objects"
|
msgid "Group of objects"
|
||||||
msgstr "Gruppe von Objekten"
|
msgstr "Gruppe von Objekten"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:527
|
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||||
msgid "Text object"
|
msgid "Text object"
|
||||||
msgstr "Text-Objekt"
|
msgstr "Text-Objekt"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:529
|
#: pretix/static/pretixcontrol/js/ui/editor.js:632
|
||||||
msgid "Barcode area"
|
msgid "Barcode area"
|
||||||
msgstr "QR-Code-Bereich"
|
msgstr "QR-Code-Bereich"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:531
|
#: pretix/static/pretixcontrol/js/ui/editor.js:634
|
||||||
msgid "Image area"
|
msgid "Image area"
|
||||||
msgstr "Bildbereich"
|
msgstr "Bildbereich"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:533
|
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||||
msgid "Powered by pretix"
|
msgid "Powered by pretix"
|
||||||
msgstr "Event-Ticketshop von pretix"
|
msgstr "Event-Ticketshop von pretix"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:535
|
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||||
msgid "Object"
|
msgid "Object"
|
||||||
msgstr "Objekt"
|
msgstr "Objekt"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:539
|
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||||
msgid "Ticket design"
|
msgid "Ticket design"
|
||||||
msgstr "Ticket-Design"
|
msgstr "Ticket-Design"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:813
|
#: pretix/static/pretixcontrol/js/ui/editor.js:932
|
||||||
msgid "Saving failed."
|
msgid "Saving failed."
|
||||||
msgstr "Speichern fehlgeschlagen."
|
msgstr "Speichern fehlgeschlagen."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:862
|
#: pretix/static/pretixcontrol/js/ui/editor.js:982
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:901
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1021
|
||||||
msgid "Error while uploading your PDF file, please try again."
|
msgid "Error while uploading your PDF file, please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Es gab ein Problem beim Hochladen der PDF-Datei, bitte erneut versuchen."
|
"Es gab ein Problem beim Hochladen der PDF-Datei, bitte erneut versuchen."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:886
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
||||||
msgid "Do you really want to leave the editor without saving your changes?"
|
msgid "Do you really want to leave the editor without saving your changes?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Möchten Sie den Editor wirklich schließen ohne Ihre Änderungen zu speichern?"
|
"Möchten Sie den Editor wirklich schließen ohne Ihre Änderungen zu speichern?"
|
||||||
@@ -532,20 +528,20 @@ msgstr "Sie erhalten %(currency)s %(amount)s zurück"
|
|||||||
msgid "Please enter the amount the organizer can keep."
|
msgid "Please enter the amount the organizer can keep."
|
||||||
msgstr "Bitte geben Sie den Betrag ein, den der Veranstalter einbehalten darf."
|
msgstr "Bitte geben Sie den Betrag ein, den der Veranstalter einbehalten darf."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:361
|
#: pretix/static/pretixpresale/js/ui/main.js:364
|
||||||
msgid "Please enter a quantity for one of the ticket types."
|
msgid "Please enter a quantity for one of the ticket types."
|
||||||
msgstr "Bitte tragen Sie eine Menge für eines der Produkte ein."
|
msgstr "Bitte tragen Sie eine Menge für eines der Produkte ein."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:397
|
#: pretix/static/pretixpresale/js/ui/main.js:400
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr "verpflichtend"
|
msgstr "verpflichtend"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:500
|
#: pretix/static/pretixpresale/js/ui/main.js:503
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:519
|
#: pretix/static/pretixpresale/js/ui/main.js:522
|
||||||
msgid "Time zone:"
|
msgid "Time zone:"
|
||||||
msgstr "Zeitzone:"
|
msgstr "Zeitzone:"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:510
|
#: pretix/static/pretixpresale/js/ui/main.js:513
|
||||||
msgid "Your local time:"
|
msgid "Your local time:"
|
||||||
msgstr "Deine lokale Zeit:"
|
msgstr "Deine lokale Zeit:"
|
||||||
|
|
||||||
@@ -825,6 +821,9 @@ msgstr "November"
|
|||||||
msgid "December"
|
msgid "December"
|
||||||
msgstr "Dezember"
|
msgstr "Dezember"
|
||||||
|
|
||||||
|
#~ msgid "Lead Scan QR"
|
||||||
|
#~ msgstr "Lead-Scanning-QR-Code"
|
||||||
|
|
||||||
#~ msgid "Invalid product"
|
#~ msgid "Invalid product"
|
||||||
#~ msgstr "Ungültiges Produkt"
|
#~ msgstr "Ungültiges Produkt"
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ Edge
|
|||||||
Eingangsscan
|
Eingangsscan
|
||||||
Einlassbuchung
|
Einlassbuchung
|
||||||
Einlassdatum
|
Einlassdatum
|
||||||
|
Einlasskontrolle
|
||||||
Einlassuhrzeit
|
Einlassuhrzeit
|
||||||
email
|
email
|
||||||
Enterprise
|
Enterprise
|
||||||
@@ -252,6 +253,7 @@ Teammitglied
|
|||||||
Teamname
|
Teamname
|
||||||
Telephone
|
Telephone
|
||||||
Ticketing
|
Ticketing
|
||||||
|
To
|
||||||
Tokengenerator
|
Tokengenerator
|
||||||
Toolbar
|
Toolbar
|
||||||
TODO
|
TODO
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2022-02-25 10:05+0000\n"
|
"POT-Creation-Date: 2022-04-12 08:33+0000\n"
|
||||||
"PO-Revision-Date: 2021-10-25 15:40+0000\n"
|
"PO-Revision-Date: 2021-10-25 15:40+0000\n"
|
||||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||||
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
||||||
@@ -317,97 +317,93 @@ msgstr "Anzahl bisheriger Eintritte seit Mitternacht"
|
|||||||
msgid "Number of days with a previous entry"
|
msgid "Number of days with a previous entry"
|
||||||
msgstr "Anzahl an Tagen mit vorherigem Eintritt"
|
msgstr "Anzahl an Tagen mit vorherigem Eintritt"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:97
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:100
|
||||||
msgid "All of the conditions below (AND)"
|
msgid "All of the conditions below (AND)"
|
||||||
msgstr "Alle der folgenden Bedingungen (UND)"
|
msgstr "Alle der folgenden Bedingungen (UND)"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:98
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:101
|
||||||
msgid "At least one of the conditions below (OR)"
|
msgid "At least one of the conditions below (OR)"
|
||||||
msgstr "Mindestens eine der folgenden Bedingungen (ODER)"
|
msgstr "Mindestens eine der folgenden Bedingungen (ODER)"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:99
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:102
|
||||||
msgid "Event start"
|
msgid "Event start"
|
||||||
msgstr "Veranstaltungsbeginn"
|
msgstr "Veranstaltungsbeginn"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:100
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:103
|
||||||
msgid "Event end"
|
msgid "Event end"
|
||||||
msgstr "Veranstaltungsende"
|
msgstr "Veranstaltungsende"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:101
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:104
|
||||||
msgid "Event admission"
|
msgid "Event admission"
|
||||||
msgstr "Einlass"
|
msgstr "Einlass"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:102
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:105
|
||||||
msgid "custom date and time"
|
msgid "custom date and time"
|
||||||
msgstr "Fester Zeitpunkt"
|
msgstr "Fester Zeitpunkt"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:103
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:106
|
||||||
msgid "custom time"
|
msgid "custom time"
|
||||||
msgstr "Feste Uhrzeit"
|
msgstr "Feste Uhrzeit"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:104
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:107
|
||||||
msgid "Tolerance (minutes)"
|
msgid "Tolerance (minutes)"
|
||||||
msgstr "Toleranz (Minuten)"
|
msgstr "Toleranz (Minuten)"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:105
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:108
|
||||||
msgid "Add condition"
|
msgid "Add condition"
|
||||||
msgstr "Bedingung hinzufügen"
|
msgstr "Bedingung hinzufügen"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:106
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:109
|
||||||
msgid "minutes"
|
msgid "minutes"
|
||||||
msgstr "Minuten"
|
msgstr "Minuten"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:69
|
|
||||||
msgid "Lead Scan QR"
|
|
||||||
msgstr "Lead-Scanning-QR-Code"
|
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:71
|
#: pretix/static/pretixcontrol/js/ui/editor.js:71
|
||||||
msgid "Check-in QR"
|
msgid "Check-in QR"
|
||||||
msgstr "Check-in-QR-Code"
|
msgstr "Check-in-QR-Code"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:313
|
#: pretix/static/pretixcontrol/js/ui/editor.js:376
|
||||||
msgid "The PDF background file could not be loaded for the following reason:"
|
msgid "The PDF background file could not be loaded for the following reason:"
|
||||||
msgstr "Die Hintergrund-PDF-Datei konnte nicht geladen werden:"
|
msgstr "Die Hintergrund-PDF-Datei konnte nicht geladen werden:"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:521
|
#: pretix/static/pretixcontrol/js/ui/editor.js:624
|
||||||
msgid "Group of objects"
|
msgid "Group of objects"
|
||||||
msgstr "Gruppe von Objekten"
|
msgstr "Gruppe von Objekten"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:527
|
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||||
msgid "Text object"
|
msgid "Text object"
|
||||||
msgstr "Text-Objekt"
|
msgstr "Text-Objekt"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:529
|
#: pretix/static/pretixcontrol/js/ui/editor.js:632
|
||||||
msgid "Barcode area"
|
msgid "Barcode area"
|
||||||
msgstr "QR-Code-Bereich"
|
msgstr "QR-Code-Bereich"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:531
|
#: pretix/static/pretixcontrol/js/ui/editor.js:634
|
||||||
msgid "Image area"
|
msgid "Image area"
|
||||||
msgstr "Bildbereich"
|
msgstr "Bildbereich"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:533
|
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||||
msgid "Powered by pretix"
|
msgid "Powered by pretix"
|
||||||
msgstr "Event-Ticketshop von pretix"
|
msgstr "Event-Ticketshop von pretix"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:535
|
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||||
msgid "Object"
|
msgid "Object"
|
||||||
msgstr "Objekt"
|
msgstr "Objekt"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:539
|
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||||
msgid "Ticket design"
|
msgid "Ticket design"
|
||||||
msgstr "Ticket-Design"
|
msgstr "Ticket-Design"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:813
|
#: pretix/static/pretixcontrol/js/ui/editor.js:932
|
||||||
msgid "Saving failed."
|
msgid "Saving failed."
|
||||||
msgstr "Speichern fehlgeschlagen."
|
msgstr "Speichern fehlgeschlagen."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:862
|
#: pretix/static/pretixcontrol/js/ui/editor.js:982
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:901
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1021
|
||||||
msgid "Error while uploading your PDF file, please try again."
|
msgid "Error while uploading your PDF file, please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Es gab ein Problem beim Hochladen der PDF-Datei, bitte erneut versuchen."
|
"Es gab ein Problem beim Hochladen der PDF-Datei, bitte erneut versuchen."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:886
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
||||||
msgid "Do you really want to leave the editor without saving your changes?"
|
msgid "Do you really want to leave the editor without saving your changes?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Möchtest du den Editor wirklich schließen ohne Ihre Änderungen zu speichern?"
|
"Möchtest du den Editor wirklich schließen ohne Ihre Änderungen zu speichern?"
|
||||||
@@ -531,20 +527,20 @@ msgstr "Du erhältst %(currency)s %(amount)s zurück"
|
|||||||
msgid "Please enter the amount the organizer can keep."
|
msgid "Please enter the amount the organizer can keep."
|
||||||
msgstr "Bitte gib den Betrag ein, den der Veranstalter einbehalten darf."
|
msgstr "Bitte gib den Betrag ein, den der Veranstalter einbehalten darf."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:361
|
#: pretix/static/pretixpresale/js/ui/main.js:364
|
||||||
msgid "Please enter a quantity for one of the ticket types."
|
msgid "Please enter a quantity for one of the ticket types."
|
||||||
msgstr "Bitte trage eine Menge für eines der Produkte ein."
|
msgstr "Bitte trage eine Menge für eines der Produkte ein."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:397
|
#: pretix/static/pretixpresale/js/ui/main.js:400
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr "verpflichtend"
|
msgstr "verpflichtend"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:500
|
#: pretix/static/pretixpresale/js/ui/main.js:503
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:519
|
#: pretix/static/pretixpresale/js/ui/main.js:522
|
||||||
msgid "Time zone:"
|
msgid "Time zone:"
|
||||||
msgstr "Zeitzone:"
|
msgstr "Zeitzone:"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:510
|
#: pretix/static/pretixpresale/js/ui/main.js:513
|
||||||
msgid "Your local time:"
|
msgid "Your local time:"
|
||||||
msgstr "Deine lokale Zeit:"
|
msgstr "Deine lokale Zeit:"
|
||||||
|
|
||||||
@@ -824,6 +820,9 @@ msgstr "November"
|
|||||||
msgid "December"
|
msgid "December"
|
||||||
msgstr "Dezember"
|
msgstr "Dezember"
|
||||||
|
|
||||||
|
#~ msgid "Lead Scan QR"
|
||||||
|
#~ msgstr "Lead-Scanning-QR-Code"
|
||||||
|
|
||||||
#~ msgid "Invalid product"
|
#~ msgid "Invalid product"
|
||||||
#~ msgstr "Ungültiges Produkt"
|
#~ msgstr "Ungültiges Produkt"
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user