Compare commits

..

1 Commits

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

View File

@@ -1,7 +1,7 @@
This file is part of pretix (Community Edition).
Copyright (C) 2014-2020 Raphael Michel and contributors
Copyright (C) 2020-today pretix GmbH and contributors
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.

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

@@ -359,65 +359,3 @@ Performing a ticket search
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer or check-in list does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested check-in list does not exist.
.. _`rest-checkin-annul`:
Annulment of a check-in
-----------------------
.. http:post:: /api/v1/organizers/(organizer)/checkinrpc/annul/
If a check-in was made in error and the person was not let in, it can be annulled. We do not recommend this to be used
in case of manual check-ins or user interfaces because it is too prone for human errors. It is mostly intended for
automated entry systems like a turnstile or automated door, where the check-in is first created, then the door is
opened, and then the check-in may be annulled if the system knows that the turnstile did not turn or was out of
order.
This endpoint supports passing multiple check-in lists for the context of a multi-event scan. However, each
check-in list passed needs to be from a distinct event.
Check-ins created by a device can only be annulled by the same device. The datetime of annulment may not be more than
15 minutes after the datetime of check-in (value subject to change).
A status code of 404 is returned if no check-in was found for the given nonce. A status code of 400 is returned when
multiple check-ins match the nonce, the input is invalid in another way, the annulment is made from the wrong device,
the check-in is already in an annulled or failed state, or the datetime constraint is not valid.
:<json string nonce: ``nonce`` value of the original check-in.
:<json array lists: List of check-in list IDs to search on. No two check-in lists may be from the same event.
:<json datetime datetime: Specifies the client-side datetime of the annulment. If not supplied, the current time will be used.
:<json string error_explanation: A human-readable description of why the check-in was annulled (optional).
:>json string status: ``"ok"``
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/checkinrpc/annul/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
{
"lists": [1],
"nonce": "Pvrk50vUzQd0DhdpNRL4I4OcXsvg70uA",
"error_explanation": "Turnstile did not turn"
}
**Example successful response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"status": "ok",
}
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error
:statuscode 400: Invalid or incomplete request, see above
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested nonce does not exist.

View File

@@ -424,9 +424,9 @@ Endpoints
:param organizer: The ``slug`` field of the organizer of the event to create.
:param event: The ``slug`` field of the event to copy settings and items from.
:statuscode 201: no error
:statuscode 400: The event could not be updated due to invalid submitted data.
:statuscode 400: The event could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to update this resource.
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/

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
========
@@ -26,8 +24,6 @@ invoice_from_country string Sender address:
invoice_from_tax_id string Sender address: Local Tax ID
invoice_from_vat_id string Sender address: EU VAT ID
invoice_to string Full recipient address
invoice_to_is_business boolean Recipient address: Business vs individual (``null`` for
invoices created before pretix 2025.6).
invoice_to_company string Recipient address: Company name
invoice_to_name string Recipient address: Person name
invoice_to_street string Recipient address: Address lines
@@ -37,7 +33,6 @@ invoice_to_state string Recipient addre
invoice_to_country string Recipient address: Country code
invoice_to_vat_id string Recipient address: EU VAT ID
invoice_to_beneficiary string Invoice beneficiary
invoice_to_transmission_info object Additional transmission info (see :ref:`rest-transmission-types`)
custom_field string Custom invoice address field
date date Invoice date
refers string Invoice number of an invoice this invoice refers to
@@ -80,12 +75,17 @@ lines list of objects The actual invo
for all invoice lines
created before this field was introduced as well as for
all lines not created by a fee (e.g. a product).
period_start datetime Start date of the service or delivery period of the invoice line.
Can be ``null`` if not known.
├ period_end datetime End date of the service or delivery period of the invoice line.
Can be ``null`` if not known.
├ event_date_from datetime Deprecated alias of ``period_start``.
├ event_date_to datetime Deprecated alias of ``period_end``.
event_date_from datetime Start date of the (sub)event this line was created for as it
was set during invoice creation. Can be ``null`` for all invoice
lines created before this was introduced as well as for lines in
an event series not created by a product (e.g. shipping or
cancellation fees).
├ event_date_to datetime End date of the (sub)event this line was created for as it
was set during invoice creation. Can be ``null`` for all invoice
lines created before this was introduced as well as for lines in
an event series not created by a product (e.g. shipping or
cancellation fees) as well as whenever the respective (sub)event
has no end date set.
├ event_location string Location of the (sub)event this line was created for as it
was set during invoice creation. Can be ``null`` for all invoice
lines created before this was introduced as well as for lines in
@@ -108,12 +108,6 @@ foreign_currency_rate decimal (string) If ``foreign_cu
foreign_currency_rate_date date If ``foreign_currency_rate`` is set, this signifies the
date at which the currency rate was obtained.
internal_reference string Customer's reference to be printed on the invoice.
transmission_type string Requested transmission channel (see :ref:`rest-transmission-types`)
transmission_provider string Selected transmission provider (depends on installed
plugins). ``null`` if not yet chosen.
transmission_status string Transmission status, one of ``unknown`` (pre-2025.6),
``pending``, ``inflight``, ``failed``, and ``completed``.
transmission_date datetime Time of last change in transmission status (may be ``null``).
===================================== ========================== =======================================================
@@ -125,76 +119,6 @@ transmission_date datetime Time of last ch
The ``tax_code`` attribute has been added.
.. versionchanged:: 2025.6
The attributes ``invoice_to_is_business``, ``invoice_to_transmission_info``, ``transmission_type``,
``transmission_provider``, ``transmission_status``, and ``transmission_date`` have been added.
.. _`rest-transmission-types`:
Transmission types
------------------
pretix supports multiple ways to transmit an invoice from the organizer to the invoice recipient.
For each transmission type, different fields are supported in the ``transmission_info`` object of the
invoice address. Currently, pretix supports the following transmission types:
Email
"""""
The identifier ``"email"`` represents the transmission of PDF invoices through email.
This is the default transmission type in pretix and has some special behavior for backwards compatibility.
Transmission is always executed through the provider ``"email_pdf"``.
The ``transmission_info`` object may contain the following properties:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
transmission_email_address string Optional. An email address other than the order address
that the invoice should be sent to.
Business customers only.
===================================== ========================== =======================================================
Peppol
""""""
The identifier ``"peppol"`` represents the transmission of XML invoices through the `Peppol`_ network.
This is only available for business addresses.
This is not supported by pretix out of the box and requires the use of a suitable plugin.
The ``transmission_info`` object may contain the following properties:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
transmission_peppol_participant_id string Required. The Peppol participant ID of the recipient.
===================================== ========================== =======================================================
Italian Exchange System
"""""""""""""""""""""""
The identifier ``"it_sdi"`` represents the transmission of XML invoices through the `Sistema di Interscambio`_ network used in Italy.
This is only available for addresses with country ``"IT"``.
This is not supported by pretix out of the box and requires the use of a suitable plugin.
The ``transmission_info`` object may contain the following properties:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
transmission_it_sdi_codice_fiscale string Required for non-business address. Fiscal code of the
recipient.
transmission_it_sdi_pec string Required for business addresses. Address for certified
electronic mail.
transmission_it_sdi_recipient_code string Required for businesses. SdI recipient code.
===================================== ========================== =======================================================
If this type is selected, ``vat_id`` is required for business addresses.
List of all invoices
--------------------
@@ -238,7 +162,6 @@ List of all invoices
"invoice_from_vat_id":"",
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT-ID: EU123456789",
"invoice_to_company": "Sample company",
"invoice_to_is_business": true,
"invoice_to_name": "John Doe",
"invoice_to_street": "Test street 12",
"invoice_to_zipcode": "12345",
@@ -247,7 +170,6 @@ List of all invoices
"invoice_to_country": "TE",
"invoice_to_vat_id": "EU123456789",
"invoice_to_beneficiary": "",
"invoice_to_transmission_info": {},
"custom_field": null,
"date": "2017-12-01",
"refers": null,
@@ -269,8 +191,6 @@ List of all invoices
"fee_internal_type": null,
"event_date_from": "2017-12-27T10:00:00Z",
"event_date_to": null,
"period_start": "2017-12-27T10:00:00Z",
"period_end": "2017-12-27T10:00:00Z",
"event_location": "Heidelberg",
"attendee_name": null,
"gross_value": "23.00",
@@ -282,11 +202,7 @@ List of all invoices
],
"foreign_currency_display": "PLN",
"foreign_currency_rate": "4.2408",
"foreign_currency_rate_date": "2017-07-24",
"transmission_type": "email",
"transmission_provider": "email_pdf",
"transmission_status": "completed",
"transmission_date": "2017-07-24T10:00:00Z"
"foreign_currency_rate_date": "2017-07-24"
}
]
}
@@ -386,7 +302,6 @@ Fetching individual invoices
"invoice_from_vat_id":"",
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT-ID: EU123456789",
"invoice_to_company": "Sample company",
"invoice_to_is_business": true,
"invoice_to_name": "John Doe",
"invoice_to_street": "Test street 12",
"invoice_to_zipcode": "12345",
@@ -395,7 +310,6 @@ Fetching individual invoices
"invoice_to_country": "TE",
"invoice_to_vat_id": "EU123456789",
"invoice_to_beneficiary": "",
"invoice_to_transmission_info": {},
"custom_field": null,
"date": "2017-12-01",
"refers": null,
@@ -417,8 +331,6 @@ Fetching individual invoices
"fee_internal_type": null,
"event_date_from": "2017-12-27T10:00:00Z",
"event_date_to": null,
"period_start": "2017-12-27T10:00:00Z",
"period_end": "2017-12-27T10:00:00Z",
"event_location": "Heidelberg",
"attendee_name": null,
"gross_value": "23.00",
@@ -430,11 +342,7 @@ Fetching individual invoices
],
"foreign_currency_display": "PLN",
"foreign_currency_rate": "4.2408",
"foreign_currency_rate_date": "2017-07-24",
"transmission_type": "email",
"transmission_provider": "email_pdf",
"transmission_status": "completed",
"transmission_date": "2017-07-24T10:00:00Z"
"foreign_currency_rate_date": "2017-07-24"
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -539,70 +447,3 @@ Invoices cannot be edited directly, but the following actions can be triggered:
:statuscode 400: The invoice has already been canceled
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
Transmitting invoices
---------------------
Invoices are transmitted automatically when created during order creation or payment receipt,
but in other cases transmission may need to be triggered manually.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(number)/transmit/
Transmits the invoice to the recipient, but only if it is in ``pending`` state.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/invoices/00001/transmit/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
Content-Type: application/pdf
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param number: The ``number`` field of the invoice to transmit
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to transmit this invoice **or** the invoice may not be transmitted
:statuscode 409: The invoice is currently in transmission
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(number)/retransmit/
Transmits the invoice to the recipient even if transmission was already attempted previously.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/invoices/00001/retransmit/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
Content-Type: application/pdf
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param number: The ``number`` field of the invoice to transmit
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to transmit this invoice **or** the invoice may not be transmitted
:statuscode 409: The invoice is currently in transmission
.. _Peppol: https://en.wikipedia.org/wiki/PEPPOL
.. _Sistema di Interscambio: https://it.wikipedia.org/wiki/Fattura_elettronica_in_Italia

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

@@ -65,16 +65,11 @@ invoice_address object Invoice address
├ state string Customer state (ISO 3166-2 code). Only supported in
AU, BR, CA, CN, MY, MX, and US.
├ internal_reference string Customer's internal reference to be printed on the invoice
├ custom_field string Custom invoice address field
├ vat_id string Customer VAT ID
vat_id_validated string ``true``, if the VAT ID has been validated against the
vat_id_validated string ``true``, if the VAT ID has been validated against the
EU VAT service and validation was successful. This only
happens in rare cases.
├ transmission_type string Transmission channel for invoice (see also :ref:`rest-transmission-types`).
Defaults to ``email``.
└ transmission_info object Transmission-channel specific information (or ``null``).
See also :ref:`rest-transmission-types`.
positions list of objects List of order positions (see below). By default, only
non-canceled positions are included.
fees list of objects List of fees included in the order total. By default, only
@@ -147,10 +142,6 @@ plugin_data object Additional data
The ``plugin_data`` attribute has been added.
.. versionchanged:: 2025.6
The ``invoice_address.transmission_type`` and ``invoice_address.transmission_info`` attributes have been added.
.. _order-position-resource:
Order position resource
@@ -377,9 +368,7 @@ List of all orders
"state": "",
"internal_reference": "",
"vat_id": "EU123456789",
"vat_id_validated": false,
"transmission_type": "email",
"transmission_info": {}
"vat_id_validated": false
},
"positions": [
{
@@ -418,7 +407,6 @@ List of all orders
"seat": null,
"checkins": [
{
"id": 1337,
"list": 44,
"type": "entry",
"gate": null,
@@ -622,9 +610,7 @@ Fetching individual orders
"state": "",
"internal_reference": "",
"vat_id": "EU123456789",
"vat_id_validated": false,
"transmission_type": "email",
"transmission_info": {}
"vat_id_validated": false
},
"positions": [
{
@@ -663,7 +649,6 @@ Fetching individual orders
"seat": null,
"checkins": [
{
"id": 1337,
"list": 44,
"type": "entry",
"gate": null,
@@ -1030,10 +1015,8 @@ Creating orders
* ``internal_reference``
* ``vat_id``
* ``vat_id_validated`` (optional) If you need support for reverse charge (rarely the case), you need to check
yourself if the passed VAT ID is a valid EU VAT ID. In that case, set this to ``true``. Only valid VAT IDs will
trigger reverse charge taxation. Don't forget to set ``is_business`` as well!
* ``transmission_type`` (optional, defaults to ``email``)
* ``transmission_info`` (optional, see also :ref:`rest-transmission-types`)
yourself if the passed VAT ID is a valid EU VAT ID. In that case, set this to ``true``. Only valid VAT IDs will
trigger reverse charge taxation. Don't forget to set ``is_business`` as well!
* ``positions``
@@ -1058,7 +1041,6 @@ Creating orders
* ``valid_until`` (optional, if both ``valid_from`` and ``valid_until`` are **missing** (not ``null``) the availability will be computed from the given product)
* ``requested_valid_from`` (optional, can be set **instead** of ``valid_from`` and ``valid_until`` to signal a user choice for the start time that may or may not be respected)
* ``use_reusable_medium`` (optional, causes the new ticket to take over the given reusable medium, identified by its ID)
* ``discount`` (optional, only possible if ``price`` is set; attention: if this is set to not-``null`` on any position, automatic calculation of discounts will not run)
* ``answers``
* ``question``
@@ -1635,7 +1617,6 @@ List of all order positions
"blocked": null,
"checkins": [
{
"id": 1337,
"list": 44,
"type": "entry",
"gate": null,
@@ -1764,7 +1745,6 @@ Fetching individual positions
"seat": null,
"checkins": [
{
"id": 1337,
"list": 44,
"type": "entry",
"gate": null,
@@ -1946,7 +1926,6 @@ Manipulating individual positions
(Full order position resource, see above.)
:query boolean check_quotas: Whether to check quotas before committing item changes, default is ``true``
:param organizer: The ``slug`` field of the organizer of the event
:param event: The ``slug`` field of the event
:param id: The ``id`` field of the order position to update
@@ -2026,7 +2005,6 @@ Manipulating individual positions
(Full order position resource, see above.)
:query boolean check_quotas: Whether to check quotas before creating the new position, default is ``true``
:param organizer: The ``slug`` field of the organizer of the event
:param event: The ``slug`` field of the event
@@ -2313,7 +2291,6 @@ otherwise, such as splitting an order or changing fees.
(Full order position resource, see above.)
:query boolean check_quotas: Whether to check quotas before patching or creating positions, default is ``true``
:param organizer: The ``slug`` field of the organizer of the event
:param event: The ``slug`` field of the event
:param code: The ``code`` field of the order to update
@@ -2509,7 +2486,6 @@ Order payment endpoints
{
"amount": "23.00",
"comment": "Overpayment",
"mark_canceled": false
}

View File

@@ -19,11 +19,6 @@ name string The organizer's
slug string A short form of the name, used e.g. in URLs.
public_url string The public, customer-facing URL of the organizer, where
the list of all events can be found (read-only).
plugins list A list of package names of the enabled plugins for this
organizer. Note that most plugins are enabled on the
event level (or both levels). If you remove a plugin
that is also enabled on some events, it will
automatically be removed from all events as well.
===================================== ========================== =======================================================
@@ -58,10 +53,7 @@ Endpoints
{
"name": "Big Events LLC",
"slug": "Big Events",
"public_url": "https://pretix.eu/bigevents/",
"plugins": [
"pretix_datev"
]
"public_url": "https://pretix.eu/bigevents/"
}
]
}
@@ -95,10 +87,7 @@ Endpoints
{
"name": "Big Events LLC",
"slug": "Big Events",
"public_url": "https://pretix.eu/bigevents/",
"plugins": [
"pretix_datev"
]
"public_url": "https://pretix.eu/bigevents/"
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -106,50 +95,6 @@ Endpoints
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
.. http:patch:: /api/v1/organizers/(organizer)/
Updates an organizer. Currently only the ``plugins`` field may be updated.
Permission required: "Can change organizer settings"
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"plugins": [
"pretix_seating"
]
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"name": "Big Events LLC",
"slug": "Big Events",
"public_url": "https://pretix.eu/bigevents/",
"plugins": [
"pretix_seating"
]
}
:param organizer: The ``slug`` field of the organizer to update
:statuscode 200: no error
:statuscode 400: The organizer could not be updated due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to update this resource.
Organizer settings
------------------

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.
@@ -38,10 +36,6 @@ available_number integer Number of avail
slightly out of date. ``null`` means unlimited.
===================================== ========================== =======================================================
.. versionchanged:: 2025.7
The attribute ``ignore_for_event_availability`` has been added.
Endpoints
---------
@@ -78,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
}
]
}
@@ -125,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
@@ -157,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**:
@@ -177,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
@@ -233,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

@@ -14,7 +14,6 @@ The voucher resource contains the following public fields:
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the voucher
created datetime The creation date of the voucher. For vouchers created before pretix 2025.7.0, this is guessed retroactively and might not be accurate.
code string The voucher code that is required to redeem the voucher
max_usages integer The maximum number of times this voucher can be
redeemed (default: 1).
@@ -50,14 +49,8 @@ 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.
===================================== ========================== =======================================================
.. versionchanged:: 2025.7
The attributes ``created``, ``budget``, and ``budget_used`` have been added.
Endpoints
---------
@@ -89,7 +82,6 @@ Endpoints
"results": [
{
"id": 1,
"created": "2020-09-18T14:17:40.971519Z",
"code": "43K6LKM37FBVR2YG",
"max_usages": 1,
"redeemed": 0,
@@ -107,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
}
]
}
@@ -162,7 +152,6 @@ Endpoints
{
"id": 1,
"created": "2020-09-18T14:17:40.971519Z",
"code": "43K6LKM37FBVR2YG",
"max_usages": 1,
"redeemed": 0,
@@ -180,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
@@ -235,7 +222,6 @@ Endpoints
{
"id": 1,
"created": "2020-09-18T14:17:40.971519Z",
"code": "43K6LKM37FBVR2YG",
"max_usages": 1,
"redeemed": 0,
@@ -253,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
@@ -329,7 +313,6 @@ Endpoints
[
{
"id": 1,
"created": "2020-09-18T14:17:40.971519Z",
"code": "43K6LKM37FBVR2YG",
}, …
@@ -376,7 +359,6 @@ Endpoints
{
"id": 1,
"created": "2020-09-18T14:17:40.971519Z",
"code": "43K6LKM37FBVR2YG",
"max_usages": 1,
"redeemed": 0,
@@ -394,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

@@ -60,9 +60,6 @@ The following values for ``action_types`` are valid with pretix core:
* ``pretix.event.added``
* ``pretix.event.changed``
* ``pretix.event.deleted``
* ``pretix.voucher.added``
* ``pretix.voucher.changed``
* ``pretix.voucher.deleted``
* ``pretix.subevent.added``
* ``pretix.subevent.changed``
* ``pretix.subevent.deleted``

View File

@@ -1,207 +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. Sync providers are registered
in the :py:attr:`pretix.base.datasync.datasync.datasync_providers` :ref:`registry <registries>`.
An :class:`OutboundSyncProvider` for subscribing event participants to a mailing list could start
like this, for example:
.. code-block:: python
from pretix.base.datasync.datasync import (OutboundSyncProvider, datasync_providers)
@datasync_providers.register
class MyListSyncProvider(OutboundSyncProvider):
identifier = "my_list"
display_name = "My Mailing List Service"
# ...
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")
Property mappings
-----------------
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
# class MyListSyncProvider, contd.
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

@@ -23,21 +23,21 @@ There are multiple signals that will be sent out in the ordering cycle:
.. automodule:: pretix.base.signals
:no-index:
:members: validate_cart, validate_cart_addons, validate_order, order_valid_if_pending, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_expiry_changed, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, build_invoice_data, invoice_line_text
:members: validate_cart, validate_cart_addons, validate_order, order_valid_if_pending, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_expiry_changed, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
Check-ins
"""""""""
.. automodule:: pretix.base.signals
:no-index:
:members: checkin_created, checkin_annulled
:members: checkin_created
Frontend
--------
.. automodule:: pretix.presale.signals
:members: html_head, html_footer, footer_link, global_footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header, seatingframe_html_head, filter_subevents
:members: html_head, html_footer, footer_link, global_footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header, seatingframe_html_head
.. automodule:: pretix.presale.signals

View File

@@ -13,12 +13,10 @@ Contents:
email
placeholder
invoice
invoicetransmission
shredder
import
customview
cookieconsent
auth
datasync
general
quality

View File

@@ -1,65 +0,0 @@
.. highlight:: python
:linenothreshold: 5
Writing an invoice transmission plugin
======================================
An invoice transmission provider transports an invoice from the sender to the recipient.
There are pre-defined types of invoice transmission in pretix, currently ``"email"``, ``"peppol"``, and ``"it_sdi"``.
You can find more information about them at :ref:`rest-transmission-types`.
New transmission types can not be added by plugins but need to be added to pretix itself.
However, plugins can provide implementations for the actual transmission.
Please read :ref:`Creating a plugin <pluginsetup>` first, if you haven't already.
Output registration
-------------------
New invoice transmission providers can be registered through the :ref:`registry <registries>` mechanism
.. code-block:: python
from pretix.base.invoicing.transmission import transmission_providers, TransmissionProvider
@transmission_providers.new()
class SdiTransmissionProvider(TransmissionProvider):
identifier = "fatturapa_providerabc"
type = "it_sdi"
verbose_name = _("FatturaPA through provider ABC")
...
The provider class
------------------
.. class:: pretix.base.invoicing.transmission.TransmissionProvider
.. autoattribute:: identifier
This is an abstract attribute, you **must** override this!
.. autoattribute:: type
This is an abstract attribute, you **must** override this!
.. autoattribute:: verbose_name
This is an abstract attribute, you **must** override this!
.. autoattribute:: priority
.. autoattribute:: testmode_supported
.. automethod:: is_ready
This is an abstract method, you **must** override this!
.. automethod:: is_available
This is an abstract method, you **must** override this!
.. automethod:: transmit
This is an abstract method, you **must** override this!
.. automethod:: settings_url

View File

@@ -56,20 +56,6 @@ restricted boolean (optional) ``False`` by default, restricts a plugin
for an event by system administrators / superusers.
experimental boolean (optional) ``False`` by default, marks a plugin as an experimental feature in the plugins list.
compatibility string Specifier for compatible pretix versions.
level string System level the plugin can be activated at.
Set to ``pretix.base.plugins.PLUGIN_LEVEL_EVENT`` for plugins that can be activated
at event level and then be active for that event only.
Set to ``pretix.base.plugins.PLUGIN_LEVEL_ORGANIZER`` for plugins that can be
activated only for the organizer as a whole and are active for any event within
that organizer.
Set to ``pretix.base.plugins.PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID`` for plugins that
can be activated at organizer level but are considered active only within events
for which they have also been specifically activated.
More levels, e.g. user-level plugins, might be invented in the future.
settings_links list List of ``((menu name, submenu name, …), urlname, url_kwargs)`` tuples that point
to the plugin's settings.
navigation_links list List of ``((menu name, submenu name, …), urlname, url_kwargs)`` tuples that point
to the plugin's system pages.
================== ==================== ===========================================================
A working example would be:
@@ -77,9 +63,9 @@ A working example would be:
.. code-block:: python
try:
from pretix.base.plugins import PluginConfig, PLUGIN_LEVEL_EVENT
from pretix.base.plugins import PluginConfig
except ImportError:
raise RuntimeError("Please use pretix 2025.7 or above to run this plugin!")
raise RuntimeError("Please use pretix 2.7 or above to run this plugin!")
from django.utils.translation import gettext_lazy as _
@@ -93,7 +79,6 @@ A working example would be:
version = '1.0.0'
category = 'PAYMENT'
picture = 'pretix_paypal/paypal_logo.svg'
level = PLUGIN_LEVEL_EVENT
visible = True
featured = False
restricted = False
@@ -157,14 +142,14 @@ method to make your receivers available:
from . import signals # NOQA
You can optionally specify code that is executed when your plugin is activated for an event
or organizer in the ``installed`` method:
in the ``installed`` method:
.. code-block:: python
class PaypalApp(AppConfig):
def installed(self, event_or_organizer):
def installed(self, event):
pass # Your code here

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

@@ -6,4 +6,4 @@ sphinxcontrib-images
sphinxcontrib-jquery
sphinxcontrib-spelling==8.*
sphinxemoji
pyenchant==3.3.*
pyenchant==3.2.*

View File

@@ -7,4 +7,4 @@ sphinxcontrib-images
sphinxcontrib-jquery
sphinxcontrib-spelling==8.*
sphinxemoji
pyenchant==3.3.*
pyenchant==3.2.*

View File

@@ -28,23 +28,23 @@ classifiers = [
dependencies = [
"arabic-reshaper==3.0.0", # Support for Arabic in reportlab
"babel",
"BeautifulSoup4==4.14.*",
"BeautifulSoup4==4.13.*",
"bleach==6.2.*",
"celery==5.5.*",
"chardet==5.2.*",
"cryptography>=44.0.0",
"css-inline==0.17.*",
"css-inline==0.15.*",
"defusedcsv>=1.1.0",
"Django[argon2]==4.2.*,>=4.2.24",
"django-bootstrap3==25.2",
"Django[argon2]==4.2.*,>=4.2.15",
"django-bootstrap3==25.1",
"django-compressor==4.5.1",
"django-countries==7.6.*",
"django-filter==25.1",
"django-formset-js-improved==0.5.0.4",
"django-formset-js-improved==0.5.0.3",
"django-formtools==2.5.1",
"django-hierarkey==2.0.*,>=2.0.1",
"django-hierarkey==1.2.*",
"django-hijack==3.7.*",
"django-i18nfield==1.11.*",
"django-i18nfield==1.10.*",
"django-libsass==0.9",
"django-localflavor==5.0",
"django-markup",
@@ -64,7 +64,7 @@ dependencies = [
"kombu==5.5.*",
"libsass==0.23.*",
"lxml",
"markdown==3.9", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
"markdown==3.8.2", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
"mt-940==4.30.*",
"oauthlib==3.3.*",
@@ -74,25 +74,25 @@ dependencies = [
"paypal-checkout-serversdk==1.0.*",
"PyJWT==2.10.*",
"phonenumberslite==9.0.*",
"Pillow==11.3.*",
"Pillow==11.2.*",
"pretix-plugin-build",
"protobuf==6.33.*",
"protobuf==6.31.*",
"psycopg2-binary",
"pycountry",
"pycparser==2.23",
"pycparser==2.22",
"pycryptodome==3.23.*",
"pypdf==6.1.*",
"pypdf==5.6.*",
"python-bidi==0.6.*", # Support for Arabic in reportlab
"python-dateutil==2.9.*",
"pytz",
"pytz-deprecation-shim==0.1.*",
"pyuca",
"qrcode==8.2",
"redis==6.4.*",
"redis==6.2.*",
"reportlab==4.4.*",
"requests==2.32.*",
"sentry-sdk==2.42.*",
"sepaxml==2.7.*",
"requests==2.31.*",
"sentry-sdk==2.31.*",
"sepaxml==2.6.*",
"stripe==7.9.*",
"text-unidecode==1.*",
"tlds>=2020041600",
@@ -100,29 +100,29 @@ dependencies = [
"ua-parser==1.0.*",
"vat_moss_forked==2020.3.20.0.11.0",
"vobject==0.9.*",
"webauthn==2.7.*",
"webauthn==2.6.*",
"zeep==4.3.*"
]
[project.optional-dependencies]
memcached = ["pylibmc"]
dev = [
"aiohttp==3.13.*",
"aiohttp==3.12.*",
"coverage",
"coveralls",
"fakeredis==2.32.*",
"fakeredis==2.30.*",
"flake8==7.3.*",
"freezegun",
"isort==6.1.*",
"isort==6.0.*",
"pep8-naming==0.15.*",
"potypo",
"pytest-asyncio>=0.24",
"pytest-cache",
"pytest-cov",
"pytest-django==4.*",
"pytest-mock==3.15.*",
"pytest-mock==3.14.*",
"pytest-sugar",
"pytest-xdist==3.8.*",
"pytest-xdist==3.7.*",
"pytest==8.4.*",
"responses",
]

View File

@@ -25,8 +25,8 @@ coverage:
coverage run -m py.test
npminstall:
# keep this in sync with pretix/_build.py!
# keep this in sync with setup.py!
mkdir -p pretix/static.dist/node_prefix/
cp -r pretix/static/npm_dir/* pretix/static.dist/node_prefix/
npm ci --prefix=pretix/static.dist/node_prefix
npm install --prefix=pretix/static.dist/node_prefix

View File

@@ -2,8 +2,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.
@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "2025.9.0"
__version__ = "2025.7.0.dev0"

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.
@@ -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

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.
@@ -39,7 +39,7 @@ def npm_install():
node_prefix = os.path.join(here, 'static.dist', 'node_prefix')
os.makedirs(node_prefix, exist_ok=True)
shutil.copytree(os.path.join(here, 'static', 'npm_dir'), node_prefix, dirs_exist_ok=True)
subprocess.check_call('npm ci', shell=True, cwd=node_prefix)
subprocess.check_call('npm install', shell=True, cwd=node_prefix)
npm_installed = True

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.
@@ -104,14 +104,3 @@ class MiniCheckinListSerializer(I18nAwareModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class CheckinRPCAnnulInputSerializer(serializers.Serializer):
lists = serializers.PrimaryKeyRelatedField(required=True, many=True, queryset=CheckinList.objects.none())
nonce = serializers.CharField(required=True, allow_null=False)
datetime = serializers.DateTimeField(required=False, allow_null=True)
error_explanation = serializers.CharField(required=False, allow_null=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['lists'].child_relation.queryset = CheckinList.objects.filter(event__in=self.context['events']).select_related('event')

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.
@@ -50,7 +50,6 @@ from rest_framework.relations import SlugRelatedField
from pretix.api.serializers import (
CompatibleJSONField, SalesChannelMigrationMixin,
)
from pretix.api.serializers.fields import PluginsField
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.settings import SettingsSerializer
from pretix.base.models import (
@@ -62,9 +61,6 @@ from pretix.base.models.items import (
ItemMetaProperty, SubEventItem, SubEventItemVariation,
)
from pretix.base.models.tax import CustomRulesValidator
from pretix.base.plugins import (
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
)
from pretix.base.services.seating import (
SeatProtected, generate_seats, validate_plan_change,
)
@@ -130,6 +126,22 @@ class SeatCategoryMappingField(Field):
}
class PluginsField(Field):
def to_representation(self, obj):
from pretix.base.plugins import get_all_plugins
return sorted([
p.module for p in get_all_plugins()
if not p.name.startswith('.') and getattr(p, 'visible', True) and p.module in obj.get_plugins()
])
def to_internal_value(self, data):
return {
'plugins': data
}
class TimeZoneField(ChoiceField):
def get_attribute(self, instance):
return instance.cache.get_or_set(
@@ -271,28 +283,17 @@ class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
from pretix.base.plugins import get_all_plugins
plugins_available = {
p.module: p for p in get_all_plugins(event=self.instance)
p.module: p for p in get_all_plugins(self.instance)
if not p.name.startswith('.') and getattr(p, 'visible', True)
}
current_plugins = self.instance.get_plugins() if self.instance and self.instance.pk else []
settings_holder = self.instance if self.instance and self.instance.pk else self.context['organizer']
allowed_levels = (PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID)
for plugin in value.get('plugins'):
if plugin not in plugins_available:
raise ValidationError(_('Unknown plugin: \'{name}\'.').format(name=plugin))
if getattr(plugins_available[plugin], 'restricted', False):
if plugin not in settings_holder.settings.allowed_restricted_plugins:
raise ValidationError(_('Restricted plugin: \'{name}\'.').format(name=plugin))
level = getattr(plugins_available[plugin], 'level', PLUGIN_LEVEL_EVENT)
if level not in allowed_levels:
raise ValidationError('Plugin cannot be enabled on this level: \'{name}\'.'.format(name=plugin))
if level == PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID and plugin not in self.context['organizer'].get_plugins():
if plugin not in current_plugins:
# Technically, this is allowed, but consumers might be confused if the API call doesn't do anything
# so we prevent this change.
raise ValidationError('Plugin should be enabled on organizer level first: \'{name}\'.'.format(name=plugin))
return value
@@ -805,7 +806,6 @@ class EventSettingsSerializer(SettingsSerializer):
'invoice_reissue_after_modify',
'invoice_include_free',
'invoice_generate',
'invoice_period',
'invoice_numbers_consecutive',
'invoice_numbers_prefix',
'invoice_numbers_prefix_cancellations',

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.
@@ -19,16 +19,45 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from django import forms
from django.conf import settings
from django.http import QueryDict
from pytz import common_timezones
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from pretix.api.serializers.forms import form_field_to_serializer_field
from pretix.base.exporter import OrganizerLevelExportMixin
from pretix.base.models import ScheduledEventExport, ScheduledOrganizerExport
from pretix.base.timeframes import SerializerDateFrameField
from pretix.base.timeframes import DateFrameField, SerializerDateFrameField
class FormFieldWrapperField(serializers.Field):
def __init__(self, *args, **kwargs):
self.form_field = kwargs.pop('form_field')
super().__init__(*args, **kwargs)
def to_representation(self, value):
return self.form_field.widget.format_value(value)
def to_internal_value(self, data):
d = self.form_field.widget.value_from_datadict({'name': data}, {}, 'name')
d = self.form_field.clean(d)
return d
simple_mappings = (
(forms.DateField, serializers.DateField, ()),
(forms.TimeField, serializers.TimeField, ()),
(forms.SplitDateTimeField, serializers.DateTimeField, ()),
(forms.DateTimeField, serializers.DateTimeField, ()),
(forms.DecimalField, serializers.DecimalField, ('max_digits', 'decimal_places', 'min_value', 'max_value')),
(forms.FloatField, serializers.FloatField, ()),
(forms.IntegerField, serializers.IntegerField, ()),
(forms.EmailField, serializers.EmailField, ()),
(forms.UUIDField, serializers.UUIDField, ()),
(forms.URLField, serializers.URLField, ()),
(forms.BooleanField, serializers.BooleanField, ()),
)
class SerializerDescriptionField(serializers.Field):
@@ -52,6 +81,13 @@ class ExporterSerializer(serializers.Serializer):
input_parameters = SerializerDescriptionField(source='_serializer')
class PrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
def to_representation(self, value):
if isinstance(value, int):
return value
return super().to_representation(value)
class JobRunSerializer(serializers.Serializer):
def __init__(self, *args, **kwargs):
ex = kwargs.pop('exporter')
@@ -66,7 +102,59 @@ class JobRunSerializer(serializers.Serializer):
many=True
)
for k, v in ex.export_form_fields.items():
self.fields[k] = form_field_to_serializer_field(v)
for m_from, m_to, m_kwargs in simple_mappings:
if isinstance(v, m_from):
self.fields[k] = m_to(
required=v.required,
allow_null=not v.required,
validators=v.validators,
**{kwarg: getattr(v, kwargs, None) for kwarg in m_kwargs}
)
break
if isinstance(v, forms.NullBooleanField):
self.fields[k] = serializers.BooleanField(
required=v.required,
allow_null=True,
validators=v.validators,
)
if isinstance(v, forms.ModelMultipleChoiceField):
self.fields[k] = PrimaryKeyRelatedField(
queryset=v.queryset,
required=v.required,
allow_empty=not v.required,
validators=v.validators,
many=True
)
elif isinstance(v, forms.ModelChoiceField):
self.fields[k] = PrimaryKeyRelatedField(
queryset=v.queryset,
required=v.required,
allow_null=not v.required,
validators=v.validators,
)
elif isinstance(v, forms.MultipleChoiceField):
self.fields[k] = serializers.MultipleChoiceField(
choices=v.choices,
required=v.required,
allow_empty=not v.required,
validators=v.validators,
)
elif isinstance(v, forms.ChoiceField):
self.fields[k] = serializers.ChoiceField(
choices=v.choices,
required=v.required,
allow_null=not v.required,
validators=v.validators,
)
elif isinstance(v, DateFrameField):
self.fields[k] = SerializerDateFrameField(
required=v.required,
allow_null=not v.required,
validators=v.validators,
)
else:
self.fields[k] = FormFieldWrapperField(form_field=v, required=v.required, allow_null=not v.required)
def to_internal_value(self, data):
if isinstance(data, QueryDict):

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.
@@ -109,19 +109,3 @@ class UploadedFileField(serializers.Field):
return None
request = self.context['request']
return request.build_absolute_uri(url)
class PluginsField(serializers.Field):
def to_representation(self, obj):
from pretix.base.plugins import get_all_plugins
return sorted([
p.module for p in get_all_plugins()
if not p.name.startswith('.') and getattr(p, 'visible', True) and p.module in obj.get_plugins()
])
def to_internal_value(self, data):
return {
'plugins': data
}

View File

@@ -1,115 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from django import forms
from rest_framework import serializers
from pretix.base.timeframes import DateFrameField, SerializerDateFrameField
simple_mappings = (
(forms.DateField, serializers.DateField, ()),
(forms.TimeField, serializers.TimeField, ()),
(forms.SplitDateTimeField, serializers.DateTimeField, ()),
(forms.DateTimeField, serializers.DateTimeField, ()),
(forms.DecimalField, serializers.DecimalField, ('max_digits', 'decimal_places', 'min_value', 'max_value')),
(forms.FloatField, serializers.FloatField, ()),
(forms.IntegerField, serializers.IntegerField, ()),
(forms.EmailField, serializers.EmailField, ()),
(forms.UUIDField, serializers.UUIDField, ()),
(forms.URLField, serializers.URLField, ()),
(forms.BooleanField, serializers.BooleanField, ()),
)
class PrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
def to_representation(self, value):
if isinstance(value, int):
return value
return super().to_representation(value)
class FormFieldWrapperField(serializers.Field):
def __init__(self, *args, **kwargs):
self.form_field = kwargs.pop('form_field')
super().__init__(*args, **kwargs)
def to_representation(self, value):
return self.form_field.widget.format_value(value)
def to_internal_value(self, data):
d = self.form_field.widget.value_from_datadict({'name': data}, {}, 'name')
d = self.form_field.clean(d)
return d
def form_field_to_serializer_field(field):
for m_from, m_to, m_kwargs in simple_mappings:
if isinstance(field, m_from):
return m_to(
required=field.required,
allow_null=not field.required,
validators=field.validators,
**{kwarg: getattr(field, kwarg, None) for kwarg in m_kwargs}
)
if isinstance(field, forms.NullBooleanField):
return serializers.BooleanField(
required=field.required,
allow_null=True,
validators=field.validators,
)
if isinstance(field, forms.ModelMultipleChoiceField):
return PrimaryKeyRelatedField(
queryset=field.queryset,
required=field.required,
allow_empty=not field.required,
validators=field.validators,
many=True
)
elif isinstance(field, forms.ModelChoiceField):
return PrimaryKeyRelatedField(
queryset=field.queryset,
required=field.required,
allow_null=not field.required,
validators=field.validators,
)
elif isinstance(field, forms.MultipleChoiceField):
return serializers.MultipleChoiceField(
choices=field.choices,
required=field.required,
allow_empty=not field.required,
validators=field.validators,
)
elif isinstance(field, forms.ChoiceField):
return serializers.ChoiceField(
choices=field.choices,
required=field.required,
allow_null=not field.required,
validators=field.validators,
)
elif isinstance(field, DateFrameField):
return SerializerDateFrameField(
required=field.required,
allow_null=not field.required,
validators=field.validators,
)
else:
return FormFieldWrapperField(form_field=field, required=field.required, allow_null=not field.required)

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.
@@ -550,7 +550,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
if full_data.get('show_during_checkin') and full_data.get('type') in Question.SHOW_DURING_CHECKIN_UNSUPPORTED:
raise ValidationError(_('This type of question cannot be shown during check-in.'))
Question.clean_items(event, full_data.get('items') or [])
Question.clean_items(event, full_data.get('items'))
return data
def validate_options(self, value):
@@ -566,7 +566,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
@transaction.atomic
def create(self, validated_data):
options_data = validated_data.pop('options') if 'options' in validated_data else []
items = validated_data.pop('items', [])
items = validated_data.pop('items')
question = Question.objects.create(**validated_data)
question.items.set(items)
@@ -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

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.
@@ -42,7 +42,6 @@ from rest_framework.reverse import reverse
from pretix.api.serializers import CompatibleJSONField
from pretix.api.serializers.event import SubEventSerializer
from pretix.api.serializers.forms import form_field_to_serializer_field
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.item import (
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
@@ -50,7 +49,6 @@ from pretix.api.serializers.item import (
from pretix.api.signals import order_api_details, orderposition_api_details
from pretix.base.decimal import round_decimal
from pretix.base.i18n import language
from pretix.base.invoicing.transmission import get_transmission_types
from pretix.base.models import (
CachedFile, Checkin, Customer, Invoice, InvoiceAddress, InvoiceLine, Item,
ItemVariation, Order, OrderPosition, Question, QuestionAnswer,
@@ -58,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
@@ -104,13 +102,6 @@ class CountryField(serializers.Field):
return str(src) if src else None
class TransmissionInfoSerializer(serializers.Serializer):
def __init__(self, *args, transmission_type, **kwargs):
super().__init__(*args, **kwargs)
for k, v in transmission_type.invoice_address_form_fields.items():
self.fields[k] = form_field_to_serializer_field(v)
class InvoiceAddressSerializer(I18nAwareModelSerializer):
country = CompatibleCountryField(source='*')
name = serializers.CharField(required=False)
@@ -118,8 +109,7 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
class Meta:
model = InvoiceAddress
fields = ('last_modified', 'is_business', 'company', 'name', 'name_parts', 'street', 'zipcode', 'city', 'country',
'state', 'vat_id', 'vat_id_validated', 'custom_field', 'internal_reference', 'transmission_type',
'transmission_info')
'state', 'vat_id', 'vat_id_validated', 'custom_field', 'internal_reference')
read_only_fields = ('last_modified',)
def __init__(self, *args, **kwargs):
@@ -157,48 +147,6 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
)
if data.get("transmission_type"):
for t in get_transmission_types():
if data.get("transmission_type") == t.identifier:
if not t.is_available(self.context["request"].event, data.get("country"), data.get("is_business")):
raise ValidationError({
"transmission_type": "The selected transmission type is not available for this country or address type."
})
ts = TransmissionInfoSerializer(transmission_type=t, data=data.get("transmission_info", {}))
try:
ts.is_valid(raise_exception=True)
except ValidationError as e:
raise ValidationError(
{"transmission_info": e.detail}
)
data["transmission_info"] = ts.validated_data
required_fields = t.invoice_address_form_fields_required(data.get("country"), data.get("is_business"))
for r in required_fields:
if r in self.fields:
if not data.get(r):
raise ValidationError(
{r: "This field is required for the selected type of invoice transmission."}
)
else:
if not ts.validated_data.get(r):
raise ValidationError(
{"transmission_info": {r: "This field is required for the selected type of invoice transmission."}}
)
break # do not call else branch of for loop
elif t.exclusive:
if t.is_available(self.context["request"].event, data.get("country"), data.get("is_business")):
raise ValidationError({
"transmission_type": "The transmission type '%s' must be used for this country or address type." % (
t.identifier,
)
})
else:
raise ValidationError(
{"transmission_type": "Unknown transmission type."}
)
return data
@@ -1005,7 +953,7 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
'company', 'street', 'zipcode', 'city', 'country', 'state', 'is_bundled',
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher', 'valid_from', 'valid_until',
'requested_valid_from', 'use_reusable_medium', 'discount')
'requested_valid_from', 'use_reusable_medium')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -1101,10 +1049,6 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
)
if data.get('price') is None and data.get('discount'):
raise ValidationError(
{'discount': ['You can only specify a discount if you do the price computation, but price is not set.']}
)
return data
@@ -1164,7 +1108,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['positions'].child.fields['voucher'].queryset = self.context['event'].vouchers.all()
self.fields['positions'].child.fields['discount'].queryset = self.context['event'].discounts.all()
self.fields['customer'].queryset = self.context['event'].organizer.customers.all()
self.fields['expires'].required = False
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
@@ -1572,22 +1515,19 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
pos.voucher_budget_use = max(listed_price - price_after_voucher, Decimal('0.00'))
order_positions = [pos_data['__instance'] for pos_data in positions_data]
if not any([p.get("discount") for p in positions_data]):
# If any discount is set by the client (i.e. pretixPOS), we do not recalculate but believe the client
# to avoid differences in end results.
discount_results = apply_discounts(
self.context['event'],
order.sales_channel,
[
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.price,
bool(cp.addon_to), cp.is_bundled, pos._voucher_discount)
for cp in order_positions
]
)
for cp, (new_price, discount) in zip(order_positions, discount_results):
if new_price != pos.price and pos._auto_generated_price:
pos.price = new_price
pos.discount = discount
discount_results = apply_discounts(
self.context['event'],
order.sales_channel,
[
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.price,
bool(cp.addon_to), cp.is_bundled, pos._voucher_discount)
for cp in order_positions
]
)
for cp, (new_price, discount) in zip(order_positions, discount_results):
if new_price != pos.price and pos._auto_generated_price:
pos.price = new_price
pos.discount = discount
# Save instances
for pos_data in positions_data:
@@ -1765,14 +1705,12 @@ class LinePositionField(serializers.IntegerField):
class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
position = LinePositionField(read_only=True)
event_date_from = serializers.DateTimeField(read_only=True, source="period_start")
event_date_to = serializers.DateTimeField(read_only=True, source="period_end")
class Meta:
model = InvoiceLine
fields = ('position', 'description', 'item', 'variation', 'subevent', 'attendee_name', 'event_date_from',
'event_date_to', 'period_start', 'period_end', 'gross_value', 'tax_value', 'tax_rate', 'tax_code',
'tax_name', 'fee_type', 'fee_internal_type', 'event_location')
'event_date_to', 'gross_value', 'tax_value', 'tax_rate', 'tax_code', 'tax_name', 'fee_type',
'fee_internal_type', 'event_location')
class InvoiceSerializer(I18nAwareModelSerializer):
@@ -1787,13 +1725,12 @@ class InvoiceSerializer(I18nAwareModelSerializer):
model = Invoice
fields = ('event', 'order', 'number', 'is_cancellation', 'invoice_from', 'invoice_from_name', 'invoice_from_zipcode',
'invoice_from_city', 'invoice_from_country', 'invoice_from_tax_id', 'invoice_from_vat_id',
'invoice_to', 'invoice_to_is_business', 'invoice_to_company', 'invoice_to_name', 'invoice_to_street',
'invoice_to_zipcode', 'invoice_to_city', 'invoice_to_state', 'invoice_to_country', 'invoice_to_vat_id',
'invoice_to_beneficiary', 'invoice_to_transmission_info', 'custom_field', 'date', 'refers', 'locale',
'invoice_to', 'invoice_to_company', 'invoice_to_name', 'invoice_to_street', 'invoice_to_zipcode',
'invoice_to_city', 'invoice_to_state', 'invoice_to_country', 'invoice_to_vat_id', 'invoice_to_beneficiary',
'custom_field', 'date', 'refers', 'locale',
'introductory_text', 'additional_text', 'payment_provider_text', 'payment_provider_stamp',
'footer_text', 'lines', 'foreign_currency_display', 'foreign_currency_rate',
'foreign_currency_rate_date', 'internal_reference', 'transmission_type', 'transmission_provider',
'transmission_status', 'transmission_date')
'foreign_currency_rate_date', 'internal_reference')
class OrderPaymentCreateSerializer(I18nAwareModelSerializer):
@@ -1846,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

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.
@@ -83,7 +83,6 @@ class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerialize
def create(self, validated_data):
ocm = self.context['ocm']
check_quotas = self.context.get('check_quotas', True)
try:
ocm.add_position(
@@ -97,7 +96,7 @@ class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerialize
valid_until=validated_data.get('valid_until'),
)
if self.context.get('commit', True):
ocm.commit(check_quotas=check_quotas)
ocm.commit()
return validated_data['order'].positions.order_by('-positionid').first()
else:
return OrderPosition() # fake to appease DRF
@@ -311,7 +310,6 @@ class OrderPositionChangeSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data):
ocm = self.context['ocm']
check_quotas = self.context.get('check_quotas', True)
current_seat = {'seat_guid': instance.seat.seat_guid} if instance.seat else None
item = validated_data.get('item', instance.item)
variation = validated_data.get('variation', instance.variation)
@@ -358,7 +356,7 @@ class OrderPositionChangeSerializer(serializers.ModelSerializer):
ocm.change_ticket_secret(instance, secret)
if self.context.get('commit', True):
ocm.commit(check_quotas=check_quotas)
ocm.commit()
instance.refresh_from_db()
except OrderError as e:
raise ValidationError(str(e))

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.
@@ -24,7 +24,6 @@ from decimal import Decimal
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from django.db.models import Q
from django.utils.crypto import get_random_string
from django.utils.translation import gettext_lazy as _
@@ -33,7 +32,6 @@ from rest_framework.exceptions import ValidationError
from pretix.api.auth.devicesecurity import get_all_security_profiles
from pretix.api.serializers import AsymmetricField
from pretix.api.serializers.fields import PluginsField
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.order import CompatibleJSONField
from pretix.api.serializers.settings import SettingsSerializer
@@ -45,10 +43,6 @@ from pretix.base.models import (
SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
)
from pretix.base.models.seating import SeatingPlanLayoutValidator
from pretix.base.plugins import (
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
PLUGIN_LEVEL_ORGANIZER,
)
from pretix.base.services.mail import SendMailException, mail
from pretix.base.settings import validate_organizer_settings
from pretix.helpers.urls import build_absolute_uri as build_global_uri
@@ -59,47 +53,13 @@ logger = logging.getLogger(__name__)
class OrganizerSerializer(I18nAwareModelSerializer):
public_url = serializers.SerializerMethodField('get_organizer_url', read_only=True)
plugins = PluginsField(required=False, source='*')
name = serializers.CharField(read_only=True)
slug = serializers.CharField(read_only=True)
def get_organizer_url(self, organizer):
return build_absolute_uri(organizer, 'presale:organizer.index')
class Meta:
model = Organizer
fields = ('name', 'slug', 'public_url', 'plugins')
def validate_plugins(self, value):
from pretix.base.plugins import get_all_plugins
plugins_available = {
p.module: p for p in get_all_plugins(organizer=self.instance)
if not p.name.startswith('.') and getattr(p, 'visible', True)
}
settings_holder = self.instance
allowed_levels = (PLUGIN_LEVEL_ORGANIZER, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID)
for plugin in value.get('plugins'):
if plugin not in plugins_available:
raise ValidationError(_('Unknown plugin: \'{name}\'.').format(name=plugin))
if getattr(plugins_available[plugin], 'restricted', False):
if plugin not in settings_holder.settings.allowed_restricted_plugins:
raise ValidationError(_('Restricted plugin: \'{name}\'.').format(name=plugin))
if getattr(plugins_available[plugin], 'level', PLUGIN_LEVEL_EVENT) not in allowed_levels:
raise ValidationError('Plugin cannot be enabled on this level: \'{name}\'.'.format(name=plugin))
return value
@transaction.atomic
def update(self, instance, validated_data):
plugins = validated_data.pop('plugins', None)
organizer = super().update(instance, validated_data)
# Plugins
if plugins is not None:
organizer.set_active_plugins(plugins)
organizer.save()
return organizer
fields = ('name', 'slug', 'public_url')
class SeatingPlanSerializer(I18nAwareModelSerializer):
@@ -484,7 +444,6 @@ class OrganizerSettingsSerializer(SettingsSerializer):
'reusable_media_type_nfc_mf0aes',
'reusable_media_type_nfc_mf0aes_autocreate_giftcard',
'reusable_media_type_nfc_mf0aes_autocreate_giftcard_currency',
'reusable_media_type_nfc_mf0aes_random_uid',
]
def __init__(self, *args, **kwargs):

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.
@@ -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', 'created', 'code', 'max_usages', 'redeemed', 'min_usages', 'valid_until', 'block_quota',
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

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.
@@ -21,22 +21,22 @@
#
from datetime import timedelta
from django.dispatch import receiver
from django.dispatch import Signal, receiver
from django.utils.timezone import now
from django_scopes import scopes_disabled
from pretix.api.models import ApiCall, WebHookCall
from pretix.base.signals import EventPluginSignal, GlobalSignal, periodic_task
from pretix.base.signals import EventPluginSignal, periodic_task
from pretix.helpers.periodic import minimum_interval
register_webhook_events = GlobalSignal()
register_webhook_events = Signal()
"""
This signal is sent out to get all known webhook events. Receivers should return an
instance of a subclass of ``pretix.api.webhooks.WebhookEvent`` or a list of such
instances.
"""
register_device_security_profile = GlobalSignal()
register_device_security_profile = Signal()
"""
This signal is sent out to get all known device security_profiles. Receivers should
return an instance of a subclass of ``pretix.api.auth.devicesecurity.BaseSecurityProfile``

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.
@@ -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')
@@ -132,8 +130,6 @@ urlpatterns = [
name="checkinrpc.redeem"),
re_path(r'^organizers/(?P<organizer>[^/]+)/checkinrpc/search/$', checkin.CheckinRPCSearchView.as_view(),
name="checkinrpc.search"),
re_path(r'^organizers/(?P<organizer>[^/]+)/checkinrpc/annul/$', checkin.CheckinRPCAnnulView.as_view(),
name="checkinrpc.annul"),
re_path(r'^organizers/(?P<organizer>[^/]+)/settings/$', organizer.OrganizerSettingsView.as_view(),
name="organizer.settings"),
re_path(r'^organizers/(?P<organizer>[^/]+)/giftcards/(?P<giftcard>[^/]+)/', include(giftcard_router.urls)),

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.
@@ -20,13 +20,12 @@
# <https://www.gnu.org/licenses/>.
#
import operator
from datetime import timedelta
from functools import reduce
import django_filters
from django.conf import settings
from django.core.exceptions import ValidationError as BaseValidationError
from django.db import connection, transaction
from django.db import transaction
from django.db.models import (
Count, Exists, F, Max, OrderBy, OuterRef, Prefetch, Q, Subquery,
prefetch_related_objects,
@@ -40,19 +39,17 @@ from django.utils.translation import gettext
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from packaging.version import parse
from rest_framework import status, views, viewsets
from rest_framework import views, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import (
NotFound, PermissionDenied, ValidationError,
)
from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.fields import DateTimeField
from rest_framework.generics import ListAPIView
from rest_framework.permissions import SAFE_METHODS
from rest_framework.response import Response
from pretix.api.serializers.checkin import (
CheckinListSerializer, CheckinRPCAnnulInputSerializer,
CheckinRPCRedeemInputSerializer, MiniCheckinListSerializer,
CheckinListSerializer, CheckinRPCRedeemInputSerializer,
MiniCheckinListSerializer,
)
from pretix.api.serializers.item import QuestionSerializer
from pretix.api.serializers.order import (
@@ -69,8 +66,6 @@ from pretix.base.models.orders import PrintLog
from pretix.base.services.checkin import (
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
)
from pretix.base.signals import checkin_annulled
from pretix.helpers import OF_SELF
with scopes_disabled():
class CheckinListFilter(FilterSet):
@@ -818,7 +813,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['expand'] = self.request.query_params.getlist('expand')
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true'
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false') == 'true'
return ctx
def get_filterset_kwargs(self):
@@ -837,9 +832,9 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
def get_queryset(self, ignore_status=False, ignore_products=False):
qs = _checkin_list_position_queryset(
[self.checkinlist],
ignore_status=self.request.query_params.get('ignore_status', 'false').lower() == 'true' or ignore_status,
ignore_status=self.request.query_params.get('ignore_status', 'false') == 'true' or ignore_status,
ignore_products=ignore_products,
pdf_data=self.request.query_params.get('pdf_data', 'false').lower() == 'true',
pdf_data=self.request.query_params.get('pdf_data', 'false') == 'true',
expand=self.request.query_params.getlist('expand'),
)
@@ -881,7 +876,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
user=self.request.user,
auth=self.request.auth,
expand=self.request.query_params.getlist('expand'),
pdf_data=self.request.query_params.get('pdf_data', 'false').lower() == 'true',
pdf_data=self.request.query_params.get('pdf_data', 'false') == 'true',
questions_supported=self.request.data.get('questions_supported', True),
canceled_supported=self.request.data.get('canceled_supported', False),
request=self.request, # this is not clean, but we need it in the serializers for URL generation
@@ -916,7 +911,7 @@ class CheckinRPCRedeemView(views.APIView):
user=self.request.user,
auth=self.request.auth,
expand=self.request.query_params.getlist('expand'),
pdf_data=self.request.query_params.get('pdf_data', 'false').lower() == 'true',
pdf_data=self.request.query_params.get('pdf_data', 'false') == 'true',
questions_supported=s.validated_data['questions_supported'],
use_order_locale=s.validated_data['use_order_locale'],
canceled_supported=True,
@@ -994,9 +989,9 @@ class CheckinRPCSearchView(ListAPIView):
def get_queryset(self, ignore_status=False, ignore_products=False):
qs = _checkin_list_position_queryset(
self.lists,
ignore_status=self.request.query_params.get('ignore_status', 'false').lower() == 'true' or ignore_status,
ignore_status=self.request.query_params.get('ignore_status', 'false') == 'true' or ignore_status,
ignore_products=ignore_products,
pdf_data=self.request.query_params.get('pdf_data', 'false').lower() == 'true',
pdf_data=self.request.query_params.get('pdf_data', 'false') == 'true',
expand=self.request.query_params.getlist('expand'),
)
@@ -1004,79 +999,3 @@ class CheckinRPCSearchView(ListAPIView):
qs = qs.none()
return qs
class CheckinRPCAnnulView(views.APIView):
def post(self, request, *args, **kwargs):
if isinstance(self.request.auth, (TeamAPIToken, Device)):
events = self.request.auth.get_events_with_permission(('can_change_orders', 'can_checkin_orders'))
elif self.request.user.is_authenticated:
events = self.request.user.get_events_with_permission(('can_change_orders', 'can_checkin_orders'), self.request).filter(
organizer=self.request.organizer
)
else:
raise ValueError("unknown authentication method")
s = CheckinRPCAnnulInputSerializer(data=request.data, context={'events': events})
s.is_valid(raise_exception=True)
with transaction.atomic():
try:
qs = Checkin.all.all()
if isinstance(request.auth, Device):
qs = qs.filter(device=request.auth)
ci = qs.select_for_update(
of=OF_SELF,
).select_related("position", "position__order", "position__order__event").get(
list__in=s.validated_data['lists'],
nonce=s.validated_data['nonce'],
)
if connection.features.has_select_for_update_of and ci.position_id:
# Lock position as well, can't do it with of= above because relation is nullable
OrderPosition.objects.select_for_update(of=OF_SELF).get(pk=ci.position_id)
if not ci.successful or not ci.position:
raise ValidationError("Cannot annul an unsuccessful checkin")
except Checkin.DoesNotExist:
raise NotFound("No check-in found based on nonce")
except Checkin.MultipleObjectsReturned:
raise ValidationError("Multiple check-ins found based on nonce")
annulment_time = s.validated_data.get("datetime") or now()
if annulment_time - ci.datetime > timedelta(minutes=15):
# Compare to sent datetime, which makes this cheatable, but allows offline annulment of checkins
ci.position.order.log_action('pretix.event.checkin.annulment.ignored', data={
'checkin': ci.pk,
'position': ci.position.id,
'positionid': ci.position.positionid,
'datetime': annulment_time,
'error_explanation': s.validated_data.get("error_explanation"),
'type': ci.type,
'list': ci.list_id,
}, user=request.user, auth=request.auth)
return Response({
"non_field_errors": ["Annulment is not allowed more than 15 minutes after check-in"]
}, status=status.HTTP_400_BAD_REQUEST)
if ci.device and ci.device != request.auth:
return Response({
"non_field_errors": ["Annulment is only allowed from the same device"]
}, status=status.HTTP_400_BAD_REQUEST)
ci.successful = False
ci.error_reason = Checkin.REASON_ANNULLED
ci.error_explanation = s.validated_data.get("error_explanation")
ci.save(update_fields=["successful", "error_reason", "error_explanation"])
ci.position.order.log_action('pretix.event.checkin.annulled', data={
'checkin': ci.pk,
'position': ci.position.id,
'positionid': ci.position.positionid,
'datetime': annulment_time,
'error_explanation': s.validated_data.get("error_explanation"),
'type': ci.type,
'list': ci.list_id,
}, user=request.user, auth=request.auth)
checkin_annulled.send(ci.position.order.event, checkin=ci)
return Response({"status": "ok"}, status=status.HTTP_200_OK)

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.
@@ -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
@@ -88,7 +87,7 @@ from pretix.base.secrets import assign_ticket_secret
from pretix.base.services import tickets
from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
regenerate_invoice, transmit_invoice,
regenerate_invoice,
)
from pretix.base.services.mail import SendMailException
from pretix.base.services.orders import (
@@ -228,7 +227,7 @@ class OrderViewSetMixin:
def get_queryset(self):
qs = self.get_base_queryset()
if 'fees' not in self.request.GET.getlist('exclude'):
if self.request.query_params.get('include_canceled_fees', 'false').lower() == 'true':
if self.request.query_params.get('include_canceled_fees', 'false') == 'true':
fqs = OrderFee.all
else:
fqs = OrderFee.objects
@@ -246,11 +245,11 @@ class OrderViewSetMixin:
return qs
def _positions_prefetch(self, request):
if request.query_params.get('include_canceled_positions', 'false').lower() == 'true':
if request.query_params.get('include_canceled_positions', 'false') == 'true':
opq = OrderPosition.all
else:
opq = OrderPosition.objects
if request.query_params.get('pdf_data', 'false').lower() == 'true' and getattr(request, 'event', None):
if request.query_params.get('pdf_data', 'false') == 'true' and getattr(request, 'event', None):
prefetch_related_objects([request.organizer], 'meta_properties')
prefetch_related_objects(
[request.event],
@@ -344,7 +343,7 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true'
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false') == 'true'
return ctx
def get_base_queryset(self):
@@ -743,7 +742,7 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
user=request.user if request.user.is_authenticated else None,
auth=request.auth,
)
order_placed.send(self.request.event, order=order, bulk=False)
order_placed.send(self.request.event, order=order)
if order.status == Order.STATUS_PAID:
order_paid.send(self.request.event, order=order)
order.log_action(
@@ -764,13 +763,7 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
) and not order.invoices.last()
invoice = None
if gen_invoice:
try:
invoice = generate_invoice(order, trigger_pdf=True)
except Exception as e:
logger.exception("Could not generate invoice.")
order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
invoice = generate_invoice(order, trigger_pdf=True)
# Refresh serializer only after running signals
prefetch_related_objects([order], self._positions_prefetch(request))
@@ -949,7 +942,6 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
@action(detail=True, methods=['POST'])
def change(self, request, **kwargs):
order = self.get_object()
check_quotas = self.request.query_params.get('check_quotas', 'true').lower() == 'true'
serializer = OrderChangeOperationSerializer(
context={'order': order, **self.get_serializer_context()},
@@ -1015,7 +1007,7 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
elif serializer.validated_data.get('recalculate_taxes') == 'keep_gross':
ocm.recalculate_taxes(keep='gross')
ocm.commit(check_quotas=check_quotas)
ocm.commit()
except OrderError as e:
raise ValidationError(str(e))
@@ -1093,18 +1085,17 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true'
ctx['check_quotas'] = self.request.query_params.get('check_quotas', 'true').lower() == 'true'
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false') == 'true'
return ctx
def get_queryset(self):
if self.request.query_params.get('include_canceled_positions', 'false').lower() == 'true':
if self.request.query_params.get('include_canceled_positions', 'false') == 'true':
qs = OrderPosition.all
else:
qs = OrderPosition.objects
qs = qs.filter(order__event=self.request.event)
if self.request.query_params.get('pdf_data', 'false').lower() == 'true':
if self.request.query_params.get('pdf_data', 'false') == 'true':
prefetch_related_objects([self.request.organizer], 'meta_properties')
prefetch_related_objects(
[self.request.event],
@@ -1669,9 +1660,6 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
else:
mark_refunded = request.data.get('mark_canceled', False)
if not isinstance(request.data.get("comment", ""), str):
return Response({'comment': 'Invalid type.'}, status=status.HTTP_400_BAD_REQUEST)
if payment.state != OrderPayment.PAYMENT_STATE_CONFIRMED:
return Response({'detail': 'Invalid state of payment.'}, status=status.HTTP_400_BAD_REQUEST)
@@ -1698,7 +1686,6 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
amount=amount,
provider=payment.provider,
info='{}',
comment=request.data.get("comment"),
)
payment.order.log_action('pretix.event.order.refund.created', {
'local_id': r.local_id,
@@ -1901,12 +1888,6 @@ class RetryException(APIException):
default_code = 'retry_later'
class CurrentlyInflightException(APIException):
status_code = 409
default_detail = 'The requested action is already in progress.'
default_code = 'currently_inflight'
class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = InvoiceSerializer
queryset = Invoice.objects.none()
@@ -1955,52 +1936,13 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
resp['Content-Disposition'] = 'attachment; filename="{}.pdf"'.format(invoice.number)
return resp
@action(detail=True, methods=['POST'])
def transmit(self, request, **kwargs):
invoice = self.get_object()
if invoice.shredded:
raise PermissionDenied('The invoice file is no longer stored on the server.')
if invoice.transmission_status != Invoice.TRANSMISSION_STATUS_PENDING:
raise PermissionDenied('The invoice is not in pending state.')
transmit_invoice.apply_async(args=(self.request.event.pk, invoice.pk, False))
return Response(status=204)
@action(detail=True, methods=['POST'])
def retransmit(self, request, **kwargs):
invoice = self.get_object()
if invoice.shredded:
raise PermissionDenied('The invoice file is no longer stored on the server.')
with transaction.atomic(durable=True):
invoice = Invoice.objects.select_for_update(of=OF_SELF).get(pk=invoice.pk)
if invoice.transmission_status == Invoice.TRANSMISSION_STATUS_INFLIGHT:
raise CurrentlyInflightException()
invoice.transmission_status = Invoice.TRANSMISSION_STATUS_PENDING
invoice.transmission_date = now()
invoice.save(update_fields=["transmission_status", "transmission_date"])
invoice.order.log_action(
'pretix.event.order.invoice.retransmitted',
user=self.request.user,
auth=self.request.auth,
data={
'invoice': invoice.pk,
'full_invoice_no': invoice.full_invoice_no,
}
)
transmit_invoice.apply_async(args=(self.request.event.pk, invoice.pk, True))
return Response(status=204)
@action(detail=True, methods=['POST'])
def regenerate(self, request, **kwargs):
inv = self.get_object()
if inv.canceled:
raise ValidationError('The invoice has already been canceled.')
if not inv.regenerate_allowed:
raise PermissionDenied('Invoice may not be regenerated.')
if not inv.event.settings.invoice_regenerate_allowed:
raise PermissionDenied('Invoices may not be changed after they are created.')
elif inv.shredded:
raise PermissionDenied('The invoice file is no longer stored on the server.')
elif inv.sent_to_organizer:
@@ -2088,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

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.
@@ -19,9 +19,7 @@
# 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 operator
from decimal import Decimal
from functools import reduce
import django_filters
from django.contrib.auth.hashers import make_password
@@ -50,18 +48,15 @@ from pretix.api.serializers.organizer import (
TeamInviteSerializer, TeamMemberSerializer, TeamSerializer,
)
from pretix.base.models import (
Customer, Device, Event, GiftCard, GiftCardTransaction, LogEntry,
Membership, MembershipType, Organizer, SalesChannel, SeatingPlan, Team,
TeamAPIToken, TeamInvite, User,
)
from pretix.base.plugins import (
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
Customer, Device, GiftCard, GiftCardTransaction, Membership,
MembershipType, Organizer, SalesChannel, SeatingPlan, Team, TeamAPIToken,
TeamInvite, User,
)
from pretix.helpers import OF_SELF
from pretix.helpers.dicts import merge_dicts
class OrganizerViewSet(mixins.UpdateModelMixin, viewsets.ReadOnlyModelViewSet):
class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = OrganizerSerializer
queryset = Organizer.objects.none()
lookup_field = 'slug'
@@ -70,7 +65,6 @@ class OrganizerViewSet(mixins.UpdateModelMixin, viewsets.ReadOnlyModelViewSet):
filter_backends = (TotalOrderingFilter,)
ordering = ('slug',)
ordering_fields = ('name', 'slug')
write_permission = "can_change_organizer_settings"
def get_queryset(self):
if self.request.user.is_authenticated:
@@ -89,67 +83,6 @@ class OrganizerViewSet(mixins.UpdateModelMixin, viewsets.ReadOnlyModelViewSet):
else:
return Organizer.objects.filter(pk=self.request.auth.team.organizer_id)
@transaction.atomic()
def perform_update(self, serializer):
from pretix.base.plugins import get_all_plugins
original_data = self.get_serializer(instance=serializer.instance).data
current_plugins_value = serializer.instance.get_plugins()
updated_plugins_value = serializer.validated_data.get('plugins', None)
super().perform_update(serializer)
if serializer.data == original_data:
# Performance optimization: If nothing was changed, we do not need to save or log anything.
# This costs us a few cycles on save, but avoids thousands of lines in our log.
return
if updated_plugins_value is not None and set(updated_plugins_value) != set(current_plugins_value):
enabled = {m: 'enabled' for m in updated_plugins_value if m not in current_plugins_value}
disabled = {m: 'disabled' for m in current_plugins_value if m not in updated_plugins_value}
changed = merge_dicts(enabled, disabled)
plugins_available = {
p.module: p
for p in get_all_plugins(organizer=serializer.instance)
if not p.name.startswith('.') and getattr(p, 'visible', True)
}
qs = []
for module in disabled:
pluginmeta = plugins_available[module]
level = getattr(pluginmeta, 'level', PLUGIN_LEVEL_EVENT)
if level == PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID:
qs.append(Q(plugins__regex='(^|,)' + module + '(,|$)'))
if qs:
events_to_disable = set(self.request.organizer.events.filter(
reduce(operator.or_, qs)
).values_list("pk", flat=True))
logentries_to_save = []
events_to_save = []
for e in self.request.organizer.events.filter(pk__in=events_to_disable):
for module in disabled:
if module in e.get_plugins():
logentries_to_save.append(
e.log_action('pretix.event.plugins.disabled', user=self.request.user, auth=self.request.auth,
data={'plugin': module}, save=False)
)
e.disable_plugin(module)
events_to_save.append(e)
Event.objects.bulk_update(events_to_save, fields=["plugins"])
LogEntry.objects.bulk_create(logentries_to_save)
for module, operation in changed.items():
serializer.instance.log_action(
'pretix.organizer.plugins.' + operation,
user=self.request.user,
auth=self.request.auth,
data={'plugin': module}
)
class SeatingPlanViewSet(viewsets.ModelViewSet):
serializer_class = SeatingPlanSerializer
@@ -546,8 +479,7 @@ class DeviceViewSet(mixins.CreateModelMixin,
class OrganizerSettingsView(views.APIView):
permission = None
write_permission = 'can_change_organizer_settings'
permission = 'can_change_organizer_settings'
def get(self, request, *args, **kwargs):
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.
@@ -78,13 +78,6 @@ class WebhookEvent:
"""
raise NotImplementedError() # NOQA
@property
def help_text(self) -> str:
"""
A human-readable description
"""
return ""
def get_all_webhook_events():
global _ALL_EVENTS
@@ -104,10 +97,9 @@ def get_all_webhook_events():
class ParametrizedWebhookEvent(WebhookEvent):
def __init__(self, action_type, verbose_name, help_text=""):
def __init__(self, action_type, verbose_name):
self._action_type = action_type
self._verbose_name = verbose_name
self._help_text = help_text
super().__init__()
@property
@@ -118,10 +110,6 @@ class ParametrizedWebhookEvent(WebhookEvent):
def verbose_name(self):
return self._verbose_name
@property
def help_text(self):
return self._help_text
class ParametrizedOrderWebhookEvent(ParametrizedWebhookEvent):
def build_payload(self, logentry: LogEntry):
@@ -173,19 +161,6 @@ class ParametrizedEventWebhookEvent(ParametrizedWebhookEvent):
}
class ParametrizedVoucherWebhookEvent(ParametrizedWebhookEvent):
def build_payload(self, logentry: LogEntry):
# do not use content_object, this is also called in deletion
return {
'notification_id': logentry.pk,
'organizer': logentry.event.organizer.slug,
'event': logentry.event.slug,
'voucher': logentry.object_id,
'action': logentry.action_type,
}
class ParametrizedSubEventWebhookEvent(ParametrizedWebhookEvent):
def build_payload(self, logentry: LogEntry):
@@ -371,9 +346,8 @@ def register_default_webhook_events(sender, **kwargs):
),
ParametrizedItemWebhookEvent(
'pretix.event.item.*',
_('Product changed'),
_('This includes product added or deleted and changes to nested objects like '
'variations or bundles.'),
_('Product changed (including product added or deleted and including changes to nested objects like '
'variations or bundles)'),
),
ParametrizedEventWebhookEvent(
'pretix.event.live.activated',
@@ -407,19 +381,6 @@ def register_default_webhook_events(sender, **kwargs):
'pretix.event.orders.waitinglist.voucher_assigned',
_('Waiting list entry received voucher'),
),
ParametrizedVoucherWebhookEvent(
'pretix.voucher.added',
_('Voucher added'),
),
ParametrizedVoucherWebhookEvent(
'pretix.voucher.changed',
_('Voucher changed'),
_('Only includes explicit changes to the voucher, not e.g. an increase of the number of redemptions.')
),
ParametrizedVoucherWebhookEvent(
'pretix.voucher.deleted',
_('Voucher deleted'),
),
ParametrizedCustomerWebhookEvent(
'pretix.customer.created',
_('Customer account created'),
@@ -439,12 +400,8 @@ def register_default_webhook_events(sender, **kwargs):
def notify_webhooks(logentry_ids: list):
if not isinstance(logentry_ids, list):
logentry_ids = [logentry_ids]
qs = LogEntry.all.select_related(
'event', 'event__organizer', 'organizer'
).order_by(
'action_type', 'organizer_id', 'event_id',
).filter(id__in=logentry_ids)
_org, _at, _ev, webhooks = None, None, None, None
qs = LogEntry.all.select_related('event', 'event__organizer', 'organizer').filter(id__in=logentry_ids)
_org, _at, webhooks = None, None, None
for logentry in qs:
if not logentry.organizer:
break # We need to know the organizer
@@ -454,7 +411,7 @@ def notify_webhooks(logentry_ids: list):
if not notification_type:
break # Ignore, no webhooks for this event type
if _org != logentry.organizer or _at != logentry.action_type or _ev != logentry.event_id or webhooks is None:
if _org != logentry.organizer or _at != logentry.action_type or webhooks is None:
_org = logentry.organizer
_at = logentry.action_type

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.
@@ -43,10 +43,10 @@ class PretixBaseConfig(AppConfig):
from . import exporter # NOQA
from . import payment # NOQA
from . import exporters # NOQA
from .invoicing import pdf, transmission, email, peppol, national # NOQA
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,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.

View File

@@ -1,8 +1,8 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
# 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.
@@ -199,7 +199,6 @@ def oidc_validate_authorization(provider, code, redirect_uri, pkce_code_verifier
params['client_id'] = provider.configuration['client_id']
params['client_secret'] = provider.configuration['client_secret']
resp = None
try:
resp = requests.post(
endpoint,
@@ -215,10 +214,7 @@ def oidc_validate_authorization(provider, code, redirect_uri, pkce_code_verifier
resp.raise_for_status()
data = resp.json()
except RequestException:
if resp:
logger.exception(f'Could not retrieve authorization token. Response: {resp.text}')
else:
logger.exception('Could not retrieve authorization token')
logger.exception('Could not retrieve authorization token')
raise ValidationError(
_('Login was not successful. Error message: "{error}".').format(
error='could not reach login provider',
@@ -226,7 +222,6 @@ def oidc_validate_authorization(provider, code, redirect_uri, pkce_code_verifier
)
if 'access_token' not in data:
logger.error(f'Could not find access token. Response: {data}')
raise ValidationError(
_('Login was not successful. Error message: "{error}".').format(
error='access token missing',
@@ -234,7 +229,6 @@ def oidc_validate_authorization(provider, code, redirect_uri, pkce_code_verifier
)
endpoint = provider.configuration['provider_config']['userinfo_endpoint']
resp = None
try:
# https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
resp = requests.get(
@@ -246,10 +240,7 @@ def oidc_validate_authorization(provider, code, redirect_uri, pkce_code_verifier
resp.raise_for_status()
userinfo = resp.json()
except RequestException:
if resp:
logger.exception(f'Could not retrieve user info. Response: {resp.text}')
else:
logger.exception('Could not retrieve user info')
logger.exception('Could not retrieve user info')
raise ValidationError(
_('Login was not successful. Error message: "{error}".').format(
error='could not fetch user info',

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-today pretix 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,450 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix 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 List, 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 PluginAwareRegistry
from pretix.helpers import OF_SELF
logger = logging.getLogger(__name__)
datasync_providers = PluginAwareRegistry({"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, immediate=False):
"""
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)
:param immediate: whether a new sync task should run immediately for this order, instead
of waiting for the next periodic_task interval
:return: Return a tuple (queue_item, created), where created is a boolean
specifying whether a new queue item was created.
"""
if not hasattr(cls, 'identifier'):
raise TypeError('Call this method on a derived class that defines an "identifier" attribute.')
queue_item, created = 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,
},
)
if immediate:
from pretix.base.services.datasync import sync_single
sync_single.apply_async(args=(queue_item.pk,))
return queue_item, created
@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_list()``.
"""
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}" does not exist. Please check your {provider_name} settings.'
).format(field_name=key, provider_name=self.display_name)])
try:
input = inputs[field.required_input]
except KeyError:
with language(self.event.settings.locale):
raise SyncConfigError([_(
'Field "{field_name}" requires {required_input}, but only got {available_inputs}. Please check your {provider_name} settings.'
).format(field_name=key, required_input=field.required_input, available_inputs=", ".join(inputs.keys()), provider_name=self.display_name)])
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: List[dict]):
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,659 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix 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 _, pgettext_lazy
from pretix.base.models import Checkin, InvoiceAddress, Order, Question
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.multidomain.urlreverse import build_absolute_uri
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 ""
def normalize_email(email):
if email:
local, host = email.split("@")
host = host.encode("idna").decode()
return f"{local}@{host}"
else:
return None
def get_email_domain(email):
if email:
local, host = email.split("@")
return host
else:
return None
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),
}
DataFieldCategory = namedtuple(
'DataFieldCategory',
field_names=('sort_index', 'label',),
)
CAT_ORDER_POSITION = DataFieldCategory(10, _('Order position details'))
CAT_ATTENDEE = DataFieldCategory(11, _('Attendee details'))
CAT_QUESTIONS = DataFieldCategory(12, _('Questions'))
CAT_PRODUCT = DataFieldCategory(20, _('Product details'))
CAT_ORDER = DataFieldCategory(21, _('Order details'))
CAT_INVOICE_ADDRESS = DataFieldCategory(22, _('Invoice address'))
CAT_EVENT = DataFieldCategory(30, _('Event information'))
CAT_EVENT_OR_SUBEVENT = DataFieldCategory(31, pgettext_lazy('subevent', 'Event or date information'))
DataFieldInfo = namedtuple(
'DataFieldInfo',
field_names=('required_input', 'category', '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,
CAT_ATTENDEE,
"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,
CAT_ATTENDEE,
"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,
CAT_ATTENDEE,
"attendee_email",
_("Attendee email"),
Question.TYPE_STRING,
None,
lambda position: normalize_email(
position.attendee_email
or (position.addon_to.attendee_email if position.addon_to else None)
),
),
DataFieldInfo(
ORDER_POSITION,
CAT_ATTENDEE,
"attendee_or_order_email",
_("Attendee or order email"),
Question.TYPE_STRING,
None,
lambda position: normalize_email(
position.attendee_email
or (position.addon_to.attendee_email if position.addon_to else None)
or position.order.email
),
),
DataFieldInfo(
ORDER_POSITION,
CAT_ATTENDEE,
"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,
CAT_ATTENDEE,
"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,
CAT_ATTENDEE,
"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,
CAT_ATTENDEE,
"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,
CAT_ATTENDEE,
"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,
CAT_INVOICE_ADDRESS,
"invoice_address_company",
_("Invoice address company"),
Question.TYPE_STRING,
None,
lambda order: get_invoice_address_or_empty(order).company,
),
DataFieldInfo(
ORDER,
CAT_INVOICE_ADDRESS,
"invoice_address_name",
_("Invoice address name"),
Question.TYPE_STRING,
None,
lambda order: get_invoice_address_or_empty(order).name,
),
]
+ [
DataFieldInfo(
ORDER,
CAT_INVOICE_ADDRESS,
"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,
CAT_INVOICE_ADDRESS,
"invoice_address_street",
_("Invoice address street"),
Question.TYPE_STRING,
None,
lambda order: get_invoice_address_or_empty(order).street,
),
DataFieldInfo(
ORDER,
CAT_INVOICE_ADDRESS,
"invoice_address_zipcode",
_("Invoice address ZIP code"),
Question.TYPE_STRING,
None,
lambda order: get_invoice_address_or_empty(order).zipcode,
),
DataFieldInfo(
ORDER,
CAT_INVOICE_ADDRESS,
"invoice_address_city",
_("Invoice address city"),
Question.TYPE_STRING,
None,
lambda order: get_invoice_address_or_empty(order).city,
),
DataFieldInfo(
ORDER,
CAT_INVOICE_ADDRESS,
"invoice_address_country",
_("Invoice address country"),
Question.TYPE_COUNTRYCODE,
None,
lambda order: str(get_invoice_address_or_empty(order).country),
),
DataFieldInfo(
ORDER,
CAT_ORDER,
"email",
_("Order email"),
Question.TYPE_STRING,
None,
lambda order: normalize_email(order.email),
),
DataFieldInfo(
ORDER,
CAT_ORDER,
"email_domain",
_("Order email domain"),
Question.TYPE_STRING,
None,
lambda order: get_email_domain(normalize_email(order.email)),
),
DataFieldInfo(
ORDER,
CAT_ORDER,
"order_code",
_("Order code"),
Question.TYPE_STRING,
None,
lambda order: order.code,
),
DataFieldInfo(
ORDER,
CAT_ORDER,
"event_order_code",
_("Event and order code"),
Question.TYPE_STRING,
None,
lambda order: order.full_code,
),
DataFieldInfo(
ORDER,
CAT_ORDER,
"order_total",
_("Order total"),
Question.TYPE_NUMBER,
None,
lambda order: str(order.total),
),
DataFieldInfo(
ORDER_POSITION,
CAT_PRODUCT,
"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,
CAT_PRODUCT,
"product_id",
_("Product ID"),
Question.TYPE_NUMBER,
None,
lambda position: str(position.item.pk),
),
DataFieldInfo(
ORDER_POSITION,
CAT_PRODUCT,
"product_is_admission",
_("Product is admission product"),
Question.TYPE_BOOLEAN,
None,
lambda position: bool(position.item.admission),
),
DataFieldInfo(
EVENT,
CAT_EVENT,
"event_slug",
_("Event short form"),
Question.TYPE_STRING,
None,
lambda event: str(event.slug),
),
DataFieldInfo(
EVENT,
CAT_EVENT,
"event_name",
_("Event name"),
Question.TYPE_STRING,
None,
lambda event: str(event.name),
),
DataFieldInfo(
EVENT_OR_SUBEVENT,
CAT_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,
CAT_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,
CAT_ORDER_POSITION,
"voucher_code",
_("Voucher code"),
Question.TYPE_STRING,
None,
lambda position: position.voucher.code if position.voucher_id else "",
),
DataFieldInfo(
ORDER_POSITION,
CAT_ORDER_POSITION,
"ticket_id",
_("Order code and position number"),
Question.TYPE_STRING,
None,
lambda position: position.code,
),
DataFieldInfo(
ORDER_POSITION,
CAT_ORDER_POSITION,
"ticket_price",
_("Ticket price"),
Question.TYPE_NUMBER,
None,
lambda position: str(position.price),
),
DataFieldInfo(
ORDER,
CAT_ORDER,
"order_status",
_("Order status"),
Question.TYPE_CHOICE,
Order.STATUS_CHOICE,
lambda order: [order.status],
),
DataFieldInfo(
ORDER_POSITION,
CAT_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,
CAT_ORDER,
"order_date",
_("Order date and time"),
Question.TYPE_DATETIME,
None,
lambda order: order.datetime.isoformat(),
),
DataFieldInfo(
ORDER,
CAT_ORDER,
"payment_date",
_("Payment date and time"),
Question.TYPE_DATETIME,
None,
get_payment_date,
),
DataFieldInfo(
ORDER,
CAT_ORDER,
"order_locale",
_("Order locale"),
Question.TYPE_CHOICE,
[(lc, lc) for lc in event.settings.locales],
lambda order: [order.locale],
),
DataFieldInfo(
ORDER_POSITION,
CAT_ORDER_POSITION,
"position_id",
_("Order position ID"),
Question.TYPE_NUMBER,
None,
lambda op: str(op.pk),
),
DataFieldInfo(
ORDER,
CAT_ORDER,
"presale_order_url",
_("Order link"),
Question.TYPE_STRING,
None,
lambda order: build_absolute_uri(
event,
'presale:event.order', kwargs={
'order': order.code,
'secret': order.secret,
}
),
),
DataFieldInfo(
ORDER_POSITION,
CAT_ORDER_POSITION,
"presale_ticket_url",
_("Ticket link"),
Question.TYPE_STRING,
None,
lambda op: build_absolute_uri(
event,
'presale:event.order.position', kwargs={
'order': op.order.code,
'secret': op.web_secret,
'position': op.positionid
}
),
),
]
+ [
DataFieldInfo(
ORDER_POSITION,
CAT_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,
CAT_QUESTIONS,
"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,
CAT_ATTENDEE,
"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,
CAT_INVOICE_ADDRESS,
"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,
CAT_ATTENDEE,
"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,
CAT_INVOICE_ADDRESS,
"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,123 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix 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 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 = []
for mapping in property_mappings:
pretix_field = mapping["pretix_field"]
if pretix_field.startswith("checkin_date_"):
old_id = int(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.
pretix_field = "_invalid_" + pretix_field
else:
pretix_field = "checkin_date_%d" % checkin_list_map[old_id].pk
mappings.append({**mapping, "pretix_field": pretix_field})
return mappings

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