Compare commits

..

1 Commits

Author SHA1 Message Date
Richard Schreiber
f2cbf82700 Fix broken widget cache 2025-07-01 11:08:46 +02:00
104 changed files with 387 additions and 36085 deletions

View File

@@ -8,7 +8,6 @@ pretix
:target: https://docs.pretix.eu/
.. image:: https://github.com/pretix/pretix/workflows/Tests/badge.svg
:target: https://github.com/pretix/pretix/actions/workflows/tests.yml
.. image:: https://codecov.io/gh/pretix/pretix/branch/master/graph/badge.svg
:target: https://codecov.io/gh/pretix/pretix

View File

@@ -349,45 +349,6 @@ Endpoints
: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/(id)/vouchers/bulk_attach/
Attaches many **existing** vouchers to an exhibitor. You need to send either the ``id`` **or** the ``code`` field of
the voucher, but you need to send the same field for all entries.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/exhibitors/1/vouchers/bulk_attach/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
[
{
"id": 15,
"exhibitor_comment": "Free ticket"
},
..
]
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{}
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of the event to use
:param id: The ``id`` field of the exhibitor to use
:statuscode 200: no error
:statuscode 400: Invalid data sent, e.g. voucher does not exist
: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.

View File

@@ -25,7 +25,6 @@ at :ref:`plugin-docs`.
seats
orders
invoices
transactions
vouchers
discounts
checkin
@@ -55,7 +54,6 @@ at :ref:`plugin-docs`.
digital
exhibitors
imported_secrets
offlinesales
shipping
billing_invoices
billing_var

View File

@@ -1,5 +1,3 @@
.. _rest-invoices:
Invoices
========

View File

@@ -1,219 +0,0 @@
Offline sales
=============
.. note:: This API is only available when the plugin **pretix-offlinesales** is installed (pretix Hosted and Enterprise only).
The offline sales module allows you to create batches of tickets intended for the sale outside the system.
Resource description
--------------------
The offline sales batch resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal batch ID
creation datetime Time of creation
testmode boolean ``true`` if orders are created in test mode
sales_channel string Sales channel of the orders
layout integer Internal ID of the chosen ticket layout
subevent integer Internal ID of the chosen subevent (or ``null``)
item integer Internal ID of the chosen product
variation integer Internal ID of the chosen variation (or ``null``)
amount integer Number of tickets in the batch
comment string Internal comment
orders list of strings List of order codes (omitted in list view for performance reasons)
===================================== ========================== =======================================================
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/offlinesalesbatches/
Returns a list of all offline sales batches
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/democon/offlinesalesbatches/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"creation": "2025-07-08T18:27:32.134368+02:00",
"testmode": False,
"sales_channel": "web",
"comment": "Batch for sale at the event",
"layout": 3,
"subevent": null,
"item": 23,
"variation": null,
"amount": 7
}
]
}
: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 a valid event
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/offlinesalesbatches/(id)/
Returns information on a given batch.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/democon/offlinesalesbatches/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: text/javascript
{
"id": 1,
"creation": "2025-07-08T18:27:32.134368+02:00",
"testmode": False,
"sales_channel": "web",
"comment": "Batch for sale at the event",
"layout": 3,
"subevent": null,
"item": 23,
"variation": null,
"amount": 7,
"orders": ["TSRNN", "3FBSL", "WMDNJ", "BHW9H", "MXSUG", "DSDAP", "URLLE"]
}
: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 batch to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/offlinesalesbatches/
With this API call, you can instruct the system to create a new batch.
Since batches can contain up to 10,000 tickets, they are created asynchronously on the server.
If your input parameters validate correctly, a ``202 Accepted`` status code is returned.
The body points you to the check URL of the result. Running a ``GET`` request on that result URL will
yield one of the following status codes:
* ``200 OK`` The creation of the batch has succeeded. The body will be your resulting batch with the same information as in the detail endpoint above.
* ``409 Conflict`` Your creation job is still running. The body will be JSON with the structure ``{"status": "running"}``. ``status`` can be ``waiting`` before the task is actually being processed. Please retry, but wait at least one second before you do.
* ``410 Gone`` Creating the batch has failed permanently (e.g. quota no longer available). The body will be JSON with the structure ``{"status": "failed", "message": "Error message"}``
* ``404 Not Found`` The job does not exist / is expired.
.. note:: To avoid performance issues, a maximum amount of 10000 is currently allowed.
.. note:: Do not wait multiple hours or more to retrieve your result. After a longer wait time, ``409`` might be returned permanently due to technical constraints, even though nothing will happen any more.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/offlinesalesbatches/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"testmode": True,
"layout": 123,
"item": 14,
"sales_channel": "web",
"amount": 10,
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"check": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/offlinesalesbatches/check/29891ede-196f-4942-9e26-d055a36e98b8/"
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:statuscode 202: no error
:statuscode 400: Invalid input options
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/offlinesalesbatches/(id)/render/
With this API call, you can render the PDF representation of a batch.
Since batches can contain up to 10,000 tickets, they are rendered asynchronously on the server.
If your input parameters validate correctly, a ``202 Accepted`` status code is returned.
The body points you to the download URL of the result. Running a ``GET`` request on that result URL will
yield one of the following status codes:
* ``200 OK`` The creation of the batch has succeeded. The body will be your resulting batch with the same information as in the detail endpoint above.
* ``409 Conflict`` Your rendering process is still running. The body will be JSON with the structure ``{"status": "running"}``. ``status`` can be ``waiting`` before the task is actually being processed. Please retry, but wait at least one second before you do.
* ``410 Gone`` Rendering the batch has failed permanently. The body will be JSON with the structure ``{"status": "failed", "message": "Error message"}``
* ``404 Not Found`` The rendering job does not exist / is expired.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/offlinesalesbatches/1/render 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
{
"download": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/offlinesalesbatches/1/download/29891ede-196f-4942-9e26-d055a36e98b8/3f279f13-c198-4137-b49b-9b360ce9fcce/"
}
: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 batch to fetch
:statuscode 202: no error
:statuscode 400: Invalid input options
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.

View File

@@ -28,8 +28,6 @@ closed boolean Whether the quo
field).
release_after_exit boolean Whether the quota regains capacity as soon as some tickets
have been scanned at an exit.
ignore_for_event_availability boolean Whether the quota is ignored when calculating the event's
availability of tickets.
available boolean Whether this quota is available. Only returned if ``with_availability=true``
is set on the request. Do not rely on this value for critical operations, it may be
slightly out of date.
@@ -74,8 +72,7 @@ Endpoints
"variations": [1, 4, 5, 7],
"subevent": null,
"close_when_sold_out": false,
"closed": false,
"ignore_for_event_availability": false
"closed": false
}
]
}
@@ -121,8 +118,7 @@ Endpoints
"variations": [1, 4, 5, 7],
"subevent": null,
"close_when_sold_out": false,
"closed": false,
"ignore_for_event_availability": false
"closed": false
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -153,8 +149,7 @@ Endpoints
"variations": [1, 4, 5, 7],
"subevent": null,
"close_when_sold_out": false,
"closed": false,
"ignore_for_event_availability": false
"closed": false
}
**Example response**:
@@ -173,8 +168,7 @@ Endpoints
"variations": [1, 4, 5, 7],
"subevent": null,
"close_when_sold_out": false,
"closed": false,
"ignore_for_event_availability": false
"closed": false
}
:param organizer: The ``slug`` field of the organizer of the event/item to create a quota for
@@ -229,8 +223,7 @@ Endpoints
],
"subevent": null,
"close_when_sold_out": false,
"closed": false,
"ignore_for_event_availability": false
"closed": false
}
:param organizer: The ``slug`` field of the organizer to modify

View File

@@ -1,232 +0,0 @@
.. _rest-transactions:
Transactions
============
Transactions are an additional way to think about orders. They are are an immutable, filterable view into an order's
history and are a good basis for financial reporting.
Our financial model
-------------------
You can think of a pretix order similar to a debtor account in double-entry bookkeeping. For example, the flow of an
order could look like this:
===================================================== ==================== =====================
Transaction Debit Credit
===================================================== ==================== =====================
Order is placed with two tickets € 500
Order is paid partially with a gift card € 200
Remainder is paid with a credit card € 300
One of the tickets is canceled **-** € 250
Refund is made to the credit card **-** € 250
**Balance** **€ 250** **€ 250**
===================================================== ==================== =====================
If an order is fully settled, the sums of both columns match. However, as the movements in both columns do not always
happen at the same time, at some times during the lifecycle of an order the sums are not balanced, in which case we
consider an order to be "pending payment" or "overpaid".
In the API, the "Debit" column is represented by the "transaction" resource listed on this page.
In many cases, the left column *usually* also matches the data returned by the :ref:`rest-invoices` resource, but there
are two important differences:
- pretix may be configured such that an invoice is not always generated for an order. In this case, only the transactions
return the full data set.
- pretix does not enforce a new invoice to be created e.g. when a ticket is changed to a different subevent. However,
pretix always creates a new transaction whenever there is a change to a ticket that concerns the **price**, **tax rate**,
**product**, or **date** (in an event series).
The :ref:`rest-orders` themselves are not a good representation of the "Debit" side of the table for accounting
purposes since they are not immutable:
They will only tell you the current state of the order, not what it was a week ago.
The "Credit" column is represented by the :ref:`order-payment-resource` and :ref:`order-refund-resource`.
Resource description
--------------------
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the transaction
order string Order code the transaction was created from
event string Event slug, only present on organizer-level API calls
created datetime The creation time of the transaction in the database
datetime datetime The time at which the transaction is financially relevant.
This is usually the same as created, but may vary for
retroactively created transactions after software bugs or
for data that preceeds this data model.
positionid integer Number of the position within the order this refers to,
is ``null`` for transactions that refer to a fee
count integer Number of items purchased, is negative for cancellations
item integer The internal ID of the item purchased (or ``null`` for fees)
variation integer The internal ID of the variation purchased (or ``null``)
subevent integer The internal ID of the event series date (or ``null``)
price money (string) Gross price of the transaction
tax_rate decimal (string) Tax rate applied in transaction
tax_rule integer The internal ID of the tax rule used (or ``null``)
tax_code string The selected tax code (or ``null``)
tax_value money (string) The computed tax value
fee_type string The type of fee (or ``null`` for products)
internal_type string Additional type classification of the fee (or ``null`` for products)
===================================== ========================== =======================================================
.. versionchanged:: 2025.7.0
This resource was added to the API.
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/transactions/
Returns a list of all transactions of an event.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/transactions/ 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": 123,
"order": "FOO",
"count": 1,
"created": "2017-12-01T10:00:00Z",
"datetime": "2017-12-01T10:00:00Z",
"item": null,
"variation": null,
"positionid": 1,
"price": "23.00",
"subevent": null,
"tax_code": "E",
"tax_rate": "0.00",
"tax_rule": 23,
"tax_value": "0.00",
"fee_type": null,
"internal_type": null
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:query string order: Only return transactions matching the given order code.
:query datetime_since: Only return transactions with a datetime at or after the given time.
:query datetime_before: Only return transactions with a datetime before the given time.
:query created_since: Only return transactions with a creation time at or after the given time.
:query created_before: Only return transactions with a creation time before the given time.
:query item: Only return transactions that match the given item ID.
:query item__in: Only return transactions that match one of the given item IDs (separated with a comma).
:query variation: Only return transactions that match the given variation ID.
:query variation__in: Only return transactions that match one of the given variation IDs (separated with a comma).
:query subevent: Only return transactions that match the given subevent ID.
:query subevent__in: Only return transactions that match one of the given subevent IDs (separated with a comma).
:query tax_rule: Only return transactions that match the given tax rule ID.
:query tax_rule__in: Only return transactions that match one of the given tax rule IDs (separated with a comma).
:query tax_code: Only return transactions that match the given tax code.
:query tax_code__in: Only return transactions that match one of the given tax codes (separated with a comma).
:query tax_rate: Only return transactions that match the given tax rate.
:query tax_rate__in: Only return transactions that match one of the given tax rates (separated with a comma).
:query fee_type: Only return transactions that match the given fee type.
:query fee_type__in: Only return transactions that match one of the given fee types (separated with a comma).
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``datetime``, ``created``, and ``id``.
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of a valid event
: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)/transactions/
Returns a list of all transactions of an organizer that you have access to.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/transactions/ 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": 123,
"event": "sampleconf",
"order": "FOO",
"count": 1,
"created": "2017-12-01T10:00:00Z",
"datetime": "2017-12-01T10:00:00Z",
"item": null,
"variation": null,
"positionid": 1,
"price": "23.00",
"subevent": null,
"tax_code": "E",
"tax_rate": "0.00",
"tax_rule": 23,
"tax_value": "0.00",
"fee_type": null,
"internal_type": null
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:query string event: Only return transactions matching the given event slug.
:query string order: Only return transactions matching the given order code.
:query datetime_since: Only return transactions with a datetime at or after the given time.
:query datetime_before: Only return transactions with a datetime before the given time.
:query created_since: Only return transactions with a creation time at or after the given time.
:query created_before: Only return transactions with a creation time before the given time.
:query item: Only return transactions that match the given item ID.
:query item__in: Only return transactions that match one of the given item IDs (separated with a comma).
:query variation: Only return transactions that match the given variation ID.
:query variation__in: Only return transactions that match one of the given variation IDs (separated with a comma).
:query subevent: Only return transactions that match the given subevent ID.
:query subevent__in: Only return transactions that match one of the given subevent IDs (separated with a comma).
:query tax_rule: Only return transactions that match the given tax rule ID.
:query tax_rule__in: Only return transactions that match one of the given tax rule IDs (separated with a comma).
:query tax_code: Only return transactions that match the given tax code.
:query tax_code__in: Only return transactions that match one of the given tax codes (separated with a comma).
:query tax_rate: Only return transactions that match the given tax rate.
:query tax_rate__in: Only return transactions that match one of the given tax rates (separated with a comma).
:query fee_type: Only return transactions that match the given fee type.
:query fee_type__in: Only return transactions that match one of the given fee types (separated with a comma).
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``datetime``, ``created``, and ``id``.
:param organizer: The ``slug`` field of a valid organizer
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.

View File

@@ -49,8 +49,6 @@ subevent integer ID of the date
show_hidden_items boolean Only if set to ``true``, this voucher allows to buy products with the property ``hide_without_voucher``. Defaults to ``true``.
all_addons_included boolean If set to ``true``, all add-on products for the product purchased with this voucher are included in the base price.
all_bundles_included boolean If set to ``true``, all bundled products for the product purchased with this voucher are added without their designated price.
budget money (string) The budget a voucher is allowed to consume before being used up (or ``null``)
budget_used money (string) The amount of budget the voucher has already used up.
===================================== ========================== =======================================================
@@ -101,9 +99,7 @@ Endpoints
"subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false,
"budget": None,
"budget_used": "0.00"
"all_bundles_included": false
}
]
}
@@ -173,9 +169,7 @@ Endpoints
"subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false,
"budget": None,
"budget_used": "0.00"
"all_bundles_included": false
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -245,9 +239,7 @@ Endpoints
"subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false,
"budget": None,
"budget_used": "0.00"
"all_bundles_included": false
}
:param organizer: The ``slug`` field of the organizer to create a voucher for
@@ -384,9 +376,7 @@ Endpoints
"subevent": null,
"show_hidden_items": false,
"all_addons_included": false,
"all_bundles_included": false,
"budget": None,
"budget_used": "0.00"
"all_bundles_included": false
}
:param organizer: The ``slug`` field of the organizer to modify

View File

@@ -1,200 +0,0 @@
.. highlight:: python
:linenothreshold: 5
Data sync providers
===================
.. warning:: This feature is considered **experimental**. It might change at any time without prior notice.
pretix provides connectivity to many external services through plugins. A common requirement
is unidirectionally sending (order, customer, ticket, ...) data into external systems.
The transfer is usually triggered by signals provided by pretix core (e.g. :data:`order_placed`),
but performed asynchronously.
Such plugins should use the :class:`OutboundSyncProvider` API to utilize the queueing, retry and mapping mechanisms as well as the user interface for configuration and monitoring.
An :class:`OutboundSyncProvider` for registering event participants in a mailing list could start
like this, for example:
.. code-block:: python
from pretix.base.datasync.datasync import OutboundSyncProvider
class MyListSyncProvider(OutboundSyncProvider):
identifier = "my_list"
display_name = "My Mailing List Service"
# ...c
The plugin must register listeners in `signals.py` for all signals that should to trigger a sync and
within it has to call :meth:`MyListSyncProvider.enqueue_order` to enqueue the order for synchronization:
.. code-block:: python
@receiver(order_placed, dispatch_uid="mylist_order_placed")
def on_order_placed(sender, order, **kwargs):
MyListSyncProvider.enqueue_order(order, "order_placed")
Furthermore, most of these plugins need to translate data from some pretix objects (e.g. orders)
into an external system's data structures. Sometimes, there is only one reasonable way or the
plugin author makes an opinionated decision what information from which objects should be
transferred into which data structures in the external system.
Otherwise, you can use a :class:`PropertyMappingFormSet` to let the user set up a mapping from pretix model fields
to external data fields. You could store the mapping information either in the event settings, or in a separate
data model. Your implementation of :attr:`OutboundSyncProvider.mappings`
needs to provide a list of mappings, which can be e.g. static objects or model instances, as long as they
have at least the properties defined in
:class:`pretix.base.datasync.datasync.StaticMapping`.
.. code-block:: python
# class MyListSyncProvider, contd.
def mappings(self):
return [
StaticMapping(
id=1, pretix_model='Order', external_object_type='Contact',
pretix_id_field='email', external_id_field='email',
property_mappings=self.event.settings.mylist_order_mapping,
))
]
Currently, we support `orders` and `order positions` as data sources, with the data fields defined in
:func:`pretix.base.datasync.sourcefields.get_data_fields`.
To perform the actual sync, implement :func:`sync_object_with_properties` and optionally
:func:`finalize_sync_order`. The former is called for each object to be created according to the ``mappings``.
For each order that was enqueued using :func:`enqueue_order`:
- each Mapping with ``pretix_model == "Order"`` results in one call to :func:`sync_object_with_properties`,
- each Mapping with ``pretix_model == "OrderPosition"`` results in one call to
:func:`sync_object_with_properties` per order position,
- :func:`finalize_sync_order` is called one time after all calls to :func:`sync_object_with_properties`.
Implementation examples
-----------------------
For example implementations, see the test cases in :mod:`tests.base.test_datasync`.
In :class:`SimpleOrderSync`, a basic data transfer of order data only is
shown. Therein, a ``sync_object_with_properties`` method is defined as follows:
.. code-block:: python
from pretix.base.datasync.utils import assign_properties
def sync_object_with_properties(
self, external_id_field, id_value, properties: list, inputs: dict,
mapping, mapped_objects: dict, **kwargs,
):
# First, we query the external service if our object-to-sync already exists there.
# This is necessary to make sure our method is idempotent, i.e. handles already synced
# data gracefully.
pre_existing_object = self.fake_api_client.retrieve_object(
mapping.external_object_type,
external_id_field,
id_value
)
# We use the helper function ``assign_properties`` to update a pre-existing object.
update_values = assign_properties(
new_values=properties,
old_values=pre_existing_object or {},
is_new=pre_existing_object is None,
list_sep=";",
)
# Then we can send our new data to the external service. The specifics of course depends
# on your API, e.g. you may need to use different endpoints for creating or updating an
# object, or pass the identifier separately instead of in the same dictionary as the
# other properties.
result = self.fake_api_client.create_or_update_object(mapping.external_object_type, {
**update_values,
external_id_field: id_value,
"_id": pre_existing_object and pre_existing_object.get("_id"),
})
# Finally, return a dictionary containing at least `object_type`, `external_id_field`,
# `id_value`, `external_link_href`, and `external_link_display_name` keys.
# Further keys may be provided for your internal use. This dictionary is provided
# in following calls in the ``mapped_objects`` dict, to allow creating associations
# to this object.
return {
"object_type": mapping.external_object_type,
"external_id_field": external_id_field,
"id_value": id_value,
"external_link_href": f"https://example.org/external-system/{mapping.external_object_type}/{id_value}/",
"external_link_display_name": f"Contact #{id_value} - Jane Doe",
"my_result": result,
}
.. note:: The result dictionaries of earlier invocations of :func:`sync_object_with_properties` are
only provided in subsequent calls of the same sync run, such that a mapping can
refer to e.g. the external id of an object created by a preceding mapping.
However, the result dictionaries are currently not provided across runs. This will
likely change in a future revision of this API, to allow easier integration of external
systems that do not allow retrieving/updating data by a pretix-provided key.
``mapped_objects`` is a dictionary of lists of dictionaries. The keys to the dictionary are
the mapping identifiers (``mapping.id``), the lists contain the result dictionaries returned
by :func:`sync_object_with_properties`.
In :class:`OrderAndTicketAssociationSync`, an example is given where orders, order positions,
and the association between them are transferred.
The OutboundSyncProvider base class
-----------------------------------
.. autoclass:: pretix.base.datasync.datasync.OutboundSyncProvider
:members:
Property mapping format
-----------------------
To allow the user to configure property mappings, you can use the PropertyMappingFormSet,
which will generate the required ``property_mappings`` value automatically. If you need
to specify the property mappings programmatically, you can refer to the description below
on their format.
.. autoclass:: pretix.control.forms.mapping.PropertyMappingFormSet
:members: to_property_mappings_json
A simple JSON-serialized ``property_mappings`` list for mapping some order information can look like this:
.. code-block:: json
[
{
"pretix_field": "email",
"external_field": "orderemail",
"value_map": "",
"overwrite": "overwrite",
},
{
"pretix_field": "order_status",
"external_field": "status",
"value_map": "{\"n\": \"pending\", \"p\": \"paid\", \"e\": \"expired\", \"c\": \"canceled\", \"r\": \"refunded\"}",
"overwrite": "overwrite",
},
{
"pretix_field": "order_total",
"external_field": "total",
"value_map": "",
"overwrite": "overwrite",
}
]
Translating mappings on Event copy
----------------------------------
Property mappings can contain references to event-specific primary keys. Therefore, plugins must register to the
event_copy_data signal and call translate_property_mappings on all property mappings they store.
.. autofunction:: pretix.base.datasync.utils.translate_property_mappings

View File

@@ -18,6 +18,5 @@ Contents:
customview
cookieconsent
auth
datasync
general
quality

View File

@@ -5,7 +5,7 @@ Development setup
This tutorial helps you to get started hacking with pretix on your own computer. You need this to
be able to contribute to pretix, but it might also be helpful if you want to write your own plugins.
If you want to install pretix on a server for actual usage, go to the `administrator documentation`_ instead.
If you want to install pretix on a server for actual usage, go to the [administrator documentation](https://docs.pretix.eu/self-hosting/) instead.
Obtain a copy of the source code
--------------------------------
@@ -221,4 +221,3 @@ your virtual environment.::
.. _Django's documentation: https://docs.djangoproject.com/en/1.11/ref/django-admin/#runserver
.. _pretixdroid: https://github.com/pretix/pretixdroid
.. _administrator documentation: https://docs.pretix.eu/self-hosting/

View File

@@ -33,7 +33,7 @@ dependencies = [
"celery==5.5.*",
"chardet==5.2.*",
"cryptography>=44.0.0",
"css-inline==0.16.*",
"css-inline==0.15.*",
"defusedcsv>=1.1.0",
"Django[argon2]==4.2.*,>=4.2.15",
"django-bootstrap3==25.1",
@@ -74,14 +74,14 @@ dependencies = [
"paypal-checkout-serversdk==1.0.*",
"PyJWT==2.10.*",
"phonenumberslite==9.0.*",
"Pillow==11.3.*",
"Pillow==11.2.*",
"pretix-plugin-build",
"protobuf==6.31.*",
"psycopg2-binary",
"pycountry",
"pycparser==2.22",
"pycryptodome==3.23.*",
"pypdf==5.8.*",
"pypdf==5.6.*",
"python-bidi==0.6.*", # Support for Arabic in reportlab
"python-dateutil==2.9.*",
"pytz",
@@ -122,7 +122,7 @@ dev = [
"pytest-django==4.*",
"pytest-mock==3.14.*",
"pytest-sugar",
"pytest-xdist==3.8.*",
"pytest-xdist==3.7.*",
"pytest==8.4.*",
"responses",
]

View File

@@ -115,7 +115,6 @@ ALL_LANGUAGES = [
('sk', _('Slovak')),
('sv', _('Swedish')),
('es', _('Spanish')),
('es-419', _('Spanish (Latin America)')),
('tr', _('Turkish')),
('uk', _('Ukrainian')),
]
@@ -173,12 +172,6 @@ EXTRA_LANG_INFO = {
'name': 'Norwegian Bokmal',
'name_local': 'norsk (bokmål)',
},
'es-419': {
'bidi': False,
'code': 'es-419',
'name': 'Spanish (Latin America)',
'name_local': 'Español',
},
}
django.conf.locale.LANG_INFO.update(EXTRA_LANG_INFO)

View File

@@ -582,7 +582,7 @@ class QuotaSerializer(I18nAwareModelSerializer):
class Meta:
model = Quota
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent', 'closed', 'close_when_sold_out',
'release_after_exit', 'available', 'available_number', 'ignore_for_event_availability')
'release_after_exit', 'available', 'available_number')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

View File

@@ -56,7 +56,7 @@ from pretix.base.models import (
)
from pretix.base.models.orders import (
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
PrintLog, RevokedTicketSecret, Transaction,
PrintLog, RevokedTicketSecret,
)
from pretix.base.pdf import get_images, get_variables
from pretix.base.services.cart import error_messages
@@ -1783,23 +1783,3 @@ class BlockedTicketSecretSerializer(I18nAwareModelSerializer):
class Meta:
model = BlockedTicketSecret
fields = ('id', 'secret', 'updated', 'blocked')
class TransactionSerializer(I18nAwareModelSerializer):
order = serializers.SlugRelatedField(slug_field="code", read_only=True)
class Meta:
model = Transaction
fields = (
"id", "order", "created", "datetime", "positionid", "count", "item", "variation",
"subevent", "price", "tax_rate", "tax_rule", "tax_code", "tax_value", "fee_type",
"internal_type"
)
class OrganizerTransactionSerializer(TransactionSerializer):
event = serializers.SlugRelatedField(source="order.event", slug_field="slug", read_only=True)
class Meta:
model = Transaction
fields = TransactionSerializer.Meta.fields + ("event",)

View File

@@ -19,8 +19,6 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from decimal import Decimal
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
@@ -66,15 +64,14 @@ class SeatGuidField(serializers.CharField):
class VoucherSerializer(I18nAwareModelSerializer):
seat = SeatGuidField(allow_null=True, required=False)
budget_used = serializers.DecimalField(read_only=True, max_digits=13, decimal_places=2, min_value=Decimal('0.00'))
class Meta:
model = Voucher
fields = ('id', 'code', 'max_usages', 'redeemed', 'min_usages', 'valid_until', 'block_quota',
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
'tag', 'comment', 'subevent', 'show_hidden_items', 'seat', 'all_addons_included',
'all_bundles_included', 'budget', 'budget_used')
read_only_fields = ('id', 'redeemed', 'budget_used')
'all_bundles_included')
read_only_fields = ('id', 'redeemed')
list_serializer_class = VoucherListSerializer
def validate(self, data):

View File

@@ -66,7 +66,6 @@ orga_router.register(r'orders', order.OrganizerOrderViewSet)
orga_router.register(r'invoices', order.InvoiceViewSet)
orga_router.register(r'scheduled_exports', exporters.ScheduledOrganizerExportViewSet)
orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters')
orga_router.register(r'transactions', order.OrganizerTransactionViewSet)
team_router = routers.DefaultRouter()
team_router.register(r'members', organizer.TeamMemberViewSet)
@@ -84,7 +83,6 @@ event_router.register(r'quotas', item.QuotaViewSet)
event_router.register(r'vouchers', voucher.VoucherViewSet)
event_router.register(r'orders', order.EventOrderViewSet)
event_router.register(r'orderpositions', order.OrderPositionViewSet)
event_router.register(r'transactions', order.TransactionViewSet)
event_router.register(r'invoices', order.InvoiceViewSet)
event_router.register(r'revokedsecrets', order.RevokedSecretViewSet, basename='revokedsecrets')
event_router.register(r'blockedsecrets', order.BlockedSecretViewSet, basename='blockedsecrets')

View File

@@ -57,9 +57,9 @@ from pretix.api.serializers.order import (
BlockedTicketSecretSerializer, InvoiceSerializer, OrderCreateSerializer,
OrderPaymentCreateSerializer, OrderPaymentSerializer,
OrderPositionSerializer, OrderRefundCreateSerializer,
OrderRefundSerializer, OrderSerializer, OrganizerTransactionSerializer,
PriceCalcSerializer, PrintLogSerializer, RevokedTicketSecretSerializer,
SimulatedOrderSerializer, TransactionSerializer,
OrderRefundSerializer, OrderSerializer, PriceCalcSerializer,
PrintLogSerializer, RevokedTicketSecretSerializer,
SimulatedOrderSerializer,
)
from pretix.api.serializers.orderchange import (
BlockNameSerializer, OrderChangeOperationSerializer,
@@ -80,7 +80,6 @@ from pretix.base.models import (
)
from pretix.base.models.orders import (
BlockedTicketSecret, PrintLog, QuestionAnswer, RevokedTicketSecret,
Transaction,
)
from pretix.base.payment import PaymentException
from pretix.base.pdf import get_images
@@ -2031,61 +2030,3 @@ class BlockedSecretViewSet(viewsets.ReadOnlyModelViewSet):
def get_queryset(self):
return BlockedTicketSecret.objects.filter(event=self.request.event)
with scopes_disabled():
class TransactionFilter(FilterSet):
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
event = django_filters.CharFilter(field_name='order__event', lookup_expr='slug__iexact')
datetime_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte')
datetime_before = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='lt')
created_since = django_filters.IsoDateTimeFilter(field_name='created', lookup_expr='gte')
created_before = django_filters.IsoDateTimeFilter(field_name='created', lookup_expr='lt')
class Meta:
model = Transaction
fields = {
'item': ['exact', 'in'],
'variation': ['exact', 'in'],
'subevent': ['exact', 'in'],
'tax_rule': ['exact', 'in'],
'tax_code': ['exact', 'in'],
'tax_rate': ['exact', 'in'],
'fee_type': ['exact', 'in'],
}
class TransactionViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = TransactionSerializer
queryset = Transaction.objects.none()
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
ordering = ('datetime', 'pk')
ordering_fields = ('datetime', 'created', 'id',)
filterset_class = TransactionFilter
permission = 'can_view_orders'
def get_queryset(self):
return Transaction.objects.filter(order__event=self.request.event).select_related("order")
class OrganizerTransactionViewSet(TransactionViewSet):
serializer_class = OrganizerTransactionSerializer
permission = None
def get_queryset(self):
qs = Transaction.objects.filter(
order__event__organizer=self.request.organizer
).select_related("order", "order__event")
if isinstance(self.request.auth, (TeamAPIToken, Device)):
qs = qs.filter(
order__event__in=self.request.auth.get_events_with_permission("can_view_orders"),
)
elif self.request.user.is_authenticated:
qs = qs.filter(
order__event__in=self.request.user.get_events_with_permission("can_view_orders", request=self.request)
)
else:
raise PermissionDenied("Unknown authentication scheme")
return qs

View File

@@ -46,7 +46,7 @@ class PretixBaseConfig(AppConfig):
from . import invoice # NOQA
from . import notifications # NOQA
from . import email # NOQA
from .services import auth, checkin, currencies, datasync, export, mail, tickets, cart, modelimport, orders, invoices, cleanup, update_check, quotas, notifications, vouchers # NOQA
from .services import auth, checkin, currencies, export, mail, tickets, cart, modelimport, orders, invoices, cleanup, update_check, quotas, notifications, vouchers # NOQA
from .models import _transactions # NOQA
from django.conf import settings

View File

@@ -1,21 +0,0 @@
#
# 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/>.
#

View File

@@ -1,437 +0,0 @@
#
# 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 json
import logging
from collections import namedtuple
from datetime import timedelta
from functools import cached_property
from typing import Optional, Protocol
import sentry_sdk
from django.db import DatabaseError, transaction
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from pretix.base.datasync.sourcefields import (
EVENT, EVENT_OR_SUBEVENT, ORDER, ORDER_POSITION, get_data_fields,
)
from pretix.base.i18n import language
from pretix.base.logentrytype_registry import make_link
from pretix.base.models.datasync import OrderSyncQueue, OrderSyncResult
from pretix.base.signals import EventPluginRegistry
from pretix.helpers import OF_SELF
logger = logging.getLogger(__name__)
datasync_providers = EventPluginRegistry({"identifier": lambda o: o.identifier})
class BaseSyncError(Exception):
def __init__(self, messages, full_message=None):
self.messages = messages
self.full_message = full_message
class UnrecoverableSyncError(BaseSyncError):
"""
A SyncProvider encountered a permanent problem, where a retry will not be successful.
"""
failure_mode = "permanent"
class SyncConfigError(UnrecoverableSyncError):
"""
A SyncProvider is misconfigured in a way where a retry without configuration change will
not be successful.
"""
failure_mode = "config"
class RecoverableSyncError(BaseSyncError):
"""
A SyncProvider has encountered a temporary problem, and the sync should be retried
at a later time.
"""
pass
class ObjectMapping(Protocol):
id: int
pretix_model: str
external_object_type: str
pretix_id_field: str
external_id_field: str
property_mappings: str
StaticMapping = namedtuple('StaticMapping', ('id', 'pretix_model', 'external_object_type', 'pretix_id_field', 'external_id_field', 'property_mappings'))
class OutboundSyncProvider:
max_attempts = 5
def __init__(self, event):
self.event = event
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
@classmethod
@property
def display_name(cls):
return str(cls.identifier)
@classmethod
def enqueue_order(cls, order, triggered_by, not_before=None):
"""
Adds an order to the sync queue. May only be called on derived classes which define an ``identifier`` attribute.
Should be called in the appropriate signal receivers, e.g.::
@receiver(order_placed, dispatch_uid="mysync_order_placed")
def on_order_placed(sender, order, **kwargs):
MySyncProvider.enqueue_order(order, "order_placed")
:param order: the Order that should be synced
:param triggered_by: the reason why the order should be synced, e.g. name of the signal
(currently only used internally for logging)
"""
if not hasattr(cls, 'identifier'):
raise TypeError('Call this method on a derived class that defines an "identifier" attribute.')
OrderSyncQueue.objects.update_or_create(
order=order,
sync_provider=cls.identifier,
in_flight=False,
defaults={
"event": order.event,
"triggered_by": triggered_by,
"not_before": not_before or now(),
"need_manual_retry": None,
},
)
@classmethod
def get_external_link_info(cls, event, external_link_href, external_link_display_name):
return {
"href": external_link_href,
"val": external_link_display_name,
}
@classmethod
def get_external_link_html(cls, event, external_link_href, external_link_display_name):
info = cls.get_external_link_info(event, external_link_href, external_link_display_name)
return make_link(info, '{val}')
def next_retry_date(self, sq):
"""
Optionally override to configure a different retry backoff behavior
"""
return now() + timedelta(hours=1)
def should_sync_order(self, order):
"""
Optionally override this method to exclude certain orders from sync by returning ``False``
"""
return True
@property
def mappings(self):
"""
Implementations must override this property to provide the data mappings as a list of objects.
They can return instances of the ``StaticMapping`` `namedtuple` defined above, or create their own
class (e.g. a Django model).
:return: The returned objects must have at least the following properties:
- `id`: Unique identifier for this mapping. If the mappings are Django models, the database primary key
should be used. This may be referenced in other mappings, to establish relations between objects.
- `pretix_model`: Which pretix model to use as data source in this mapping. Possible values are
the keys of ``sourcefields.AVAILABLE_MODELS``
- `external_object_type`: Destination object type in the target system. opaque string of maximum 128 characters.
- `pretix_id_field`: Which pretix data field should be used to identify the mapped object. Any ``DataFieldInfo.key``
returned by ``sourcefields.get_data_fields()`` for the combination of ``Event`` and ``pretix_model``.
- `external_id_field`: Destination identifier field in the target system.
- `property_mappings`: Mapping configuration as generated by ``PropertyMappingFormSet.to_property_mappings_json()``.
"""
raise NotImplementedError
def sync_queued_orders(self, queued_orders):
"""
This method should catch all Exceptions and handle them appropriately. It should never throw
an Exception, as that may block the entire queue.
"""
for queue_item in queued_orders:
with transaction.atomic():
try:
sq = (
OrderSyncQueue.objects
.select_for_update(of=OF_SELF, nowait=True)
.select_related("order")
.get(pk=queue_item.pk)
)
if sq.in_flight:
continue
sq.in_flight = True
sq.in_flight_since = now()
sq.save()
except DatabaseError:
# Either select_for_update failed to lock the row, or we couldn't set in_flight
# as this order is already in flight (UNIQUE violation). In either case, we ignore
# this order for now.
continue
try:
mapped_objects = self.sync_order(sq.order)
if not all(all(not res or res.sync_info.get("action", "") == "nothing_to_do" for res in res_list) for res_list in mapped_objects.values()):
sq.order.log_action("pretix.event.order.data_sync.success", {
"provider": self.identifier,
"objects": {
mapping_id: [osr and osr.to_result_dict() for osr in results]
for mapping_id, results in mapped_objects.items()
},
})
sq.delete()
except UnrecoverableSyncError as e:
sq.set_sync_error(e.failure_mode, e.messages, e.full_message)
except RecoverableSyncError as e:
sq.failed_attempts += 1
sq.not_before = self.next_retry_date(sq)
# model changes saved by set_sync_error / clear_in_flight calls below
if sq.failed_attempts >= self.max_attempts:
logger.exception('Failed to sync order (max attempts exceeded)')
sentry_sdk.capture_exception(e)
sq.set_sync_error("exceeded", e.messages, e.full_message)
else:
logger.info(
f"Could not sync order {sq.order.code} to {type(self).__name__} "
f"(transient error, attempt #{sq.failed_attempts}, next {sq.not_before})",
exc_info=True,
)
sq.clear_in_flight()
except Exception as e:
logger.exception('Failed to sync order (unhandled exception)')
sentry_sdk.capture_exception(e)
sq.set_sync_error("internal", [], str(e))
@cached_property
def data_fields(self):
return {
f.key: f
for f in get_data_fields(self.event)
}
def get_field_value(self, inputs, mapping_entry):
key = mapping_entry["pretix_field"]
try:
field = self.data_fields[key]
except KeyError:
with language(self.event.settings.locale):
raise SyncConfigError([_(
'Field "{field_name}" is not valid for {available_inputs}. Please check your {provider_name} settings.'
).format(key=key, available_inputs="/".join(inputs.keys()), provider_name=self.display_name)])
input = inputs[field.required_input]
val = field.getter(input)
if isinstance(val, list):
if field.enum_opts and mapping_entry.get("value_map"):
map = json.loads(mapping_entry["value_map"])
try:
val = [map[el] for el in val]
except KeyError:
with language(self.event.settings.locale):
raise SyncConfigError([_(
'Please update value mapping for field "{field_name}" - option "{val}" not assigned'
).format(field_name=key, val=val)])
val = ",".join(val)
return val
def get_properties(self, inputs: dict, property_mappings: str):
property_mappings = json.loads(property_mappings)
return [
(m["external_field"], self.get_field_value(inputs, m), m["overwrite"])
for m in property_mappings
]
def sync_object_with_properties(
self,
external_id_field: str,
id_value,
properties: list,
inputs: dict,
mapping: ObjectMapping,
mapped_objects: dict,
**kwargs,
) -> Optional[dict]:
"""
This method is called for each object that needs to be created/updated in the external system -- which these are is
determined by the implementation of the `mapping` property.
:param external_id_field: Identifier field in the external system as provided in ``mapping.external_identifier``
:param id_value: Identifier contents as retrieved from the property specified by ``mapping.pretix_identifier`` of the model
specified by ``mapping.pretix_model``
:param properties: All properties defined in ``mapping.property_mappings``, as list of three-tuples
``(external_field, value, overwrite)``
:param inputs: All pretix model instances from which data can be retrieved for this mapping.
Dictionary mapping from sourcefields.ORDER_POSITION, .ORDER, .EVENT, .EVENT_OR_SUBEVENT to the
relevant Django model.
Most providers don't need to use this parameter directly, as `properties` and `id_value`
already contain the values as evaluated from the available inputs.
:param mapping: The mapping object as returned by ``self.mappings``
:param mapped_objects: Information about objects that were synced in the same sync run, by mapping definitions
*before* the current one in order of ``self.mappings``.
Type is a dictionary ``{mapping.id: [list of OrderSyncResult objects]}``
Useful to create associations between objects in the target system.
Example code to create return value::
return {
# optional:
"action": "nothing_to_do", # to inform that no action was taken, because the data was already up-to-date.
# other values for action (e.g. create, update) currently have no special
# meaning, but are visible for debugging purposes to admins.
# optional:
"external_link_href": "https://external-system.example.com/backend/link/to/contact/123/",
"external_link_display_name": "Contact #123 - Jane Doe",
"...optionally further values you need in mapped_objects for association": 123456789,
}
The return value needs to be a JSON serializable dict, or None.
Return None only in case you decide this object should not be synced at all in this mapping. Do not return None in
case the object is already up-to-date in the target system (return "action": "nothing_to_do" instead).
This method needs to be idempotent, i.e. calling it multiple times with the same input values should create
only a single object in the target system.
Subsequent calls with the same mapping and id_value should update the existing object, instead of creating a new one.
In a SQL database, you might use an `INSERT OR UPDATE` or `UPSERT` statement; many REST APIs provide an equivalent API call.
"""
raise NotImplementedError()
def sync_object(
self,
inputs: dict,
mapping,
mapped_objects: dict,
):
logger.debug("Syncing object %r, %r, %r", inputs, mapping, mapped_objects)
properties = self.get_properties(inputs, mapping.property_mappings)
logger.debug("Properties: %r", properties)
id_value = self.get_field_value(inputs, {"pretix_field": mapping.pretix_id_field})
if not id_value:
return None
info = self.sync_object_with_properties(
external_id_field=mapping.external_id_field,
id_value=id_value,
properties=properties,
inputs=inputs,
mapping=mapping,
mapped_objects=mapped_objects,
)
if not info:
return None
external_link_href = info.pop('external_link_href', None)
external_link_display_name = info.pop('external_link_display_name', None)
obj, created = OrderSyncResult.objects.update_or_create(
order=inputs.get(ORDER), order_position=inputs.get(ORDER_POSITION), sync_provider=self.identifier,
mapping_id=mapping.id,
defaults=dict(
external_object_type=mapping.external_object_type,
external_id_field=mapping.external_id_field,
id_value=id_value,
external_link_href=external_link_href,
external_link_display_name=external_link_display_name,
sync_info=info,
transmitted=now(),
)
)
return obj
def sync_order(self, order):
if not self.should_sync_order(order):
logger.debug("Skipping order %r", order)
return
logger.debug("Syncing order %r", order)
positions = list(
order.all_positions
.prefetch_related("answers", "answers__question")
.select_related(
"voucher",
)
)
order_inputs = {ORDER: order, EVENT: self.event}
mapped_objects = {}
for mapping in self.mappings:
if mapping.pretix_model == 'Order':
mapped_objects[mapping.id] = [
self.sync_object(order_inputs, mapping, mapped_objects)
]
elif mapping.pretix_model == 'OrderPosition':
mapped_objects[mapping.id] = [
self.sync_object({
**order_inputs, EVENT_OR_SUBEVENT: op.subevent or self.event, ORDER_POSITION: op
}, mapping, mapped_objects)
for op in positions
]
else:
raise SyncConfigError("Invalid pretix model '{}'".format(mapping.pretix_model))
self.finalize_sync_order(order)
return mapped_objects
def filter_mapped_objects(self, mapped_objects, inputs):
"""
For order positions, only
"""
if ORDER_POSITION in inputs:
return {
mapping_id: [
osr for osr in results
if osr and (osr.order_position_id is None or osr.order_position_id == inputs[ORDER_POSITION].id)
]
for mapping_id, results in mapped_objects.items()
}
else:
return mapped_objects
def finalize_sync_order(self, order):
"""
Called after ``sync_object`` has been called successfully for all objects of a specific order. Can
be used for saving bulk information per order.
"""
pass
def close(self):
"""
Called after all orders of an event have been synced. Can be used for clean-up tasks (e.g. closing
a session).
"""
pass

View File

@@ -1,534 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from collections import namedtuple
from functools import partial
from django.db.models import Max, Q
from django.utils.translation import gettext_lazy as _
from pretix.base.models import Checkin, InvoiceAddress, Order, Question
from pretix.base.settings import PERSON_NAME_SCHEMES
def get_answer(op, question_identifier=None):
a = None
if op.addon_to:
if "answers" in getattr(op.addon_to, "_prefetched_objects_cache", {}):
try:
a = [
a
for a in op.addon_to.answers.all()
if a.question.identifier == question_identifier
][0]
except IndexError:
pass
else:
a = op.addon_to.answers.filter(
question__identifier=question_identifier
).first()
if "answers" in getattr(op, "_prefetched_objects_cache", {}):
try:
a = [
a
for a in op.answers.all()
if a.question.identifier == question_identifier
][0]
except IndexError:
pass
else:
a = op.answers.filter(question__identifier=question_identifier).first()
if not a:
return ""
else:
if a.question.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
return [str(o.identifier) for o in a.options.all()]
if a.question.type == Question.TYPE_BOOLEAN:
return a.answer == "True"
return a.answer
def get_payment_date(order):
if order.status == Order.STATUS_PENDING:
return None
return isoformat_or_none(order.payments.aggregate(m=Max("payment_date"))["m"])
def isoformat_or_none(dt):
return dt and dt.isoformat()
def first_checkin_on_list(list_pk, position):
checkin = position.checkins.filter(
list__pk=list_pk, type=Checkin.TYPE_ENTRY
).first()
if checkin:
return isoformat_or_none(checkin.datetime)
def split_name_on_last_space(name, part):
name_parts = name.rsplit(" ", 1)
return name_parts[part] if len(name_parts) > part else ""
ORDER_POSITION = 'position'
ORDER = 'order'
EVENT = 'event'
EVENT_OR_SUBEVENT = 'event_or_subevent'
AVAILABLE_MODELS = {
'OrderPosition': (ORDER_POSITION, ORDER, EVENT_OR_SUBEVENT, EVENT),
'Order': (ORDER, EVENT),
}
DataFieldInfo = namedtuple(
'DataFieldInfo',
field_names=('required_input', 'key', 'label', 'type', 'enum_opts', 'getter', 'deprecated'),
defaults=[False]
)
def get_invoice_address_or_empty(order):
try:
return order.invoice_address
except InvoiceAddress.DoesNotExist:
return InvoiceAddress()
def get_data_fields(event, for_model=None):
"""
Returns tuple of (required_input, key, label, type, enum_opts, getter)
Type is one of the Question types as defined in Question.TYPE_CHOICES.
The data type of the return value of `getter` depends on `type`:
- TYPE_CHOICE_MULTIPLE: list of strings
- TYPE_CHOICE: list, containing zero or one strings
- TYPE_BOOLEAN: boolean
- all other (including TYPE_NUMBER): string
"""
name_scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
name_headers = []
if name_scheme and len(name_scheme["fields"]) > 1:
for k, label, w in name_scheme["fields"]:
name_headers.append(label)
src_fields = (
[
DataFieldInfo(
ORDER_POSITION,
"attendee_name",
_("Attendee name"),
Question.TYPE_STRING,
None,
lambda position: position.attendee_name
or (position.addon_to.attendee_name if position.addon_to else None),
),
]
+ [
DataFieldInfo(
ORDER_POSITION,
"attendee_name_" + k,
_("Attendee") + ": " + label,
Question.TYPE_STRING,
None,
partial(
lambda k, position: (
position.attendee_name_parts
or (position.addon_to.attendee_name_parts if position.addon_to else {})
or {}
).get(k, ""),
k,
),
deprecated=len(name_scheme["fields"]) == 1,
)
for k, label, w in name_scheme["fields"]
]
+ [
DataFieldInfo(
ORDER_POSITION,
"attendee_email",
_("Attendee email"),
Question.TYPE_STRING,
None,
lambda position: position.attendee_email
or (position.addon_to.attendee_email if position.addon_to else None),
),
DataFieldInfo(
ORDER_POSITION,
"attendee_or_order_email",
_("Attendee or order email"),
Question.TYPE_STRING,
None,
lambda position: position.attendee_email
or (position.addon_to.attendee_email if position.addon_to else None)
or position.order.email,
),
DataFieldInfo(
ORDER_POSITION,
"attendee_company",
_("Attendee company"),
Question.TYPE_STRING,
None,
lambda position: position.company or (position.addon_to.company if position.addon_to else None),
),
DataFieldInfo(
ORDER_POSITION,
"attendee_street",
_("Attendee address street"),
Question.TYPE_STRING,
None,
lambda position: position.street or (position.addon_to.street if position.addon_to else None),
),
DataFieldInfo(
ORDER_POSITION,
"attendee_zipcode",
_("Attendee address ZIP code"),
Question.TYPE_STRING,
None,
lambda position: position.zipcode or (position.addon_to.zipcode if position.addon_to else None),
),
DataFieldInfo(
ORDER_POSITION,
"attendee_city",
_("Attendee address city"),
Question.TYPE_STRING,
None,
lambda position: position.city or (position.addon_to.city if position.addon_to else None),
),
DataFieldInfo(
ORDER_POSITION,
"attendee_country",
_("Attendee address country"),
Question.TYPE_COUNTRYCODE,
None,
lambda position: str(
position.country or (position.addon_to.attendee_name if position.addon_to else "")
),
),
DataFieldInfo(
ORDER,
"invoice_address_company",
_("Invoice address company"),
Question.TYPE_STRING,
None,
lambda order: get_invoice_address_or_empty(order).company,
),
DataFieldInfo(
ORDER,
"invoice_address_name",
_("Invoice address name"),
Question.TYPE_STRING,
None,
lambda order: get_invoice_address_or_empty(order).name,
),
]
+ [
DataFieldInfo(
ORDER,
"invoice_address_name_" + k,
_("Invoice address") + ": " + label,
Question.TYPE_STRING,
None,
partial(
lambda k, order: (get_invoice_address_or_empty(order).name_parts or {}).get(
k, ""
),
k,
),
deprecated=len(name_scheme["fields"]) == 1,
)
for k, label, w in name_scheme["fields"]
]
+ [
DataFieldInfo(
ORDER,
"invoice_address_street",
_("Invoice address street"),
Question.TYPE_STRING,
None,
lambda order: get_invoice_address_or_empty(order).street,
),
DataFieldInfo(
ORDER,
"invoice_address_zipcode",
_("Invoice address ZIP code"),
Question.TYPE_STRING,
None,
lambda order: get_invoice_address_or_empty(order).zipcode,
),
DataFieldInfo(
ORDER,
"invoice_address_city",
_("Invoice address city"),
Question.TYPE_STRING,
None,
lambda order: get_invoice_address_or_empty(order).city,
),
DataFieldInfo(
ORDER,
"invoice_address_country",
_("Invoice address country"),
Question.TYPE_COUNTRYCODE,
None,
lambda order: str(get_invoice_address_or_empty(order).country),
),
DataFieldInfo(
ORDER,
"email",
_("Order email"),
Question.TYPE_STRING,
None,
lambda order: order.email,
),
DataFieldInfo(
ORDER,
"order_code",
_("Order code"),
Question.TYPE_STRING,
None,
lambda order: order.code,
),
DataFieldInfo(
ORDER,
"event_order_code",
_("Event and order code"),
Question.TYPE_STRING,
None,
lambda order: order.full_code,
),
DataFieldInfo(
ORDER,
"order_total",
_("Order total"),
Question.TYPE_NUMBER,
None,
lambda order: str(order.total),
),
DataFieldInfo(
ORDER_POSITION,
"product",
_("Product and variation name"),
Question.TYPE_STRING,
None,
lambda position: str(
str(position.item.internal_name or position.item.name)
+ ((" " + str(position.variation.value)) if position.variation else "")
),
),
DataFieldInfo(
ORDER_POSITION,
"product_id",
_("Product ID"),
Question.TYPE_NUMBER,
None,
lambda position: str(position.item.pk),
),
DataFieldInfo(
EVENT,
"event_slug",
_("Event short form"),
Question.TYPE_STRING,
None,
lambda event: str(event.slug),
),
DataFieldInfo(
EVENT,
"event_name",
_("Event name"),
Question.TYPE_STRING,
None,
lambda event: str(event.name),
),
DataFieldInfo(
EVENT_OR_SUBEVENT,
"event_date_from",
_("Event start date"),
Question.TYPE_DATETIME,
None,
lambda event_or_subevent: isoformat_or_none(event_or_subevent.date_from),
),
DataFieldInfo(
EVENT_OR_SUBEVENT,
"event_date_to",
_("Event end date"),
Question.TYPE_DATETIME,
None,
lambda event_or_subevent: isoformat_or_none(event_or_subevent.date_to),
),
DataFieldInfo(
ORDER_POSITION,
"voucher_code",
_("Voucher code"),
Question.TYPE_STRING,
None,
lambda position: position.voucher.code if position.voucher_id else "",
),
DataFieldInfo(
ORDER_POSITION,
"ticket_id",
_("Order code and position number"),
Question.TYPE_STRING,
None,
lambda position: position.code,
),
DataFieldInfo(
ORDER_POSITION,
"ticket_price",
_("Ticket price"),
Question.TYPE_NUMBER,
None,
lambda position: str(position.price),
),
DataFieldInfo(
ORDER,
"order_status",
_("Order status"),
Question.TYPE_CHOICE,
Order.STATUS_CHOICE,
lambda order: [order.status],
),
DataFieldInfo(
ORDER_POSITION,
"ticket_status",
_("Ticket status"),
Question.TYPE_CHOICE,
Order.STATUS_CHOICE,
lambda position: [Order.STATUS_CANCELED if position.canceled else position.order.status],
),
DataFieldInfo(
ORDER,
"order_date",
_("Order date and time"),
Question.TYPE_DATETIME,
None,
lambda order: order.datetime.isoformat(),
),
DataFieldInfo(
ORDER,
"payment_date",
_("Payment date and time"),
Question.TYPE_DATETIME,
None,
get_payment_date,
),
DataFieldInfo(
ORDER,
"order_locale",
_("Order language code"),
Question.TYPE_CHOICE,
[(lc, lc) for lc in event.settings.locales],
lambda order: [order.locale],
),
DataFieldInfo(
ORDER_POSITION,
"position_id",
_("Order position ID"),
Question.TYPE_NUMBER,
None,
lambda op: str(op.pk),
),
]
+ [
DataFieldInfo(
ORDER_POSITION,
"checkin_date_" + str(cl.pk),
_("Check-in datetime on list {}").format(cl.name),
Question.TYPE_DATETIME,
None,
partial(first_checkin_on_list, cl.pk),
)
for cl in event.checkin_lists.all()
]
+ [
DataFieldInfo(
ORDER_POSITION,
"question_" + q.identifier,
_("Question: {name}").format(name=str(q.question)),
q.type,
get_enum_opts(q),
partial(lambda qq, position: get_answer(position, qq.identifier), q),
)
for q in event.questions.filter(~Q(type=Question.TYPE_FILE)).prefetch_related("options")
]
)
if not any(field_name == "given_name" for field_name, label, weight in name_scheme["fields"]):
src_fields += [
DataFieldInfo(
ORDER_POSITION,
"attendee_name_given_name",
_("Attendee") + ": " + _("Given name") + " (⚠️ auto-generated, not recommended)",
Question.TYPE_STRING,
None,
lambda position: split_name_on_last_space(position.attendee_name, part=0),
deprecated=True,
),
DataFieldInfo(
ORDER,
"invoice_address_name_given_name",
_("Invoice address") + ": " + _("Given name") + " (⚠️ auto-generated, not recommended)",
Question.TYPE_STRING,
None,
lambda order: split_name_on_last_space(get_invoice_address_or_empty(order).name, part=0),
deprecated=True,
),
]
if not any(field_name == "family_name" for field_name, label, weight in name_scheme["fields"]):
src_fields += [
DataFieldInfo(
ORDER_POSITION,
"attendee_name_family_name",
_("Attendee") + ": " + _("Family name") + " (⚠️ auto-generated, not recommended)",
Question.TYPE_STRING,
None,
lambda position: split_name_on_last_space(position.attendee_name, part=1),
deprecated=True,
),
DataFieldInfo(
ORDER,
"invoice_address_name_family_name",
_("Invoice address") + ": " + _("Family name") + " (⚠️ auto-generated, not recommended)",
Question.TYPE_STRING,
None,
lambda order: split_name_on_last_space(get_invoice_address_or_empty(order).name, part=1),
deprecated=True,
),
]
if for_model:
available_inputs = AVAILABLE_MODELS[for_model]
return [
f for f in src_fields if f.required_input in available_inputs
]
else:
return src_fields
def get_enum_opts(q):
if q.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
return [(opt.identifier, opt.answer) for opt in q.options.all()]
else:
return None

View File

@@ -1,122 +0,0 @@
#
# 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 json
from typing import List, Tuple
from pretix.base.datasync.datasync import SyncConfigError
from pretix.base.models.datasync import (
MODE_APPEND_LIST, MODE_OVERWRITE, MODE_SET_IF_EMPTY, MODE_SET_IF_NEW,
)
def assign_properties(
new_values: List[Tuple[str, str, str]], old_values: dict, is_new, list_sep
):
"""
Generates a dictionary mapping property keys to new values, handling conditional overwrites and list updates
according to an update mode specified per property.
Supported update modes are:
- `MODE_OVERWRITE`: Replaces the existing value with the new value.
- `MODE_SET_IF_NEW`: Only sets the property if `is_new` is True.
- `MODE_SET_IF_EMPTY`: Only sets the property if the field is empty or missing in old_values.
- `MODE_APPEND_LIST`: Appends the new value to the list from old_values (or the empty list if missing),
using `list_sep` as a separator.
:param new_values: List of tuples, where each tuple contains (field_name, new_value, update_mode).
:param old_values: Dictionary, current property values in the external system.
:param is_new: Boolean, whether the object will be newly created in the external system.
:param list_sep: If string, used as a separator for MODE_APPEND_LIST. If None, native lists are used.
:raises SyncConfigError: If an invalid update mode is specified.
:returns: A dictionary containing the properties that need to be updated in the external system.
"""
out = {}
for field_name, new_value, update_mode in new_values:
if update_mode == MODE_OVERWRITE:
out[field_name] = new_value
continue
elif update_mode == MODE_SET_IF_NEW and not is_new:
continue
if not new_value:
continue
current_value = old_values.get(field_name, out.get(field_name, ""))
if update_mode in (MODE_SET_IF_EMPTY, MODE_SET_IF_NEW):
if not current_value:
out[field_name] = new_value
elif update_mode == MODE_APPEND_LIST:
_add_to_list(out, field_name, current_value, new_value, list_sep)
else:
raise SyncConfigError(["Invalid update mode " + update_mode])
return out
def _add_to_list(out, field_name, current_value, new_item, list_sep):
new_item = str(new_item)
if list_sep is not None:
new_item = new_item.replace(list_sep, "")
current_value = current_value.split(list_sep) if current_value else []
elif not isinstance(current_value, (list, tuple)):
current_value = [str(current_value)]
if new_item not in current_value:
new_list = current_value + [new_item]
if list_sep is not None:
new_list = list_sep.join(new_list)
out[field_name] = new_list
def translate_property_mappings(property_mappings, checkin_list_map):
"""
To properly handle copied events, users of data fields as provided by get_data_fields need to register to the
event_copy_data signal and translate all stored references to those fields using this method.
For example, if you store your mappings in a custom Django model with a ForeignKey to Event:
.. code-block:: python
@receiver(signal=event_copy_data, dispatch_uid="my_sync_event_copy_data")
def event_copy_data_receiver(sender, other, checkin_list_map, **kwargs):
object_mappings = other.my_object_mappings.all()
object_mapping_map = {}
for om in object_mappings:
om = copy.copy(om)
object_mapping_map[om.pk] = om
om.pk = None
om.event = sender
om.property_mappings = translate_property_mappings(om.property_mappings, checkin_list_map)
om.save()
"""
mappings = json.loads(property_mappings)
for mapping in mappings:
if mapping["pretix_field"].startswith("checkin_date_"):
old_id = int(mapping["pretix_field"][len("checkin_date_"):])
if old_id not in checkin_list_map:
# old_id might not be in checkin_list_map, because copying of an event series only copies check-in
# lists covering the whole series, not individual dates.
mapping["pretix_field"] = "_invalid_" + mapping["pretix_field"]
else:
mapping["pretix_field"] = "checkin_date_%d" % checkin_list_map[old_id].pk
return json.dumps(mappings)

View File

@@ -68,7 +68,6 @@ from ...control.forms.filter import get_all_payment_providers
from ...helpers import GroupConcat
from ...helpers.iter import chunked_iterable
from ...helpers.safe_openpyxl import remove_invalid_excel_chars
from ...multidomain.urlreverse import build_absolute_uri
from ..exporter import (
ListExporter, MultiSheetListExporter, OrganizerLevelExportMixin,
)
@@ -288,7 +287,6 @@ class OrderListExporter(MultiSheetListExporter):
headers.append(_('Email address verified'))
headers.append(_('External customer ID'))
headers.append(_('Payment providers'))
headers.append(_('Order link'))
if form_data.get('include_payment_amounts'):
payment_methods = self._get_all_payment_methods(qs)
for id, vn in payment_methods:
@@ -404,13 +402,6 @@ class OrderListExporter(MultiSheetListExporter):
if p and p != 'free'
]))
row.append(
build_absolute_uri(order.event, 'presale:event.order', kwargs={
'order': order.code,
'secret': order.secret,
})
)
if form_data.get('include_payment_amounts'):
payment_methods = self._get_all_payment_methods(qs)
for id, vn in payment_methods:

View File

@@ -639,16 +639,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
))
if self.invoice.introductory_text:
# While all intro fields are appended without any blank lines; we do want one before the optional intro
# text. However, if there are no prior intro fields, adding an additional spacer will waste space.
if story:
story.append(Spacer(1, 5 * mm))
story.append(Paragraph(
self._clean_text(self.invoice.introductory_text, tags=['br']),
self.stylesheet['Normal']
))
story.append(Spacer(1, 5 * mm))
story.append(Spacer(1, 10 * mm))
return story

View File

@@ -1,35 +0,0 @@
#
# 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 django.core.management.commands import makemessages
def is_valid_locale(locale):
return re.match(r"^[a-z]+$", locale) or re.match(r"^[a-z]+_[A-Z0-9].*$", locale)
makemessages.is_valid_locale = is_valid_locale
class Command(makemessages.Command):
pass

View File

@@ -38,7 +38,6 @@ import traceback
from django.conf import settings
from django.core.cache import cache
from django.core.management.base import BaseCommand
from django.db import close_old_connections
from django.dispatch.dispatcher import NO_RECEIVERS
from pretix.helpers.periodic import SKIPPED
@@ -80,8 +79,6 @@ class Command(BaseCommand):
self.stdout.write(f'INFO Running {name}')
t0 = time.time()
try:
# Check if the DB connection is still good, it might be closed if the previous task took too long.
close_old_connections()
r = receiver(signal=periodic_task, sender=self)
except Exception as err:
if isinstance(err, KeyboardInterrupt):

View File

@@ -1,54 +0,0 @@
# Generated by Django 4.2.21 on 2025-06-27 13:32
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0283_taxrule_default_taxrule_backfill'),
]
operations = [
migrations.CreateModel(
name='OrderSyncResult',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('sync_provider', models.CharField(max_length=128)),
('mapping_id', models.IntegerField()),
('external_object_type', models.CharField(max_length=128)),
('external_id_field', models.CharField(max_length=128)),
('id_value', models.CharField(max_length=128)),
('external_link_href', models.CharField(max_length=255, null=True)),
('external_link_display_name', models.CharField(max_length=255, null=True)),
('transmitted', models.DateTimeField(auto_now_add=True)),
('sync_info', models.JSONField()),
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sync_results', to='pretixbase.order')),
('order_position', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sync_results', to='pretixbase.orderposition')),
],
options={
'indexes': [models.Index(fields=['order', 'sync_provider'], name='pretixbase__order_i_3e3c84_idx')],
},
),
migrations.CreateModel(
name='OrderSyncQueue',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('sync_provider', models.CharField(max_length=128)),
('triggered_by', models.CharField(max_length=128)),
('triggered', models.DateTimeField(auto_now_add=True)),
('failed_attempts', models.PositiveIntegerField(default=0)),
('not_before', models.DateTimeField(db_index=True)),
('need_manual_retry', models.CharField(null=True, max_length=20)),
('in_flight', models.BooleanField(default=False)),
('in_flight_since', models.DateTimeField(blank=True, null=True)),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='queued_sync_jobs', to='pretixbase.event')),
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='queued_sync_jobs', to='pretixbase.order')),
],
options={
'ordering': ('triggered',),
'unique_together': {('order', 'sync_provider', 'in_flight')},
},
),
]

View File

@@ -111,13 +111,6 @@ class ImportColumn:
"""
return gettext_lazy('Keep empty')
@property
def help_text(self):
"""
Additional description of the column
"""
return None
def __init__(self, event):
self.event = event

View File

@@ -57,7 +57,6 @@ from pretix.base.signals import order_import_columns
class EmailColumn(ImportColumn):
identifier = 'email'
verbose_name = gettext_lazy('Email address')
order_level = True
def clean(self, value, previous_values):
if value:
@@ -68,24 +67,9 @@ class EmailColumn(ImportColumn):
order.email = value
class GroupingColumn(ImportColumn):
identifier = 'grouping'
verbose_name = gettext_lazy('Grouping')
help_text = gettext_lazy(
'Only applicable when "Import mode" is set to "Group multiple lines together...". Lines with the same grouping '
'value will be put in the same order, but MUST be consecutive lines of the input file.'
)
order_level = True
default_label = "---"
def assign(self, value, order, position, invoice_address, **kwargs):
pass
class PhoneColumn(ImportColumn):
identifier = 'phone'
verbose_name = gettext_lazy('Phone number')
order_level = True
def clean(self, value, previous_values):
if value:
@@ -110,10 +94,6 @@ class SubeventColumn(SubeventColumnMixin, ImportColumn):
identifier = 'subevent'
verbose_name = pgettext_lazy('subevents', 'Date')
default_value = None
help_text = pgettext_lazy(
'subevents', 'The date can be specified through its full name, full date and time, or internal ID, provided '
'only one date in the system matches the input.'
)
def clean(self, value, previous_values):
if not value:
@@ -128,7 +108,6 @@ class ItemColumn(ImportColumn):
identifier = 'item'
verbose_name = gettext_lazy('Product')
default_value = None
help_text = gettext_lazy('The product can be specified by its internal ID, full name or internal name.')
@cached_property
def items(self):
@@ -158,7 +137,6 @@ class ItemColumn(ImportColumn):
class Variation(ImportColumn):
identifier = 'variation'
verbose_name = gettext_lazy('Product variation')
help_text = gettext_lazy('The variation can be specified by its internal ID or full name.')
@cached_property
def items(self):
@@ -192,7 +170,6 @@ class Variation(ImportColumn):
class InvoiceAddressCompany(ImportColumn):
identifier = 'invoice_address_company'
order_level = True
@property
def verbose_name(self):
@@ -204,8 +181,6 @@ class InvoiceAddressCompany(ImportColumn):
class InvoiceAddressNamePart(ImportColumn):
order_level = True
def __init__(self, event, key, label):
self.key = key
self.label = label
@@ -225,7 +200,6 @@ class InvoiceAddressNamePart(ImportColumn):
class InvoiceAddressStreet(ImportColumn):
identifier = 'invoice_address_street'
order_level = True
@property
def verbose_name(self):
@@ -237,7 +211,6 @@ class InvoiceAddressStreet(ImportColumn):
class InvoiceAddressZip(ImportColumn):
identifier = 'invoice_address_zipcode'
order_level = True
@property
def verbose_name(self):
@@ -249,7 +222,6 @@ class InvoiceAddressZip(ImportColumn):
class InvoiceAddressCity(ImportColumn):
identifier = 'invoice_address_city'
order_level = True
@property
def verbose_name(self):
@@ -262,8 +234,6 @@ class InvoiceAddressCity(ImportColumn):
class InvoiceAddressCountry(ImportColumn):
identifier = 'invoice_address_country'
default_value = None
help_text = gettext_lazy('The country needs to be specified using a two-letter country code.')
order_level = True
@property
def initial(self):
@@ -287,8 +257,6 @@ class InvoiceAddressCountry(ImportColumn):
class InvoiceAddressState(ImportColumn):
identifier = 'invoice_address_state'
help_text = gettext_lazy('The state can be specified by its short form or full name.')
order_level = True
@property
def verbose_name(self):
@@ -314,7 +282,6 @@ class InvoiceAddressState(ImportColumn):
class InvoiceAddressVATID(ImportColumn):
identifier = 'invoice_address_vat_id'
order_level = True
@property
def verbose_name(self):
@@ -326,7 +293,6 @@ class InvoiceAddressVATID(ImportColumn):
class InvoiceAddressReference(ImportColumn):
identifier = 'invoice_address_internal_reference'
order_level = True
@property
def verbose_name(self):
@@ -414,7 +380,6 @@ class AttendeeCity(ImportColumn):
class AttendeeCountry(ImportColumn):
identifier = 'attendee_country'
default_value = None
help_text = gettext_lazy('The country needs to be specified using a two-letter country code.')
@property
def initial(self):
@@ -438,7 +403,6 @@ class AttendeeCountry(ImportColumn):
class AttendeeState(ImportColumn):
identifier = 'attendee_state'
help_text = gettext_lazy('The state can be specified by its short form or full name.')
@property
def verbose_name(self):
@@ -507,7 +471,6 @@ class Locale(ImportColumn):
identifier = 'locale'
verbose_name = gettext_lazy('Order locale')
default_value = None
order_level = True
@property
def initial(self):
@@ -551,7 +514,6 @@ class ValidUntil(DatetimeColumnMixin, ImportColumn):
class Expires(DatetimeColumnMixin, ImportColumn):
identifier = 'expires'
verbose_name = gettext_lazy('Expiry date')
order_level = True
def clean(self, value, previous_values):
if not value:
@@ -578,8 +540,6 @@ class Saleschannel(ImportColumn):
verbose_name = gettext_lazy('Sales channel')
default_value = None
initial = 'static:web'
help_text = gettext_lazy('The sales channel can be specified by it\'s internal identifier or its full name.')
order_level = True
@cached_property
def channels(self):
@@ -608,7 +568,6 @@ class Saleschannel(ImportColumn):
class SeatColumn(ImportColumn):
identifier = 'seat'
verbose_name = gettext_lazy('Seat ID')
help_text = gettext_lazy('The seat needs to be specified by its internal ID.')
def __init__(self, *args):
self._cached = set()
@@ -640,8 +599,7 @@ class SeatColumn(ImportColumn):
class Comment(ImportColumn):
identifier = 'comment'
verbose_name = gettext_lazy('Order comment')
order_level = True
verbose_name = gettext_lazy('Comment')
def assign(self, value, order, position, invoice_address, **kwargs):
order.comment = value or ''
@@ -650,7 +608,6 @@ class Comment(ImportColumn):
class CheckinAttentionColumn(BooleanColumnMixin, ImportColumn):
identifier = 'checkin_attention'
verbose_name = gettext_lazy('Requires special attention')
order_level = True
def assign(self, value, order, position, invoice_address, **kwargs):
order.checkin_attention = value
@@ -659,7 +616,6 @@ class CheckinAttentionColumn(BooleanColumnMixin, ImportColumn):
class CheckinTextColumn(ImportColumn):
identifier = 'checkin_text'
verbose_name = gettext_lazy('Check-in text')
order_level = True
def assign(self, value, order, position, invoice_address, **kwargs):
order.checkin_text = value
@@ -740,7 +696,6 @@ class QuestionColumn(ImportColumn):
class CustomerColumn(ImportColumn):
identifier = 'customer'
verbose_name = gettext_lazy('Customer')
order_level = True
def clean(self, value, previous_values):
if value:
@@ -765,7 +720,6 @@ def get_order_import_columns(event):
if event.has_subevents:
default.append(SubeventColumn(event))
default += [
GroupingColumn(event),
EmailColumn(event),
PhoneColumn(event),
ItemColumn(event),

View File

@@ -1,149 +0,0 @@
#
# 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
from functools import cached_property
from django.db import IntegrityError, models
from django.utils.translation import gettext as _
from pretix.base.models import Event, Order, OrderPosition
logger = logging.getLogger(__name__)
MODE_OVERWRITE = "overwrite"
MODE_SET_IF_NEW = "if_new"
MODE_SET_IF_EMPTY = "if_empty"
MODE_APPEND_LIST = "append"
class OrderSyncQueue(models.Model):
order = models.ForeignKey(
Order, on_delete=models.CASCADE, related_name="queued_sync_jobs"
)
event = models.ForeignKey(
Event, on_delete=models.CASCADE, related_name="queued_sync_jobs"
)
sync_provider = models.CharField(blank=False, null=False, max_length=128)
triggered_by = models.CharField(blank=False, null=False, max_length=128)
triggered = models.DateTimeField(blank=False, null=False, auto_now_add=True)
failed_attempts = models.PositiveIntegerField(default=0)
not_before = models.DateTimeField(blank=False, null=False, db_index=True)
need_manual_retry = models.CharField(blank=True, null=True, max_length=20, choices=[
('exceeded', _('Temporary error, auto-retry limit exceeded')),
('permanent', _('Provider reported a permanent error')),
('config', _('Misconfiguration, please check provider settings')),
('internal', _('System error, needs manual intervention')),
('timeout', _('System error, needs manual intervention')),
])
in_flight = models.BooleanField(default=False)
in_flight_since = models.DateTimeField(blank=True, null=True)
class Meta:
unique_together = (("order", "sync_provider", "in_flight"),)
ordering = ("triggered",)
@cached_property
def _provider_class_info(self):
from pretix.base.datasync.datasync import datasync_providers
return datasync_providers.get(identifier=self.sync_provider)
@property
def provider_class(self):
return self._provider_class_info[0]
@property
def provider_display_name(self):
return self.provider_class.display_name
@property
def is_provider_active(self):
return self._provider_class_info[1]
@property
def max_retry_attempts(self):
return self.provider_class.max_attempts
def set_sync_error(self, failure_mode, messages, full_message):
logger.exception(
f"Could not sync order {self.order.code} to {type(self).__name__} ({failure_mode})"
)
self.order.log_action(f"pretix.event.order.data_sync.failed.{failure_mode}", {
"provider": self.sync_provider,
"error": messages,
"full_message": full_message,
})
self.need_manual_retry = failure_mode
self.clear_in_flight()
def clear_in_flight(self):
self.in_flight = False
self.in_flight_since = None
try:
self.save()
except IntegrityError:
# if setting in_flight=False fails due to UNIQUE constraint, just delete the current instance
self.delete()
class OrderSyncResult(models.Model):
order = models.ForeignKey(
Order, on_delete=models.CASCADE, related_name="sync_results"
)
sync_provider = models.CharField(blank=False, null=False, max_length=128)
order_position = models.ForeignKey(
OrderPosition, on_delete=models.CASCADE, related_name="sync_results", blank=True, null=True,
)
mapping_id = models.IntegerField(blank=False, null=False)
external_object_type = models.CharField(blank=False, null=False, max_length=128)
external_id_field = models.CharField(blank=False, null=False, max_length=128)
id_value = models.CharField(blank=False, null=False, max_length=128)
external_link_href = models.CharField(blank=True, null=True, max_length=255)
external_link_display_name = models.CharField(blank=True, null=True, max_length=255)
transmitted = models.DateTimeField(blank=False, null=False, auto_now_add=True)
sync_info = models.JSONField()
class Meta:
indexes = [
models.Index(fields=("order", "sync_provider")),
]
def external_link_html(self):
if not self.external_link_display_name:
return None
from pretix.base.datasync.datasync import datasync_providers
prov, meta = datasync_providers.get(identifier=self.sync_provider)
if prov:
return prov.get_external_link_html(self.order.event, self.external_link_href, self.external_link_display_name)
def to_result_dict(self):
return {
"position": self.order_position_id,
"object_type": self.external_object_type,
"external_id_field": self.external_id_field,
"id_value": self.id_value,
"external_link_href": self.external_link_href,
"external_link_display_name": self.external_link_display_name,
**self.sync_info,
}

View File

@@ -159,17 +159,8 @@ DEFAULT_VARIABLES = OrderedDict((
"editor_sample": _("123.45 EUR"),
"evaluate": lambda op, order, event: money_filter(op.price, event.currency)
}),
("price_with_bundled", {
"label": _("Price including bundled products"),
"editor_sample": _("123.45 EUR"),
"evaluate": lambda op, order, event: money_filter(op.price + sum(
p.price
for p in op.addons.all()
if not p.canceled and p.is_bundled
), event.currency)
}),
("price_with_addons", {
"label": _("Price including add-ons and bundled products"),
"label": _("Price including add-ons"),
"editor_sample": _("123.45 EUR"),
"evaluate": lambda op, order, event: money_filter(op.price + sum(
p.price

View File

@@ -28,9 +28,6 @@ from dateutil import parser
from django import forms
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.formats import get_format
from django.utils.functional import lazy
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
@@ -209,27 +206,14 @@ class RelativeDateTimeWidget(forms.MultiWidget):
def __init__(self, *args, **kwargs):
self.status_choices = kwargs.pop('status_choices')
base_choices = kwargs.pop('base_choices')
def placeholder_datetime_format():
df = get_format('DATETIME_INPUT_FORMATS')[0]
return now().replace(
year=2000, month=12, day=31, hour=18, minute=0, second=0, microsecond=0
).strftime(df)
def placeholder_time_format():
tf = get_format('TIME_INPUT_FORMATS')[0]
return datetime.time(8, 30, 0).strftime(tf)
widgets = reldatetimeparts(
status=forms.RadioSelect(choices=self.status_choices),
absolute=forms.DateTimeInput(
attrs={'placeholder': lazy(placeholder_datetime_format, str), 'class': 'datetimepicker'}
attrs={'class': 'datetimepicker'}
),
rel_days_number=forms.NumberInput(),
rel_mins_relationto=forms.Select(choices=base_choices),
rel_days_timeofday=forms.TimeInput(
attrs={'placeholder': lazy(placeholder_time_format, str), 'class': 'timepickerfield'}
),
rel_days_timeofday=forms.TimeInput(attrs={'placeholder': _('Time'), 'class': 'timepickerfield'}),
rel_mins_number=forms.NumberInput(),
rel_days_relationto=forms.Select(choices=base_choices),
rel_mins_relation=forms.Select(choices=BEFORE_AFTER_CHOICE),

View File

@@ -1,83 +0,0 @@
#
# 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
from datetime import timedelta
from itertools import groupby
from django.db.models import F, Window
from django.db.models.functions import RowNumber
from django.dispatch import receiver
from django.utils.timezone import now
from django_scopes import scope, scopes_disabled
from pretix.base.datasync.datasync import datasync_providers
from pretix.base.models.datasync import OrderSyncQueue
from pretix.base.signals import periodic_task
from pretix.celery_app import app
logger = logging.getLogger(__name__)
@receiver(periodic_task, dispatch_uid="data_sync_periodic_sync_all")
def periodic_sync_all(sender, **kwargs):
sync_all.apply_async()
@receiver(periodic_task, dispatch_uid="data_sync_periodic_reset_in_flight")
def periodic_reset_in_flight(sender, **kwargs):
for sq in OrderSyncQueue.objects.filter(
in_flight=True,
in_flight_since__lt=now() - timedelta(minutes=20),
):
sq.set_sync_error('timeout', [], 'Timeout')
@app.task()
def sync_all():
with scopes_disabled():
queue = (
OrderSyncQueue.objects
.filter(
in_flight=False,
not_before__lt=now(),
need_manual_retry__isnull=True,
)
.order_by(Window(
expression=RowNumber(),
partition_by=[F("event_id")],
order_by="not_before",
))
.prefetch_related("event")
[:1000]
)
grouped = groupby(sorted(queue, key=lambda q: (q.sync_provider, q.event.pk)), lambda q: (q.sync_provider, q.event))
for (target, event), queued_orders in grouped:
target_cls, meta = datasync_providers.get(identifier=target, active_in=event)
if not target_cls:
# sync plugin not found (plugin deactivated or uninstalled) -> drop outstanding jobs
OrderSyncQueue.objects.filter(pk__in=[sq.pk for sq in queued_orders]).delete()
with scope(organizer=event.organizer):
with target_cls(event=event) as p:
p.sync_queued_orders(queued_orders)

View File

@@ -89,9 +89,6 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user,
_('Orders cannot have more than %(max)s positions.') % {'max': django_settings.PRETIX_MAX_ORDER_SIZE}
)
used_groupers = set()
current_grouper = []
current_order_level_data = {}
orders = []
order = None
@@ -100,28 +97,7 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user,
lock_seats = []
for i, record in enumerate(data):
try:
create_new_order = (
order is None or
settings['orders'] == 'many' or
(settings['orders'] == 'mixed' and record["grouping"] != current_grouper)
)
if create_new_order:
if settings['orders'] == 'mixed':
if record["grouping"] in used_groupers:
raise DataImportError(
_('The grouping "%(value)s" occurs on non-consecutive lines (seen again on line %(row)s).') % {
"value": record["grouping"],
"row": i + 1,
}
)
current_grouper = record["grouping"]
used_groupers.add(current_grouper)
current_order_level_data = {
c.identifier: record.get(c.identifier)
for c in cols if getattr(c, "order_level", False)
}
if order is None or settings['orders'] == 'many':
order = Order(
event=event,
testmode=settings['testmode'],
@@ -132,12 +108,6 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user,
order._address.name_parts = {'_scheme': event.settings.name_scheme}
orders.append(order)
if settings['orders'] == 'mixed' and len(order._positions) >= django_settings.PRETIX_MAX_ORDER_SIZE:
raise DataImportError(
_('Orders cannot have more than %(max)s positions.') % {
'max': django_settings.PRETIX_MAX_ORDER_SIZE}
)
position = OrderPosition(positionid=len(order._positions) + 1)
position.attendee_name_parts = {'_scheme': event.settings.name_scheme}
position.meta_info = {}
@@ -145,24 +115,13 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user,
position.assign_pseudonymization_id()
for c in cols:
value = record.get(c.identifier)
if getattr(c, "order_level", False) and value != current_order_level_data.get(c.identifier):
raise DataImportError(
_('Inconsistent data in row {row}: Column {col} contains value "{val_line}", but '
'for this order, the value has already been set to "{val_order}".').format(
row=i + 1,
col=c.verbose_name,
val_line=value,
val_order=current_order_level_data.get(c.identifier) or "",
)
)
c.assign(value, order, position, order._address)
c.assign(record.get(c.identifier), order, position, order._address)
if position.seat is not None:
lock_seats.append((order.sales_channel, position.seat))
except (ValidationError, ImportError) as e:
raise DataImportError(
_('Invalid data in row {row}: {message}').format(row=i + 1, message=str(e))
_('Invalid data in row {row}: {message}').format(row=i, message=str(e))
)
try:

View File

@@ -257,14 +257,8 @@ class Registry:
When a new entry is registered, all accessor functions are called with the new entry as parameter.
Their return value is stored as the metadata value for that key.
"""
self.keys = keys
self.clear()
def clear(self):
"""
Removes all entries from the registry.
"""
self.registered_entries = dict()
self.keys = keys
self.by_key = {key: {} for key in self.keys.keys()}
def register(self, *objs):
@@ -339,23 +333,6 @@ class EventPluginRegistry(Registry):
def __init__(self, keys):
super().__init__({"plugin": lambda o: get_defining_app(o), **keys})
def filter(self, active_in=None, **kwargs):
result = super().filter(**kwargs)
if active_in is not None:
result = (
(entry, meta)
for entry, meta in result
if is_app_active(active_in, meta['plugin'])
)
return result
def get(self, active_in=None, **kwargs):
item, meta = super().get(**kwargs)
if meta and active_in is not None:
if not is_app_active(active_in, meta['plugin']):
return None, None
return item, meta
event_live_issues = EventPluginSignal()
"""

View File

@@ -42,4 +42,3 @@ class PretixControlConfig(AppConfig):
def ready(self):
from .views import dashboards # noqa
from . import logdisplay # noqa
from .views import datasync # noqa

View File

@@ -215,9 +215,3 @@ class CheckinListSimulatorForm(forms.Form):
)
self.fields['gate'].widget.choices = self.fields['gate'].choices
self.fields['gate'].label = _('Gate')
class CheckinResetForm(forms.Form):
ok = forms.BooleanField(
label=_("I am sure that the check-in state of the entire event should be reset.")
)

View File

@@ -113,6 +113,7 @@ class EventWizardFoundationForm(forms.Form):
attrs={
'data-model-select2': 'generic',
'data-select2-url': reverse('control:organizers.select2') + '?can_create=1',
'data-placeholder': _('Organizer')
}
),
empty_label=None,
@@ -983,10 +984,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
mail_bcc = forms.CharField(
label=_("Bcc address"),
help_text=' '.join([
str(_("All emails will be sent to this address as a Bcc copy.")),
str(_("You can specify multiple recipients separated by commas.")),
]),
help_text=_("All emails will be sent to this address as a Bcc copy"),
validators=[multimail_validate],
required=False,
max_length=255

View File

@@ -1246,7 +1246,9 @@ class SubEventFilterForm(FilterForm):
)
query = forms.CharField(
label=_('Event name'),
widget=forms.TextInput(),
widget=forms.TextInput(attrs={
'placeholder': _('Event name'),
}),
required=False
)
@@ -1691,7 +1693,9 @@ class EventFilterForm(FilterForm):
)
query = forms.CharField(
label=_('Event name'),
widget=forms.TextInput(),
widget=forms.TextInput(attrs={
'placeholder': _('Event name'),
}),
required=False
)
date_from = forms.DateField(
@@ -2444,7 +2448,7 @@ class CheckinFilterForm(FilterForm):
'event': self.event.slug,
'organizer': self.event.organizer.slug,
}),
'data-placeholder': _('All check-in lists'),
'data-placeholder': _('Check-in list'),
}
)
self.fields['checkin_list'].widget.choices = self.fields['checkin_list'].choices

View File

@@ -47,7 +47,9 @@ from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.html import escape, format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as __, gettext_lazy as _
from django.utils.translation import (
gettext as __, gettext_lazy as _, pgettext_lazy,
)
from django_scopes.forms import (
SafeModelChoiceField, SafeModelMultipleChoiceField,
)
@@ -328,6 +330,7 @@ class QuotaForm(I18nModelForm):
'event': self.event.slug,
'organizer': self.event.organizer.slug,
}),
'data-placeholder': pgettext_lazy('subevent', 'Date')
}
)
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
@@ -349,9 +352,6 @@ class QuotaForm(I18nModelForm):
field_classes = {
'subevent': SafeModelChoiceField,
}
widgets = {
'size': forms.NumberInput(attrs={'placeholder': _('Unlimited')})
}
def save(self, *args, **kwargs):
creating = not self.instance.pk

View File

@@ -1,127 +0,0 @@
#
# 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 json
from django import forms
from django.forms import formset_factory
from django.utils.translation import gettext_lazy as _
from pretix.base.models import Question
from pretix.base.models.datasync import (
MODE_APPEND_LIST, MODE_OVERWRITE, MODE_SET_IF_EMPTY, MODE_SET_IF_NEW,
)
class PropertyMappingForm(forms.Form):
pretix_field = forms.CharField()
external_field = forms.CharField()
value_map = forms.CharField(required=False)
overwrite = forms.ChoiceField(
choices=[
(MODE_OVERWRITE, _("Overwrite")),
(MODE_SET_IF_NEW, _("Fill if new")),
(MODE_SET_IF_EMPTY, _("Fill if empty")),
(MODE_APPEND_LIST, _("Add to list")),
]
)
def __init__(self, pretix_fields, external_fields_id, available_modes, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["pretix_field"] = forms.ChoiceField(
label=_("pretix field"),
choices=pretix_fields_choices(pretix_fields, kwargs.get("initial", {}).get("pretix_field")),
required=False,
)
if external_fields_id:
self.fields["external_field"] = forms.ChoiceField(
widget=forms.Select(
attrs={
"data-model-select2": "json_script",
"data-select2-src": "#" + external_fields_id,
},
),
)
self.fields["external_field"].choices = [
(self["external_field"].value(), self["external_field"].value()),
]
self.fields["overwrite"].choices = [
(key, label) for (key, label) in self.fields["overwrite"].choices if key in available_modes
]
class PropertyMappingFormSet(formset_factory(
PropertyMappingForm,
can_order=True,
can_delete=True,
extra=0,
)):
template_name = "pretixcontrol/datasync/property_mappings_formset.html"
def __init__(self, pretix_fields, external_fields, available_modes, prefix, *args, initial_json=None, **kwargs):
if initial_json:
kwargs["initial"] = json.loads(initial_json)
super().__init__(
form_kwargs={
"pretix_fields": pretix_fields,
"external_fields_id": prefix + "external-fields" if external_fields else None,
"available_modes": available_modes,
},
prefix=prefix,
*args, **kwargs)
self.external_fields = external_fields
def get_context(self):
ctx = super().get_context()
ctx["external_fields"] = self.external_fields
ctx["external_fields_id"] = self.prefix + "external-fields"
return ctx
def to_property_mappings_json(self):
"""
Returns a property mapping configuration as a JSON-serialized list of dictionaries.
Each entry specifies how to transfer data from one pretix field to one field in the external system:
- `pretix_field`: Name of a pretix data source field as declared in `pretix.base.datasync.sourcefields.get_data_fields`.
- `external_field`: Name of the target field in the external system. Implementation-defined by the sync provider.
- `value_map`: Dictionary mapping pretix value to external value. Only used for enumeration-type fields.
- `overwrite`: Mode of operation if the object already exists in the target system.
- `MODE_OVERWRITE` (`"overwrite"`) to always overwrite existing value.
- `MODE_SET_IF_NEW` (`"if_new"`) to only set the value if object does not exist in target system yet.
- `MODE_SET_IF_EMPTY` (`"if_empty"`) to only set the value if object does not exist in target system,
or the field is currently empty in target system.
- `MODE_APPEND_LIST` (`"append"`) if the field is an array or a multi-select: add the value to the list.
"""
mappings = [f.cleaned_data for f in self.ordered_forms]
return json.dumps(mappings)
QUESTION_TYPE_LABELS = dict(Question.TYPE_CHOICES)
def pretix_fields_choices(pretix_fields, initial_choice):
return [
(f.key, f.label + " [" + QUESTION_TYPE_LABELS[f.type] + "]")
for f in pretix_fields
if not f.deprecated or f.key == initial_choice
]

View File

@@ -20,7 +20,6 @@
# <https://www.gnu.org/licenses/>.
#
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from pretix.base.modelimport_orders import get_order_import_columns
@@ -63,8 +62,7 @@ class ProcessForm(forms.Form):
choices=choices,
widget=forms.Select(
attrs={'data-static': 'true'}
),
help_text=c.help_text,
)
)
def get_columns(self):
@@ -77,17 +75,14 @@ class OrdersProcessForm(ProcessForm):
choices=(
('many', _('Create a separate order for each line')),
('one', _('Create one order with one position per line')),
('mixed', _('Group multiple lines together into the same order based on a grouping column')),
),
widget=forms.RadioSelect,
)
)
status = forms.ChoiceField(
label=_('Order status'),
choices=(
('paid', _('Create orders as fully paid')),
('pending', _('Create orders as pending and still require payment')),
),
widget=forms.RadioSelect,
)
)
testmode = forms.BooleanField(
label=_('Create orders as test mode orders'),
@@ -104,17 +99,6 @@ class OrdersProcessForm(ProcessForm):
def get_columns(self):
return get_order_import_columns(self.event)
def clean(self):
data = super().clean()
grouping = data.get("grouping") and data.get("grouping") != "empty"
if data.get("orders") != "mixed" and grouping:
raise ValidationError({"grouping": [_("A grouping cannot be specified for this import mode.")]})
if data.get("orders") == "mixed" and not grouping:
raise ValidationError({"grouping": [_("A grouping needs to be specified for this import mode.")]})
return data
class VouchersProcessForm(ProcessForm):

View File

@@ -409,6 +409,7 @@ class OrderPositionAddForm(forms.Form):
'event': order.event.slug,
'organizer': order.event.organizer.slug,
}),
'data-placeholder': pgettext_lazy('subevent', 'Date')
}
)
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
@@ -704,6 +705,7 @@ class OrderContactForm(forms.ModelForm):
'data-select2-url': reverse('control:organizer.customers.select2', kwargs={
'organizer': self.instance.event.organizer.slug,
}),
'data-placeholder': _('Customer')
}
)
self.fields['customer'].widget.choices = self.fields['customer'].choices

View File

@@ -581,10 +581,7 @@ class MailSettingsForm(SettingsForm):
mail_bcc = forms.CharField(
label=_("Bcc address"),
help_text=''.join([
str(_("All emails will be sent to this address as a Bcc copy.")),
str(_("You can specify multiple recipients separated by commas.")),
]),
help_text=_("All emails will be sent to this address as a Bcc copy"),
validators=[multimail_validate],
required=False,
max_length=255
@@ -781,6 +778,7 @@ class GiftCardUpdateForm(forms.ModelForm):
'data-select2-url': reverse('control:organizer.ticket_select2', kwargs={
'organizer': organizer.slug,
}),
'data-placeholder': _('Ticket')
}
)
self.fields['owner_ticket'].widget.choices = self.fields['owner_ticket'].choices
@@ -816,6 +814,7 @@ class ReusableMediumUpdateForm(forms.ModelForm):
'data-select2-url': reverse('control:organizer.ticket_select2', kwargs={
'organizer': organizer.slug,
}),
'data-placeholder': _('Ticket')
}
)
self.fields['linked_orderposition'].widget.choices = self.fields['linked_orderposition'].choices
@@ -828,6 +827,7 @@ class ReusableMediumUpdateForm(forms.ModelForm):
'data-select2-url': reverse('control:organizer.giftcards.select2', kwargs={
'organizer': organizer.slug,
}),
'data-placeholder': _('Gift card')
}
)
self.fields['linked_giftcard'].widget.choices = self.fields['linked_giftcard'].choices
@@ -841,6 +841,7 @@ class ReusableMediumUpdateForm(forms.ModelForm):
'data-select2-url': reverse('control:organizer.customers.select2', kwargs={
'organizer': organizer.slug,
}),
'data-placeholder': _('Customer')
}
)
self.fields['customer'].widget.choices = self.fields['customer'].choices

View File

@@ -133,12 +133,16 @@ class SubEventBulkEditForm(I18nModelForm):
# i18n fields
if k in self.mixed_values:
self.fields[k].widget.attrs['placeholder'] = '[{}]'.format(_('Selection contains various values'))
else:
self.fields[k].widget.attrs['placeholder'] = ''
self.fields[k].one_required = False
for k in ('geo_lat', 'geo_lon', 'comment'):
# scalar fields
if k in self.mixed_values:
self.fields[k].widget.attrs['placeholder'] = '[{}]'.format(_('Selection contains various values'))
else:
self.fields[k].widget.attrs['placeholder'] = ''
self.fields[k].widget.is_required = False
self.fields[k].required = False

View File

@@ -41,7 +41,7 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.validators import EmailValidator
from django.db.models.functions import Upper
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes.forms import SafeModelChoiceField
from pretix.base.email import get_available_placeholders
@@ -115,6 +115,7 @@ class VoucherForm(I18nModelForm):
'event': instance.event.slug,
'organizer': instance.event.organizer.slug,
}),
'data-placeholder': pgettext_lazy('subevent', 'Date')
}
)
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
@@ -207,15 +208,12 @@ class VoucherForm(I18nModelForm):
if self.instance and self.instance.pk:
cnt -= self.instance.redeemed # these do not need quota any more
try:
Voucher.clean_item_properties(
data, self.instance.event,
self.instance.quota, self.instance.item, self.instance.variation,
seats_given=data.get('seat') or data.get('seats'),
block_quota=data.get('block_quota')
)
except ValidationError as e:
raise ValidationError({"itemvar": e.message})
Voucher.clean_item_properties(
data, self.instance.event,
self.instance.quota, self.instance.item, self.instance.variation,
seats_given=data.get('seat') or data.get('seats'),
block_quota=data.get('block_quota')
)
if not data.get('show_hidden_items') and (
(self.instance.quota and all(i.hide_without_voucher for i in self.instance.quota.items.all()))
or (self.instance.item and self.instance.item.hide_without_voucher)
@@ -226,17 +224,10 @@ class VoucherForm(I18nModelForm):
'them.')
]
})
try:
Voucher.clean_subevent(
data, self.instance.event
)
except ValidationError as e:
raise ValidationError({"subevent": e.message})
try:
Voucher.clean_max_usages(data, self.instance.redeemed)
except ValidationError as e:
raise ValidationError({"max_usages": e.message})
Voucher.clean_subevent(
data, self.instance.event
)
Voucher.clean_max_usages(data, self.instance.redeemed)
check_quota = Voucher.clean_quota_needs_checking(
data, self.initial_instance_data,
item_changed=data.get('itemvar') != self.initial.get('itemvar'),

View File

@@ -43,11 +43,9 @@ from django.dispatch import receiver
from django.urls import reverse
from django.utils.formats import date_format
from django.utils.html import escape, format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from i18nfield.strings import LazyI18nString
from pretix.base.datasync.datasync import datasync_providers
from pretix.base.logentrytypes import (
DiscountLogEntryType, EventLogEntryType, ItemCategoryLogEntryType,
ItemLogEntryType, LogEntryType, OrderLogEntryType, QuestionLogEntryType,
@@ -423,51 +421,6 @@ class OrderPrintLogEntryType(OrderLogEntryType):
)
class OrderDataSyncLogEntryType(OrderLogEntryType):
def display(self, logentry, data):
try:
from pretix.base.datasync.datasync import datasync_providers
provider_class, meta = datasync_providers.get(identifier=data['provider'])
data['provider_display_name'] = provider_class.display_name
except (KeyError, AttributeError):
data['provider_display_name'] = data.get('provider')
return super().display(logentry, data)
@log_entry_types.new_from_dict({
"pretix.event.order.data_sync.success": _("Data successfully transferred to {provider_display_name}."),
})
class OrderDataSyncSuccessLogEntryType(OrderDataSyncLogEntryType):
def display(self, logentry, data):
links = []
if data.get('provider') and data.get('objects'):
prov, meta = datasync_providers.get(identifier=data['provider'])
if prov:
for objs in data['objects'].values():
links.append(", ".join(
prov.get_external_link_html(logentry.event, obj['external_link_href'], obj['external_link_display_name'])
for obj in objs
if obj and 'external_link_href' in obj and 'external_link_display_name' in obj
))
return mark_safe(escape(super().display(logentry, data)) + "".join("<p>" + link + "</p>" for link in links))
@log_entry_types.new_from_dict({
"pretix.event.order.data_sync.failed.config": _("Transferring data to {provider_display_name} failed due to invalid configuration:"),
"pretix.event.order.data_sync.failed.exceeded": _("Maximum number of retries exceeded while transferring data to {provider_display_name}:"),
"pretix.event.order.data_sync.failed.permanent": _("Error while transferring data to {provider_display_name}:"),
"pretix.event.order.data_sync.failed.internal": _("Internal error while transferring data to {provider_display_name}."),
"pretix.event.order.data_sync.failed.timeout": _("Internal error while transferring data to {provider_display_name}."),
})
class OrderDataSyncErrorLogEntryType(OrderDataSyncLogEntryType):
def display(self, logentry, data):
errmes = data["error"]
if not isinstance(errmes, list):
errmes = [errmes]
return mark_safe(escape(super().display(logentry, data)) + "".join("<p>" + escape(msg) + "</p>" for msg in errmes))
@receiver(signal=logentry_display, dispatch_uid="pretixcontrol_logentry_display")
def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
@@ -769,7 +722,6 @@ class CoreUserImpersonatedLogEntryType(UserImpersonatedLogEntryType):
'pretix.giftcards.transaction.manual': _('A manual transaction has been performed.'),
'pretix.team.token.created': _('The token "{name}" has been created.'),
'pretix.team.token.deleted': _('The token "{name}" has been revoked.'),
'pretix.event.checkin.reset': _('The check-in and print log state has been reset.')
})
class CoreLogEntryType(LogEntryType):
pass

View File

@@ -451,11 +451,6 @@ def get_global_navigation(request):
'url': reverse('control:global.sysreport'),
'active': (url.url_name == 'global.sysreport'),
},
{
'label': _('Data sync problems'),
'url': reverse('control:global.datasync.failedjobs'),
'active': (url.url_name == 'global.datasync.failedjobs'),
},
]
})
@@ -660,18 +655,6 @@ def get_organizer_navigation(request):
'icon': 'download',
})
if 'can_change_organizer_settings' in request.orgapermset:
merge_in(nav, [{
'parent': reverse('control:organizer.export', kwargs={
'organizer': request.organizer.slug,
}),
'label': _('Data sync problems'),
'url': reverse('control:organizer.datasync.failedjobs', kwargs={
'organizer': request.organizer.slug,
}),
'active': (url.url_name == 'organizer.datasync.failedjobs'),
}])
merge_in(nav, sorted(
sum((list(a[1]) for a in nav_organizer.send(request.organizer, request=request, organizer=request.organizer)),
[]),

View File

@@ -83,13 +83,6 @@
<a href="{% url "control:organizer.devices" organizer=request.organizer.slug %}"
class="btn btn-default"><i class="fa fa-tablet"></i> {% trans "Connected devices" %}</a>
{% endif %}
{% if "can_change_orders" in request.eventpermset %}
<a href="{% url "control:event.orders.checkinlists.reset" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-default">
<span class="fa fa-repeat"></span>
{% trans "Reset check-in" %}
</a>
{% endif %}
</p>
<div class="table-responsive">
<table class="table table-hover table-quotas">

View File

@@ -1,50 +0,0 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Reset check-in" %}{% endblock %}
{% block inside %}
<h1>{% trans "Reset check-in" %}</h1>
<form action="" method="post" class="" data-asynctask>
{% csrf_token %}
<p>
{% blocktrans trimmed %}
With this feature, you can reset the entire check-in state of the event.
This will delete all check-in records as well as all records of printed tickets or badges.
We recommend to use this feature after testing your hardware setup but only before your
event started, and you admitted any real attendees or printed any real badges or tickets.
{% endblocktrans %}
</p>
<p class="alert alert-danger">
{% blocktrans trimmed count count=checkins %}
This will permanently delete <strong>1 check-in</strong>.
{% plural %}
This will permanently delete <strong>{{ count }} check-ins</strong>.
{% endblocktrans %}
{% blocktrans trimmed count count=printlogs %}
Additionally, <strong>1 print log</strong> will be deleted.
{% plural %}
Additionally, <strong>{{ count }} print logs</strong> will be deleted.
{% endblocktrans %}
<br>
<strong>
{% trans "This cannot be reverted!" %}
</strong>
</p>
<p>
{% blocktrans trimmed %}
The deleted entries will still show up in the "Order history" section, but for all other
purposes the system will behave as if they never existed.
{% endblocktrans %}
</p>
{% bootstrap_form form layout="inline" %}
<div class="form-group submit-group">
<a href="{% url "control:event.orders.checkinlists" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger btn-save">
{% trans "Proceed with reset" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -1,74 +0,0 @@
{% load i18n %}
{% load eventurl %}
{% load bootstrap3 %}
{% load escapejson %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Data transfer to external systems" %}
</h3>
</div>
<ul class="list-group">
{% for identifier, display_name, pending, objects in providers %}
<li class="list-group-item">
<form action="{% url "control:event.order.sync_job" organizer=event.organizer.slug event=event.slug code=order.code provider=identifier %}" method="post" class="form-inline pull-right">
{% csrf_token %}
{% if pending %}
{% if pending.not_before > now or pending.need_manual_retry %}
<button type="submit" name="run_job_now" value="{{ pending.pk }}" class="btn btn-default"><i class="fa fa-refresh"></i> {% trans "Retry now" %}</button>
{% endif %}
<button type="submit" name="cancel_job" value="{{ pending.pk }}" class="btn btn-danger"><i class="fa fa-times"></i> {% trans "Cancel" %}</button>
{% else %}
<button type="submit" class="btn btn-default"><i class="fa fa-refresh"></i> {% trans "Sync now" %}</button>
<input type="hidden" name="queue_sync" value="true">
{% endif %}
</form>
<p><b>{{ display_name }}</b></p>
{% if pending %}
<p>
{% if pending.need_manual_retry %}
<i class="fa fa-warning"></i>
{% trans "Error" %}: {{ pending.get_need_manual_retry_display }}
{% elif pending.failed_attempts %}
<i class="fa fa-warning"></i>
{% blocktrans trimmed with num=pending.failed_attempts max=pending.max_retry_attempts %}
Error. Retry {{ num }} of {{ max }}.
{% endblocktrans %}
{% if pending.not_before %}
{% blocktrans trimmed with datetime=pending.not_before|date:"SHORT_DATETIME_FORMAT" %}
Waiting until {{ datetime }}
{% endblocktrans %}
{% endif %}
{% elif pending.not_before > now %}
{% blocktrans trimmed with datetime=pending.not_before|date:"SHORT_DATETIME_FORMAT" %}
Waiting until {{ datetime }}
{% endblocktrans %}
{% else %}
<i class="fa fa-hourglass"></i> {% trans "Pending" %}
{% endif %}
<span class="text-muted">({% blocktrans trimmed with datetime=pending.triggered|date:"SHORT_DATETIME_FORMAT" %}triggered at {{ datetime }}
{% endblocktrans %})</span>
<!-- {{ pending.triggered_by }} / {{ pending.triggered }} -->
</p>
{% endif %}
<ul>
{% for obj in objects %}
<li>
{% if obj.external_link_html %}
{{ obj.external_link_html }}
{% else %}
{{ obj.external_object_type }}
{% trans "identified by" %} {{ obj.external_id_field }}
<em>{{ obj.id_value }}</em>
{% endif %}
&nbsp; <time class="text-muted" datetime="{{ obj.transmitted.isoformat }}">{{ obj.transmitted|date:"SHORT_DATETIME_FORMAT" }}</time>
</li>
{% empty %}
<li>{% trans "No data transmitted." %}</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
</div>

View File

@@ -1,86 +0,0 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% block content %}
<h2>{% trans "Sync problems" %}</h2>
<p>
{% blocktrans trimmed %}
On this page, we provide a list of orders where data synchronisation to an external system has failed.
You can start another attempt to sync them manually.
{% endblocktrans %}
</p>
<form method="post">
{% csrf_token %}
<table class="table table-hover">
<thead>
<tr>
<th>
{% if queue_items %}
<label aria-label="{% trans "select all rows for batch-operation" %}"
class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
{% endif %}
</th>
<th>{% trans "Order" %}</th>
<th>{% trans "Sync provider" %}</th>
<th>{% trans "Date" %}</th>
<th>{% trans "Failure mode" %}</th>
{% if staff_session %}
<th>in_flight</th><th>retry</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for item in queue_items %}
<tr>
<td><input type="checkbox" name="idlist" value="{{ item.pk }}"></td>
<td>
{% if staff_session %}{{ item.order.event.organizer.slug }} -{% endif %}
<a href="{% url "control:event.order" event=item.order.event.slug organizer=item.order.event.organizer.slug code=item.order.code %}">
{{ item.order.full_code }}
</a>
</td>
<td>{{ item.provider_display_name }}</td>
<td>
{{ item.triggered }}
{% if staff_session %}({{ item.triggered_by }}){% endif %}
</td>
<td>
{% if item.need_manual_retry %}
{{ item.get_need_manual_retry_display }}
{% else %}
{% blocktrans trimmed with datetime=item.not_before|date:"SHORT_DATETIME_FORMAT" %}
Temporary error, will retry after {{ datetime }}
{% endblocktrans %}
{% endif %}
{% if staff_session %}({{ item.need_manual_retry }}){% endif %}
</td>
{% if staff_session %}
<td>{{ item.in_flight }} ({{ item.in_flight_since }})</td><td>{{ item.failed_attempts }} / {{ item.max_retry_attempts }} ({{ item.not_before }})</td>
{% endif %}
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center">{% trans "No problems." %}</td>
{% if staff_session %}
<td></td><td></td>
{% endif %}
</tr>
{% endfor %}
</tbody>
{% if queue_items %}
<tfoot>
<tr>
<td colspan="5">
<button type="submit" name="action" value="retry" class="btn btn-primary"><i class="fa fa-refresh"></i> {% trans "Retry selected" %}</button>
<button type="submit" name="action" value="cancel" class="btn btn-danger"><i class="fa fa-times"></i> {% trans "Cancel selected" %}</button>
</td>
{% if staff_session %}
<td></td><td></td>
{% endif %}
</tr>
</tfoot>
{% endif %}
</table>
</form>
{% include "pretixcontrol/pagination.html" %}
{% endblock %}

View File

@@ -1,81 +0,0 @@
{% load i18n %}
{% load bootstrap3 %}
{% load escapejson %}
{% load formset_tags %}
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
{{ formset.management_form }}
{% bootstrap_formset_errors formset %}
<div data-formset-body>
{% for f in formset %}
{% bootstrap_form_errors f %}
<div class="row formset-row" data-formset-form>
<div class="sr-only">
{{ f.id }}
{% bootstrap_field f.DELETE form_group_class="" layout="inline" %}
{% bootstrap_field f.ORDER form_group_class="" layout="inline" %}
</div>
<div class="col-md-4">
{% bootstrap_field f.pretix_field layout='inline' form_group_class="" %}
</div>
<div class="col-md-4">
{% bootstrap_field f.external_field layout='inline' form_group_class="" %}
</div>
<div class="col-md-2">
{% bootstrap_field f.overwrite layout='inline' form_group_class="" %}
</div>
{{ f.value_map.as_hidden }}
<div class="col-md-2 text-right flip">
<i class="fa fa-warning hidden" data-toggle="tooltip" title=""></i>
<button type="button" class="btn btn-default hidden" data-edit-value-map data-toggle="modal"
data-target="#editValueMapModal" title="{% trans "Edit value mapping" %}">
<i class="fa fa-edit"></i></button>
<button type="button" class="btn btn-default" data-formset-move-up-button>
<i class="fa fa-arrow-up"></i></button>
<button type="button" class="btn btn-default" data-formset-move-down-button>
<i class="fa fa-arrow-down"></i></button>
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="row formset-row" data-formset-form>
<div class="sr-only">
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-md-4">
{% bootstrap_field formset.empty_form.pretix_field layout='inline' form_group_class="" %}
</div>
<div class="col-md-4">
{% bootstrap_field formset.empty_form.external_field layout='inline' form_group_class="" %}
</div>
<div class="col-md-2">
{% bootstrap_field formset.empty_form.overwrite layout='inline' form_group_class="" %}
</div>
{{ f.value_map.as_hidden }}
<div class="col-md-2 text-right flip">
<i class="fa fa-warning hidden" data-toggle="tooltip" title=""></i>
<button type="button" class="btn btn-default hidden" data-edit-value-map data-toggle="modal"
data-target="#editValueMapModal" title="{% trans "Edit value mapping" %}">
<i class="fa fa-edit"></i></button>
<button type="button" class="btn btn-default" data-formset-move-up-button>
<i class="fa fa-arrow-up"></i></button>
<button type="button" class="btn btn-default" data-formset-move-down-button>
<i class="fa fa-arrow-down"></i></button>
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add property" %}</button>
</p>
</div>
{% if external_fields %}
{{ external_fields|json_script:external_fields_id }}
{% endif %}

View File

@@ -15,7 +15,7 @@
<span class="optional">{% trans "Optional" %}</span>
</label>
<div class="col-md-4">
{% bootstrap_field form.geo_lat layout="inline" placeholder=_("Latitude") %}
{% bootstrap_field form.geo_lat layout="inline" %}
{% if global_settings.opencagedata_apikey %}
<p class="attrib">
<a href="https://openstreetmap.org/" target="_blank">
@@ -25,7 +25,7 @@
{% endif %}
</div>
<div class="col-md-4">
{% bootstrap_field form.geo_lon layout="inline" placeholder=_("Longitude") %}
{% bootstrap_field form.geo_lon layout="inline" %}
</div>
<div class="col-md-1">
</div>

View File

@@ -79,15 +79,6 @@
class="btn btn-primary">{% trans "Show affected orders" %}</a>
</div>
{% endif %}
{% if has_sync_problems %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
Orders in this event could not be <strong>synced to an external system</strong> as configured.
{% endblocktrans %}
<a href="{% url "control:event.datasync.failedjobs" event=request.event.slug organizer=request.event.organizer.slug %}"
class="btn btn-primary">{% trans "Show sync problems" %}</a>
</div>
{% endif %}
{% eventsignal request.event "pretix.control.signals.event_dashboard_top" request=request %}
{% if request.event.has_subevents %}

View File

@@ -6,7 +6,7 @@
<fieldset>
<legend>{% trans "General information" %}</legend>
{% bootstrap_field form.name layout="control" %}
<div class="form-group{% if form.slug.errors %} has-error{% endif %}">
<div class="form-group">
<label class="col-md-3 control-label" for="{{ form.slug.id_for_label }}">{{ form.slug.label }}</label>
<div class="col-md-9 form-inline">
<button class="btn btn-default pull-right flip" type="button" id="event-slug-random-generate"
@@ -14,9 +14,6 @@
{% trans "Set to random" %}
</button>
{% bootstrap_field form.slug form_group_class="helper-display-inline" show_label=False layout="inline" %}
{% for error in form.slug.errors %}
<div class="help-block">{{ error }}</div>
{% endfor %}
<div class="help-block">
{% blocktrans trimmed %}
This is the address users can buy your tickets at. Should be short, only contain lowercase

View File

@@ -500,18 +500,6 @@
{% eventsignal event "pretix.control.signals.order_position_buttons" order=order position=line request=request %}
</div>
{% endif %}
{% if staff_session %}
<div class="admin-only print-logs">
{% for pl in line.print_logs.all %}
<span class="fa fa-print"></span>
{{ pl.datetime|date:"SHORT_DATETIME_FORMAT" }}
{{ pl.get_type_display }}
({{ pl.source }}{% if pl.device %}, #{{ pl.device.device_id }}{% endif %})
{% if not pl.successful %}<span class="fa fa-warning fa-fw"></span>{% endif %}
<br>
{% endfor %}
</div>
{% endif %}
{% if line.issued_gift_cards %}
<dl>
{% for gc in line.issued_gift_cards.all %}

View File

@@ -21,17 +21,8 @@
<script type="text/json" data-replace-with-qr>{{ qrdata|safe }}</script><br>
{% trans "If your app/device does not support scanning a QR code, you can also enter the following information:" %}
<br>
<strong>{% trans "System URL:" %}</strong> <code id="system_url">{{ settings.SITE_URL }}</code>
<button type="button" class="btn btn-default btn-xs btn-clipboard js-only" data-clipboard-target="#system_url">
<i class="fa fa-clipboard" aria-hidden="true"></i>
<span class="sr-only">{% trans "Copy to clipboard" %}</span>
</button>
<br>
<strong>{% trans "Token:" %}</strong> <code id="init_token">{{ device.initialization_token }}</code>
<button type="button" class="btn btn-default btn-xs btn-clipboard js-only" data-clipboard-target="#init_token">
<i class="fa fa-clipboard" aria-hidden="true"></i>
<span class="sr-only">{% trans "Copy to clipboard" %}</span>
</button>
<strong>{% trans "System URL:" %}</strong> <code>{{ settings.SITE_URL }}</code><br>
<strong>{% trans "Token:" %}</strong> <code>{{ device.initialization_token }}</code>
</li>
</ol>
</div>

View File

@@ -289,13 +289,13 @@
{% bootstrap_field f.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-sm-4">
{% bootstrap_field f.time_from layout="inline" placeholder=time_begin_sample %}
{% bootstrap_field f.time_from layout="inline" %}
</div>
<div class="col-sm-4">
{% bootstrap_field f.time_to layout="inline" placeholder=time_end_sample %}
{% bootstrap_field f.time_to layout="inline" %}
</div>
<div class="col-sm-3">
{% bootstrap_field f.time_admission layout="inline" placeholder=time_admission_sample %}
{% bootstrap_field f.time_admission layout="inline" %}
</div>
<div class="col-sm-1 text-right flip">
<button type="button" class="btn btn-danger btn-block"
@@ -315,13 +315,13 @@
{% bootstrap_field time_formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-sm-4">
{% bootstrap_field time_formset.empty_form.time_from layout="inline" placeholder=time_begin_sample %}
{% bootstrap_field time_formset.empty_form.time_from layout="inline" %}
</div>
<div class="col-sm-4">
{% bootstrap_field time_formset.empty_form.time_to layout="inline" placeholder=time_end_sample %}
{% bootstrap_field time_formset.empty_form.time_to layout="inline" %}
</div>
<div class="col-sm-3">
{% bootstrap_field time_formset.empty_form.time_admission layout="inline" placeholder=time_admission_sample %}
{% bootstrap_field time_formset.empty_form.time_admission layout="inline" %}
</div>
<div class="col-sm-1 text-right flip">
<button type="button" class="btn btn-danger btn-block"
@@ -338,13 +338,13 @@
<label for="subevent_add_many_slots_first">
<strong>{% trans "Start of first slot" %}</strong>
</label>
<input class="form-control timepickerfield" id="subevent_add_many_slots_first" value="{{ time_begin_sample }}" placeholder="{{ time_begin_sample }}">
<input class="form-control timepickerfield" id="subevent_add_many_slots_first" value="{{ time_begin_sample }}">
</div>
<div class="col-md-2 col-sm-12">
<label for="subevent_add_many_slots_end">
<strong>{% trans "End of time slots" %}</strong>
</label>
<input class="form-control timepickerfield" id="subevent_add_many_slots_end" value="{{ time_end_sample }}" placeholder="{{ time_end_sample }}">
<input class="form-control timepickerfield" id="subevent_add_many_slots_end" value="{{ time_end_sample }}">
</div>
<div class="col-md-3 col-sm-12">
<label for="subevent_add_many_slots_length">
@@ -409,8 +409,8 @@
</fieldset>
<fieldset>
<legend>{% trans "Timeline" %}</legend>
{% bootstrap_field form.rel_presale_start layout="control" placeholder=datetime_sample %}
{% bootstrap_field form.rel_presale_end layout="control" placeholder=datetime_sample %}
{% bootstrap_field form.rel_presale_start layout="control" %}
{% bootstrap_field form.rel_presale_end layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Quotas" %}</legend>

View File

@@ -51,7 +51,7 @@
<div class="field-content">
<div class="row">
<div class="col-md-6">
{% bootstrap_field form.geo_lat layout="inline" placeholder=_("Latitude") %}
{% bootstrap_field form.geo_lat layout="inline" %}
{% if global_settings.opencagedata_apikey %}
<p class="attrib">
<a href="https://openstreetmap.org/" target="_blank" tabindex="-1">
@@ -61,7 +61,7 @@
{% endif %}
</div>
<div class="col-md-6">
{% bootstrap_field form.geo_lon layout="inline" placeholder=_("Longitude") %}
{% bootstrap_field form.geo_lon layout="inline" %}
</div>
</div>
</div>

View File

@@ -37,9 +37,9 @@ from django.urls import include, re_path
from django.views.generic.base import RedirectView
from pretix.control.views import (
auth, checkin, dashboards, datasync, discounts, event, geo,
global_settings, item, main, modelimport, oauth, orders, organizer, pdf,
search, shredder, subevents, typeahead, user, users, vouchers, waitinglist,
auth, checkin, dashboards, discounts, event, geo, global_settings, item,
main, modelimport, oauth, orders, organizer, pdf, search, shredder,
subevents, typeahead, user, users, vouchers, waitinglist,
)
urlpatterns = [
@@ -58,7 +58,6 @@ urlpatterns = [
re_path(r'^global/license/$', global_settings.LicenseCheckView.as_view(), name='global.license'),
re_path(r'^global/sysreport/$', global_settings.SysReportView.as_view(), name='global.sysreport'),
re_path(r'^global/message/$', global_settings.MessageView.as_view(), name='global.message'),
re_path(r'^global/datasync/failedjobs/$', datasync.GlobalFailedSyncJobsView.as_view(), name='global.datasync.failedjobs'),
re_path(r'^logdetail/$', global_settings.LogDetailView.as_view(), name='global.logdetail'),
re_path(r'^logdetail/payment/$', global_settings.PaymentDetailView.as_view(), name='global.paymentdetail'),
re_path(r'^logdetail/refund/$', global_settings.RefundDetailView.as_view(), name='global.refunddetail'),
@@ -249,7 +248,6 @@ urlpatterns = [
re_path(r'^organizer/(?P<organizer>[^/]+)/export/(?P<pk>[^/]+)/delete$', organizer.DeleteScheduledExportView.as_view(),
name='organizer.export.scheduled.delete'),
re_path(r'^organizer/(?P<organizer>[^/]+)/ticket_select2$', typeahead.ticket_select2, name='organizer.ticket_select2'),
re_path(r'^organizer/(?P<organizer>[^/]+)/datasync/failedjobs/$', datasync.OrganizerFailedSyncJobsView.as_view(), name='organizer.datasync.failedjobs'),
re_path(r'^nav/typeahead/$', typeahead.nav_context_list, name='nav.typeahead'),
re_path(r'^events/$', main.EventList.as_view(), name='events'),
re_path(r'^events/add$', main.EventWizard.as_view(), name='events.add'),
@@ -430,8 +428,6 @@ urlpatterns = [
re_path(r'^orders/(?P<code>[0-9A-Z]+)/cancellationrequests/(?P<req>\d+)/delete$',
orders.OrderCancellationRequestDelete.as_view(),
name='event.order.cancellationrequests.delete'),
re_path(r'^orders/(?P<code>[0-9A-Z]+)/sync_job/(?P<provider>[^/]+)/$', datasync.ControlSyncJob.as_view(),
name='event.order.sync_job'),
re_path(r'^orders/(?P<code>[0-9A-Z]+)/transactions/$', orders.OrderTransactions.as_view(), name='event.order.transactions'),
re_path(r'^orders/(?P<code>[0-9A-Z]+)/$', orders.OrderDetail.as_view(), name='event.order'),
re_path(r'^invoice/(?P<invoice>[^/]+)$', orders.InvoiceDownload.as_view(),
@@ -468,7 +464,6 @@ urlpatterns = [
re_path(r'^checkins/$', checkin.CheckinListView.as_view(), name='event.orders.checkins'),
re_path(r'^checkinlists/$', checkin.CheckinListList.as_view(), name='event.orders.checkinlists'),
re_path(r'^checkinlists/add$', checkin.CheckinListCreate.as_view(), name='event.orders.checkinlists.add'),
re_path(r'^checkinlists/reset$', checkin.CheckInResetView.as_view(), name='event.orders.checkinlists.reset'),
re_path(r'^checkinlists/select2$', typeahead.checkinlist_select2, name='event.orders.checkinlists.select2'),
re_path(r'^checkinlists/(?P<list>\d+)/$', checkin.CheckInListShow.as_view(), name='event.orders.checkinlists.show'),
re_path(r'^checkinlists/(?P<list>\d+)/simulator$', checkin.CheckInListSimulator.as_view(), name='event.orders.checkinlists.simulator'),
@@ -478,7 +473,6 @@ urlpatterns = [
name='event.orders.checkinlists.edit'),
re_path(r'^checkinlists/(?P<list>\d+)/delete$', checkin.CheckinListDelete.as_view(),
name='event.orders.checkinlists.delete'),
re_path(r'^datasync/failedjobs/$', datasync.EventFailedSyncJobsView.as_view(), name='event.datasync.failedjobs'),
])),
re_path(r'^event/(?P<organizer>[^/]+)/$', RedirectView.as_view(pattern_name='control:organizer'), name='event.organizerredirect'),
]

View File

@@ -50,16 +50,15 @@ from i18nfield.strings import LazyI18nString
from pretix.api.views.checkin import _redeem_process
from pretix.base.media import MEDIA_TYPES
from pretix.base.models import Checkin, LogEntry, Order, OrderPosition
from pretix.base.models import Checkin, Order, OrderPosition
from pretix.base.models.checkin import CheckinList
from pretix.base.models.orders import PrintLog
from pretix.base.services.checkin import (
LazyRuleVars, _logic_annotate_for_graphic_explain,
)
from pretix.base.signals import checkin_created
from pretix.base.views.tasks import AsyncFormView, AsyncPostView
from pretix.base.views.tasks import AsyncPostView
from pretix.control.forms.checkin import (
CheckinListForm, CheckinListSimulatorForm, CheckinResetForm,
CheckinListForm, CheckinListSimulatorForm,
)
from pretix.control.forms.filter import (
CheckinFilterForm, CheckinListAttendeeFilterForm, CheckinListFilterForm,
@@ -571,55 +570,3 @@ class CheckInListSimulator(EventPermissionRequiredMixin, FormView):
for q in self.result["questions"]:
q["question"] = LazyI18nString(q["question"])
return self.get(self.request, self.args, self.kwargs)
class CheckInResetView(CheckInListQueryMixin, EventPermissionRequiredMixin, AsyncFormView):
form_class = CheckinResetForm
permission = "can_change_orders"
template_name = "pretixcontrol/checkin/reset.html"
def get_error_url(self, *args):
return reverse(
"control:event.orders.checkinlists",
kwargs={
"event": self.request.event.slug,
"organizer": self.request.organizer.slug,
},
)
def get_success_url(self, *args):
return reverse(
"control:event.orders.checkinlists",
kwargs={
"event": self.request.event.slug,
"organizer": self.request.organizer.slug,
},
)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['checkins'] = Checkin.all.filter(list__event=self.request.event).count()
ctx['printlogs'] = PrintLog.objects.filter(position__order__event=self.request.event).count()
return ctx
def async_form_valid(self, task, form):
with transaction.atomic():
qs = Checkin.all.filter(list__event=self.request.event).select_related("position", "position__order")
logentries = []
for ci in qs:
if ci.position:
logentries.append(ci.position.order.log_action('pretix.event.checkin.reverted', data={
'position': ci.position.id,
'positionid': ci.position.positionid,
'list': ci.list_id,
'web': True
}, user=self.request.user, save=False))
Order.objects.filter(pk__in=qs.values_list("position__order_id", flat=True)).update(last_modified=now())
qs.delete()
LogEntry.objects.bulk_create(logentries)
pl = PrintLog.objects.filter(position__order__event=self.request.event)
pl.delete()
self.request.event.log_action('pretix.event.checkin.reset', user=self.request.user)
self.request.event.cache.clear()

View File

@@ -383,10 +383,6 @@ def event_index(request, organizer, event):
ctx['has_cancellation_requests'] = can_view_orders and CancellationRequest.objects.filter(
order__event=request.event
).exists()
ctx['has_sync_problems'] = can_change_event_settings and request.event.queued_sync_jobs.filter(
Q(need_manual_retry__isnull=False)
| Q(failed_attempts__gt=0)
).exists()
ctx['timeline'] = [
{

View File

@@ -1,150 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from itertools import groupby
from django.contrib import messages
from django.db.models import Q
from django.dispatch import receiver
from django.http import HttpResponseNotAllowed
from django.shortcuts import redirect
from django.template.loader import get_template
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django.views.generic import ListView
from pretix.base.datasync.datasync import datasync_providers
from pretix.base.models import Event, Order
from pretix.base.models.datasync import OrderSyncQueue
from pretix.control.permissions import (
AdministratorPermissionRequiredMixin, EventPermissionRequiredMixin,
OrganizerPermissionRequiredMixin,
)
from pretix.control.signals import order_info
from pretix.control.views.orders import OrderView
@receiver(order_info, dispatch_uid="datasync_control_order_info")
def on_control_order_info(sender: Event, request, order: Order, **kwargs):
providers = [provider for provider, meta in datasync_providers.filter(active_in=sender)]
if not providers:
return ""
queued = {p.sync_provider: p for p in order.queued_sync_jobs.all()}
objects = {
provider: list(objects)
for (provider, objects)
in groupby(order.sync_results.order_by('sync_provider').all(), key=lambda o: o.sync_provider)
}
providers = [(provider.identifier, provider.display_name, queued.get(provider.identifier), objects.get(provider.identifier)) for provider in providers]
template = get_template("pretixcontrol/datasync/control_order_info.html")
ctx = {
"order": order,
"request": request,
"event": sender,
"providers": providers,
"now": now(),
}
return template.render(ctx, request=request)
class ControlSyncJob(OrderView):
permission = 'can_change_orders'
def post(self, request, provider, *args, **kwargs):
prov, meta = datasync_providers.get(active_in=self.request.event, identifier=provider)
if self.request.POST.get("queue_sync") == "true":
prov.enqueue_order(self.order, 'user')
messages.success(self.request, _('The sync job has been enqueued and will run in the next minutes.'))
elif self.request.POST.get("cancel_job"):
job = self.order.queued_sync_jobs.get(pk=self.request.POST.get("cancel_job"))
if job.in_flight:
messages.warning(self.request, _('The sync job is already in progress.'))
else:
job.delete()
messages.success(self.request, _('The sync job has been canceled.'))
elif self.request.POST.get("run_job_now"):
job = self.order.queued_sync_jobs.get(pk=self.request.POST.get("run_job_now"))
job.not_before = now()
job.need_manual_retry = None
job.save()
messages.success(self.request, _('The sync job has been set to run as soon as possible.'))
return redirect(self.get_order_url())
def get(self, *args, **kwargs):
return HttpResponseNotAllowed(['POST'])
class FailedSyncJobsView(ListView):
template_name = 'pretixcontrol/datasync/failed_jobs.html'
model = OrderSyncQueue
context_object_name = 'queue_items'
paginate_by = 100
ordering = ('triggered',)
def get_queryset(self):
return super().get_queryset().filter(
Q(need_manual_retry__isnull=False)
| Q(failed_attempts__gt=0)
).select_related(
'order'
)
def post(self, request, *args, **kwargs):
items = self.get_queryset().filter(pk__in=request.POST.getlist('idlist'))
if self.request.POST.get("action") == "retry":
for item in items:
item.not_before = now()
item.need_manual_retry = None
item.save()
messages.success(self.request, _('The selected jobs have been set to run as soon as possible.'))
elif self.request.POST.get("action") == "cancel":
items.delete()
messages.success(self.request, _('The selected jobs have been canceled.'))
return redirect(request.get_full_path())
class GlobalFailedSyncJobsView(AdministratorPermissionRequiredMixin, FailedSyncJobsView):
pass
class OrganizerFailedSyncJobsView(OrganizerPermissionRequiredMixin, FailedSyncJobsView):
permission = "can_change_organizer_settings"
def get_queryset(self):
return super().get_queryset().filter(
event__organizer=self.request.organizer
)
class EventFailedSyncJobsView(EventPermissionRequiredMixin, FailedSyncJobsView):
permission = "can_change_event_settings"
def get_queryset(self):
return super().get_queryset().filter(
event=self.request.event
)

View File

@@ -83,7 +83,6 @@ from pretix.base.models import (
)
from pretix.base.models.orders import (
CancellationRequest, OrderFee, OrderPayment, OrderPosition, OrderRefund,
PrintLog,
)
from pretix.base.models.tax import ask_for_vat_id
from pretix.base.payment import PaymentException
@@ -598,7 +597,6 @@ class OrderDetail(OrderView):
'item__questions', 'issued_gift_cards', 'owned_gift_cards', 'linked_media',
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options').select_related('question')),
Prefetch('all_checkins', queryset=Checkin.all.select_related('list').order_by('datetime')),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device').order_by('datetime')),
).order_by('positionid')
positions = []

View File

@@ -49,7 +49,7 @@ from django.shortcuts import redirect, render
from django.urls import reverse
from django.utils.formats import get_format
from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now
from django.utils.timezone import make_aware
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django.views import View
from django.views.generic import CreateView, FormView, ListView, UpdateView
@@ -768,15 +768,8 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Asyn
ctx['time_formset'] = self.time_formset
tf = get_format('TIME_INPUT_FORMATS')[0]
ctx['time_admission_sample'] = time(8, 30, 0).strftime(tf)
ctx['time_begin_sample'] = time(9, 0, 0).strftime(tf)
ctx['time_end_sample'] = time(18, 0, 0).strftime(tf)
df = get_format('DATETIME_INPUT_FORMATS')[0]
ctx['datetime_sample'] = now().replace(
year=2000, month=12, day=31, hour=18, minute=0, second=0, microsecond=0
).strftime(df)
return ctx
@cached_property

View File

@@ -32,6 +32,7 @@ from django.conf import settings
from django.utils import translation
from django.utils.formats import get_format
from django.utils.translation import to_locale
from django.utils.translation.trans_real import TranslationCatalog
date_conversion_to_moment = {
'%a': 'ddd',
@@ -174,7 +175,7 @@ def get_language_score(locale):
Note that there is no valid score for "en", since it's technically not "translated".
"""
catalog = {}
catalog = None
app_configs = reversed(apps.get_app_configs())
for app in app_configs:
@@ -197,15 +198,10 @@ def get_language_score(locale):
)
except:
continue
catalog.update(translation._catalog.copy())
# Also add fallback catalog (e.g. es for es-419, de for de-informal, …)
while translation._fallback:
if not locale.startswith(translation._fallback.info().get("language", "XX")):
break
translation = translation._fallback
catalog.update(translation._catalog.copy())
if catalog is None:
catalog = TranslationCatalog(translation)
else:
catalog.update(translation)
# Add pretix' main translation folder as well as installation-specific translation folders
for localedir in reversed(settings.LOCALE_PATHS):
@@ -218,13 +214,10 @@ def get_language_score(locale):
)
except:
continue
catalog.update(translation._catalog.copy())
while translation._fallback:
if not locale.startswith(translation._fallback.info().get("language", "XX")):
break
translation = translation._fallback
catalog.update(translation._catalog.copy())
if catalog is None:
catalog = TranslationCatalog(translation)
else:
catalog.update(translation)
if not catalog:
score = 1

View File

@@ -5,7 +5,7 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-26 09:09+0000\n"
"PO-Revision-Date: 2025-07-02 22:00+0000\n"
"PO-Revision-Date: 2025-06-26 15:17+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/de/"
">\n"
@@ -34021,8 +34021,9 @@ msgid ""
"address you specified."
msgstr ""
"Bitte speichern Sie den Link zu genau dieser Seite, wenn Sie später auf Ihre "
"Bestellung zugreifen wollen. Wir haben Ihnen außerdem soeben einen Link mit "
"dieser Adresse an Ihre E-Mail-Adresse geschickt."
"Bestellung zugreifen oder Ihre Angaben ändern wollen. Wir haben Ihnen "
"außerdem soeben einen Link mit dieser Adresse an Ihre E-Mail-Adresse "
"geschickt."
#: pretix/presale/templates/pretixpresale/event/order.html:59
msgid ""
@@ -34030,8 +34031,8 @@ msgid ""
"also sent you an email containing the link to the address you specified."
msgstr ""
"Bitte speichern Sie folgenden Link ab, wenn Sie später auf Ihre Bestellung "
"zugreifen wollen. Wir haben Ihnen außerdem soeben einen Link mit dieser "
"Adresse an Ihre E-Mail-Adresse geschickt."
"zugreifen oder Ihre Angaben ändern wollen. Wir haben Ihnen außerdem soeben "
"einen Link mit dieser Adresse an Ihre E-Mail-Adresse geschickt."
#: pretix/presale/templates/pretixpresale/event/order.html:74
#: pretix/presale/templates/pretixpresale/event/position.html:18

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-26 09:09+0000\n"
"PO-Revision-Date: 2025-06-30 01:00+0000\n"
"PO-Revision-Date: 2025-05-30 11:15+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
"es/>\n"
@@ -659,7 +659,7 @@ msgstr "{system} Usuario"
#: pretix/presale/templates/pretixpresale/event/checkout_customer.html:30
#: pretix/presale/templates/pretixpresale/event/order.html:300
msgid "Email"
msgstr "Correo electrónico"
msgstr "Email"
#: pretix/base/auth.py:157 pretix/base/forms/auth.py:164
#: pretix/base/forms/auth.py:218 pretix/base/models/auth.py:675
@@ -3353,7 +3353,7 @@ msgstr "Precio unitario: {net_price} neto / {gross_price} bruto"
#, python-brace-format
msgctxt "invoice"
msgid "Single price: {price}"
msgstr "Precio único: {price}"
msgstr "Precio individual: {price}"
#: pretix/base/invoice.py:742 pretix/base/invoice.py:748
msgctxt "invoice"
@@ -3453,7 +3453,7 @@ msgstr "El plugin correspondiente no está activado."
#: pretix/base/logentrytypes.py:49
msgid "(deleted)"
msgstr "(eliminado)"
msgstr "borrado"
#: pretix/base/logentrytypes.py:78
#, python-brace-format
@@ -3502,9 +3502,11 @@ msgid "Tax rule {val}"
msgstr "Regla de impuesto {val}"
#: pretix/base/logentrytypes.py:151
#, python-brace-format
#, fuzzy, python-brace-format
#| msgctxt "subevent"
#| msgid "Date {val}"
msgid "{val}"
msgstr "{val}"
msgstr "Fecha {val}"
#: pretix/base/media.py:71
msgid "Barcode / QR-Code"
@@ -4074,7 +4076,7 @@ msgstr "Razón Social / Organización"
#: pretix/base/models/orders.py:3276 pretix/base/settings.py:83
#: pretix/plugins/stripe/payment.py:272
msgid "Select country"
msgstr "Seleccionar país"
msgstr "Seleccione país"
#: pretix/base/models/customers.py:381
msgctxt "openidconnect"
@@ -5627,9 +5629,6 @@ msgid ""
"with changing the type of question without data loss. Consider hiding this "
"question and creating a new one instead."
msgstr ""
"El sistema ya contiene respuestas a esta pregunta que no son compatibles con "
"el cambio del tipo de pregunta sin pérdida de datos. Considera ocultar esta "
"pregunta y crear una nueva en su lugar."
#: pretix/base/models/items.py:1961
#: pretix/control/templates/pretixcontrol/items/question.html:90
@@ -13000,10 +12999,12 @@ msgstr ""
"disponible"
#: pretix/base/timeline.py:351
#, python-brace-format
#, fuzzy, python-brace-format
#| msgctxt "timeline"
#| msgid "Payment provider \"{name}\" can no longer be selected"
msgctxt "timeline"
msgid "Payment provider \"{name}\" becomes active"
msgstr "El proveedor de pagos «{name}» pasa a estar activo"
msgstr "El proveedor de pagos \"{name}\" ya no se puede seleccionar"
#: pretix/base/timeline.py:369
#, python-brace-format
@@ -17479,7 +17480,7 @@ msgstr "Registrarse"
#: pretix/presale/templates/pretixpresale/organizers/customer_resetpw.html:28
#: pretix/presale/templates/pretixpresale/organizers/customer_resetpw.html:44
msgid "Log in"
msgstr "Conectarse"
msgstr "Iniciar sesión"
#: pretix/control/templates/pretixcontrol/auth/login.html:38
msgid "Lost password?"
@@ -17700,7 +17701,7 @@ msgstr "Salir"
#: pretix/control/templates/pretixcontrol/base.html:249
msgid "Organizer account"
msgstr "Cuenta de organizador"
msgstr "Cuenta del organizador"
#: pretix/control/templates/pretixcontrol/base.html:272
msgid "Search for events"
@@ -19784,7 +19785,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/event/settings.html:13
#: pretix/control/templates/pretixcontrol/user/settings.html:11
msgid "General settings"
msgstr "Configuración general"
msgstr "Parametrizaciones generales"
#: pretix/control/templates/pretixcontrol/event/settings.html:21
msgid "Basics"
@@ -19889,8 +19890,10 @@ msgstr "Lista de productos"
#: pretix/control/templates/pretixcontrol/event/settings.html:253
#: pretix/control/templates/pretixcontrol/event/settings.html:389
#, fuzzy
#| msgid "Invoice settings"
msgid "Incompatible settings"
msgstr "Ajustes incompatibles"
msgstr "Configuración de la factura"
#: pretix/control/templates/pretixcontrol/event/settings.html:254
#: pretix/control/templates/pretixcontrol/event/settings.html:390
@@ -19898,8 +19901,6 @@ msgid ""
"Customers won't be able to add themselves to the waiting list, because "
"\"Hide all products that are sold out\" is enabled."
msgstr ""
"Los clientes no podrán añadirse a la lista de espera, porque la opción «"
"Ocultar todos los productos agotados» está activada."
#: pretix/control/templates/pretixcontrol/event/settings.html:261
msgctxt "subevents"
@@ -21419,14 +21420,16 @@ msgid "Count"
msgstr "Cantidad"
#: pretix/control/templates/pretixcontrol/items/question.html:92
#, python-format
#, fuzzy, python-format
#| msgid "Copy answers"
msgid "%% of answers"
msgstr "%% de respuestas"
msgstr "Copiar las respuestas"
#: pretix/control/templates/pretixcontrol/items/question.html:93
#, python-format
#, fuzzy, python-format
#| msgid "Number of tickets"
msgid "%% of tickets"
msgstr "%% de billetes"
msgstr "Número de entradas"
#: pretix/control/templates/pretixcontrol/items/question.html:112
#: pretix/control/templates/pretixcontrol/order/transactions.html:67
@@ -22269,7 +22272,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html:230
msgid "Contact email"
msgstr "Correo electrónico de contacto"
msgstr "Correo electrónico"
#: pretix/control/templates/pretixcontrol/order/index.html:234
msgid ""
@@ -25240,10 +25243,15 @@ msgid "Item prices"
msgstr "Precios de artículo"
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:197
#, fuzzy
#| msgid ""
#| "You selected a set of dates that currently have different check-in list "
#| "setups. You can therefore not change their check-in lists in bulk."
msgid "You selected a set of dates that currently have different quota setups."
msgstr ""
"Ha seleccionado un conjunto de fechas que actualmente tienen diferentes "
"configuraciones de cuota."
"Seleccionó un conjunto de fechas que actualmente tienen diferentes "
"configuraciones de lista de asistentes. Por lo tanto, no puede cambiar sus "
"lista de asistentes de forma masiva."
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:198
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:373
@@ -25251,8 +25259,6 @@ msgid ""
"Using this option will <strong>delete all current quotas</strong> from "
"<strong>all selected dates</strong>."
msgstr ""
"Al utilizar esta opción se <strong> eliminarán todas las cuotas actuales </"
"strong> de <strong>todas las fechas seleccionadas</strong>."
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:277
msgid ""
@@ -25264,16 +25270,20 @@ msgstr ""
"lista de asistentes de forma masiva."
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:372
#, fuzzy
#| msgid "Add to existing quota"
msgid "Delete existing quotas"
msgstr "Eliminar cuotas existentes"
msgstr "Añadir a la cuota existente"
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:374
msgid "This cannot be reverted. Are you sure to proceed?"
msgstr "Esto no se puede revertir. ¿Está seguro de continuar?"
msgstr ""
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:381
#, fuzzy
#| msgid "Process refund"
msgid "Proceed"
msgstr "Proceder"
msgstr "Procesar el reembolso"
#: pretix/control/templates/pretixcontrol/subevents/delete.html:4
#: pretix/control/templates/pretixcontrol/subevents/delete.html:6
@@ -26355,9 +26365,9 @@ msgid ""
"address bar and make sure it is correct and that the link has not been used "
"before."
msgstr ""
"Ha utilizado un enlace no válido. Copie el enlace de su correo electrónico "
"en la barra de direcciones y asegúrese de que es correcto y de que el enlace "
"no se ha utilizado antes."
"Usaste un enlace inválido. Por favor, copie el enlace de su correo "
"electrónico a la barra de direcciones y asegúrese de que sea correcto y de "
"que el enlace no haya sido utilizado anteriormente."
#: pretix/control/views/auth.py:252
msgid ""
@@ -30806,8 +30816,10 @@ msgstr ""
"El titular de un billete de este pedido recibió un correo electrónico masivo."
#: pretix/plugins/sendmail/signals.py:134
#, fuzzy
#| msgid "The order received a mass email."
msgid "The person on the waiting list received a mass email."
msgstr "La persona en lista de espera recibió un correo electrónico masivo."
msgstr "El pedido recibió un correo electrónico masivo."
#: pretix/plugins/sendmail/signals.py:139
msgid "An email rule was created"
@@ -33431,8 +33443,10 @@ msgid "Renew reservation"
msgstr "Renovar reserva"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:522
#, fuzzy
#| msgid "Reservation period"
msgid "Reservation renewed"
msgstr "Reserva renovada"
msgstr "Período de reserva"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:528
msgid "Overview of your ordered products."
@@ -34672,7 +34686,7 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/fragment_calendar_nav.html:5
msgid "Event overview by month, week, etc."
msgstr "Resumen de eventos por mes, semana, etc."
msgstr ""
#: pretix/presale/templates/pretixpresale/fragment_calendar_nav.html:26
msgid "iCal"
@@ -34923,7 +34937,7 @@ msgstr "Inicie sesión en su cuenta en %(org)s"
#: pretix/presale/templates/pretixpresale/organizers/customer_login.html:47
#: pretix/presale/templates/pretixpresale/organizers/customer_registration.html:38
msgid "Create account"
msgstr "Crear una cuenta"
msgstr "Crear cuenta"
#: pretix/presale/templates/pretixpresale/organizers/customer_membership.html:6
#: pretix/presale/templates/pretixpresale/organizers/customer_membership.html:16

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-26 09:09+0000\n"
"PO-Revision-Date: 2025-06-30 01:00+0000\n"
"PO-Revision-Date: 2025-05-30 11:06+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/fr/"
">\n"
@@ -658,7 +658,7 @@ msgstr "Utilisateur {system}"
#: pretix/presale/templates/pretixpresale/event/checkout_customer.html:30
#: pretix/presale/templates/pretixpresale/event/order.html:300
msgid "Email"
msgstr "E-Mail"
msgstr "E-mail"
#: pretix/base/auth.py:157 pretix/base/forms/auth.py:164
#: pretix/base/forms/auth.py:218 pretix/base/models/auth.py:675
@@ -1521,7 +1521,7 @@ msgstr "rue"
#: pretix/presale/templates/pretixpresale/event/order.html:323
msgctxt "address"
msgid "State"
msgstr "État"
msgstr "État/Province/Région"
#: pretix/base/exporters/invoices.py:221 pretix/base/exporters/invoices.py:347
#: pretix/base/models/orders.py:3288 pretix/base/models/orders.py:3323
@@ -3509,9 +3509,11 @@ msgid "Tax rule {val}"
msgstr "Règle fiscale {val}"
#: pretix/base/logentrytypes.py:151
#, python-brace-format
#, fuzzy, python-brace-format
#| msgctxt "subevent"
#| msgid "Date {val}"
msgid "{val}"
msgstr "{val}"
msgstr "Date {val}"
#: pretix/base/media.py:71
msgid "Barcode / QR-Code"
@@ -4090,7 +4092,7 @@ msgstr "Nom de l'entreprise"
#: pretix/base/models/orders.py:3276 pretix/base/settings.py:83
#: pretix/plugins/stripe/payment.py:272
msgid "Select country"
msgstr "Sélectionner le pays"
msgstr "Sélectionnez le pays"
#: pretix/base/models/customers.py:381
msgctxt "openidconnect"
@@ -5658,9 +5660,6 @@ msgid ""
"with changing the type of question without data loss. Consider hiding this "
"question and creating a new one instead."
msgstr ""
"Le système contient déjà des réponses à cette question qui ne sont pas "
"compatibles avec un changement de type de question sans perte de données. "
"Envisagez de masquer cette question et d'en créer une nouvelle à la place."
#: pretix/base/models/items.py:1961
#: pretix/control/templates/pretixcontrol/items/question.html:90
@@ -13131,10 +13130,12 @@ msgstr ""
"La variation de produit \"{product} - {variation}\" devient indisponible"
#: pretix/base/timeline.py:351
#, python-brace-format
#, fuzzy, python-brace-format
#| msgctxt "timeline"
#| msgid "Payment provider \"{name}\" can no longer be selected"
msgctxt "timeline"
msgid "Payment provider \"{name}\" becomes active"
msgstr "Le prestataire de paiement « {name} » devient actif"
msgstr "Le fournisseur de paiement « {name} » ne peut plus être sélectionné"
#: pretix/base/timeline.py:369
#, python-brace-format
@@ -17606,7 +17607,7 @@ msgstr "Connexion"
#: pretix/control/templates/pretixcontrol/auth/login.html:43
#: pretix/control/templates/pretixcontrol/auth/register.html:22
msgid "Register"
msgstr "S'enregistrer"
msgstr "Inscription"
#: pretix/control/templates/pretixcontrol/auth/login.html:27
#: pretix/presale/templates/pretixpresale/fragment_login_status.html:19
@@ -17836,7 +17837,7 @@ msgstr "Se déconnecter"
#: pretix/control/templates/pretixcontrol/base.html:249
msgid "Organizer account"
msgstr "Compte organisateur"
msgstr "Compte de l'organisateur"
#: pretix/control/templates/pretixcontrol/base.html:272
msgid "Search for events"
@@ -19235,7 +19236,7 @@ msgstr "Optionnel"
#: pretix/control/templates/pretixcontrol/event/fragment_geodata.html:22
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:58
msgid "Geocoding data © OpenStreetMap"
msgstr "Données de géocodage © OpenStreetMap"
msgstr "Données de géocodage © OpenStreetMap"
#: pretix/control/templates/pretixcontrol/event/fragment_geodata_autoupdate.html:4
msgid "Failed to retrieve geo coordinates"
@@ -20040,8 +20041,10 @@ msgstr "Liste des produits"
#: pretix/control/templates/pretixcontrol/event/settings.html:253
#: pretix/control/templates/pretixcontrol/event/settings.html:389
#, fuzzy
#| msgid "Invoice settings"
msgid "Incompatible settings"
msgstr "Paramètres incompatibles"
msgstr "Paramètres de facturation"
#: pretix/control/templates/pretixcontrol/event/settings.html:254
#: pretix/control/templates/pretixcontrol/event/settings.html:390
@@ -20049,8 +20052,6 @@ msgid ""
"Customers won't be able to add themselves to the waiting list, because "
"\"Hide all products that are sold out\" is enabled."
msgstr ""
"Les clients ne pourront pas s'ajouter à la liste d'attente, car l'option « "
"Cacher tous les produits épuisés » est activée."
#: pretix/control/templates/pretixcontrol/event/settings.html:261
msgctxt "subevents"
@@ -21580,14 +21581,16 @@ msgid "Count"
msgstr "Compter"
#: pretix/control/templates/pretixcontrol/items/question.html:92
#, python-format
#, fuzzy, python-format
#| msgid "Copy answers"
msgid "%% of answers"
msgstr "%% de réponses"
msgstr "Copier les réponses"
#: pretix/control/templates/pretixcontrol/items/question.html:93
#, python-format
#, fuzzy, python-format
#| msgid "Number of tickets"
msgid "%% of tickets"
msgstr "%% de billets"
msgstr "Nombre de billets"
#: pretix/control/templates/pretixcontrol/items/question.html:112
#: pretix/control/templates/pretixcontrol/order/transactions.html:67
@@ -22436,7 +22439,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html:230
msgid "Contact email"
msgstr "Email de contact"
msgstr "E-mail de contact"
#: pretix/control/templates/pretixcontrol/order/index.html:234
msgid ""
@@ -24644,7 +24647,7 @@ msgstr "Authentification à deux facteurs désactivée"
#: pretix/control/templates/pretixcontrol/organizers/team_members.html:57
msgid "invited, pending response"
msgstr "invité, en attente de réponse"
msgstr "invité, en attente d'une réponse"
#: pretix/control/templates/pretixcontrol/organizers/team_members.html:59
msgid "resend invite"
@@ -25437,10 +25440,15 @@ msgid "Item prices"
msgstr "Prix des articles"
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:197
#, fuzzy
#| msgid ""
#| "You selected a set of dates that currently have different check-in list "
#| "setups. You can therefore not change their check-in lists in bulk."
msgid "You selected a set of dates that currently have different quota setups."
msgstr ""
"Vous avez sélectionné un ensemble de dates qui ont actuellement des "
"configurations de quotas différentes."
"Vous avez sélectionné un ensemble de dates qui ont actuellement différentes "
"configurations de liste darchivage. Vous ne pouvez donc pas modifier leurs "
"listes denregistrement en masse."
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:198
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:373
@@ -25448,8 +25456,6 @@ msgid ""
"Using this option will <strong>delete all current quotas</strong> from "
"<strong>all selected dates</strong>."
msgstr ""
"Cette option permet de <strong> supprimer tous les quotas actuels</strong> "
"de <strong>toutes les dates sélectionnées</strong>."
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:277
msgid ""
@@ -25461,18 +25467,20 @@ msgstr ""
"listes denregistrement en masse."
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:372
#, fuzzy
#| msgid "Add to existing quota"
msgid "Delete existing quotas"
msgstr "Supprimer les quotas existants"
msgstr "Ajouter au quota existant"
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:374
msgid "This cannot be reverted. Are you sure to proceed?"
msgstr ""
"Il n'est pas possible de revenir en arrière. Êtes-vous sûr de pouvoir "
"continuer ?"
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:381
#, fuzzy
#| msgid "Process refund"
msgid "Proceed"
msgstr "Procéder"
msgstr "Processus de remboursement"
#: pretix/control/templates/pretixcontrol/subevents/delete.html:4
#: pretix/control/templates/pretixcontrol/subevents/delete.html:6
@@ -26557,9 +26565,9 @@ msgid ""
"address bar and make sure it is correct and that the link has not been used "
"before."
msgstr ""
"Vous avez utilisé un lien non valide. Veuillez copier le lien de votre "
"courriel dans la barre d'adresse et assurez-vous qu'il est correct et qu'il "
"n'a pas été utilisé auparavant."
"Vous avez utilisé un lien invalide. Veuillez copier le lien de votre email "
"dans la barre d'adresse et assurez-vous qu'il est correct et que le lien n' "
"a jamais été utilisé auparavant."
#: pretix/control/views/auth.py:252
msgid ""
@@ -31061,8 +31069,10 @@ msgid "A ticket holder of this order received a mass email."
msgstr "Un détenteur de billet de cette commande a reçu un courriel de masse."
#: pretix/plugins/sendmail/signals.py:134
#, fuzzy
#| msgid "The order received a mass email."
msgid "The person on the waiting list received a mass email."
msgstr "La personne inscrite sur la liste d'attente a reçu un e-mail de masse."
msgstr "La commande a reçu un email de masse."
#: pretix/plugins/sendmail/signals.py:139
msgid "An email rule was created"
@@ -33706,8 +33716,10 @@ msgid "Renew reservation"
msgstr "Renouveler la réservation"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:522
#, fuzzy
#| msgid "Reservation period"
msgid "Reservation renewed"
msgstr "Réservation renouvelée"
msgstr "Période de réservation"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:528
msgid "Overview of your ordered products."
@@ -34982,7 +34994,7 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/fragment_calendar_nav.html:5
msgid "Event overview by month, week, etc."
msgstr "Aperçu des événements par mois, semaine, etc."
msgstr ""
#: pretix/presale/templates/pretixpresale/fragment_calendar_nav.html:26
msgid "iCal"

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-26 09:09+0000\n"
"PO-Revision-Date: 2025-07-04 00:00+0000\n"
"Last-Translator: Rosariocastellana <rosariocastellana@gmail.com>\n"
"PO-Revision-Date: 2025-05-05 09:40+0000\n"
"Last-Translator: \"Luca Martinelli [Sannita]\" <sannita@gmail.com>\n"
"Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix/"
"it/>\n"
"Language: it\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.11.4\n"
"X-Generator: Weblate 5.11.1\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -12027,7 +12027,7 @@ msgstr ""
#: pretix/base/templates/404.html:9
msgid "I'm afraid we could not find the the resource you requested."
msgstr "Temo che non siamo riusciti a trovare la risorsa richiesta."
msgstr ""
#: pretix/base/templates/500.html:4 pretix/base/templates/500.html:8
msgid "Internal Server Error"
@@ -33228,8 +33228,6 @@ msgstr "Il prodotto è stato aggiunto al tuo carrello."
#: pretix/presale/views/widget.py:395
msgid "Tickets for this event cannot be purchased on this sales channel."
msgstr ""
"I biglietti per questo evento non possono essere acquistati su questo canale "
"di vendita."
#: pretix/presale/views/cart.py:763
msgid ""

View File

@@ -7,10 +7,10 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-26 09:09+0000\n"
"PO-Revision-Date: 2025-07-06 01:00+0000\n"
"Last-Translator: Jan Van Haver <jan.van.haver@gmail.com>\n"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/>"
"\n"
"PO-Revision-Date: 2025-06-10 04:00+0000\n"
"Last-Translator: Tim Maurizio Dullaart <Tim.maurizio@gmail.com>\n"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/"
">\n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -3486,9 +3486,11 @@ msgid "Tax rule {val}"
msgstr "Belastingregel {val}"
#: pretix/base/logentrytypes.py:151
#, python-brace-format
#, fuzzy, python-brace-format
#| msgctxt "subevent"
#| msgid "Date {val}"
msgid "{val}"
msgstr "{val}"
msgstr "Datum {val}"
#: pretix/base/media.py:71
msgid "Barcode / QR-Code"
@@ -5605,9 +5607,6 @@ msgid ""
"with changing the type of question without data loss. Consider hiding this "
"question and creating a new one instead."
msgstr ""
"Het systeem bevat al antwoorden op deze vraag die niet compatibel zijn met "
"het wijzigen van het vraagtype zonder gegevensverlies. Wellicht is het beter "
"om deze vraag te verbergen en in plaats daarvan een nieuwe vraag te maken."
#: pretix/base/models/items.py:1961
#: pretix/control/templates/pretixcontrol/items/question.html:90
@@ -12959,10 +12958,12 @@ msgid "Product variation \"{product} {variation}\" becomes unavailable"
msgstr "Product \"{product} - {variation}\" wordt niet meer beschikbaar"
#: pretix/base/timeline.py:351
#, python-brace-format
#, fuzzy, python-brace-format
#| msgctxt "timeline"
#| msgid "Payment provider \"{name}\" can no longer be selected"
msgctxt "timeline"
msgid "Payment provider \"{name}\" becomes active"
msgstr "Betalingsprovider \"{name}\" wordt actief"
msgstr "Betalingsprovider \"{name}\" kan niet meer worden gekozen"
#: pretix/base/timeline.py:369
#, python-brace-format
@@ -19824,8 +19825,10 @@ msgstr "Productlijst"
#: pretix/control/templates/pretixcontrol/event/settings.html:253
#: pretix/control/templates/pretixcontrol/event/settings.html:389
#, fuzzy
#| msgid "Invoice settings"
msgid "Incompatible settings"
msgstr "Niet-compatibele instellingen"
msgstr "Factuurinstellingen"
#: pretix/control/templates/pretixcontrol/event/settings.html:254
#: pretix/control/templates/pretixcontrol/event/settings.html:390
@@ -19833,8 +19836,6 @@ msgid ""
"Customers won't be able to add themselves to the waiting list, because "
"\"Hide all products that are sold out\" is enabled."
msgstr ""
"Klanten kunnen zichzelf niet toevoegen aan de wachtlijst, omdat "
"\"Verberg alle producten die zijn uitverkocht\" is ingeschakeld."
#: pretix/control/templates/pretixcontrol/event/settings.html:261
msgctxt "subevents"
@@ -21341,14 +21342,16 @@ msgid "Count"
msgstr "Aantal"
#: pretix/control/templates/pretixcontrol/items/question.html:92
#, python-format
#, fuzzy, python-format
#| msgid "Copy answers"
msgid "%% of answers"
msgstr "%% antwoorden"
msgstr "Kopieer antwoorden"
#: pretix/control/templates/pretixcontrol/items/question.html:93
#, python-format
#, fuzzy, python-format
#| msgid "Number of tickets"
msgid "%% of tickets"
msgstr "%% tickets"
msgstr "Aantal tickets"
#: pretix/control/templates/pretixcontrol/items/question.html:112
#: pretix/control/templates/pretixcontrol/order/transactions.html:67
@@ -22189,7 +22192,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html:230
msgid "Contact email"
msgstr "Contact e-mailadres"
msgstr "Contact-e-mailadres"
#: pretix/control/templates/pretixcontrol/order/index.html:234
msgid ""
@@ -25159,9 +25162,14 @@ msgid "Item prices"
msgstr "Productprijzen"
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:197
#, fuzzy
#| msgid ""
#| "You selected a set of dates that currently have different check-in list "
#| "setups. You can therefore not change their check-in lists in bulk."
msgid "You selected a set of dates that currently have different quota setups."
msgstr ""
"De gekozen datums hebben op dit moment verschillende quota-instellingen."
"De inchecklijstinstellingen van de gekozen datums verschillen en kunnen "
"hierom niet in één keer aangepast worden."
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:198
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:373
@@ -25169,8 +25177,6 @@ msgid ""
"Using this option will <strong>delete all current quotas</strong> from "
"<strong>all selected dates</strong>."
msgstr ""
"Als u deze optie gebruikt, worden <strong>alle huidige quota's verwijderd</"
"strong> van <strong>alle geselecteerde datums</strong>."
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:277
msgid ""
@@ -25181,16 +25187,20 @@ msgstr ""
"hierom niet in één keer aangepast worden."
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:372
#, fuzzy
#| msgid "Add to existing quota"
msgid "Delete existing quotas"
msgstr "Verwijder bestaande quota's"
msgstr "Toevoegen aan bestaand quotum"
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:374
msgid "This cannot be reverted. Are you sure to proceed?"
msgstr "Deze stap is onomkeerbaar. Weet u zeker dat u door wilt gaan?"
msgstr ""
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:381
#, fuzzy
#| msgid "Process refund"
msgid "Proceed"
msgstr "Verdergaan"
msgstr "Verwerk terugbetaling"
#: pretix/control/templates/pretixcontrol/subevents/delete.html:4
#: pretix/control/templates/pretixcontrol/subevents/delete.html:6
@@ -30313,7 +30323,7 @@ msgstr "(excl. belasting)"
#: pretix/plugins/reports/exporters.py:275
msgid "(incl. taxes)"
msgstr "(incl. btw)"
msgstr "(incl. belasting)"
#: pretix/plugins/reports/exporters.py:285
#: pretix/plugins/reports/exporters.py:304
@@ -30692,8 +30702,10 @@ msgid "A ticket holder of this order received a mass email."
msgstr "Een tickethouder van deze bestelling ontving een massamail."
#: pretix/plugins/sendmail/signals.py:134
#, fuzzy
#| msgid "The order received a mass email."
msgid "The person on the waiting list received a mass email."
msgstr "De persoon op de wachtlijst ontving een massamail."
msgstr "De bestelling ontving een massamail."
#: pretix/plugins/sendmail/signals.py:139
msgid "An email rule was created"
@@ -33303,8 +33315,10 @@ msgid "Renew reservation"
msgstr "Vernieuw reservering"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:522
#, fuzzy
#| msgid "Reservation period"
msgid "Reservation renewed"
msgstr "Reservering vernieuwd"
msgstr "Reserveerperiode"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:528
msgid "Overview of your ordered products."
@@ -34564,7 +34578,7 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/fragment_calendar_nav.html:5
msgid "Event overview by month, week, etc."
msgstr "Overzicht van de events per maand, week, enz."
msgstr ""
#: pretix/presale/templates/pretixpresale/fragment_calendar_nav.html:26
msgid "iCal"

View File

@@ -176,6 +176,7 @@ class AutoCheckinRuleForm(forms.ModelForm):
"organizer": self.event.organizer.slug,
},
),
"data-placeholder": _("Check-in list"),
}
)
self.fields["list"].widget.choices = self.fields["list"].choices

View File

@@ -2,7 +2,7 @@
{% load ibanformat %}
{% load bootstrap3 %}
{% bootstrap_field form.payer layout="inline" placeholder=_("Account holder") %}
{% bootstrap_field form.iban layout="inline" placeholder=_("IBAN") %}
{% bootstrap_field form.bic layout="inline" placeholder=_("BIC (optional)") %}
{% bootstrap_field form.payer layout="inline" %}
{% bootstrap_field form.iban layout="inline" %}
{% bootstrap_field form.bic layout="inline" %}
{% bootstrap_form_errors form error_types="all" %}

View File

@@ -54,7 +54,6 @@ from paypalrestsdk.openid_connect import Tokeninfo
from requests import RequestException
from pretix.base.decimal import round_decimal
from pretix.base.forms import SecretKeySettingsField
from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota
from pretix.base.payment import BasePaymentProvider, PaymentException
from pretix.base.services.mail import SendMailException
@@ -120,7 +119,7 @@ class Paypal(BasePaymentProvider):
)
)),
('secret',
SecretKeySettingsField(
forms.CharField(
label=_('Secret'),
max_length=80,
min_length=80,

View File

@@ -50,7 +50,6 @@ from paypalcheckoutsdk.payments import CapturesRefundRequest, RefundsGetRequest
from paypalhttp import HttpError
from pretix.base.decimal import round_decimal
from pretix.base.forms import SecretKeySettingsField
from pretix.base.forms.questions import guess_country
from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota
from pretix.base.payment import BasePaymentProvider, PaymentException
@@ -117,7 +116,7 @@ class PaypalSettingsHolder(BasePaymentProvider):
)
)),
('secret',
SecretKeySettingsField(
forms.CharField(
label=_('Secret'),
max_length=80,
min_length=80,

View File

@@ -279,6 +279,7 @@ class OrderMailForm(BaseMailForm):
'event': event.slug,
'organizer': event.organizer.slug,
}),
'data-placeholder': pgettext_lazy('subevent', 'Date')
}
)
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
@@ -359,6 +360,7 @@ class RuleForm(FormPlaceholderMixin, I18nModelForm):
'event': self.event.slug,
'organizer': self.event.organizer.slug,
}),
'data-placeholder': pgettext_lazy('subevent', 'Date')
}
)
self.fields['subevent'].widget.choices = self.fields['subevent'].choices

View File

@@ -219,7 +219,7 @@
{% endblock %}
{% block footernav %}
{% if request.event.settings.contact_mail %}
<li><a href="mailto:{{ request.event.settings.contact_mail }}" target="_blank" rel="noopener">{% trans "Contact" %}</a></li>
<li><a href="mailto:{{ request.event.settings.contact_mail }}" target="_blank" rel="noopener">{% trans "Contact event organizer" %}</a></li>
{% endif %}
{% if request.event.settings.privacy_url %}
<li><a href="{% safelink request.event.settings.privacy_url %}" target="_blank" rel="noopener">{% trans "Privacy policy" %}</a></li>

View File

@@ -72,11 +72,9 @@
{% endblocktrans %}
</span>
<span aria-hidden="true">{{ item.min_price|money:event.currency }} {{ item.max_price|money:event.currency }}</span>
{% elif not item.min_price and not item.max_price %}
{% if not item.mandatory_priced_addons %}
<span class="text-uppercase">{% trans "free" context "price" %}</span>
{% endif %}
{% else %}
{% elif not item.min_price and not item.max_price and not item.mandatory_priced_addons %}
<span class="text-uppercase">{% trans "free" context "price" %}</span>
{% elif not item.mandatory_priced_addons %}
{{ item.min_price|money:event.currency }}
{% endif %}
</div>

View File

@@ -97,7 +97,7 @@
{% endblock %}
{% block footernav %}
{% if not request.event and request.organizer.settings.contact_mail %}
<li><a href="mailto:{{ request.organizer.settings.contact_mail }}" target="_blank" rel="noopener">{% trans "Contact" %}</a></li>
<li><a href="mailto:{{ request.organizer.settings.contact_mail }}" target="_blank" rel="noopener">{% trans "Contact event organizer" %}</a></li>
{% endif %}
{% if not request.event and request.organizer.settings.privacy_url %}
<li><a href="{% safelink request.organizer.settings.privacy_url %}" target="_blank" rel="noopener">{% trans "Privacy policy" %}</a></li>

View File

@@ -711,7 +711,6 @@ BOOTSTRAP3 = {
'bulkedit_inline': 'pretix.control.forms.renderers.InlineBulkEditFieldRenderer',
'checkout': 'pretix.presale.forms.renderers.CheckoutFieldRenderer',
},
'set_placeholder': False,
}
PASSWORD_HASHERS = [

View File

@@ -8,7 +8,7 @@
"name": "pretix",
"version": "0.0.0",
"dependencies": {
"@babel/core": "^7.27.7",
"@babel/core": "^7.27.4",
"@babel/preset-env": "^7.27.2",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-node-resolve": "^16.0.1",
@@ -54,20 +54,21 @@
}
},
"node_modules/@babel/core": {
"version": "7.27.7",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.7.tgz",
"integrity": "sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w==",
"version": "7.27.4",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz",
"integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.27.5",
"@babel/generator": "^7.27.3",
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-module-transforms": "^7.27.3",
"@babel/helpers": "^7.27.6",
"@babel/parser": "^7.27.7",
"@babel/helpers": "^7.27.4",
"@babel/parser": "^7.27.4",
"@babel/template": "^7.27.2",
"@babel/traverse": "^7.27.7",
"@babel/types": "^7.27.7",
"@babel/traverse": "^7.27.4",
"@babel/types": "^7.27.3",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -102,11 +103,12 @@
}
},
"node_modules/@babel/generator": {
"version": "7.27.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz",
"integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==",
"version": "7.27.3",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.3.tgz",
"integrity": "sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.27.5",
"@babel/parser": "^7.27.3",
"@babel/types": "^7.27.3",
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25",
@@ -402,23 +404,25 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.27.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz",
"integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==",
"version": "7.27.4",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.4.tgz",
"integrity": "sha512-Y+bO6U+I7ZKaM5G5rDUZiYfUvQPUibYmAFe7EnKdnKBbVXDZxvp+MWOH5gYciY0EPk4EScsuFMQBbEfpdRKSCQ==",
"license": "MIT",
"dependencies": {
"@babel/template": "^7.27.2",
"@babel/types": "^7.27.6"
"@babel/types": "^7.27.3"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.27.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.7.tgz",
"integrity": "sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==",
"version": "7.27.4",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.4.tgz",
"integrity": "sha512-BRmLHGwpUqLFR2jzx9orBuX/ABDkj2jLKOXrHDTN2aOKL+jFDDKaRNo9nyYsIl9h/UE/7lMKdDjKQQyxKKDZ7g==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.27.7"
"@babel/types": "^7.27.3"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -1468,15 +1472,16 @@
}
},
"node_modules/@babel/traverse": {
"version": "7.27.7",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.7.tgz",
"integrity": "sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw==",
"version": "7.27.4",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz",
"integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.27.5",
"@babel/parser": "^7.27.7",
"@babel/generator": "^7.27.3",
"@babel/parser": "^7.27.4",
"@babel/template": "^7.27.2",
"@babel/types": "^7.27.7",
"@babel/types": "^7.27.3",
"debug": "^4.3.1",
"globals": "^11.1.0"
},
@@ -1485,9 +1490,10 @@
}
},
"node_modules/@babel/types": {
"version": "7.27.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.7.tgz",
"integrity": "sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw==",
"version": "7.27.3",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.3.tgz",
"integrity": "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1"
@@ -3839,20 +3845,20 @@
"integrity": "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw=="
},
"@babel/core": {
"version": "7.27.7",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.7.tgz",
"integrity": "sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w==",
"version": "7.27.4",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz",
"integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
"requires": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.27.5",
"@babel/generator": "^7.27.3",
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-module-transforms": "^7.27.3",
"@babel/helpers": "^7.27.6",
"@babel/parser": "^7.27.7",
"@babel/helpers": "^7.27.4",
"@babel/parser": "^7.27.4",
"@babel/template": "^7.27.2",
"@babel/traverse": "^7.27.7",
"@babel/types": "^7.27.7",
"@babel/traverse": "^7.27.4",
"@babel/types": "^7.27.3",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -3873,11 +3879,11 @@
}
},
"@babel/generator": {
"version": "7.27.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz",
"integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==",
"version": "7.27.3",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.3.tgz",
"integrity": "sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q==",
"requires": {
"@babel/parser": "^7.27.5",
"@babel/parser": "^7.27.3",
"@babel/types": "^7.27.3",
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25",
@@ -4082,20 +4088,20 @@
}
},
"@babel/helpers": {
"version": "7.27.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz",
"integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==",
"version": "7.27.4",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.4.tgz",
"integrity": "sha512-Y+bO6U+I7ZKaM5G5rDUZiYfUvQPUibYmAFe7EnKdnKBbVXDZxvp+MWOH5gYciY0EPk4EScsuFMQBbEfpdRKSCQ==",
"requires": {
"@babel/template": "^7.27.2",
"@babel/types": "^7.27.6"
"@babel/types": "^7.27.3"
}
},
"@babel/parser": {
"version": "7.27.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.7.tgz",
"integrity": "sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==",
"version": "7.27.4",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.4.tgz",
"integrity": "sha512-BRmLHGwpUqLFR2jzx9orBuX/ABDkj2jLKOXrHDTN2aOKL+jFDDKaRNo9nyYsIl9h/UE/7lMKdDjKQQyxKKDZ7g==",
"requires": {
"@babel/types": "^7.27.7"
"@babel/types": "^7.27.3"
}
},
"@babel/plugin-bugfix-firefox-class-in-computed-class-key": {
@@ -4714,23 +4720,23 @@
}
},
"@babel/traverse": {
"version": "7.27.7",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.7.tgz",
"integrity": "sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw==",
"version": "7.27.4",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz",
"integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==",
"requires": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.27.5",
"@babel/parser": "^7.27.7",
"@babel/generator": "^7.27.3",
"@babel/parser": "^7.27.4",
"@babel/template": "^7.27.2",
"@babel/types": "^7.27.7",
"@babel/types": "^7.27.3",
"debug": "^4.3.1",
"globals": "^11.1.0"
}
},
"@babel/types": {
"version": "7.27.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.7.tgz",
"integrity": "sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw==",
"version": "7.27.3",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.3.tgz",
"integrity": "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==",
"requires": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1"

View File

@@ -4,7 +4,7 @@
"private": true,
"scripts": {},
"dependencies": {
"@babel/core": "^7.27.7",
"@babel/core": "^7.27.4",
"@babel/preset-env": "^7.27.2",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-node-resolve": "^16.0.1",

View File

@@ -330,7 +330,6 @@ var form_handlers = function (el) {
dependent.prop('disabled', !enabled).closest('.form-group, .form-field-boundary').toggleClass('disabled', !enabled);
if (!enabled && !dependent.is('[data-checkbox-dependency-visual]')) {
dependent.prop('checked', false);
dependent.trigger('change')
}
};
update();

View File

@@ -126,7 +126,6 @@ pre[lang=eg], input[lang=eg], textarea[lang=eg], div[lang=eg] { background-image
pre[lang=eh], input[lang=eh], textarea[lang=eh], div[lang=eh] { background-image: url(static('pretixbase/img/flags/eh.png')); }
pre[lang=er], input[lang=er], textarea[lang=er], div[lang=er] { background-image: url(static('pretixbase/img/flags/er.png')); }
pre[lang=es], input[lang=es], textarea[lang=es], div[lang=es] { background-image: url(static('pretixbase/img/flags/es.png')); }
pre[lang=es-419], input[lang=es-419], textarea[lang=es-419], div[lang=es-419] { background-image: url(static('pretixbase/img/flags/es.png')); }
pre[lang=et], input[lang=et], textarea[lang=et], div[lang=et] { background-image: url(static('pretixbase/img/flags/et.png')); }
pre[lang=fi], input[lang=fi], textarea[lang=fi], div[lang=fi] { background-image: url(static('pretixbase/img/flags/fi.png')); }
pre[lang=fj], input[lang=fj], textarea[lang=fj], div[lang=fj] { background-image: url(static('pretixbase/img/flags/fj.png')); }

View File

@@ -34,6 +34,10 @@ footer {
margin: auto;
padding-bottom: 0;
.control-label {
display: none;
}
.buttons {
text-align: right;
}

View File

@@ -636,10 +636,6 @@ details summary {
.position-buttons {
padding-left: 20px;
}
.print-logs {
padding-left: 20px;
font-size: $font-size-small;
}
.pos-canceled * {
color: $brand-danger;

View File

@@ -1277,7 +1277,7 @@ Vue.component('pretix-widget-event-list-entry', {
if (this.event.availability.reason) {
o['pretix-widget-event-availability-' + this.event.availability.reason] = true;
}
return o;
return o
},
location: function () {
return this.event.location.replace(/\s*\n\s*/g, ', ');

View File

@@ -837,7 +837,6 @@ $table-bg-accent: rgba(128, 128, 128, 0.05);
.pretix-widget-alert-holder,
.pretix-widget-frame-holder,
.pretix-widget-lightbox-holder {
margin: auto;
border: none;
background: transparent;
overflow: visible;
@@ -961,7 +960,6 @@ $table-bg-accent: rgba(128, 128, 128, 0.05);
.pretix-widget-frame-inner {
width: 80vw;
max-width: 1080px;
height: 80vh;
}
.pretix-widget-frame-inner iframe {
@@ -1088,49 +1086,3 @@ $table-bg-accent: rgba(128, 128, 128, 0.05);
}
}
@media (max-width: 800px) {
.pretix-widget-frame-inner {
width: calc(100vw - 50px);
height: calc(100vh - 50px);
}
}
@media (max-width: 480px) {
.pretix-widget-alert-holder,
.pretix-widget-frame-holder:not(.pretix-widget-frame-isloading),
.pretix-widget-lightbox-holder:not(.pretix-widget-lightbox-isloading) {
margin: 0;
padding: 0;
width: 100%;
max-width: 100vw;
}
.pretix-widget-frame-inner,
.pretix-widget-lightbox-inner,
.pretix-widget-alert-box {
width: 100%;
height: 100vh;
border-radius: 0;
-moz-border-radius: 0;
-webkit-border-radius: 0;
box-shadow: none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-sizing: border-box;
padding: 40px 0 0;
background: var(--pretix-brand-primary);
}
.pretix-widget-frame-close,
.pretix-widget-lightbox-close {
top: 8px;
right: 12px;
button {
background-color: #fff;
path {
fill: var(--pretix-brand-primary) !important;
}
&:focus {
outline-color: #fff;
}
}
}
}

View File

@@ -1,55 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from django.db import DEFAULT_DB_ALIAS, connections
from django.test.utils import CaptureQueriesContext
class _AssertNumQueriesContext(CaptureQueriesContext):
# Inspired by /django/test/testcases.py
# but copied over to work without the unit test module
def __init__(self, num, connection):
self.num = num
super(_AssertNumQueriesContext, self).__init__(connection)
def __exit__(self, exc_type, exc_value, traceback):
super(_AssertNumQueriesContext, self).__exit__(exc_type, exc_value, traceback)
if exc_type is not None:
return
executed = len(self)
assert executed == self.num, "%d queries executed, %d expected\nCaptured queries were:\n%s" % (
executed, self.num,
'\n'.join(
query['sql'] for query in self.captured_queries
)
)
def assert_num_queries(num, func=None, *args, **kwargs):
using = kwargs.pop("using", DEFAULT_DB_ALIAS)
conn = connections[using]
context = _AssertNumQueriesContext(num, conn)
if func is None:
return context
with context:
func(*args, **kwargs)

View File

@@ -53,7 +53,7 @@ base_patterns = [
name='healthcheck'),
re_path(r'^redirect/$', redirect.redir_view, name='redirect'),
re_path(r'^site.webmanifest$', webmanifest.webmanifest, name='site.webmanifest'),
re_path(r'^jsi18n/(?P<lang>[a-zA-Z0-9_-]+)/$', js_catalog.js_catalog, name='javascript-catalog'),
re_path(r'^jsi18n/(?P<lang>[a-zA-Z-_]+)/$', js_catalog.js_catalog, name='javascript-catalog'),
re_path(r'^metrics$', metrics.serve_metrics,
name='metrics'),
re_path(r'^csp_report/$', csp.csp_report, name='csp.report'),

View File

@@ -19,3 +19,37 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from django.db import DEFAULT_DB_ALIAS, connections
from django.test.utils import CaptureQueriesContext
class _AssertNumQueriesContext(CaptureQueriesContext):
# Inspired by /django/test/testcases.py
# but copied over to work without the unit test module
def __init__(self, num, connection):
self.num = num
super(_AssertNumQueriesContext, self).__init__(connection)
def __exit__(self, exc_type, exc_value, traceback):
super(_AssertNumQueriesContext, self).__exit__(exc_type, exc_value, traceback)
if exc_type is not None:
return
executed = len(self)
assert executed == self.num, "%d queries executed, %d expected\nCaptured queries were:\n%s" % (
executed, self.num,
'\n'.join(
query['sql'] for query in self.captured_queries
)
)
def assert_num_queries(num, func=None, *args, **kwargs):
using = kwargs.pop("using", DEFAULT_DB_ALIAS)
conn = connections[using]
context = _AssertNumQueriesContext(num, conn)
if func is None:
return context
with context:
func(*args, **kwargs)

View File

@@ -43,13 +43,13 @@ from django.core.files.base import ContentFile
from django.utils.timezone import now
from django_countries.fields import Country
from django_scopes import scope, scopes_disabled
from tests import assert_num_queries
from tests.const import SAMPLE_PNG
from pretix.base.models import (
Event, InvoiceAddress, Order, OrderPosition, Organizer, SeatingPlan,
)
from pretix.base.models.orders import OrderFee
from pretix.testutils.queries import assert_num_queries
@pytest.fixture
@@ -1750,7 +1750,7 @@ def test_event_expand_seat_filter_and_querycount(token_client, organizer, event,
with scope(organizer=organizer):
v0 = event.vouchers.create(item=item, seat=event.seats.get(seat_guid='0-0'))
with assert_num_queries(14):
with assert_num_queries(13):
resp = token_client.get('/api/v1/organizers/{}/events/{}/seats/'
'?expand=orderposition&expand=cartposition&expand=voucher&is_available=false'
.format(organizer.slug, event.slug))
@@ -1769,7 +1769,7 @@ def test_event_expand_seat_filter_and_querycount(token_client, organizer, event,
v1 = event.vouchers.create(item=item, seat=event.seats.get(seat_guid='0-1'))
v2 = event.vouchers.create(item=item, seat=event.seats.get(seat_guid='0-2'))
with assert_num_queries(16):
with assert_num_queries(13):
resp = token_client.get('/api/v1/organizers/{}/events/{}/seats/'
'?expand=orderposition&expand=cartposition&expand=voucher&is_available=false'
.format(organizer.slug, event.slug))

View File

@@ -1896,8 +1896,7 @@ TEST_QUOTA_RES = {
"subevent": None,
"close_when_sold_out": False,
"release_after_exit": False,
"closed": False,
"ignore_for_event_availability": False,
"closed": False
}

View File

@@ -64,8 +64,6 @@ event_permission_sub_urls = [
('get', 'can_view_orders', 'revokedsecrets/1/', 404),
('get', 'can_view_orders', 'blockedsecrets/', 200),
('get', 'can_view_orders', 'blockedsecrets/1/', 404),
('get', 'can_view_orders', 'transactions/', 200),
('get', 'can_view_orders', 'transactions/1/', 404),
('get', 'can_view_orders', 'orders/', 200),
('get', 'can_view_orders', 'orderpositions/', 200),
('delete', 'can_change_orders', 'orderpositions/1/', 404),

View File

@@ -1,254 +0,0 @@
#
# 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 copy
import datetime
import json
from decimal import Decimal
import freezegun
import pytest
from django_scopes import scopes_disabled
from pretix.base.models import Order, OrderPosition
from pretix.base.models.orders import OrderFee
@pytest.fixture
def item(event):
return event.items.create(name="Budget Ticket", default_price=23)
@pytest.fixture
def taxrule(event):
return event.tax_rules.create(rate=Decimal("19.00"), code="S/standard")
@pytest.fixture
def order(event, item, device, taxrule):
with freezegun.freeze_time("2017-12-01T10:00:00"):
o = Order.objects.create(
code="FOO",
event=event,
email="dummy@dummy.test",
status=Order.STATUS_PENDING,
secret="k24fiuwvu8kxz3y1",
datetime=datetime.datetime(
2017, 12, 1, 10, 0, 0, tzinfo=datetime.timezone.utc
),
expires=datetime.datetime(
2017, 12, 10, 10, 0, 0, tzinfo=datetime.timezone.utc
),
sales_channel=event.organizer.sales_channels.get(identifier="web"),
total=23,
locale="en",
)
o.fees.create(
fee_type=OrderFee.FEE_TYPE_PAYMENT,
value=Decimal("0.25"),
tax_rate=Decimal("19.00"),
tax_value=Decimal("0.05"),
tax_rule=taxrule,
tax_code=taxrule.code,
)
OrderPosition.objects.create(
order=o,
item=item,
variation=None,
price=Decimal("23"),
attendee_name_parts={"full_name": "Peter", "_scheme": "full"},
secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
pseudonymization_id="ABCDEFGHKL",
positionid=1,
)
o.create_transactions()
return o
TEST_TRANSACTION_RES_OP = {
"count": 1,
"created": "2017-12-01T10:00:00Z",
"datetime": "2017-12-01T10:00:00Z",
"fee_type": None,
"internal_type": None,
"item": None,
"order": "FOO",
"positionid": 1,
"price": "23.00",
"subevent": None,
"tax_code": None,
"tax_rate": "0.00",
"tax_rule": None,
"tax_value": "0.00",
"variation": None,
}
TEST_TRANSACTION_RES_FEE = {
"count": 1,
"created": "2017-12-01T10:00:00Z",
"datetime": "2017-12-01T10:00:00Z",
"fee_type": "payment",
"internal_type": "",
"item": None,
"order": "FOO",
"positionid": None,
"price": "0.25",
"subevent": None,
"tax_code": "S/standard",
"tax_rate": "19.00",
"tax_rule": 1,
"tax_value": "0.05",
"variation": None,
}
@pytest.mark.django_db
def test_transaction_list(token_client, organizer, event, order, item, taxrule):
res_op = copy.deepcopy(TEST_TRANSACTION_RES_OP)
res_fee = copy.deepcopy(TEST_TRANSACTION_RES_FEE)
with scopes_disabled():
res_fee["id"] = order.transactions.get(fee_type="payment").pk
res_fee["tax_rule"] = taxrule.pk
res_op["id"] = order.transactions.get(item__isnull=False).pk
res_op["item"] = item.pk
resp = token_client.get(
"/api/v1/organizers/{}/events/{}/transactions/".format(
organizer.slug,
event.slug,
)
)
assert resp.status_code == 200
assert res_op in resp.data["results"]
assert res_fee in resp.data["results"]
assert resp.data["count"] == 2
resp = token_client.get(
"/api/v1/organizers/{}/events/{}/transactions/?order=FOO".format(
organizer.slug,
event.slug,
)
)
assert resp.data["count"] == 2
resp = token_client.get(
"/api/v1/organizers/{}/events/{}/transactions/?order=BAR".format(
organizer.slug,
event.slug,
)
)
assert resp.data["count"] == 0
resp = token_client.get(
"/api/v1/organizers/{}/events/{}/transactions/?datetime_since=2017-12-01T09:00:00Z".format(
organizer.slug,
event.slug,
)
)
assert resp.data["count"] == 2
resp = token_client.get(
"/api/v1/organizers/{}/events/{}/transactions/?datetime_since=2017-12-02T09:00:00Z".format(
organizer.slug,
event.slug,
)
)
assert resp.data["count"] == 0
resp = token_client.get(
"/api/v1/organizers/{}/events/{}/transactions/?item={}".format(
organizer.slug, event.slug, item.pk
)
)
assert resp.data["count"] == 1
assert res_op in resp.data["results"]
resp = token_client.get(
"/api/v1/organizers/{}/events/{}/transactions/?fee_type={}".format(
organizer.slug, event.slug, "payment"
)
)
assert resp.data["count"] == 1
assert res_fee in resp.data["results"]
@pytest.mark.django_db
def test_order_detail(token_client, organizer, event, order, item, taxrule):
res_fee = copy.deepcopy(TEST_TRANSACTION_RES_FEE)
with scopes_disabled():
tx = order.transactions.get(fee_type="payment")
res_fee["id"] = tx.pk
res_fee["tax_rule"] = taxrule.pk
resp = token_client.get(
"/api/v1/organizers/{}/events/{}/transactions/{}/".format(
organizer.slug, event.slug, tx.pk
)
)
assert resp.status_code == 200
assert json.loads(json.dumps(res_fee)) == json.loads(json.dumps(resp.data))
@pytest.mark.django_db
def test_organizer_list(token_client, team, organizer, event, order, item, taxrule):
resp = token_client.get(
"/api/v1/organizers/{}/transactions/".format(
organizer.slug,
)
)
assert resp.status_code == 200
assert resp.data["count"] == 2
assert "event" in resp.data["results"][0]
resp = token_client.get(
"/api/v1/organizers/{}/transactions/?event=dummy".format(
organizer.slug,
)
)
assert resp.status_code == 200
assert resp.data["count"] == 2
resp = token_client.get(
"/api/v1/organizers/{}/transactions/?event=test".format(
organizer.slug,
)
)
assert resp.status_code == 200
assert resp.data["count"] == 0
team.all_events = False
team.save()
resp = token_client.get(
"/api/v1/organizers/{}/transactions/".format(
organizer.slug,
)
)
assert resp.status_code == 200
assert resp.data["count"] == 0
team.all_events = True
team.can_view_orders = False
team.save()
resp = token_client.get(
"/api/v1/organizers/{}/transactions/".format(
organizer.slug,
)
)
assert resp.status_code == 200
assert resp.data["count"] == 0

View File

@@ -81,8 +81,6 @@ TEST_VOUCHER_RES = {
'all_bundles_included': False,
'subevent': None,
'seat': None,
'budget': None,
'budget_used': "0.00",
}

View File

@@ -28,10 +28,10 @@ import pytest
from django.utils.timezone import now
from django_scopes import scopes_disabled
from freezegun import freeze_time
from tests import assert_num_queries
from pretix.base.models import CartPosition, Discount, Event, Organizer
from pretix.base.services.cross_selling import CrossSellingService
from pretix.testutils.queries import assert_num_queries
@pytest.fixture

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