Compare commits

..

3 Commits

Author SHA1 Message Date
Richard Schreiber
ed1a2dd57f add safe fallback for attendee_name_parts being None 2022-05-10 11:18:57 +02:00
Richard Schreiber
6da65ab13e improve order of fields 2022-05-10 09:32:47 +02:00
Richard Schreiber
d1d6afe721 add name_for_salutation to editor/pdf 2022-05-10 09:27:29 +02:00
281 changed files with 83466 additions and 104983 deletions

View File

@@ -31,7 +31,7 @@ RUN apt-get update && \
echo 'pretixuser ALL=(ALL) NOPASSWD:SETENV: /usr/bin/supervisord' >> /etc/sudoers && \
mkdir /static && \
mkdir /etc/supervisord && \
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash - && \
curl -fsSL https://deb.nodesource.com/setup_15.x | sudo -E bash - && \
apt-get install -y nodejs && \
curl -qL https://www.npmjs.com/install.sh | sh

View File

@@ -2,7 +2,6 @@
file=/tmp/supervisor.sock
[supervisord]
environment = AUTOMIGRATE="skip"
logfile=/dev/stdout
logfile_maxbytes=0
loglevel=info

View File

@@ -3155,14 +3155,14 @@ a .fa, a .wy-menu-vertical li span.toctree-expand, .wy-menu-vertical li a span.t
vertical-align: -15%
}
.wy-alert, .rst-content .note, .rst-content .attention, .rst-content .caution, .rst-content .danger, .rst-content .error, .rst-content .hint, .rst-content .important, .rst-content .tip, .rst-content .warning, .rst-content .seealso, .rst-content .admonition-todo, .rst-content div.deprecated {
.wy-alert, .rst-content .note, .rst-content .attention, .rst-content .caution, .rst-content .danger, .rst-content .error, .rst-content .hint, .rst-content .important, .rst-content .tip, .rst-content .warning, .rst-content .seealso, .rst-content .admonition-todo {
padding: 12px;
line-height: 24px;
margin-bottom: 24px;
background: #e7f2fa
}
.wy-alert-title, .rst-content .admonition-title, .rst-content .deprecated .versionmodified {
.wy-alert-title, .rst-content .admonition-title {
color: #fff;
font-weight: bold;
display: block;

View File

@@ -240,9 +240,6 @@ The following snippet is an example on how to configure a nginx proxy for pretix
listen 80 default_server;
listen [::]:80 ipv6only=on default_server;
server_name pretix.mydomain.com;
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 default_server;

View File

@@ -225,9 +225,6 @@ The following snippet is an example on how to configure a nginx proxy for pretix
listen 80 default_server;
listen [::]:80 ipv6only=on default_server;
server_name pretix.mydomain.com;
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 default_server;

View File

@@ -172,6 +172,8 @@ Cart position endpoints
* does not check or calculate prices but believes any prices you send
* does not support the redemption of vouchers
* does not prevent you from buying items that can only be bought with a voucher
* does not support file upload questions
@@ -189,7 +191,6 @@ Cart position endpoints
* ``expires`` (optional)
* ``includes_tax`` (optional, **deprecated**, do not use, will be removed)
* ``sales_channel`` (optional)
* ``voucher`` (optional, expect a voucher code)
* ``answers``
* ``question``

View File

@@ -1,346 +0,0 @@
.. spelling:: checkin
.. _rest-checkin:
Check-in
========
This page describes special APIs built for ticket scanning apps. For managing check-in configuration or other operations,
please also see :ref:`rest-checkinlists`. The check-in list API also contains endpoints to obtain statistics or log
failed scans.
.. versionchanged:: 4.12
The endpoints listed on this page have been added.
.. _`rest-checkin-redeem`:
Checking a ticket in
--------------------
.. http:post:: /api/v1/organizers/(organizer)/checkinrpc/redeem/
Tries to redeem an order position, i.e. checks the attendee in (or out). This is the recommended endpoint to use
if you build any kind of scanning app that performs check-ins for scanned barcodes. It is safe to use with untrusted
inputs in the ``secret`` field.
This endpoint supports passing multiple check-in lists to perform a multi-event scan. However, each check-in list
passed needs to be from a distinct event.
:<json string secret: Scanned QR code corresponding to the ``secret`` attribute of a ticket.
:<json array lists: List of check-in list IDs to search on. No two check-in lists may be from the same event.
:<json string type: Send ``"exit"`` for an exit and ``"entry"`` (default) for an entry.
:<json datetime datetime: Specifies the datetime of the check-in. If not supplied, the current time will be used.
:<json boolean force: Specifies that the check-in should succeed regardless of revoked barcode, previous check-ins or required
questions that have not been filled. This is usually used to upload offline scans that already happened,
because there's no point in validating them since they happened whether they are valid or not. Defaults to ``false``.
:<json boolean questions_supported: When this parameter is set to ``true``, handling of questions is supported. If
you do not implement question handling in your user interface, you **must**
set this to ``false``. In that case, questions will just be ignored. Defaults
to ``true``.
:<json boolean ignore_unpaid: Specifies that the check-in should succeed even if the order is in pending state.
Defaults to ``false`` and only works when ``include_pending`` is set on the check-in
list.
:<json object answers: If questions are supported/required, you may/must supply a mapping of question IDs to their
respective answers. The answers should always be strings. In case of (multiple-)choice-type
answers, the string should contain the (comma-separated) IDs of the selected options.
:<json string nonce: You can set this parameter to a unique random value to identify this check-in. If you're sending
this request twice with the same nonce, the second request will also succeed but will always
create only one check-in object even when the previous request was successful as well. This
allows for a certain level of idempotency and enables you to re-try after a connection failure.
:>json string status: ``"ok"``, ``"incomplete"``, or ``"error"``
:>json string reason: Reason code, only set on status ``"error"``, see below for possible values.
:>json string reason_explanation: Human-readable explanation, only set on status ``"error"`` and reason ``"rules"``, can be null.
:>json object position: Copy of the matching order position (if any was found). The contents are the same as the
:ref:`order-position-resource`, with the following differences: (1) The ``checkins`` value
will only include check-ins for the selected list. (2) An additional boolean property
``require_attention`` will inform you whether either the order or the item have the
``checkin_attention`` flag set. (3) If ``attendee_name`` is empty, it may automatically fall
back to values from a parent product or from invoice addresses.
:>json boolean require_attention: Whether or not the ``require_attention`` flag is set on the item or order.
:>json object list: Excerpt of information about the matching :ref:`check-in list <rest-checkinlists>` (if any was found),
including the attributes ``id``, ``name``, ``event``, ``subevent``, and ``include_pending``.
:>json object questions: List of questions to be answered for check-in, only set on status ``"incomplete"``.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/checkinrpc/redeem/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
{
"secret": "M5BO19XmFwAjLd4nDYUAL9ISjhti0e9q",
"lists": [1],
"force": false,
"ignore_unpaid": false,
"nonce": "Pvrk50vUzQd0DhdpNRL4I4OcXsvg70uA",
"datetime": null,
"questions_supported": true,
"answers": {
"4": "XS"
}
}
**Example successful response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"status": "ok",
"position": {
},
"require_attention": false,
"list": {
"id": 1,
"name": "Default check-in list",
"event": "sampleconf",
"subevent": null,
"include_pending": false
}
}
**Example response with required questions**:
.. sourcecode:: http
HTTP/1.1 400 Bad Request
Content-Type: text/json
{
"status": "incomplete",
"position": {
},
"require_attention": false,
"list": {
"id": 1,
"name": "Default check-in list",
"event": "sampleconf",
"subevent": null,
"include_pending": false
},
"questions": [
{
"id": 1,
"question": {"en": "T-Shirt size"},
"type": "C",
"required": false,
"items": [1, 2],
"position": 1,
"identifier": "WY3TP9SL",
"ask_during_checkin": true,
"options": [
{
"id": 1,
"identifier": "LVETRWVU",
"position": 0,
"answer": {"en": "S"}
},
{
"id": 2,
"identifier": "DFEMJWMJ",
"position": 1,
"answer": {"en": "M"}
},
{
"id": 3,
"identifier": "W9AH7RDE",
"position": 2,
"answer": {"en": "L"}
}
]
}
]
}
**Example error response (invalid ticket)**:
.. sourcecode:: http
HTTP/1.1 404 Not Found
Content-Type: text/json
{
"detail": "Not found.",
"status": "error",
"reason": "invalid",
"reason_explanation": null,
"require_attention": false
}
**Example error response (known, but invalid ticket)**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: text/json
{
"status": "error",
"reason": "unpaid",
"reason_explanation": null,
"require_attention": false,
"list": {
"id": 1,
"name": "Default check-in list",
"event": "sampleconf",
"subevent": null,
"include_pending": false
},
"position": {
}
}
Possible error reasons:
* ``invalid`` - Ticket is not known.
* ``unpaid`` - Ticket is not paid for.
* ``canceled`` Ticket is canceled or expired.
* ``already_redeemed`` - Ticket already has been redeemed.
* ``product`` - Tickets with this product may not be scanned at this device.
* ``rules`` - Check-in prevented by a user-defined rule.
* ``ambiguous`` - Multiple tickets match scan, rejected.
* ``revoked`` - Ticket code has been revoked.
* ``error`` - Internal error.
In case of reason ``rules``, there might be an additional response field ``reason_explanation`` with a human-readable
description of the violated rules. However, that field can also be missing or be ``null``.
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 201: 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 order position does not exist.
Performing a ticket search
--------------------------
.. http:get:: /api/v1/organizers/(organizer)/checkinrpc/search/
Returns a list of all order positions matching a given search request. The result is the same as
the :ref:`order-position-resource`, with the following differences:
* The ``checkins`` value will only include check-ins for the selected list.
* An additional boolean property ``require_attention`` will inform you whether either the order or the item
have the ``checkin_attention`` flag set.
* If ``attendee_name`` is empty, it will automatically fall back to values from a parent product or from invoice
addresses.
This endpoint supports passing multiple check-in lists to perform a multi-event search. However, each check-in list
passed needs to be from a distinct event.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/checkinrpc/search/?list=1&search=Peter 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": 23442,
"order": "ABC12",
"positionid": 1,
"item": 1345,
"variation": null,
"price": "23.00",
"attendee_name": "Peter",
"attendee_name_parts": {
"full_name": "Peter",
},
"attendee_email": null,
"voucher": null,
"tax_rate": "0.00",
"tax_rule": null,
"tax_value": "0.00",
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
"addon_to": null,
"subevent": null,
"pseudonymization_id": "MQLJvANO3B",
"seat": null,
"checkins": [
{
"list": 1,
"type": "entry",
"gate": null,
"device": 2,
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": true
}
],
"answers": [
{
"question": 12,
"answer": "Foo",
"options": []
}
],
"downloads": [
{
"output": "pdf",
"url": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/pdf/"
}
]
}
]
}
:query string search: Fuzzy search matching the attendee name, order code, invoice address name as well as to the beginning of the secret.
:query integer list: The check-in list to search on, can be passed multiple times.
:query integer page: The page number in case of a multi-page result set, default is 1
:query string ignore_status: If set to ``true``, results will be returned regardless of the state of
the order they belong to and you will need to do your own filtering by order status.
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``order__code``,
``order__datetime``, ``positionid``, ``attendee_name``, ``last_checked_in`` and ``order__email``. Default:
``attendee_name,positionid``
:query string order: Only return positions of the order with the given order code
:query string search: Fuzzy search matching the attendee name, order code, invoice address name as well as to the beginning of the secret.
:query string expand: Expand a field into a full object. Currently only ``subevent``, ``item``, and ``variation`` are supported. Can be passed multiple times.
:query integer item: Only return positions with the purchased item matching the given ID.
:query integer item__in: Only return positions with the purchased item matching one of the given comma-separated IDs.
:query integer variation: Only return positions with the purchased item variation matching the given ID.
:query integer variation__in: Only return positions with one of the purchased item variation matching the given
comma-separated IDs.
:query string attendee_name: Only return positions with the given value in the attendee_name field. Also, add-on
products positions are shown if they refer to an attendee with the given name.
:query string secret: Only return positions with the given ticket secret.
:query string order__status: Only return positions with the given order status.
:query string order__status__in: Only return positions with one the given comma-separated order status.
:query boolean has_checkin: If set to ``true`` or ``false``, only return positions that have or have not been
checked in already.
:query integer subevent: Only return positions of the sub-event with the given ID
:query integer subevent__in: Only return positions of one of the sub-events with the given comma-separated IDs
:query integer addon_to: Only return positions that are add-ons to the position with the given ID.
:query integer addon_to__in: Only return positions that are add-ons to one of the positions with the given
comma-separated IDs.
:query string voucher: Only return positions with a specific voucher.
:query string voucher__code: Only return positions with a specific voucher code.
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error
: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.

View File

@@ -1,7 +1,5 @@
.. spelling:: checkin
.. _rest-checkinlists:
Check-in lists
==============
@@ -36,7 +34,6 @@ allow_multiple_entries boolean If ``true``, su
allow_entry_after_exit boolean If ``true``, subsequent scans of a ticket on this list are valid if the last scan of the ticket was an exit scan.
rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged.
exit_all_at datetime Automatically check out (i.e. perform an exit scan) at this point in time. After this happened, this property will automatically be set exactly one day into the future. Note that this field is considered "internal configuration" and if you pull the list with ``If-Modified-Since``, the daily change in this field will not trigger a response.
addon_match boolean If ``true``, tickets on this list can be redeemed by scanning their parent ticket if this still leads to an unambiguous match.
===================================== ========================== =======================================================
.. versionchanged:: 3.9
@@ -56,10 +53,6 @@ addon_match boolean If ``true``, ti
The ``ends_after`` and ``expand`` query parameters have been added.
.. versionchanged:: 4.12
The ``addon_match`` attribute has been added.
Endpoints
---------
@@ -101,7 +94,6 @@ Endpoints
"allow_entry_after_exit": true,
"exit_all_at": null,
"rules": {},
"addon_match": false,
"auto_checkin_sales_channels": [
"pretixpos"
]
@@ -154,7 +146,6 @@ Endpoints
"allow_entry_after_exit": true,
"exit_all_at": null,
"rules": {},
"addon_match": false,
"auto_checkin_sales_channels": [
"pretixpos"
]
@@ -254,7 +245,6 @@ Endpoints
"subevent": null,
"allow_multiple_entries": false,
"allow_entry_after_exit": true,
"addon_match": false,
"auto_checkin_sales_channels": [
"pretixpos"
]
@@ -279,7 +269,6 @@ Endpoints
"subevent": null,
"allow_multiple_entries": false,
"allow_entry_after_exit": true,
"addon_match": false,
"auto_checkin_sales_channels": [
"pretixpos"
]
@@ -334,7 +323,6 @@ Endpoints
"subevent": null,
"allow_multiple_entries": false,
"allow_entry_after_exit": true,
"addon_match": false,
"auto_checkin_sales_channels": [
"pretixpos"
]
@@ -427,9 +415,6 @@ Order position endpoints
* If ``attendee_name`` is empty, it will automatically fall back to values from a parent product or from invoice
addresses.
You can use this endpoint to implement a ticket search. We also provide a dedicated search input as part of our
:ref:`check-in API <rest-checkin>` that supports search across multiple events.
**Example request**:
.. sourcecode:: http
@@ -619,23 +604,15 @@ Order position endpoints
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order position or check-in list does not exist.
.. _`rest-checkin-redeem`:
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(list)/positions/(id)/redeem/
Tries to redeem an order position, identified by its internal ID, i.e. checks the attendee in. This endpoint
accepts a number of optional requests in the body.
**Tip:** Instead of an ID, you can also use the ``secret`` field as the lookup parameter. In this case, you should
always set ``untrusted_input=true`` as a query parameter to avoid security issues.
**Tip:** Instead of an ID, you can also use the ``secret`` field as the lookup parameter.
.. note::
We no longer recommend using this API if you're building a ticket scanning application, as it has a few design
flaws that can lead to `security issues`_ or compatibility issues due to barcode content characters that are not
URL-safe. We recommend to use our new :ref:`check-in API <rest-checkin>` instead.
:query boolean untrusted_input: If set to true, the lookup parameter is **always** interpreted as a ``secret``, never
as an ``id``. This should be always set if you are passing through untrusted, scanned
data to avoid guessing of ticket IDs.
:<json boolean questions_supported: When this parameter is set to ``true``, handling of questions is supported. If
you do not implement question handling in your user interface, you **must**
set this to ``false``. In that case, questions will just be ignored. Defaults
@@ -756,15 +733,12 @@ Order position endpoints
Possible error reasons:
* ``invalid`` - Ticket code not known.
* ``unpaid`` - Ticket is not paid for.
* ``canceled`` Ticket is canceled or expired. This reason is only sent when your request sets.
* ``unpaid`` - Ticket is not paid for
* ``canceled`` Ticket is canceled or expired. This reason is only sent when your request sets
``canceled_supported`` to ``true``, otherwise these orders return ``unpaid``.
* ``already_redeemed`` - Ticket already has been redeemed.
* ``product`` - Tickets with this product may not be scanned at this device.
* ``rules`` - Check-in prevented by a user-defined rule.
* ``ambiguous`` - Multiple tickets match scan, rejected.
* ``revoked`` - Ticket code has been revoked.
* ``already_redeemed`` - Ticket already has been redeemed
* ``product`` - Tickets with this product may not be scanned at this device
* ``rules`` - Check-in prevented by a user-defined rule
In case of reason ``rules``, there might be an additional response field ``reason_explanation`` with a human-readable
description of the violated rules. However, that field can also be missing or be ``null``.
@@ -778,6 +752,3 @@ Order position endpoints
: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 order position or check-in list does not exist.
.. _security issues: https://pretix.eu/about/de/blog/20220705-release-4111/

View File

@@ -131,9 +131,7 @@ Endpoints
.. http:post:: /api/v1/organizers/(organizer)/customers/
Creates a new customer. In addition to the fields defined on the resource, you can pass the field ``send_email``
to control whether the system should send an account activation email with a password reset link (defaults to
``false``).
Creates a new customer
**Example request**:
@@ -145,8 +143,7 @@ Endpoints
Content-Type: application/json
{
"email": "test@example.org",
"send_email": true
"email": "test@example.org"
}
**Example response**:
@@ -176,8 +173,8 @@ Endpoints
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
want to change.
You can change all fields of the resource except the ``identifier``, ``last_login``, ``date_joined``,
``name`` (which is auto-generated from ``name_parts``), and ``last_modified`` fields.
You can change all fields of the resource except the ``identifier``, ``last_login``, ``date_joined``, ``name``,
and ``last_modified`` fields.
**Example request**:

View File

@@ -182,7 +182,7 @@ endpoints:
Content-Type: application/json
{
"download": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/exporters/orderlist/download/29891ede-196f-4942-9e26-d055a36e98b8/3f279f13-c198-4137-b49b-9b360ce9fcce/"
"download": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/orderlist/download/29891ede-196f-4942-9e26-d055a36e98b8/3f279f13-c198-4137-b49b-9b360ce9fcce/"
}
:param organizer: The ``slug`` field of the organizer to fetch

View File

@@ -25,7 +25,6 @@ at :ref:`plugin-docs`.
invoices
vouchers
discounts
checkin
checkinlists
waitinglist
customers
@@ -38,7 +37,6 @@ at :ref:`plugin-docs`.
webhooks
seatingplans
exporters
shredders
sendmail_rules
billing_invoices
billing_var

View File

@@ -60,7 +60,6 @@ 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
EU VAT service and validation was successful. This only
@@ -428,7 +427,7 @@ List of all orders
``last_modified``, and ``status``. Default: ``datetime``
:query string code: Only return orders that match the given order code
:query string status: Only return orders in the given order status (see above)
:query string search: Only return orders matching a given search query (matching for names, email addresses, and company names)
:query string search: Only return orders matching a given search query
:query integer item: Only return orders with a position that contains this item ID. *Warning:* Result will also include orders if they contain mixed items, and it will even return orders where the item is only contained in a canceled position.
:query integer variation: Only return orders with a position that contains this variation ID. *Warning:* Result will also include orders if they contain mixed items and variations, and it will even return orders where the variation is only contained in a canceled position.
:query boolean testmode: Only return orders with ``testmode`` set to ``true`` or ``false``
@@ -610,17 +609,13 @@ Fetching individual orders
Order ticket download
---------------------
.. versionchanged:: 4.10
The API now supports ticket downloads for pending orders if allowed by the event settings.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/download/(output)/
Download tickets for an order, identified by its order code. Depending on the chosen output, the response might
be a ZIP file, PDF file or something else. The order details response contains a list of output options for this
particular order.
Tickets can only be downloaded if ticket downloads are active and depending on event settings the order is either paid or pending. Note that in some cases the
Tickets can be only downloaded if the order is paid and if ticket downloads are active. Note that in some cases the
ticket file might not yet have been created. In that case, you will receive a status code :http:statuscode:`409` and
you are expected to retry the request after a short period of waiting.
@@ -853,7 +848,7 @@ Creating orders
You can supply the following fields of the resource:
* ``code`` (optional) Only ``A-Z`` and ``0-9``, but without ``O`` and ``1``.
* ``code`` (optional)
* ``status`` (optional) Defaults to pending for non-free orders and paid for free orders. You can only set this to
``"n"`` for pending or ``"p"`` for paid. We will create a payment object for this order either in state ``created``
or in state ``confirmed``, depending on this value. If you create a paid order, the ``order_paid`` signal will
@@ -1640,10 +1635,6 @@ Fetching individual positions
Order position ticket download
------------------------------
.. versionchanged:: 4.10
The API now supports ticket downloads for pending orders if allowed by the event settings.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/download/(output)/
Download tickets for one order position, identified by its internal ID.
@@ -1655,7 +1646,7 @@ Order position ticket download
The referenced URL can provide a download or a regular, human-viewable website - so it is advised to open this URL
in a webbrowser and leave it up to the user to handle the result.
Tickets can only be downloaded if ticket downloads are active and depending on event settings the order is either paid or pending. Also, depending on event
Tickets can be only downloaded if the order is paid and if ticket downloads are active. Also, depending on event
configuration downloads might be only unavailable for add-on products or non-admission products.
Note that in some cases the ticket file might not yet have been created. In that case, you will receive a status
code :http:statuscode:`409` and you are expected to retry the request after a short period of waiting.

View File

@@ -89,8 +89,7 @@ Endpoints
:query integer page: The page number in case of a multi-page result set, default is 1
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id`` and ``position``.
Default: ``position``
:query integer subevent: Only return quotas of the sub-event with the given ID.
:query integer subevent__in: Only return quotas of sub-events with one the given IDs (comma-separated).
:query integer subevent: Only return quotas of the sub-event with the given ID
:query string with_availability: Set to ``true`` to get availability information. Can lead to increased answer times.
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch

View File

@@ -1,177 +0,0 @@
.. spelling:: checkin
Data shredders
==============
pretix and it's plugins include a number of data shredders that allow you to clear personal information from the system.
This page shows you how to use these shredders through the API.
.. versionchanged:: 4.12
This feature has been added to the API.
.. warning::
Unlike the user interface, the API will not force you to download tax-relevant data before you delete it.
Listing available shredders
---------------------------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/shredders/
Returns a list of all exporters shredders for a given event.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/shredders/ 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": [
{
"identifier": "question_answers",
"verbose_name": "Answers to questions"
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
Running an export
-----------------
Before you can delete data, you need to start a data export.
Since exports often include large data sets, they might take longer than the duration of an HTTP request. Therefore,
creating an export is a two-step process. First you need to start an export task with one of the following to API
endpoints:
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/shredders/export/
Starts an export task. If your input parameters validate correctly, a ``202 Accepted`` status code is returned.
The body points you to the download URL of the result as well as the URL for the next step.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/shredders/export/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"shredders": ["question_answers"]
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 202 Accepted
Vary: Accept
Content-Type: application/json
{
"download": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/shredders/download/29891ede-196f-4942-9e26-d055a36e98b8/3f279f13-c198-4137-b49b-9b360ce9fcce/",
"shred": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/shredders/shred/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 identifier: The ``identifier`` field of the exporter to run
:statuscode 202: no error
:statuscode 400: Invalid input options or event data is not ready to be deleted
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
Downloading the result
----------------------
When starting an export, you receive a ``download`` URL for downloading the result. Running a ``GET`` request on that result will
yield one of the following status codes:
* ``200 OK`` The export succeeded. The body will be your resulting file. Might be large!
* ``409 Conflict`` Your export 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`` Running the export has failed permanently. The body will be JSON with the structure ``{"status": "failed", "message": "Error message"}``
* ``404 Not Found`` The export does not exist / is expired / belongs to a different API key.
Shredding the data
------------------
When starting an export, you receive a ``shred`` URL for actually shredding the data.
You can only start the actual shredding process after the export file was generated, however you are not forced to download
the file (we'd recommend it in most cases, though).
The download will no longer be possible after the shredding.
Since shredding often requires deleting large data sets, it might take longer than the duration of an HTTP request.
Therefore, shredding again is a two-step process. First you need to start a shredder task with one of the following to API
endpoints:
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/shredders/shred/(id1)/(id2)/
Starts an export task. If your input parameters validate correctly, a ``202 Accepted`` status code is returned.
The body points you to an URL you can use to check the status.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/shredders/shred/29891ede-196f-4942-9e26-d055a36e98b8/3f279f13-c198-4137-b49b-9b360ce9fcce/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 202 Accepted
Vary: Accept
Content-Type: application/json
{
"status": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/shredders/status/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 id1: Opaque value given to you in the previous response
:param id2: Opaque value given to you in the previous response
:statuscode 202: no error
:statuscode 400: Invalid input options
:statuscode 401: Authentication failure
:statuscode 404: The export does not exist / is expired / belongs to a different API key.
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 409: Your export 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.
:statuscode 410: Either the job has timed out or running the export has failed permanently. The body will be JSON with the structure ``{"status": "failed", "message": "Error message"}``
Checking the result
-------------------
When starting to shred, you receive a ``status`` URL for checking for success.
Running a ``GET`` request on that result will yield one of the following status codes:
* ``200 OK`` The shredding succeeded.
* ``409 Conflict`` Shredding 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`` We no longer know about this process, probably the process was started more than an hour ago. Might also occur after successful operations on small pretix installations without asynchronous task handling.
* ``417 Expectation Failed`` Running the export has failed permanently. The body will be JSON with the structure ``{"status": "failed", "message": "Error message"}``

View File

@@ -474,7 +474,6 @@ Endpoints
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned.
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned.
:query sales_channel: If set to a sales channel identifier, the response will only contain subevents from events available on this sales channel.
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 KiB

After

Width:  |  Height:  |  Size: 236 KiB

View File

@@ -2,25 +2,8 @@
partition "data-based check" {
"Check based on local database" -down-> "Is addon_match set to true?"
"Check based on local database" --> "Is the order in status PAID or PENDING\nand is the position not canceled?"
--> if "" then
-down->[no] "Is the order in status PAID or PENDING\nand is the position not canceled?"
else
-right->[yes] "Build a list that includes the position\nas well as all its add-ons"
-down-> "Filter list for products that are part of the check-in list"
--> if "" then
-down->[one found] Proceed with the matching position
--> "Is the order in status PAID or PENDING\nand is the position not canceled?"
else
--> if "" then
-right->[none found] "Return error PRODUCT "
else
-down->[multiple found] Return error AMBIGUOUS
endif
endif
endif
"Is the order in status PAID or PENDING\nand is the position not canceled?" --> if "" then
-right->[no] "Return error CANCELED"
else
-down->[yes] "Is the product part of the check-in list?"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

After

Width:  |  Height:  |  Size: 147 KiB

View File

@@ -19,25 +19,8 @@ else
endif
===CHECK=== -down-> "Is addon_match set to true?"
===CHECK=== -down-> "Is the order in status PAID or PENDING\nand is the position not canceled?"
--> if "" then
-down->[no] "Is the order in status PAID or PENDING\nand is the position not canceled?"
else
-right->[yes] "Build a list that includes the position\nas well as all its add-ons"
-down-> "Filter list for products that are part of the check-in list"
--> if "" then
-down->[one found] Proceed with the matching position
--> "Is the order in status PAID or PENDING\nand is the position not canceled?"
else
--> if "" then
-right->[none found] "Return error PRODUCT "
else
-down->[multiple found] Return error AMBIGUOUS
endif
endif
endif
"Is the order in status PAID or PENDING\nand is the position not canceled?" --> if "" then
-right->[no] "Return error CANCELED"
else
-down->[yes] "Is the product part of the check-in list?"

View File

@@ -227,108 +227,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:get:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/(id)/vouchers/
Returns a list of all vouchers connected to an exhibitor. The response contains the same data as described in
:ref:`rest-vouchers`.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/exhibitors/1/vouchers/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"code": "43K6LKM37FBVR2YG",
"max_usages": 1,
"redeemed": 0,
"valid_until": null,
"block_quota": false,
"allow_ignore_quota": false,
"price_mode": "set",
"value": "12.00",
"item": 1,
"variation": null,
"quota": null,
"tag": "testvoucher",
"comment": "",
"seat": null,
"subevent": null
}
]
}
:query page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of the event to fetch
:param id: The ``id`` field of the exhibitor to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer or event or exhibitor does not exist **or** you have no permission to view it.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/(id)/vouchers/attach/
Attaches an **existing** voucher to an exhibitor. You need to send either the ``id`` **or** the ``code`` field of
the voucher.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/exhibitors/1/vouchers/attach/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
{
"id": 15
}
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/exhibitors/1/vouchers/attach/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
{
"code": "43K6LKM37FBVR2YG"
}
**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.
@@ -532,7 +430,6 @@ event object Object describi
├ imprint_url string URL to legal notice page. If not ``null``, a button in the app should link to this page.
├ privacy_url string URL to privacy notice page. If not ``null``, a button in the app should link to this page.
├ help_url string URL to help page. If not ``null``, a button in the app should link to this page.
├ terms_url string URL to terms of service. If not ``null``, a button in the app should link to this page.
├ logo_url string URL to event logo. If not ``null``, this logo may be shown in the app.
├ slug string Event short form
└ organizer string Organizer short form
@@ -568,7 +465,6 @@ scan_types list of objects Only used for a
"imprint_url": null,
"privacy_url": null,
"help_url": null,
"terms_url": null,
"logo_url": null,
"organizer": "sampleconf"
},

View File

@@ -19,7 +19,6 @@ If you want to **create** a plugin, please go to the
certificates
digital
exhibitors
shipping
imported_secrets
webinar
presale-saml

View File

@@ -1,235 +0,0 @@
Shipping
========
The shipping plugin provides a HTTP API that exposes the various layouts used to generate PDF badges.
Shipping address resource
-------------------------
The shipping address resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
company string Customer company name
name string Customer name
street string Customer street
zipcode string Customer ZIP code
city string Customer city
country string Customer country code
state string Customer state (ISO 3166-2 code). Only supported in
AU, BR, CA, CN, MY, MX, and US.
gift boolean Request by customer to not disclose prices in the shipping
===================================== ========================== =======================================================
Shipping status resource
------------------------
The shipping status resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
method integer Internal ID of shipping method
status string Status, one of ``"new"`` or ``"shipped"``
method_type string Method type, one of ``"ship"``, ``"online"``, or ``"collect"``
===================================== ========================== =======================================================
Print job resource
------------------
The print job resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
code string Order code of the ticket order
event string Event slug
status string Status, one of ``"new"`` or ``"shipped"``
method string Method type, one of ``"ship"``, ``"online"``, or ``"collect"``
===================================== ========================== =======================================================
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/shippingaddress/
Returns the shipping address of an order
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/democon/orders/ABC12/shippingaddress/ 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
{
"company": "ACME Corp",
"name": "John Doe",
"street": "Sesame Street 12\nAp. 5",
"zipcode": "12345",
"city": "Berlin",
"country": "DE",
"state": "",
"gift": false
}
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of a valid event
:param order: The ``code`` field of a valid order
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
:statuscode 404: The order does not exist or no shipping address is attached.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/shippingaddress/
Returns the shipping status of an order
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/democon/orders/ABC12/shippingstatus/ 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
{
"method": 23,
"method_type": "ship",
"status": "new"
}
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of a valid event
:param order: The ``code`` field of a valid order
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
:statuscode 404: The order does not exist or no shipping address is attached.
.. http:get:: /api/v1/organizers/(organizer)/printjobs/
Returns a list of ticket orders, only useful with some query filters
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/printjobs/?method=ship&status=new 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": [
{
"event": "democon",
"order": "ABC12",
"method": "ship",
"status": "new"
}
]
}
:query string method: Filter by response field ``method`` (can be passed multiple times)
:query string status: Filter by response field ``status``
:query string event: Filter by response field ``event``
: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)/printjobs/poll/
Returns the PDF file for the next job to print.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/printjobs/poll/?method=ship&status=new 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/pdf
X-Pretix-Order-Code: ABC12
...
:query string method: Filter by response field ``method`` (can be passed multiple times)
:query string status: Filter by response field ``status``
:query string event: Filter by response field ``event``
: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:post:: /api/v1/organizers/(organizer)/printjobs/(order)/ack/
Change an order's status to "shipped".
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/printjobs/ABC12/ack/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of a valid event
:param order: The ``code`` field of a valid order
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
:statuscode 404: The order does not exist.

View File

@@ -78,7 +78,7 @@ Synchronization setting any
----------------------------------------------- ----------------------------------- ----------------------------------------------------------------------- -----------------------------------------------------------------------
Ticket secrets any Random Signed Random Signed
=============================================== =================================== =================================== =================================== ================================= =====================================
Scenario supported on platforms Android, Desktop, iOS Android, Desktop, iOS Android, Desktop Android, Desktop, iOS Android, Desktop, iOS
Scenario supported on platforms Android, Desktop, iOS Android, Desktop, iOS Android, Desktop Android, Desktop Android, Desktop
Synchronization speed for large data sets slow slow fast fast
Tickets can be scanned yes yes yes no yes
Ticket is valid after sale immediately next sync (~5 minutes) immediately never immediately
@@ -90,7 +90,6 @@ Name and seat visible on scanner yes
Order-specific check-in attention flag yes yes yes (except directly after sale) n/a no
Ticket search by order code or name yes yes yes (except directly after sale) no no
Check-in statistics on scanner yes yes mostly accurate no no
Support for add-on check-in with main ticket yes yes yes (except directly after sale) no no
=============================================== =================================== =================================== =================================== ================================= =====================================
.. _EdDSA: https://en.wikipedia.org/wiki/EdDSA#Ed25519

View File

@@ -135,10 +135,6 @@ Alternatively, you can select one or more categories to be shown::
<pretix-widget event="https://pretix.eu/demo/democon/" categories="12,25"></pretix-widget>
Or variation IDs::
<pretix-widget event="https://pretix.eu/demo/democon/" variations="15,2,68"></pretix-widget>
Multi-event selection
---------------------

View File

@@ -14,8 +14,6 @@ recursive-include pretix/plugins/manualpayment/templates *
recursive-include pretix/plugins/manualpayment/static *
recursive-include pretix/plugins/paypal/templates *
recursive-include pretix/plugins/paypal/static *
recursive-include pretix/plugins/paypal2/templates *
recursive-include pretix/plugins/paypal2/static *
recursive-include pretix/plugins/pretixdroid/templates *
recursive-include pretix/plugins/pretixdroid/static *
recursive-include pretix/plugins/sendmail/templates *

View File

@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "4.13.0.dev0"
__version__ = "4.10.0.dev0"

View File

@@ -45,7 +45,6 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
allowlist = (
('GET', 'api-v1:version'),
('GET', 'api-v1:device.eventselection'),
('GET', 'api-v1:idempotency.query'),
('POST', 'api-v1:device.update'),
('POST', 'api-v1:device.revoke'),
('POST', 'api-v1:device.roll'),
@@ -68,8 +67,6 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:orderposition-pdf_image'),
('GET', 'api-v1:event.settings'),
('POST', 'api-v1:upload'),
('POST', 'api-v1:checkinrpc.redeem'),
('GET', 'api-v1:checkinrpc.search'),
)
@@ -79,7 +76,6 @@ class PretixScanNoSyncNoSearchSecurityProfile(AllowListSecurityProfile):
allowlist = (
('GET', 'api-v1:version'),
('GET', 'api-v1:device.eventselection'),
('GET', 'api-v1:idempotency.query'),
('POST', 'api-v1:device.update'),
('POST', 'api-v1:device.revoke'),
('POST', 'api-v1:device.roll'),
@@ -100,8 +96,6 @@ class PretixScanNoSyncNoSearchSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:orderposition-pdf_image'),
('GET', 'api-v1:event.settings'),
('POST', 'api-v1:upload'),
('POST', 'api-v1:checkinrpc.redeem'),
('GET', 'api-v1:checkinrpc.search'),
)
@@ -111,7 +105,6 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
allowlist = (
('GET', 'api-v1:version'),
('GET', 'api-v1:device.eventselection'),
('GET', 'api-v1:idempotency.query'),
('POST', 'api-v1:device.update'),
('POST', 'api-v1:device.revoke'),
('POST', 'api-v1:device.roll'),
@@ -133,8 +126,6 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:orderposition-pdf_image'),
('GET', 'api-v1:event.settings'),
('POST', 'api-v1:upload'),
('POST', 'api-v1:checkinrpc.redeem'),
('GET', 'api-v1:checkinrpc.search'),
)
@@ -144,7 +135,6 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
allowlist = (
('GET', 'api-v1:version'),
('GET', 'api-v1:device.eventselection'),
('GET', 'api-v1:idempotency.query'),
('POST', 'api-v1:device.update'),
('POST', 'api-v1:device.revoke'),
('POST', 'api-v1:device.roll'),
@@ -200,8 +190,6 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
('GET', 'plugins:pretix_seating:event.plan'),
('GET', 'plugins:pretix_seating:selection.simple'),
('POST', 'api-v1:upload'),
('POST', 'api-v1:checkinrpc.redeem'),
('GET', 'api-v1:checkinrpc.search'),
)

View File

@@ -23,7 +23,6 @@ import os
from datetime import timedelta
from django.core.files import File
from django.db.models import Q
from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django.utils.translation import gettext_lazy
@@ -34,7 +33,7 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.order import (
AnswerCreateSerializer, AnswerSerializer, InlineSeatSerializer,
)
from pretix.base.models import Quota, Seat, Voucher
from pretix.base.models import Quota, Seat
from pretix.base.models.orders import CartPosition
@@ -62,12 +61,11 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
seat = serializers.CharField(required=False, allow_null=True)
sales_channel = serializers.CharField(required=False, default='sales_channel')
includes_tax = serializers.BooleanField(required=False, allow_null=True)
voucher = serializers.CharField(required=False, allow_null=True)
class Meta:
model = CartPosition
fields = ('cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
'subevent', 'expires', 'includes_tax', 'answers', 'seat', 'sales_channel', 'voucher')
'subevent', 'expires', 'includes_tax', 'answers', 'seat', 'sales_channel')
def create(self, validated_data):
answers_data = validated_data.pop('answers')
@@ -127,46 +125,14 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
raise ValidationError('The specified seat ID is not unique.')
else:
validated_data['seat'] = seat
if not seat.is_available(
sales_channel=validated_data.get('sales_channel', 'web'),
distance_ignore_cart_id=validated_data['cart_id'],
):
raise ValidationError(gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
elif seated:
raise ValidationError('The specified product requires to choose a seat.')
if validated_data.get('voucher'):
try:
voucher = self.context['event'].vouchers.get(code__iexact=validated_data.get('voucher'))
except Voucher.DoesNotExist:
raise ValidationError('The specified voucher does not exist.')
if voucher and not voucher.applies_to(validated_data.get('item'), validated_data.get('variation')):
raise ValidationError('The specified voucher is not valid for the given item and variation.')
if voucher and voucher.seat and voucher.seat != validated_data.get('seat'):
raise ValidationError('The specified voucher is not valid for this seat.')
if voucher and voucher.subevent_id and (not validated_data.get('subevent') or voucher.subevent_id != validated_data['subevent'].pk):
raise ValidationError('The specified voucher is not valid for this subevent.')
if voucher.valid_until is not None and voucher.valid_until < now():
raise ValidationError('The specified voucher is expired.')
redeemed_in_carts = CartPosition.objects.filter(
Q(voucher=voucher) & Q(event=self.context['event']) & Q(expires__gte=now())
)
cart_count = redeemed_in_carts.count()
v_avail = voucher.max_usages - voucher.redeemed - cart_count
if v_avail < 1:
raise ValidationError('The specified voucher has already been used the maximum number of times.')
validated_data['voucher'] = voucher
if validated_data.get('seat'):
if not validated_data['seat'].is_available(
sales_channel=validated_data.get('sales_channel', 'web'),
distance_ignore_cart_id=validated_data['cart_id'],
ignore_voucher_id=validated_data['voucher'].pk if validated_data.get('voucher') else None,
):
raise ValidationError(
gettext_lazy('The selected seat "{seat}" is not available.').format(seat=validated_data['seat'].name))
validated_data.pop('sales_channel')
# todo: does this make sense?
validated_data['custom_price_input'] = validated_data['price']

View File

@@ -26,7 +26,7 @@ from rest_framework.exceptions import ValidationError
from pretix.api.serializers.event import SubEventSerializer
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.channels import get_all_sales_channels
from pretix.base.models import Checkin, CheckinList
from pretix.base.models import CheckinList
class CheckinListSerializer(I18nAwareModelSerializer):
@@ -37,7 +37,7 @@ class CheckinListSerializer(I18nAwareModelSerializer):
model = CheckinList
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
'include_pending', 'auto_checkin_sales_channels', 'allow_multiple_entries', 'allow_entry_after_exit',
'rules', 'exit_all_at', 'addon_match')
'rules', 'exit_all_at')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -78,31 +78,3 @@ class CheckinListSerializer(I18nAwareModelSerializer):
CheckinList.validate_rules(data.get('rules'))
return data
class CheckinRPCRedeemInputSerializer(serializers.Serializer):
lists = serializers.PrimaryKeyRelatedField(required=True, many=True, queryset=CheckinList.objects.none())
secret = serializers.CharField(required=True, allow_null=False)
force = serializers.BooleanField(default=False, required=False)
type = serializers.ChoiceField(choices=Checkin.CHECKIN_TYPES, default=Checkin.TYPE_ENTRY)
ignore_unpaid = serializers.BooleanField(default=False, required=False)
questions_supported = serializers.BooleanField(default=True, required=False)
nonce = serializers.CharField(required=False, allow_null=True)
datetime = serializers.DateTimeField(required=False, allow_null=True)
answers = serializers.JSONField(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')
class MiniCheckinListSerializer(I18nAwareModelSerializer):
event = serializers.SlugRelatedField(slug_field='slug', read_only=True)
subevent = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = CheckinList
fields = ('id', 'name', 'event', 'subevent', 'include_pending')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

View File

@@ -54,10 +54,7 @@ from pretix.base.models.items import SubEventItem, SubEventItemVariation
from pretix.base.services.seating import (
SeatProtected, generate_seats, validate_plan_change,
)
from pretix.base.settings import (
PERSON_NAME_SALUTATIONS, PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS,
LazyI18nStringList, validate_event_settings,
)
from pretix.base.settings import LazyI18nStringList, validate_event_settings
from pretix.base.signals import api_event_settings_fields
logger = logging.getLogger(__name__)
@@ -765,7 +762,6 @@ class EventSettingsSerializer(SettingsSerializer):
'cancel_allow_user_paid_adjust_fees_step',
'cancel_allow_user_paid_refund_as_giftcard',
'cancel_allow_user_paid_require_approval',
'cancel_allow_user_paid_require_approval_fee_unknown',
'change_allow_user_variation',
'change_allow_user_addons',
'change_allow_user_until',
@@ -780,7 +776,6 @@ class EventSettingsSerializer(SettingsSerializer):
'logo_image_large',
'logo_show_title',
'og_image',
'name_scheme',
]
def __init__(self, *args, **kwargs):
@@ -846,25 +841,4 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
'invoice_address_from_country',
'invoice_address_from_tax_id',
'invoice_address_from_vat_id',
'name_scheme',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['_name_scheme_fields'] = serializers.JSONField(
read_only=True,
default=[{"key": k, "label": str(v), "weight": w} for k, v, w, *__ in PERSON_NAME_SCHEMES.get(self.event.settings.name_scheme)['fields']]
)
self.fields['_name_scheme_salutations'] = serializers.JSONField(
read_only=True,
default=[{"key": k, "label": str(v)} for k, v in PERSON_NAME_SALUTATIONS]
)
self.fields['_name_scheme_titles'] = serializers.JSONField(
read_only=True,
default=(
[{"key": k, "label": k}
for k in PERSON_NAME_TITLE_GROUPS.get(self.event.settings.name_scheme_titles)[1]]
if self.event.settings.name_scheme_titles
else []
)
)

View File

@@ -184,9 +184,8 @@ class ItemSerializer(I18nAwareModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.read_only:
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
self.fields['grant_membership_type'].queryset = self.context['event'].organizer.membership_types.all()
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
self.fields['grant_membership_type'].queryset = self.context['event'].organizer.membership_types.all()
def validate(self, data):
data = super().validate(data)

View File

@@ -92,7 +92,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')
'state', 'vat_id', 'vat_id_validated', 'internal_reference')
read_only_fields = ('last_modified',)
def __init__(self, *args, **kwargs):
@@ -341,10 +341,10 @@ class PdfDataSerializer(serializers.Field):
# we serialize a list.
if 'vars' not in self.context:
self.context['vars'] = get_variables(self.context['event'])
self.context['vars'] = get_variables(self.context['request'].event)
if 'vars_images' not in self.context:
self.context['vars_images'] = get_images(self.context['event'])
self.context['vars_images'] = get_images(self.context['request'].event)
for k, f in self.context['vars'].items():
try:
@@ -422,14 +422,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
request = self.context.get('request')
pdf_data_forbidden = (
# We check this based on permission if we are on /events/…/orders/ or /events/…/orderpositions/ or
# /events/…/checkinlists/…/positions/
# We're unable to check this on this level if we're on /checkinrpc/, in which case we rely on the view
# layer to not set pdf_data=true in the first place.
request and hasattr(request, 'event') and 'can_view_orders' not in request.eventpermset
)
if ('pdf_data' in self.context and not self.context['pdf_data']) or pdf_data_forbidden:
if request and (not request.query_params.get('pdf_data', 'false') == 'true' or 'can_view_orders' not in request.eventpermset):
self.fields.pop('pdf_data', None)
def validate(self, data):
@@ -488,13 +481,13 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'subevent' in self.context['expand']:
if 'subevent' in self.context['request'].query_params.getlist('expand'):
self.fields['subevent'] = SubEventSerializer(read_only=True)
if 'item' in self.context['expand']:
if 'item' in self.context['request'].query_params.getlist('expand'):
self.fields['item'] = ItemSerializer(read_only=True, context=self.context)
if 'variation' in self.context['expand']:
if 'variation' in self.context['request'].query_params.getlist('expand'):
self.fields['variation'] = InlineItemVariationSerializer(read_only=True)
@@ -597,10 +590,10 @@ class OrderSerializer(I18nAwareModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.context['pdf_data']:
if not self.context['request'].query_params.get('pdf_data', 'false') == 'true':
self.fields['positions'].child.fields.pop('pdf_data', None)
for exclude_field in self.context['exclude']:
for exclude_field in self.context['request'].query_params.getlist('exclude'):
p = exclude_field.split('.')
if p[0] in self.fields:
if len(p) == 1:
@@ -984,18 +977,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
else:
ia = None
lock_required = False
for pos_data in positions_data:
pos_data['_quotas'] = list(
pos_data.get('variation').quotas.filter(subevent=pos_data.get('subevent'))
if pos_data.get('variation')
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent'))
)
if pos_data.get('voucher') or pos_data.get('seat') or any(q.size is not None for q in pos_data['_quotas']):
lock_required = True
lockfn = self.context['event'].lock
if simulate or not lock_required:
if simulate:
lockfn = NoLockManager
with lockfn() as now_dt:
free_seats = set()
@@ -1119,7 +1102,9 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
str(pos_data.get('item'))
)]
new_quotas = pos_data['_quotas']
new_quotas = (pos_data.get('variation').quotas.filter(subevent=pos_data.get('subevent'))
if pos_data.get('variation')
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
if len(new_quotas) == 0:
errs[i]['item'] = [gettext_lazy('The product "{}" is not assigned to a quota.').format(
str(pos_data.get('item'))
@@ -1173,7 +1158,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
pos_data['attendee_name_parts'] = {
'_legacy': attendee_name
}
pos = OrderPosition(**{k: v for k, v in pos_data.items() if k != 'answers' and k != '_quotas'})
pos = OrderPosition(**{k: v for k, v in pos_data.items() if k != 'answers'})
if simulate:
pos.order = order._wrapped
else:

View File

@@ -396,6 +396,7 @@ class OrderChangeOperationSerializer(serializers.Serializer):
def validate(self, data):
seen_positions = set()
for d in data.get('patch_positions', []):
print(d, seen_positions)
if d['position'] in seen_positions:
raise ValidationError({'patch_positions': ['You have specified the same object twice.']})
seen_positions.add(d['position'])

View File

@@ -75,14 +75,6 @@ class CustomerSerializer(I18nAwareModelSerializer):
'locale', 'last_modified', 'notes')
class CustomerCreateSerializer(CustomerSerializer):
send_email = serializers.BooleanField(default=False, required=False, allow_null=True)
class Meta:
model = Customer
fields = CustomerSerializer.Meta.fields + ('send_email',)
class MembershipTypeSerializer(I18nAwareModelSerializer):
class Meta:

View File

@@ -1,36 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from rest_framework import serializers
class ShredderSerializer(serializers.Serializer):
identifier = serializers.CharField()
verbose_name = serializers.CharField()
class JobRunSerializer(serializers.Serializer):
shredders = serializers.MultipleChoiceField(choices=[])
def __init__(self, *args, **kwargs):
shredders = kwargs.pop('shredders')
super().__init__(*args, **kwargs)
self.fields['shredders'].choices = ((k.identifier, k.identifier) for k in shredders)

View File

@@ -41,9 +41,8 @@ from rest_framework import routers
from pretix.api.views import cart
from .views import (
checkin, device, discount, event, exporters, idempotency, item, oauth,
order, organizer, shredders, upload, user, version, voucher, waitinglist,
webhooks,
checkin, device, discount, event, exporters, item, oauth, order, organizer,
upload, user, version, voucher, waitinglist, webhooks,
)
router = routers.DefaultRouter()
@@ -85,7 +84,6 @@ event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
event_router.register(r'checkinlists', checkin.CheckinListViewSet)
event_router.register(r'cartpositions', cart.CartPositionViewSet)
event_router.register(r'exporters', exporters.EventExportersViewSet, basename='exporters')
event_router.register(r'shredders', shredders.EventShreddersViewSet, basename='shredders')
checkinlist_router = routers.DefaultRouter()
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet, basename='checkinlistpos')
@@ -114,10 +112,6 @@ for app in apps.get_app_configs():
urlpatterns = [
re_path(r'^', include(router.urls)),
re_path(r'^organizers/(?P<organizer>[^/]+)/', include(orga_router.urls)),
re_path(r'^organizers/(?P<organizer>[^/]+)/checkinrpc/redeem/$', checkin.CheckinRPCRedeemView.as_view(),
name="checkinrpc.redeem"),
re_path(r'^organizers/(?P<organizer>[^/]+)/checkinrpc/search/$', checkin.CheckinRPCSearchView.as_view(),
name="checkinrpc.search"),
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)),
@@ -139,7 +133,6 @@ urlpatterns = [
re_path(r"^device/roll$", device.RollKeyView.as_view(), name="device.roll"),
re_path(r"^device/revoke$", device.RevokeKeyView.as_view(), name="device.revoke"),
re_path(r"^device/eventselection$", device.EventSelectionView.as_view(), name="device.eventselection"),
re_path(r"^idempotency_query$", idempotency.IdempotencyQueryView.as_view(), name="idempotency.query"),
re_path(r"^upload$", upload.UploadView.as_view(), name="upload"),
re_path(r"^me$", user.MeView.as_view(), name="user.me"),
re_path(r"^version$", version.VersionView.as_view(), name="version"),

View File

@@ -19,16 +19,12 @@
# 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 functools import reduce
import django_filters
from django.conf import settings
from django.core.exceptions import ValidationError as BaseValidationError
from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import (
Count, Exists, F, Max, OrderBy, OuterRef, Prefetch, Q, Subquery,
prefetch_related_objects,
)
from django.db.models.functions import Coalesce
from django.http import Http404
@@ -38,18 +34,13 @@ from django.utils.timezone import now
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from packaging.version import parse
from rest_framework import views, viewsets
from rest_framework import viewsets
from rest_framework.decorators import action
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, CheckinRPCRedeemInputSerializer,
MiniCheckinListSerializer,
)
from pretix.api.serializers.checkin import CheckinListSerializer
from pretix.api.serializers.item import QuestionSerializer
from pretix.api.serializers.order import (
CheckinListOrderPositionSerializer, FailedCheckinSerializer,
@@ -59,7 +50,7 @@ from pretix.api.views.order import OrderPositionFilter
from pretix.base.i18n import language
from pretix.base.models import (
CachedFile, Checkin, CheckinList, Device, Event, Order, OrderPosition,
Question, RevokedTicketSecret, TeamAPIToken,
Question,
)
from pretix.base.services.checkin import (
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
@@ -166,7 +157,6 @@ class CheckinListViewSet(viewsets.ModelViewSet):
list=self.get_object(),
successful=False,
forced=True,
force_sent=True,
device=self.request.auth if isinstance(self.request.auth, Device) else None,
gate=self.request.auth.gate if isinstance(self.request.auth, Device) else None,
**kwargs,
@@ -274,399 +264,6 @@ with scopes_disabled():
return queryset.filter(SQLLogic(self.checkinlist).apply(self.checkinlist.rules))
def _handle_file_upload(data, user, auth):
try:
cf = CachedFile.objects.get(
session_key=f'api-upload-{str(type(user or auth))}-{(user or auth).pk}',
file__isnull=False,
pk=data[len("file:"):],
)
except (ValidationError, BaseValidationError, IndexError): # invalid uuid
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
except CachedFile.DoesNotExist:
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
allowed_types = (
'image/png', 'image/jpeg', 'image/gif', 'application/pdf'
)
if cf.type not in allowed_types:
raise ValidationError('The submitted file "{fid}" has a file type that is not allowed in this field.'.format(fid=data))
if cf.file.size > settings.FILE_UPLOAD_MAX_SIZE_OTHER:
raise ValidationError('The submitted file "{fid}" is too large to be used in this field.'.format(fid=data))
return cf.file
def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_products=False, pdf_data=False, expand=None):
list_by_event = {cl.event_id: cl for cl in checkinlists}
if not checkinlists:
raise ValidationError('No check-in list passed.')
if len(list_by_event) != len(checkinlists):
raise ValidationError('Selecting two check-in lists from the same event is unsupported.')
cqs = Checkin.objects.filter(
position_id=OuterRef('pk'),
list_id__in=[cl.pk for cl in checkinlists]
).order_by().values('position_id').annotate(
m=Max('datetime')
).values('m')
qs = OrderPosition.objects.filter(
order__event__in=list_by_event.keys(),
).annotate(
last_checked_in=Subquery(cqs)
).prefetch_related('order__event', 'order__event__organizer')
lists_qs = []
for checkinlist in checkinlists:
list_q = Q(order__event_id=checkinlist.event_id)
if checkinlist.subevent:
list_q &= Q(subevent=checkinlist.subevent)
if not ignore_status:
list_q &= Q(order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if checkinlist.include_pending else [Order.STATUS_PAID])
if not checkinlist.all_products and not ignore_products:
list_q &= Q(item__in=checkinlist.limit_products.values_list('id', flat=True))
lists_qs.append(list_q)
qs = qs.filter(reduce(operator.or_, lists_qs))
if pdf_data:
qs = qs.prefetch_related(
Prefetch(
lookup='checkins',
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists])
),
'answers', 'answers__options', 'answers__question',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
Prefetch(
'event',
Event.objects.select_related('organizer')
),
Prefetch(
'positions',
OrderPosition.objects.prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.all()),
'item', 'variation', 'answers', 'answers__options', 'answers__question',
)
)
))
).select_related(
'item', 'variation', 'item__category', 'addon_to', 'order', 'order__invoice_address', 'seat'
)
else:
qs = qs.prefetch_related(
Prefetch(
lookup='checkins',
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists])
),
'answers', 'answers__options', 'answers__question',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat')
if expand and 'subevent' in expand:
qs = qs.prefetch_related(
'subevent', 'subevent__event', 'subevent__subeventitem_set', 'subevent__subeventitemvariation_set',
'subevent__seat_category_mappings', 'subevent__meta_values'
)
if expand and 'item' in expand:
qs = qs.prefetch_related('item', 'item__addons', 'item__bundles', 'item__meta_values',
'item__variations').select_related('item__tax_rule')
if expand and 'variation' in expand:
qs = qs.prefetch_related('variation')
return qs
def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force, checkin_type, ignore_unpaid, nonce,
untrusted_input, user, auth, expand, pdf_data, request, questions_supported, canceled_supported,
legacy_url_support=False):
if not checkinlists:
raise ValidationError('No check-in list passed.')
list_by_event = {cl.event_id: cl for cl in checkinlists}
prefetch_related_objects([cl for cl in checkinlists if not cl.all_products], 'limit_products')
device = auth if isinstance(auth, Device) else None
gate = auth.gate if isinstance(auth, Device) else None
context = {
'request': request,
'expand': expand,
}
def _make_context(context, event):
return {
**context,
'event': op.order.event,
'pdf_data': pdf_data and (
user if user and user.is_authenticated else auth
).has_event_permission(request.organizer, event, 'can_view_orders', request),
}
common_checkin_args = dict(
raw_barcode=raw_barcode,
type=checkin_type,
list=checkinlists[0],
datetime=datetime,
device=device,
gate=gate,
nonce=nonce,
forced=force,
)
raw_barcode_for_checkin = None
from_revoked_secret = False
# 1. Gather a list of positions that could be the one we looking fore, either from their ID, secret or
# parent secret
queryset = _checkin_list_position_queryset(checkinlists, pdf_data=pdf_data, ignore_status=True, ignore_products=True).order_by(
F('addon_to').asc(nulls_first=True)
)
q = Q(secret=raw_barcode)
if any(cl.addon_match for cl in checkinlists):
q |= Q(addon_to__secret=raw_barcode)
if raw_barcode.isnumeric() and not untrusted_input and legacy_url_support:
q |= Q(pk=raw_barcode)
op_candidates = list(queryset.filter(q))
if not op_candidates and '+' in raw_barcode and legacy_url_support:
# In application/x-www-form-urlencoded, you can encodes space ' ' with '+' instead of '%20'.
# `id`, however, is part of a path where this technically is not allowed. Old versions of our
# scan apps still do it, so we try work around it!
q = Q(secret=raw_barcode.replace('+', ' '))
if any(cl.addon_match for cl in checkinlists):
q |= Q(addon_to__secret=raw_barcode.replace('+', ' '))
op_candidates = list(queryset.filter(q))
# 2. Handle the "nothing found" case: Either it's really a bogus secret that we don't know (-> error), or it
# might be a revoked one that we actually know (-> error, but with better error message and logging and
# with respecting the force option).
if not op_candidates:
revoked_matches = list(RevokedTicketSecret.objects.filter(event_id__in=list_by_event.keys(), secret=raw_barcode))
if len(revoked_matches) == 0:
checkinlists[0].event.log_action('pretix.event.checkin.unknown', data={
'datetime': datetime,
'type': checkin_type,
'list': checkinlists[0].pk,
'barcode': raw_barcode,
'searched_lists': [cl.pk for cl in checkinlists]
}, user=user, auth=auth)
for cl in checkinlists:
for k, s in cl.event.ticket_secret_generators.items():
try:
parsed = s.parse_secret(raw_barcode)
common_checkin_args.update({
'raw_item': parsed.item,
'raw_variation': parsed.variation,
'raw_subevent': parsed.subevent,
})
except:
pass
Checkin.objects.create(
position=None,
successful=False,
error_reason=Checkin.REASON_INVALID,
**common_checkin_args,
)
if force and legacy_url_support and isinstance(auth, Device):
# There was a bug in libpretixsync: If you scanned a ticket in offline mode that was
# valid at the time but no longer exists at time of upload, the device would retry to
# upload the same scan over and over again. Since we can't update all devices quickly,
# here's a dirty workaround to make it stop.
try:
brand = auth.software_brand
ver = parse(auth.software_version)
legacy_mode = (
(brand == 'pretixSCANPROXY' and ver < parse('0.0.3')) or
(brand == 'pretixSCAN Android' and ver < parse('1.11.2')) or
(brand == 'pretixSCAN' and ver < parse('1.11.2'))
)
if legacy_mode:
return Response({
'status': 'error',
'reason': Checkin.REASON_ALREADY_REDEEMED,
'reason_explanation': None,
'require_attention': False,
'__warning': 'Compatibility hack active due to detected old pretixSCAN version',
}, status=400)
except: # we don't care e.g. about invalid version numbers
pass
return Response({
'detail': 'Not found.', # for backwards compatibility
'status': 'error',
'reason': Checkin.REASON_INVALID,
'reason_explanation': None,
'require_attention': False,
'list': MiniCheckinListSerializer(checkinlists[0]).data,
}, status=404)
elif revoked_matches and force:
op_candidates = [revoked_matches[0].position]
if list_by_event[revoked_matches[0].event_id].addon_match:
op_candidates += list(revoked_matches[0].position.addons.all())
raw_barcode_for_checkin = raw_barcode
from_revoked_secret = True
else:
op = revoked_matches[0].position
op.order.log_action('pretix.event.checkin.revoked', data={
'datetime': datetime,
'type': checkin_type,
'list': list_by_event[revoked_matches[0].event_id].pk,
'barcode': raw_barcode
}, user=user, auth=auth)
common_checkin_args['list'] = list_by_event[revoked_matches[0].event_id]
Checkin.objects.create(
position=op,
successful=False,
error_reason=Checkin.REASON_REVOKED,
**common_checkin_args
)
return Response({
'status': 'error',
'reason': Checkin.REASON_REVOKED,
'reason_explanation': None,
'require_attention': False,
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, revoked_matches[0].event)).data,
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
}, status=400)
# 3. Handle the "multiple options found" case: Except for the unlikely case of a secret being also a valid primary
# key on the same list, we're probably dealing with the ``addon_match`` case here and need to figure out
# which add-on has the right product.
if len(op_candidates) > 1:
op_candidates_matching_product = [
op for op in op_candidates
if (
(list_by_event[op.order.event_id].addon_match or op.secret == raw_barcode or legacy_url_support) and
(list_by_event[op.order.event_id].all_products or op.item_id in {i.pk for i in list_by_event[op.order.event_id].limit_products.all()})
)
]
if len(op_candidates_matching_product) == 0:
# None of the found add-ons has the correct product, too bad! We could just error out here, but
# instead we just continue with *any* product and have it rejected by the check in perform_checkin.
# This has the advantage of a better error message.
op_candidates = [op_candidates[0]]
elif len(op_candidates_matching_product) > 1:
# It's still ambiguous, we'll error out.
# We choose the first match (regardless of product) for the logging since it's most likely to be the
# base product according to our order_by above.
op = op_candidates[0]
op.order.log_action('pretix.event.checkin.denied', data={
'position': op.id,
'positionid': op.positionid,
'errorcode': Checkin.REASON_AMBIGUOUS,
'reason_explanation': None,
'force': force,
'datetime': datetime,
'type': checkin_type,
'list': list_by_event[op.order.event_id].pk,
}, user=user, auth=auth)
common_checkin_args['list'] = list_by_event[op.order.event_id]
Checkin.objects.create(
position=op,
successful=False,
error_reason=Checkin.REASON_AMBIGUOUS,
error_explanation=None,
**common_checkin_args,
)
return Response({
'status': 'error',
'reason': Checkin.REASON_AMBIGUOUS,
'reason_explanation': None,
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
}, status=400)
else:
op_candidates = op_candidates_matching_product
op = op_candidates[0]
common_checkin_args['list'] = list_by_event[op.order.event_id]
# 5. Pre-validate all incoming answers, handle file upload
given_answers = {}
if answers_data:
for q in op.item.questions.filter(ask_during_checkin=True):
if str(q.pk) in answers_data:
try:
if q.type == Question.TYPE_FILE:
given_answers[q] = _handle_file_upload(answers_data[str(q.pk)], user, auth)
else:
given_answers[q] = q.clean_answer(answers_data[str(q.pk)])
except (ValidationError, BaseValidationError):
pass
# 6. Pass to our actual check-in logic
with language(op.order.event.settings.locale):
try:
perform_checkin(
op=op,
clist=list_by_event[op.order.event_id],
given_answers=given_answers,
force=force,
ignore_unpaid=ignore_unpaid,
nonce=nonce,
datetime=datetime,
questions_supported=questions_supported,
canceled_supported=canceled_supported,
user=user,
auth=auth,
type=checkin_type,
raw_barcode=raw_barcode_for_checkin,
from_revoked_secret=from_revoked_secret,
)
except RequiredQuestionsError as e:
return Response({
'status': 'incomplete',
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
'questions': [
QuestionSerializer(q).data for q in e.questions
],
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
}, status=400)
except CheckInError as e:
op.order.log_action('pretix.event.checkin.denied', data={
'position': op.id,
'positionid': op.positionid,
'errorcode': e.code,
'reason_explanation': e.reason,
'force': force,
'datetime': datetime,
'type': checkin_type,
'list': list_by_event[op.order.event_id].pk,
}, user=user, auth=auth)
Checkin.objects.create(
position=op,
successful=False,
error_reason=e.code,
error_explanation=e.reason,
**common_checkin_args,
)
return Response({
'status': 'error',
'reason': e.code,
'reason_explanation': e.reason,
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
}, status=400)
else:
return Response({
'status': 'ok',
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
}, status=201)
class ExtendedBackend(DjangoFilterBackend):
def get_filterset_kwargs(self, request, queryset, view):
kwargs = super().get_filterset_kwargs(request, queryset, view)
@@ -682,7 +279,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = CheckinListOrderPositionSerializer
queryset = OrderPosition.all.none()
filter_backends = (ExtendedBackend, RichOrderingFilter)
ordering = (F('attendee_name_cached').asc(nulls_last=True), 'positionid')
ordering = ('attendee_name_cached', 'positionid')
ordering_fields = (
'order__code', 'order__datetime', 'positionid', 'attendee_name',
'last_checked_in', 'order__email',
@@ -711,8 +308,6 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
def get_serializer_context(self):
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') == 'true'
return ctx
def get_filterset_kwargs(self):
@@ -723,18 +318,80 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
@cached_property
def checkinlist(self):
try:
return get_object_or_404(self.request.event.checkin_lists, pk=self.kwargs.get("list"))
return get_object_or_404(CheckinList, event=self.request.event, pk=self.kwargs.get("list"))
except ValueError:
raise Http404()
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') == 'true' or ignore_status,
ignore_products=ignore_products,
pdf_data=self.request.query_params.get('pdf_data', 'false') == 'true',
expand=self.request.query_params.getlist('expand'),
)
cqs = Checkin.objects.filter(
position_id=OuterRef('pk'),
list_id=self.checkinlist.pk
).order_by().values('position_id').annotate(
m=Max('datetime')
).values('m')
qs = OrderPosition.objects.filter(
order__event=self.request.event,
).annotate(
last_checked_in=Subquery(cqs)
).prefetch_related('order__event', 'order__event__organizer')
if self.checkinlist.subevent:
qs = qs.filter(
subevent=self.checkinlist.subevent
)
if self.request.query_params.get('ignore_status', 'false') != 'true' and not ignore_status:
qs = qs.filter(
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.checkinlist.include_pending else [Order.STATUS_PAID]
)
if self.request.query_params.get('pdf_data', 'false') == 'true':
qs = qs.prefetch_related(
Prefetch(
lookup='checkins',
queryset=Checkin.objects.filter(list_id=self.checkinlist.pk)
),
'answers', 'answers__options', 'answers__question',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
Prefetch(
'event',
Event.objects.select_related('organizer')
),
Prefetch(
'positions',
OrderPosition.objects.prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.all()),
'item', 'variation', 'answers', 'answers__options', 'answers__question',
)
)
))
).select_related(
'item', 'variation', 'item__category', 'addon_to', 'order', 'order__invoice_address', 'seat'
)
else:
qs = qs.prefetch_related(
Prefetch(
lookup='checkins',
queryset=Checkin.objects.filter(list_id=self.checkinlist.pk)
),
'answers', 'answers__options', 'answers__question',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat')
if not self.checkinlist.all_products and not ignore_products:
qs = qs.filter(item__in=self.checkinlist.limit_products.values_list('id', flat=True))
if 'subevent' in self.request.query_params.getlist('expand'):
qs = qs.prefetch_related(
'subevent', 'subevent__event', 'subevent__subeventitem_set', 'subevent__subeventitemvariation_set',
'subevent__seat_category_mappings', 'subevent__meta_values'
)
if 'item' in self.request.query_params.getlist('expand'):
qs = qs.prefetch_related('item', 'item__addons', 'item__bundles', 'item__meta_values', 'item__variations').select_related('item__tax_rule')
if 'variation' in self.request.query_params.getlist('expand'):
qs = qs.prefetch_related('variation')
if 'pk' not in self.request.resolver_match.kwargs and 'can_view_orders' not in self.request.eventpermset \
and len(self.request.query_params.get('search', '')) < 3:
@@ -745,153 +402,217 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
@action(detail=False, methods=['POST'], url_name='redeem', url_path='(?P<pk>.*)/redeem')
def redeem(self, *args, **kwargs):
force = bool(self.request.data.get('force', False))
checkin_type = self.request.data.get('type', None) or Checkin.TYPE_ENTRY
if checkin_type not in dict(Checkin.CHECKIN_TYPES):
type = self.request.data.get('type', None) or Checkin.TYPE_ENTRY
if type not in dict(Checkin.CHECKIN_TYPES):
raise ValidationError("Invalid check-in type.")
ignore_unpaid = bool(self.request.data.get('ignore_unpaid', False))
nonce = self.request.data.get('nonce')
untrusted_input = (
self.request.GET.get('untrusted_input', '') not in ('0', 'false', 'False', '')
or (isinstance(self.request.auth, Device) and 'pretixscan' in (self.request.auth.software_brand or '').lower())
)
if 'datetime' in self.request.data:
dt = DateTimeField().to_internal_value(self.request.data.get('datetime'))
else:
dt = now()
answers_data = self.request.data.get('answers')
return _redeem_process(
checkinlists=[self.checkinlist],
raw_barcode=kwargs['pk'],
answers_data=answers_data,
common_checkin_args = dict(
raw_barcode=self.kwargs['pk'],
type=type,
list=self.checkinlist,
datetime=dt,
force=force,
checkin_type=checkin_type,
ignore_unpaid=ignore_unpaid,
device=self.request.auth if isinstance(self.request.auth, Device) else None,
gate=self.request.auth.gate if isinstance(self.request.auth, Device) else None,
nonce=nonce,
untrusted_input=untrusted_input,
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') == '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
legacy_url_support=True,
forced=force,
)
raw_barcode_for_checkin = None
from_revoked_secret = False
try:
queryset = self.get_queryset(ignore_status=True, ignore_products=True)
if self.kwargs['pk'].isnumeric():
op = queryset.get(Q(pk=self.kwargs['pk']) | Q(secret=self.kwargs['pk']))
else:
# In application/x-www-form-urlencoded, you can encodes space ' ' with '+' instead of '%20'.
# `id`, however, is part of a path where this technically is not allowed. Old versions of our
# scan apps still do it, so we try work around it!
try:
op = queryset.get(secret=self.kwargs['pk'])
except OrderPosition.DoesNotExist:
op = queryset.get(secret=self.kwargs['pk'].replace('+', ' '))
except OrderPosition.DoesNotExist:
revoked_matches = list(self.request.event.revoked_secrets.filter(secret=self.kwargs['pk']))
if len(revoked_matches) == 0:
self.request.event.log_action('pretix.event.checkin.unknown', data={
'datetime': dt,
'type': type,
'list': self.checkinlist.pk,
'barcode': self.kwargs['pk']
}, user=self.request.user, auth=self.request.auth)
class CheckinRPCRedeemView(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
for k, s in self.request.event.ticket_secret_generators.items():
try:
parsed = s.parse_secret(self.kwargs['pk'])
common_checkin_args.update({
'raw_item': parsed.item,
'raw_variation': parsed.variation,
'raw_subevent': parsed.subevent,
})
except:
pass
Checkin.objects.create(
position=None,
successful=False,
error_reason=Checkin.REASON_INVALID,
**common_checkin_args,
)
if force and isinstance(self.request.auth, Device):
# There was a bug in libpretixsync: If you scanned a ticket in offline mode that was
# valid at the time but no longer exists at time of upload, the device would retry to
# upload the same scan over and over again. Since we can't update all devices quickly,
# here's a dirty workaround to make it stop.
try:
brand = self.request.auth.software_brand
ver = parse(self.request.auth.software_version)
legacy_mode = (
(brand == 'pretixSCANPROXY' and ver < parse('0.0.3')) or
(brand == 'pretixSCAN Android' and ver < parse('1.11.2')) or
(brand == 'pretixSCAN' and ver < parse('1.11.2'))
)
if legacy_mode:
return Response({
'status': 'error',
'reason': Checkin.REASON_ALREADY_REDEEMED,
'reason_explanation': None,
'require_attention': False,
'__warning': 'Compatibility hack active due to detected old pretixSCAN version',
}, status=400)
except: # we don't care e.g. about invalid version numbers
pass
return Response({
'detail': 'Not found.', # for backwards compatibility
'status': 'error',
'reason': Checkin.REASON_INVALID,
'reason_explanation': None,
'require_attention': False,
}, status=404)
elif revoked_matches and force:
op = revoked_matches[0].position
raw_barcode_for_checkin = self.kwargs['pk']
from_revoked_secret = True
else:
op = revoked_matches[0].position
op.order.log_action('pretix.event.checkin.revoked', data={
'datetime': dt,
'type': type,
'list': self.checkinlist.pk,
'barcode': self.kwargs['pk']
}, user=self.request.user, auth=self.request.auth)
Checkin.objects.create(
position=op,
successful=False,
error_reason=Checkin.REASON_REVOKED,
**common_checkin_args
)
return Response({
'status': 'error',
'reason': Checkin.REASON_REVOKED,
'reason_explanation': None,
'require_attention': False,
'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data
}, status=400)
given_answers = {}
if 'answers' in self.request.data:
aws = self.request.data.get('answers')
for q in op.item.questions.filter(ask_during_checkin=True):
if str(q.pk) in aws:
try:
if q.type == Question.TYPE_FILE:
given_answers[q] = self._handle_file_upload(aws[str(q.pk)])
else:
given_answers[q] = q.clean_answer(aws[str(q.pk)])
except ValidationError:
pass
with language(self.request.event.settings.locale):
try:
perform_checkin(
op=op,
clist=self.checkinlist,
given_answers=given_answers,
force=force,
ignore_unpaid=ignore_unpaid,
nonce=nonce,
datetime=dt,
questions_supported=self.request.data.get('questions_supported', True),
canceled_supported=self.request.data.get('canceled_supported', False),
user=self.request.user,
auth=self.request.auth,
type=type,
raw_barcode=raw_barcode_for_checkin,
from_revoked_secret=from_revoked_secret,
)
except RequiredQuestionsError as e:
return Response({
'status': 'incomplete',
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data,
'questions': [
QuestionSerializer(q).data for q in e.questions
]
}, status=400)
except CheckInError as e:
op.order.log_action('pretix.event.checkin.denied', data={
'position': op.id,
'positionid': op.positionid,
'errorcode': e.code,
'reason_explanation': e.reason,
'force': force,
'datetime': dt,
'type': type,
'list': self.checkinlist.pk
}, user=self.request.user, auth=self.request.auth)
Checkin.objects.create(
position=op,
successful=False,
error_reason=e.code,
error_explanation=e.reason,
**common_checkin_args,
)
return Response({
'status': 'error',
'reason': e.code,
'reason_explanation': e.reason,
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data
}, status=400)
else:
return Response({
'status': 'ok',
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data
}, status=201)
def _handle_file_upload(self, data):
try:
cf = CachedFile.objects.get(
session_key=f'api-upload-{str(type(self.request.user or self.request.auth))}-{(self.request.user or self.request.auth).pk}',
file__isnull=False,
pk=data[len("file:"):],
)
else:
raise ValueError("unknown authentication method")
except (ValidationError, IndexError): # invalid uuid
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
except CachedFile.DoesNotExist:
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
s = CheckinRPCRedeemInputSerializer(data=request.data, context={'events': events})
s.is_valid(raise_exception=True)
return _redeem_process(
checkinlists=s.validated_data['lists'],
raw_barcode=s.validated_data['secret'],
answers_data=s.validated_data.get('answers'),
datetime=s.validated_data.get('datetime') or now(),
force=s.validated_data['force'],
checkin_type=s.validated_data['type'],
ignore_unpaid=s.validated_data['ignore_unpaid'],
nonce=s.validated_data.get('nonce'),
untrusted_input=True,
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') == 'true',
questions_supported=s.validated_data['questions_supported'],
canceled_supported=True,
request=self.request, # this is not clean, but we need it in the serializers for URL generation
legacy_url_support=False,
allowed_types = (
'image/png', 'image/jpeg', 'image/gif', 'application/pdf'
)
if cf.type not in allowed_types:
raise ValidationError('The submitted file "{fid}" has a file type that is not allowed in this field.'.format(fid=data))
if cf.file.size > settings.FILE_UPLOAD_MAX_SIZE_OTHER:
raise ValidationError('The submitted file "{fid}" is too large to be used in this field.'.format(fid=data))
class CheckinRPCSearchView(ListAPIView):
serializer_class = CheckinListOrderPositionSerializer
queryset = OrderPosition.all.none()
filter_backends = (ExtendedBackend, RichOrderingFilter)
ordering = (F('attendee_name_cached').asc(nulls_last=True), 'positionid')
ordering_fields = (
'order__code', 'order__datetime', 'positionid', 'attendee_name',
'last_checked_in', 'order__email',
)
ordering_custom = {
'attendee_name': {
'_order': F('display_name').asc(nulls_first=True),
'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached')
},
'-attendee_name': {
'_order': F('display_name').desc(nulls_last=True),
'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached')
},
'last_checked_in': {
'_order': OrderBy(F('last_checked_in'), nulls_first=True),
},
'-last_checked_in': {
'_order': OrderBy(F('last_checked_in'), nulls_last=True, descending=True),
},
}
filterset_class = OrderPositionFilter
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['expand'] = self.request.query_params.getlist('expand')
ctx['pdf_data'] = False
return ctx
@cached_property
def lists(self):
if isinstance(self.request.auth, (TeamAPIToken, Device)):
events = self.request.auth.get_events_with_permission(('can_view_orders', 'can_checkin_orders'))
elif self.request.user.is_authenticated:
events = self.request.user.get_events_with_permission(('can_view_orders', 'can_checkin_orders'), self.request).filter(
organizer=self.request.organizer
)
else:
raise ValueError("unknown authentication method")
requested_lists = [int(l) for l in self.request.query_params.getlist('list') if l.isdigit()]
lists = list(
CheckinList.objects.filter(event__in=events).select_related('event').filter(id__in=requested_lists)
)
if len(lists) != len(requested_lists):
missing_lists = set(requested_lists) - {l.pk for l in lists}
raise PermissionDenied("You requested lists that do not exist or that you do not have access to: " + ", ".join(str(l) for l in missing_lists))
return lists
@cached_property
def has_full_access_permission(self):
if isinstance(self.request.auth, (TeamAPIToken, Device)):
events = self.request.auth.get_events_with_permission('can_view_orders')
elif self.request.user.is_authenticated:
events = self.request.user.get_events_with_permission('can_view_orders', self.request).filter(
organizer=self.request.organizer
)
else:
raise ValueError("unknown authentication method")
full_access_lists = CheckinList.objects.filter(event__in=events).filter(id__in=[c.pk for c in self.lists]).count()
return len(self.lists) == full_access_lists
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') == 'true' or ignore_status,
ignore_products=ignore_products,
pdf_data=self.request.query_params.get('pdf_data', 'false') == 'true',
expand=self.request.query_params.getlist('expand'),
)
if len(self.request.query_params.get('search', '')) < 3 and not self.has_full_access_permission:
qs = qs.none()
return qs
return cf.file

View File

@@ -321,7 +321,6 @@ with scopes_disabled():
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
sales_channel = django_filters.rest_framework.CharFilter(method='sales_channel_qs')
class Meta:
model = SubEvent
@@ -354,9 +353,6 @@ with scopes_disabled():
else:
return queryset.exclude(expr)
def sales_channel_qs(self, queryset, name, value):
return queryset.filter(event__sales_channels__contains=value)
class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer_class = SubEventSerializer

View File

@@ -1,80 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import json
import logging
from hashlib import sha1
from django.conf import settings
from django.http import HttpResponse, JsonResponse
from rest_framework import status
from rest_framework.views import APIView
from pretix.api.models import ApiCall
logger = logging.getLogger(__name__)
class IdempotencyQueryView(APIView):
# Experimental feature, therefore undocumented for now
authentication_classes = ()
permission_classes = ()
def get(self, request, format=None):
idempotency_key = request.GET.get("key")
auth_hash_parts = '{}:{}'.format(
request.headers.get('Authorization', ''),
request.COOKIES.get(settings.SESSION_COOKIE_NAME, '')
)
auth_hash = sha1(auth_hash_parts.encode()).hexdigest()
if not idempotency_key:
return JsonResponse({
'detail': 'No idempotency key given.'
}, status=status.HTTP_404_NOT_FOUND)
try:
call = ApiCall.objects.get(
auth_hash=auth_hash,
idempotency_key=idempotency_key,
)
except ApiCall.DoesNotExist:
return JsonResponse({
'detail': 'Idempotency key not seen before.'
}, status=status.HTTP_404_NOT_FOUND)
if call.locked:
r = JsonResponse(
{'detail': 'Concurrent request with idempotency key.'},
status=status.HTTP_409_CONFLICT,
)
r['Retry-After'] = 5
return r
content = call.response_body
if isinstance(content, memoryview):
content = content.tobytes()
r = HttpResponse(
content=content,
status=call.response_code,
)
for k, v in json.loads(call.response_headers).values():
r[k] = v
return r

View File

@@ -461,9 +461,7 @@ with scopes_disabled():
class QuotaFilter(FilterSet):
class Meta:
model = Quota
fields = {
'subevent': ['exact', 'in'],
}
fields = ['subevent']
class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):

View File

@@ -27,9 +27,7 @@ from decimal import Decimal
import django_filters
import pytz
from django.db import transaction
from django.db.models import (
Exists, F, OuterRef, Prefetch, Q, Subquery, prefetch_related_objects,
)
from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery
from django.db.models.functions import Coalesce, Concat
from django.http import FileResponse, HttpResponse
from django.shortcuts import get_object_or_404
@@ -63,10 +61,9 @@ from pretix.api.serializers.orderchange import (
)
from pretix.base.i18n import language
from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Checkin, Device, EventMetaValue,
Invoice, InvoiceAddress, ItemMetaValue, Order, OrderFee, OrderPayment,
OrderPosition, OrderRefund, Quota, SubEvent, SubEventMetaValue, TaxRule,
TeamAPIToken, generate_secret,
CachedCombinedTicket, CachedTicket, Checkin, Device, Event, Invoice,
InvoiceAddress, Order, OrderFee, OrderPayment, OrderPosition, OrderRefund,
Quota, SubEvent, TaxRule, TeamAPIToken, generate_secret,
)
from pretix.base.models.orders import QuestionAnswer, RevokedTicketSecret
from pretix.base.payment import PaymentException
@@ -188,8 +185,6 @@ class OrderViewSet(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') == 'true'
ctx['exclude'] = self.request.query_params.getlist('exclude')
return ctx
def get_queryset(self):
@@ -207,49 +202,36 @@ class OrderViewSet(viewsets.ModelViewSet):
if 'invoice_address' not in self.request.GET.getlist('exclude'):
qs = qs.select_related('invoice_address')
qs = qs.prefetch_related(self._positions_prefetch(self.request))
return qs
def _positions_prefetch(self, request):
if request.query_params.get('include_canceled_positions', 'false') == 'true':
if self.request.query_params.get('include_canceled_positions', 'false') == 'true':
opq = OrderPosition.all
else:
opq = OrderPosition.objects
if request.query_params.get('pdf_data', 'false') == 'true':
prefetch_related_objects([request.organizer], 'meta_properties')
prefetch_related_objects(
[request.event],
Prefetch('meta_values', queryset=EventMetaValue.objects.select_related('property'), to_attr='meta_values_cached'),
'questions',
'item_meta_properties',
)
return Prefetch(
'positions',
opq.all().prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.all()),
Prefetch('item', queryset=self.request.event.items.prefetch_related(
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'), to_attr='meta_values_cached')
)),
'variation',
'answers', 'answers__options', 'answers__question',
'item__category',
'addon_to__answers', 'addon_to__answers__options', 'addon_to__answers__question',
Prefetch('subevent', queryset=self.request.event.subevents.prefetch_related(
Prefetch('meta_values', to_attr='meta_values_cached', queryset=SubEventMetaValue.objects.select_related('property'))
)),
Prefetch('addons', opq.select_related('item', 'variation', 'seat'))
).select_related('seat', 'addon_to', 'addon_to__seat')
)
else:
return Prefetch(
'positions',
opq.all().prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.all()),
'item', 'variation',
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options', 'question').order_by('question__position')),
'seat',
if self.request.query_params.get('pdf_data', 'false') == 'true':
qs = qs.prefetch_related(
Prefetch(
'positions',
opq.all().prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.all()),
'item', 'variation', 'answers', 'answers__options', 'answers__question',
'item__category', 'addon_to', 'seat',
Prefetch('addons', opq.select_related('item', 'variation', 'seat'))
)
)
)
else:
qs = qs.prefetch_related(
Prefetch(
'positions',
opq.all().prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.all()),
'item', 'variation',
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options', 'question').order_by('question__position')),
'seat',
)
)
)
return qs
def _get_output_provider(self, identifier):
responses = register_ticket_outputs.send(self.request.event)
@@ -279,11 +261,8 @@ class OrderViewSet(viewsets.ModelViewSet):
provider = self._get_output_provider(output)
order = self.get_object()
if order.status in (Order.STATUS_CANCELED, Order.STATUS_EXPIRED):
raise PermissionDenied("Downloads are not available for canceled or expired orders.")
if order.status == Order.STATUS_PENDING and not request.event.settings.ticket_download_pending:
raise PermissionDenied("Downloads are not available for pending orders.")
if order.status != Order.STATUS_PAID:
raise PermissionDenied("Downloads are not available for unpaid orders.")
ct = CachedCombinedTicket.objects.filter(
order=order, provider=provider.identifier, file__isnull=False
@@ -637,7 +616,6 @@ class OrderViewSet(viewsets.ModelViewSet):
serializer = SimulatedOrderSerializer(order, context=serializer.context)
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
prefetch_related_objects([order], self._positions_prefetch(request))
serializer = OrderSerializer(order, context=serializer.context)
order.log_action(
@@ -950,7 +928,6 @@ 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') == 'true'
return ctx
def get_queryset(self):
@@ -961,49 +938,25 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
qs = qs.filter(order__event=self.request.event)
if self.request.query_params.get('pdf_data', 'false') == 'true':
prefetch_related_objects([self.request.organizer], 'meta_properties')
prefetch_related_objects(
[self.request.event],
Prefetch('meta_values', queryset=EventMetaValue.objects.select_related('property'), to_attr='meta_values_cached'),
'questions',
'item_meta_properties',
)
qs = qs.prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.all()),
Prefetch('item', queryset=self.request.event.items.prefetch_related(
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
to_attr='meta_values_cached')
)),
'variation',
'answers', 'answers__options', 'answers__question',
'item__category',
'addon_to__answers', 'addon_to__answers__options', 'addon_to__answers__question',
Prefetch('addons', qs.select_related('item', 'variation')),
Prefetch('subevent', queryset=self.request.event.subevents.prefetch_related(
Prefetch('meta_values', to_attr='meta_values_cached',
queryset=SubEventMetaValue.objects.select_related('property'))
)),
Prefetch('order', self.request.event.orders.select_related('invoice_address').prefetch_related(
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
Prefetch(
'event',
Event.objects.select_related('organizer')
),
Prefetch(
'positions',
qs.prefetch_related(
'item', 'variation', 'answers', 'answers__options', 'answers__question',
Prefetch('checkins', queryset=Checkin.objects.all()),
Prefetch('item', queryset=self.request.event.items.prefetch_related(
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
to_attr='meta_values_cached')
)),
'variation', 'answers', 'answers__options', 'answers__question',
'item__category',
Prefetch('subevent', queryset=self.request.event.subevents.prefetch_related(
Prefetch('meta_values', to_attr='meta_values_cached',
queryset=SubEventMetaValue.objects.select_related('property'))
)),
Prefetch('addons', qs.select_related('item', 'variation', 'seat'))
).select_related('addon_to', 'seat', 'addon_to__seat')
)
)
))
).select_related(
'addon_to', 'seat', 'addon_to__seat'
'item', 'variation', 'item__category', 'addon_to', 'seat'
)
else:
qs = qs.prefetch_related(
@@ -1166,11 +1119,8 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
provider = self._get_output_provider(output)
pos = self.get_object()
if pos.order.status in (Order.STATUS_CANCELED, Order.STATUS_EXPIRED):
raise PermissionDenied("Downloads are not available for canceled or expired orders.")
if pos.order.status == Order.STATUS_PENDING and not request.event.settings.ticket_download_pending:
raise PermissionDenied("Downloads are not available for pending orders.")
if pos.order.status != Order.STATUS_PAID:
raise PermissionDenied("Downloads are not available for unpaid orders.")
if not pos.generate_ticket:
raise PermissionDenied("Downloads are not enabled for this product.")

View File

@@ -22,7 +22,6 @@
from decimal import Decimal
import django_filters
from django.contrib.auth.hashers import make_password
from django.db import transaction
from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property
@@ -39,8 +38,8 @@ from rest_framework.viewsets import GenericViewSet
from pretix.api.models import OAuthAccessToken
from pretix.api.serializers.organizer import (
CustomerCreateSerializer, CustomerSerializer, DeviceSerializer,
GiftCardSerializer, GiftCardTransactionSerializer, MembershipSerializer,
CustomerSerializer, DeviceSerializer, GiftCardSerializer,
GiftCardTransactionSerializer, MembershipSerializer,
MembershipTypeSerializer, OrganizerSerializer, OrganizerSettingsSerializer,
SeatingPlanSerializer, TeamAPITokenSerializer, TeamInviteSerializer,
TeamMemberSerializer, TeamSerializer,
@@ -515,24 +514,15 @@ class CustomerViewSet(viewsets.ModelViewSet):
raise MethodNotAllowed("Customers cannot be deleted.")
@transaction.atomic()
def perform_create(self, serializer, send_email=False):
customer = serializer.save(organizer=self.request.organizer, password=make_password(None))
def perform_create(self, serializer):
inst = serializer.save(organizer=self.request.organizer)
serializer.instance.log_action(
'pretix.customer.created',
user=self.request.user,
auth=self.request.auth,
data=self.request.data,
)
if send_email:
customer.send_activation_mail()
return customer
def create(self, request, *args, **kwargs):
serializer = CustomerCreateSerializer(data=request.data, context=self.get_serializer_context())
serializer.is_valid(raise_exception=True)
self.perform_create(serializer, send_email=serializer.validated_data.pop('send_email', False))
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
return inst
@transaction.atomic()
def perform_update(self, serializer):

View File

@@ -1,240 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from datetime import timedelta
from celery.result import AsyncResult
from django.conf import settings
from django.http import Http404
from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property
from django.utils.timezone import now
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.response import Response
from rest_framework.reverse import reverse
from pretix.api.serializers.shredders import (
JobRunSerializer, ShredderSerializer,
)
from pretix.base.models import CachedFile
from pretix.base.services.shredder import export, shred
from pretix.base.shredder import shred_constraints
from pretix.helpers.http import ChunkBasedFileResponse
class ShreddersMixin:
def list(self, request, *args, **kwargs):
res = ShredderSerializer(self.shredders, many=True)
return Response({
"count": len(self.shredders),
"next": None,
"previous": None,
"results": res.data
})
def get_object(self):
instances = [e for e in self.shredders if e.identifier == self.kwargs.get('pk')]
if not instances:
raise Http404()
return instances[0]
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializer = ShredderSerializer(instance)
return Response(serializer.data)
@action(detail=False, methods=['GET'], url_name='download', url_path='download/(?P<asyncid>[^/]+)/(?P<cfid>[^/]+)')
def download(self, *args, **kwargs):
cf = get_object_or_404(
CachedFile,
id=kwargs['cfid'],
session_key=f'api-shredder-{str(type(self.request.user or self.request.auth))}-{(self.request.user or self.request.auth).pk}'
)
if cf.file:
resp = ChunkBasedFileResponse(cf.file.file, content_type=cf.type)
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename).encode("ascii", "ignore")
return resp
elif not settings.HAS_CELERY:
return Response(
{'status': 'failed', 'message': 'Unknown file ID or export failed'},
status=status.HTTP_410_GONE
)
res = AsyncResult(kwargs['asyncid'])
if res.failed():
if isinstance(res.info, dict) and res.info['exc_type'] in ('ShredError', 'ExportError'):
msg = res.info['exc_message']
else:
msg = 'Internal error'
return Response(
{'status': 'failed', 'message': msg},
status=status.HTTP_410_GONE
)
return Response(
{
'status': 'running' if res.state in ('PROGRESS', 'STARTED', 'SUCCESS') else 'waiting',
},
status=status.HTTP_409_CONFLICT
)
@action(detail=False, methods=['GET'], url_name='status', url_path='status/(?P<asyncid>[^/]+)/(?P<cfid>[^/]+)')
def status(self, *args, **kwargs):
if settings.HAS_CELERY:
res = AsyncResult(kwargs['asyncid'])
if res.failed():
if isinstance(res.info, dict) and res.info['exc_type'] in ('ShredError', 'ExportError'):
msg = res.info['exc_message']
else:
msg = 'Internal error'
return Response(
{'status': 'failed', 'message': msg},
status=status.HTTP_417_EXPECTATION_FAILED
)
elif res.successful():
return Response(
{'status': 'ok', 'message': 'OK'},
status=status.HTTP_200_OK
)
try:
CachedFile.objects.get(
id=kwargs['cfid'],
session_key=f'api-shredder-{str(type(self.request.user or self.request.auth))}-{(self.request.user or self.request.auth).pk}'
)
except CachedFile.DoesNotExist:
return Response(
{'status': 'gone', 'message': 'May have succeeded or timed out'},
status=status.HTTP_410_GONE
)
return Response(
{
'status': 'running' if res.state in ('PROGRESS', 'STARTED', 'SUCCESS') else 'waiting',
},
status=status.HTTP_409_CONFLICT
)
@action(detail=False, methods=['POST'], url_name='shred', url_path='shred/(?P<asyncid>[^/]+)/(?P<cfid>[^/]+)')
def shred(self, *args, **kwargs):
cf = get_object_or_404(
CachedFile,
id=kwargs['cfid'],
session_key=f'api-shredder-{str(type(self.request.user or self.request.auth))}-{(self.request.user or self.request.auth).pk}'
)
if cf.file:
async_result = self.do_shred(cf)
url_kwargs = {
'asyncid': str(async_result.id),
'cfid': str(cf.id),
}
url_kwargs.update(self.kwargs)
return Response({
'status': reverse('api-v1:shredders-status', kwargs=url_kwargs, request=self.request),
}, status=status.HTTP_202_ACCEPTED)
elif not settings.HAS_CELERY:
return Response(
{'status': 'failed', 'message': 'Unknown file ID or export failed'},
status=status.HTTP_410_GONE
)
res = AsyncResult(kwargs['asyncid'])
if res.failed():
if isinstance(res.info, dict) and res.info['exc_type'] in ('ShredError', 'ExportError'):
msg = res.info['exc_message']
else:
msg = 'Internal error'
return Response(
{'status': 'failed', 'message': msg},
status=status.HTTP_410_GONE
)
return Response(
{
'status': 'running' if res.state in ('PROGRESS', 'STARTED', 'SUCCESS') else 'waiting',
},
status=status.HTTP_409_CONFLICT
)
@action(detail=False, methods=['POST'])
def export(self, *args, **kwargs):
serializer = JobRunSerializer(shredders=self.shredders, data=self.request.data, **self.get_serializer_kwargs())
serializer.is_valid(raise_exception=True)
cf = CachedFile(web_download=False)
cf.date = now()
cf.expires = now() + timedelta(hours=2)
cf.session_key = f'api-shredder-{str(type(self.request.user or self.request.auth))}-{(self.request.user or self.request.auth).pk}'
cf.save()
d = serializer.data
for k, v in d.items():
if isinstance(v, set):
d[k] = list(v)
async_result = self.do_export(cf, serializer.validated_data['shredders'])
url_kwargs = {
'asyncid': str(async_result.id),
'cfid': str(cf.id),
}
url_kwargs.update(self.kwargs)
return Response({
'download': reverse('api-v1:shredders-download', kwargs=url_kwargs, request=self.request),
'shred': reverse('api-v1:shredders-shred', kwargs=url_kwargs, request=self.request),
}, status=status.HTTP_202_ACCEPTED)
class EventShreddersViewSet(ShreddersMixin, viewsets.ViewSet):
permission = 'can_change_orders'
def get_serializer_kwargs(self):
return {}
@cached_property
def shredders(self):
shredders = []
for k, v in sorted(self.request.event.get_data_shredders().items(), key=lambda s: s[1].verbose_name):
shredders.append(v)
return shredders
def do_export(self, cf, shredders):
constr = shred_constraints(self.request.event)
if constr:
raise ValidationError(constr)
return export.apply_async(args=(
self.request.event.id,
list(shredders),
f'api-shredder-{str(type(self.request.user or self.request.auth))}-{(self.request.user or self.request.auth).pk}',
str(cf.pk)
))
def do_shred(self, cf):
constr = shred_constraints(self.request.event)
if constr:
raise ValidationError(constr)
return shred.apply_async(args=(
self.request.event.id,
str(cf.pk),
True,
))

View File

@@ -25,7 +25,7 @@ from django.db import transaction
from django.db.models import F, Q
from django.utils.timezone import now
from django_filters.rest_framework import (
BooleanFilter, CharFilter, DjangoFilterBackend, FilterSet,
BooleanFilter, DjangoFilterBackend, FilterSet,
)
from django_scopes import scopes_disabled
from rest_framework import status, viewsets
@@ -40,7 +40,6 @@ from pretix.base.models import Voucher
with scopes_disabled():
class VoucherFilter(FilterSet):
active = BooleanFilter(method='filter_active')
code = CharFilter(lookup_expr='iexact')
class Meta:
model = Voucher

View File

@@ -43,7 +43,6 @@ from pretix.base.i18n import (
LazyCurrencyNumber, LazyDate, LazyExpiresDate, LazyNumber,
)
from pretix.base.models import Event
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import (
register_html_mail_renderers, register_mail_placeholders,
@@ -470,30 +469,14 @@ def base_placeholders(sender, **kwargs):
}
),
),
SimpleFunctionalMailTextPlaceholder(
'order_modification_deadline_date_and_time', ['order', 'event'],
lambda order, event:
date_format(order.modify_deadline.astimezone(event.timezone), 'SHORT_DATETIME_FORMAT')
if order.modify_deadline
else '',
lambda event: date_format(
event.settings.get(
'last_order_modification_date', as_type=RelativeDateWrapper
).datetime(event).astimezone(event.timezone),
'SHORT_DATETIME_FORMAT'
) if event.settings.get('last_order_modification_date') else '',
),
SimpleFunctionalMailTextPlaceholder(
'event_location', ['event_or_subevent'], lambda event_or_subevent: str(event_or_subevent.location or ''),
lambda event: str(event.location or ''),
),
SimpleFunctionalMailTextPlaceholder(
'event_admission_time', ['event_or_subevent'],
lambda event_or_subevent:
date_format(event_or_subevent.date_admission.astimezone(event_or_subevent.timezone), 'TIME_FORMAT')
if event_or_subevent.date_admission
else '',
lambda event: date_format(event.date_admission.astimezone(event.timezone), 'TIME_FORMAT') if event.date_admission else '',
lambda event_or_subevent: date_format(event_or_subevent.date_admission, 'TIME_FORMAT') if event_or_subevent.date_admission else '',
lambda event: date_format(event.date_admission, 'TIME_FORMAT') if event.date_admission else '',
),
SimpleFunctionalMailTextPlaceholder(
'subevent', ['waiting_list_entry', 'event'],

View File

@@ -23,7 +23,6 @@ from .answers import * # noqa
from .dekodi import * # noqa
from .events import * # noqa
from .invoices import * # noqa
from .items import * # noqa
from .json import * # noqa
from .mail import * # noqa
from .orderlist import * # noqa

View File

@@ -1,222 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from django.db.models import Prefetch
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.translation import gettext_lazy as _
from openpyxl.styles import Alignment
from openpyxl.utils import get_column_letter
from ...helpers.safe_openpyxl import SafeCell
from ..channels import get_all_sales_channels
from ..exporter import ListExporter
from ..models import ItemMetaValue
from ..signals import register_data_exporters
def _max(a1, a2):
if a1 and a2:
return max(a1, a2)
return a1 or a2
def _min(a1, a2):
if a1 and a2:
return min(a1, a2)
return a1 or a2
class ItemDataExporter(ListExporter):
identifier = 'itemdata'
verbose_name = _('Product data')
def iterate_list(self, form_data):
locales = self.event.settings.locales
scs = get_all_sales_channels()
header = [
_("Product ID"),
_("Variation ID"),
_("Product category"),
_("Internal name"),
]
for l in locales:
header.append(
_("Item name") + f" ({l})"
)
for l in locales:
header.append(
_("Variation") + f" ({l})"
)
header += [
_("Active"),
_("Sales channels"),
_("Default price"),
_("Free price input"),
_("Sales tax"),
_("Is an admission ticket"),
_("Generate tickets"),
_("Waiting list"),
_("Available from"),
_("Available until"),
_("This product can only be bought using a voucher."),
_("This product will only be shown if a voucher matching the product is redeemed."),
_("Buying this product requires approval"),
_("Only sell this product as part of a bundle"),
_("Allow product to be canceled or changed"),
_("Minimum amount per order"),
_("Maximum amount per order"),
_("Requires special attention"),
_("Original price"),
_("This product is a gift card"),
_("Require a valid membership"),
_("Hide without a valid membership"),
]
props = list(self.event.item_meta_properties.all())
for p in props:
header.append(p.name)
if form_data["_format"] == "xlsx":
row = []
for h in header:
c = SafeCell(self.__ws, value=h)
c.alignment = Alignment(wrap_text=True, vertical='top')
row.append(c)
else:
row = header
yield row
for i in self.event.items.prefetch_related(
'variations',
Prefetch(
'meta_values',
ItemMetaValue.objects.select_related('property'),
to_attr='meta_values_cached'
)
).select_related('category', 'tax_rule'):
m = i.meta_data
vars = list(i.variations.all())
if vars:
for v in vars:
row = [
i.pk,
v.pk,
str(i.category) if i.category else "",
i.internal_name or "",
]
for l in locales:
row.append(i.name.localize(l))
for l in locales:
row.append(v.value.localize(l))
row += [
_("Yes") if i.active and v.active else "",
", ".join([str(sn.verbose_name) for s, sn in scs.items() if s in i.sales_channels and s in v.sales_channels]),
v.default_price or i.default_price,
_("Yes") if i.free_price else "",
str(i.tax_rule) if i.tax_rule else "",
_("Yes") if i.admission else "",
_("Yes") if i.generate_tickets else (_("Default") if i.generate_tickets is None else ""),
_("Yes") if i.allow_waitinglist else "",
date_format(_max(i.available_from, v.available_from).astimezone(self.timezone),
"SHORT_DATETIME_FORMAT") if i.available_from or v.available_from else "",
date_format(_min(i.available_until, v.available_until).astimezone(self.timezone),
"SHORT_DATETIME_FORMAT") if i.available_until or v.available_until else "",
_("Yes") if i.require_voucher else "",
_("Yes") if i.hide_without_voucher or v.hide_without_voucher else "",
_("Yes") if i.require_approval or v.require_approval else "",
_("Yes") if i.require_bundling else "",
_("Yes") if i.allow_cancel else "",
i.min_per_order if i.min_per_order is not None else "",
i.max_per_order if i.max_per_order is not None else "",
_("Yes") if i.checkin_attention else "",
v.original_price or i.original_price or "",
_("Yes") if i.issue_giftcard else "",
_("Yes") if i.require_membership or v.require_membership else "",
_("Yes") if i.require_membership_hidden or v.require_membership_hidden else "",
]
row += [
m.get(p.name, '') for p in props
]
yield row
else:
row = [
i.pk,
"",
str(i.category) if i.category else "",
i.internal_name or "",
]
for l in locales:
row.append(i.name.localize(l))
for l in locales:
row.append("")
row += [
_("Yes") if i.active else "",
", ".join([str(sn.verbose_name) for s, sn in scs.items() if s in i.sales_channels]),
i.default_price,
_("Yes") if i.free_price else "",
str(i.tax_rule) if i.tax_rule else "",
_("Yes") if i.admission else "",
_("Yes") if i.generate_tickets else (_("Default") if i.generate_tickets is None else ""),
_("Yes") if i.allow_waitinglist else "",
date_format(i.available_from.astimezone(self.timezone),
"SHORT_DATETIME_FORMAT") if i.available_from else "",
date_format(i.available_until.astimezone(self.timezone),
"SHORT_DATETIME_FORMAT") if i.available_until else "",
_("Yes") if i.require_voucher else "",
_("Yes") if i.hide_without_voucher else "",
_("Yes") if i.require_approval else "",
_("Yes") if i.require_bundling else "",
_("Yes") if i.allow_cancel else "",
i.min_per_order if i.min_per_order is not None else "",
i.max_per_order if i.max_per_order is not None else "",
_("Yes") if i.checkin_attention else "",
i.original_price or "",
_("Yes") if i.issue_giftcard else "",
_("Yes") if i.require_membership else "",
_("Yes") if i.require_membership_hidden else "",
]
row += [
m.get(p.name, '') for p in props
]
yield row
def get_filename(self):
return '{}_products'.format(self.events.first().organizer.slug)
def prepare_xlsx_sheet(self, ws):
self.__ws = ws
ws.freeze_panes = 'A1'
ws.column_dimensions['C'].width = 25
ws.column_dimensions['D'].width = 25
for i in range(len(self.event.settings.locales)):
ws.column_dimensions[get_column_letter(5 + 2 * i + 0)].width = 25
ws.column_dimensions[get_column_letter(5 + 2 * i + 1)].width = 25
ws.column_dimensions[get_column_letter(5 + 2 * len(self.event.settings.locales) + 1)].width = 25
ws.row_dimensions[1].height = 40
@receiver(register_data_exporters, dispatch_uid="exporter_itemdata")
def register_itemdata_exporter(sender, **kwargs):
return ItemDataExporter

View File

@@ -610,10 +610,7 @@ class OrderListExporter(MultiSheetListExporter):
for k, label, w in name_scheme['fields']:
headers.append(_('Invoice address name') + ': ' + str(label))
headers += [
_('Invoice address street'), _('Invoice address ZIP code'), _('Invoice address city'),
_('Invoice address country'),
pgettext('address', 'Invoice address state'),
_('VAT ID'),
_('Address'), _('ZIP code'), _('City'), _('Country'), pgettext('address', 'State'), _('VAT ID'),
]
headers += [
_('Sales channel'), _('Order locale'),

View File

@@ -196,16 +196,10 @@ class SecretKeySettingsWidget(forms.TextInput):
attrs.update({
'autocomplete': 'new-password' # see https://bugs.chromium.org/p/chromium/issues/detail?id=370363#c7
})
self.__reflect_value = False
super().__init__(attrs)
def value_from_datadict(self, data, files, name):
value = super().value_from_datadict(data, files, name)
self.__reflect_value = value and value != SECRET_REDACTED
return value
def get_context(self, name, value, attrs):
if value and not self.__reflect_value:
if value:
value = SECRET_REDACTED
return super().get_context(name, value, attrs)

View File

@@ -253,7 +253,7 @@ class NamePartsFormField(forms.MultiValueField):
if self.require_all_fields and not all(v for v in value):
raise forms.ValidationError(self.error_messages['incomplete'], code='required')
if sum(len(v) for v in value.values() if v) > 250:
if sum(len(v) for v in value if v) > 250:
raise forms.ValidationError(_('Please enter a shorter name.'), code='max_length')
return value

View File

@@ -28,7 +28,6 @@ from typing import Tuple
import bleach
import vat_moss.exchange_rates
from django.contrib.staticfiles import finders
from django.db.models import Sum
from django.dispatch import receiver
from django.utils.formats import date_format, localize
from django.utils.translation import (
@@ -48,7 +47,7 @@ from reportlab.platypus import (
)
from pretix.base.decimal import round_decimal
from pretix.base.models import Event, Invoice, Order, OrderPayment
from pretix.base.models import Event, Invoice, Order
from pretix.base.signals import register_invoice_renderers
from pretix.base.templatetags.money import money_filter
from pretix.helpers.reportlab import ThumbnailingImageReader
@@ -590,33 +589,15 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
])
colwidths = [a * doc.width for a in (.65, .05, .30)]
if self.invoice.event.settings.invoice_show_payments and not self.invoice.is_cancellation:
if self.invoice.order.status == Order.STATUS_PENDING:
pending_sum = self.invoice.order.pending_sum
if pending_sum != total:
tdata.append([pgettext('invoice', 'Received payments')] + (['', '', ''] if has_taxes else ['']) + [
money_filter(pending_sum - total, self.invoice.event.currency)
])
tdata.append([pgettext('invoice', 'Outstanding payments')] + (['', '', ''] if has_taxes else ['']) + [
money_filter(pending_sum, self.invoice.event.currency)
])
tstyledata += [
('FONTNAME', (0, len(tdata) - 3), (-1, len(tdata) - 3), self.font_bold),
]
elif self.invoice.order.payments.filter(
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED), provider='giftcard'
).exists():
giftcard_sum = self.invoice.order.payments.filter(
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
provider='giftcard'
).aggregate(
s=Sum('amount')
)['s'] or Decimal('0.00')
tdata.append([pgettext('invoice', 'Paid by gift card')] + (['', '', ''] if has_taxes else ['']) + [
money_filter(giftcard_sum, self.invoice.event.currency)
if self.invoice.event.settings.invoice_show_payments and not self.invoice.is_cancellation and \
self.invoice.order.status == Order.STATUS_PENDING:
pending_sum = self.invoice.order.pending_sum
if pending_sum != total:
tdata.append([pgettext('invoice', 'Received payments')] + (['', '', ''] if has_taxes else ['']) + [
money_filter(pending_sum - total, self.invoice.event.currency)
])
tdata.append([pgettext('invoice', 'Remaining amount')] + (['', '', ''] if has_taxes else ['']) + [
money_filter(total - giftcard_sum, self.invoice.event.currency)
tdata.append([pgettext('invoice', 'Outstanding payments')] + (['', '', ''] if has_taxes else ['']) + [
money_filter(pending_sum, self.invoice.event.currency)
])
tstyledata += [
('FONTNAME', (0, len(tdata) - 3), (-1, len(tdata) - 3), self.font_bold),

View File

@@ -4,6 +4,7 @@
from django.core.exceptions import ImproperlyConfigured
from django.db import migrations, models
from django_mysql.checks import mysql_connections
from django_mysql.utils import connection_is_mariadb
def set_attendee_name_parts(apps, schema_editor):
@@ -30,7 +31,7 @@ def check_mysqlversion(apps, schema_editor):
conns = list(mysql_connections())
found = 'Unknown version'
for alias, conn in conns:
if hasattr(conn, 'mysql_is_mariadb') and conn.mysql_is_mariadb and hasattr(conn, 'mysql_version'):
if connection_is_mariadb(conn) and hasattr(conn, 'mysql_version'):
if conn.mysql_version >= (10, 2, 7):
any_conn_works = True
else:

View File

@@ -1,22 +0,0 @@
# Generated by Django 3.2.12 on 2022-05-12 15:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0214_customer_notes_ext_id'),
]
operations = [
migrations.AlterField(
model_name='customer',
name='identifier',
field=models.CharField(db_index=True, max_length=190),
),
migrations.AlterUniqueTogether(
name='customer',
unique_together={('organizer', 'email'), ('organizer', 'identifier')},
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2022-04-29 13:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0215_customer_organizer_identifier_unique'),
]
operations = [
migrations.AddField(
model_name='checkin',
name='force_sent',
field=models.BooleanField(default=False, null=True),
),
]

View File

@@ -1,33 +0,0 @@
# Generated by Django 3.2.12 on 2022-06-15 08:10
import django.db.models.deletion
import i18nfield.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0216_checkin_forced_sent'),
]
operations = [
migrations.CreateModel(
name='OrganizerFooterLink',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('label', i18nfield.fields.I18nCharField(max_length=200)),
('url', models.URLField()),
('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='footer_links', to='pretixbase.organizer')),
],
),
migrations.CreateModel(
name='EventFooterLink',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('label', i18nfield.fields.I18nCharField(max_length=200)),
('url', models.URLField()),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='footer_links', to='pretixbase.event')),
],
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.2.2 on 2022-06-29 17:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0217_eventfooterlink_organizerfooterlink'),
]
operations = [
migrations.AddField(
model_name='checkinlist',
name='addon_match',
field=models.BooleanField(default=False),
),
]

View File

@@ -34,9 +34,7 @@
import binascii
import json
import operator
from datetime import timedelta
from functools import reduce
from urllib.parse import urlparse
import webauthn
@@ -493,14 +491,11 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
if request and self.has_active_staff_session(request.session.session_key):
return Event.objects.all()
if isinstance(permission, (tuple, list)):
q = reduce(operator.or_, [Q(**{p: True}) for p in permission])
else:
q = Q(**{permission: True})
kwargs = {permission: True}
return Event.objects.filter(
Q(organizer_id__in=self.teams.filter(q, all_events=True).values_list('organizer', flat=True))
| Q(id__in=self.teams.filter(q).values_list('limit_events__id', flat=True))
Q(organizer_id__in=self.teams.filter(all_events=True, **kwargs).values_list('organizer', flat=True))
| Q(id__in=self.teams.filter(**kwargs).values_list('limit_events__id', flat=True))
)
@scopes_disabled()

View File

@@ -56,12 +56,6 @@ class CheckinList(LoggedModel):
default=False,
help_text=_('With this option, people will be able to check in even if the '
'order has not been paid.'))
addon_match = models.BooleanField(
verbose_name=_('Allow checking in add-on tickets by scanning the main ticket'),
default=False,
help_text=_('A scan will only be possible if the check-in list is configured such that there is always exactly '
'one matching add-on ticket. Ambiguous scans will be rejected..')
)
gates = models.ManyToManyField(
'Gate', verbose_name=_("Gates"), blank=True,
help_text=_("Does not have any effect for the validation of tickets, only for the automatic configuration of "
@@ -264,7 +258,6 @@ class Checkin(models.Model):
REASON_REVOKED = 'revoked'
REASON_INCOMPLETE = 'incomplete'
REASON_ALREADY_REDEEMED = 'already_redeemed'
REASON_AMBIGUOUS = 'ambiguous'
REASON_ERROR = 'error'
REASONS = (
(REASON_CANCELED, _('Order canceled')),
@@ -275,7 +268,6 @@ class Checkin(models.Model):
(REASON_INCOMPLETE, _('Information required')),
(REASON_ALREADY_REDEEMED, _('Ticket already used')),
(REASON_PRODUCT, _('Ticket type not allowed here')),
(REASON_AMBIGUOUS, _('Ticket code is ambiguous on list')),
(REASON_ERROR, _('Server error')),
)
@@ -334,13 +326,7 @@ class Checkin(models.Model):
type = models.CharField(max_length=100, choices=CHECKIN_TYPES, default=TYPE_ENTRY)
nonce = models.CharField(max_length=190, null=True, blank=True)
# Whether or not the scan was made offline
force_sent = models.BooleanField(default=False, null=True, blank=True)
# Whether the scan was made offline AND would have not been possible online
forced = models.BooleanField(default=False)
device = models.ForeignKey(
'pretixbase.Device', related_name='checkins', on_delete=models.PROTECT, null=True, blank=True
)

View File

@@ -24,7 +24,6 @@ from django.conf import settings
from django.contrib.auth.hashers import (
check_password, is_password_usable, make_password,
)
from django.core.validators import RegexValidator
from django.db import models
from django.db.models import F, Q
from django.utils.crypto import get_random_string, salted_hmac
@@ -45,18 +44,7 @@ class Customer(LoggedModel):
"""
id = models.BigAutoField(primary_key=True)
organizer = models.ForeignKey(Organizer, related_name='customers', on_delete=models.CASCADE)
identifier = models.CharField(
max_length=190,
db_index=True,
help_text=_('You can enter any value here to make it easier to match the data with other sources. If you do '
'not input one, we will generate one automatically.'),
validators=[
RegexValidator(
regex=r"^[a-zA-Z0-9]([a-zA-Z0-9.\-_]*[a-zA-Z0-9])?$",
message=_("The identifier may only contain letters, numbers, dots, dashes, and underscores. It must start and end with a letter or number."),
),
],
)
identifier = models.CharField(max_length=190, db_index=True, unique=True)
email = models.EmailField(db_index=True, null=True, blank=False, verbose_name=_('E-mail'), max_length=190)
phone = PhoneNumberField(null=True, blank=True, verbose_name=_('Phone number'))
password = models.CharField(verbose_name=_('Password'), max_length=128)
@@ -77,7 +65,7 @@ class Customer(LoggedModel):
objects = ScopedManager(organizer='organizer')
class Meta:
unique_together = [['organizer', 'email'], ['organizer', 'identifier']]
unique_together = [['organizer', 'email']]
ordering = ('email',)
def get_email_field_name(self):
@@ -216,27 +204,6 @@ class Customer(LoggedModel):
testmode=testmode,
)
def send_activation_mail(self):
from pretix.base.services.mail import mail
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.presale.forms.customer import TokenGenerator
ctx = self.get_email_context()
token = TokenGenerator().make_token(self)
ctx['url'] = build_absolute_uri(
self.organizer,
'presale:organizer.customer.activate'
) + '?id=' + self.identifier + '&token=' + token
mail(
self.email,
_('Activate your account at {organizer}').format(organizer=self.organizer.name),
self.organizer.settings.mail_text_customer_registration,
ctx,
locale=self.locale,
customer=self,
organizer=self.organizer,
)
class AttendeeProfile(models.Model):
customer = models.ForeignKey(

View File

@@ -255,9 +255,7 @@ class Device(LoggedModel):
:param request: Ignored, for compatibility with User model
:return: Iterable of Events
"""
if (
isinstance(permission, (list, tuple)) and any(p in self.permission_set() for p in permission)
) or (isinstance(permission, str) and permission in self.permission_set()):
if permission in self.permission_set():
return self.get_events_with_any_permission()
else:
return self.organizer.events.none()

View File

@@ -608,14 +608,11 @@ class Event(EventMixin, LoggedModel):
return super().presale_has_ended
def delete_all_orders(self, really=False):
from .orders import (
OrderFee, OrderPayment, OrderPosition, OrderRefund, Transaction,
)
from .orders import OrderFee, OrderPayment, OrderPosition, OrderRefund
if not really:
raise TypeError("Pass really=True as a parameter.")
Transaction.objects.filter(order__event=self).delete()
OrderPosition.all.filter(order__event=self, addon_to__isnull=False).delete()
OrderPosition.all.filter(order__event=self).delete()
OrderFee.objects.filter(order__event=self).delete()
@@ -721,11 +718,6 @@ class Event(EventMixin, LoggedModel):
self.save()
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
for fl in EventFooterLink.objects.filter(event=other):
fl.pk = None
fl.event = self
fl.save(force_insert=True)
tax_map = {}
for t in other.tax_rules.all():
tax_map[t.pk] = t
@@ -1234,7 +1226,7 @@ class Event(EventMixin, LoggedModel):
self.set_active_plugins(plugins_active)
plugins_available = self.get_available_plugins()
if module in plugins_available and hasattr(plugins_available[module].app, 'uninstalled'):
if hasattr(plugins_available[module].app, 'uninstalled'):
getattr(plugins_available[module].app, 'uninstalled')(self)
regenerate_css.apply_async(args=(self.pk,))
@@ -1448,10 +1440,7 @@ class SubEvent(EventMixin, LoggedModel):
@property
def meta_data(self):
data = self.event.meta_data
if hasattr(self, 'meta_values_cached'):
data.update({v.property.name: v.value for v in self.meta_values_cached})
else:
data.update({v.property.name: v.value for v in self.meta_values.select_related('property').all()})
data.update({v.property.name: v.value for v in self.meta_values.select_related('property').all()})
return data
@property
@@ -1623,25 +1612,3 @@ class SubEventMetaValue(LoggedModel):
super().save(*args, **kwargs)
if self.subevent:
self.subevent.event.cache.clear()
class EventFooterLink(models.Model):
"""
A footer link assigned to an event.
"""
event = models.ForeignKey('Event', on_delete=models.CASCADE, related_name='footer_links')
label = I18nCharField(
max_length=200,
verbose_name=_("Link text"),
)
url = models.URLField(
verbose_name=_("Link URL"),
)
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
self.event.cache.clear()
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
self.event.cache.clear()

View File

@@ -1243,13 +1243,7 @@ class Question(LoggedModel):
max_length=190,
verbose_name=_("Internal identifier"),
help_text=_('You can enter any value here to make it easier to match the data with other sources. If you do '
'not input one, we will generate one automatically.'),
validators=[
RegexValidator(
regex=r"^[a-zA-Z0-9.\-_]+$",
message=_("The identifier may only contain letters, numbers, dots, dashes, and underscores."),
),
],
'not input one, we will generate one automatically.')
)
help_text = I18nTextField(
verbose_name=_("Help text"),
@@ -1467,17 +1461,7 @@ class Question(LoggedModel):
class QuestionOption(models.Model):
question = models.ForeignKey('Question', related_name='options', on_delete=models.CASCADE)
identifier = models.CharField(
max_length=190,
help_text=_('You can enter any value here to make it easier to match the data with other sources. If you do '
'not input one, we will generate one automatically.'),
validators=[
RegexValidator(
regex=r"^[a-zA-Z0-9.\-_]+$",
message=_("The identifier may only contain letters, numbers, dots, dashes, and underscores."),
),
],
)
identifier = models.CharField(max_length=190)
answer = I18nCharField(verbose_name=_('Answer'))
position = models.IntegerField(default=0)

View File

@@ -746,19 +746,6 @@ class Order(LockModel, LoggedModel):
length += 1
iteration = 0
@property
def modify_deadline(self):
modify_deadline = self.event.settings.get('last_order_modification_date', as_type=RelativeDateWrapper)
if self.event.has_subevents and modify_deadline:
dates = [
modify_deadline.datetime(se)
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
]
return min(dates) if dates else None
elif modify_deadline:
return modify_deadline.datetime(self.event)
return None
@property
def can_modify_answers(self) -> bool:
"""
@@ -771,7 +758,16 @@ class Order(LockModel, LoggedModel):
if self.status not in (Order.STATUS_PENDING, Order.STATUS_PAID, Order.STATUS_EXPIRED):
return False
modify_deadline = self.modify_deadline
modify_deadline = self.event.settings.get('last_order_modification_date', as_type=RelativeDateWrapper)
if self.event.has_subevents and modify_deadline:
dates = [
modify_deadline.datetime(se)
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
]
modify_deadline = min(dates) if dates else None
elif modify_deadline:
modify_deadline = modify_deadline.datetime(self.event)
if modify_deadline is not None and now() > modify_deadline:
return False
@@ -847,7 +843,7 @@ class Order(LockModel, LoggedModel):
if terms:
term_last = min(terms)
else:
return None
term_last = None
else:
term_last = term_last.datetime(self.event).date()
term_last = make_aware(datetime.combine(
@@ -1592,7 +1588,7 @@ class OrderPayment(models.Model):
if status_change:
self.order.create_transactions()
def fail(self, info=None, user=None, auth=None, log_data=None):
def fail(self, info=None, user=None, auth=None):
"""
Marks the order as failed and sets info to ``info``, but only if the order is in ``created`` or ``pending``
state. This is equivalent to setting ``state`` to ``OrderPayment.PAYMENT_STATE_FAILED`` and logging a failure,
@@ -1620,7 +1616,6 @@ class OrderPayment(models.Model):
'local_id': self.local_id,
'provider': self.provider,
'info': info,
'data': log_data,
}, user=user, auth=auth)
def confirm(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text='',

View File

@@ -46,7 +46,6 @@ from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.timezone import get_current_timezone, make_aware, now
from django.utils.translation import gettext_lazy as _
from i18nfield.fields import I18nCharField
from pretix.base.models.base import LoggedModel
from pretix.base.validators import OrganizerSlugBanlistValidator
@@ -461,31 +460,7 @@ class TeamAPIToken(models.Model):
:param request: Ignored, for compatibility with User model
:return: Iterable of Events
"""
if (
isinstance(permission, (list, tuple)) and any(getattr(self.team, p, False) for p in permission)
) or (isinstance(permission, str) and getattr(self.team, permission, False)):
if getattr(self.team, permission, False):
return self.get_events_with_any_permission()
else:
return self.team.organizer.events.none()
class OrganizerFooterLink(models.Model):
"""
A footer link assigned to an organizer.
"""
organizer = models.ForeignKey('Organizer', on_delete=models.CASCADE, related_name='footer_links')
label = I18nCharField(
max_length=200,
verbose_name=_("Link text"),
)
url = models.URLField(
verbose_name=_("Link URL"),
)
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
self.organizer.cache.clear()
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
self.organizer.cache.clear()

View File

@@ -392,7 +392,7 @@ class InvoiceAddressCountry(ImportColumn):
return list(countries)
def clean(self, value, previous_values):
if value and not (Country(value).numeric or value in settings.COUNTRIES_OVERRIDE):
if value and not Country(value).numeric:
raise ValidationError(_("Please enter a valid country code."))
return value
@@ -538,7 +538,7 @@ class AttendeeCountry(ImportColumn):
return list(countries)
def clean(self, value, previous_values):
if value and not (Country(value).numeric or value in settings.COUNTRIES_OVERRIDE):
if value and not Country(value).numeric:
raise ValidationError(_("Please enter a valid country code."))
return value

View File

@@ -57,7 +57,7 @@ from django.utils.html import conditional_escape
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext
from i18nfield.strings import LazyI18nString
from PyPDF2 import PdfReader
from PyPDF2 import PdfFileReader
from pytz import timezone
from reportlab.graphics import renderPDF
from reportlab.graphics.barcode.qr import QrCodeWidget
@@ -530,10 +530,7 @@ def _get_attendee_name_part(key, op, order, ev):
def _get_ia_name_part(key, op, order, ev):
value = order.invoice_address.name_parts.get(key, '') if getattr(order, 'invoice_address', None) else ''
if key == 'salutation' and value:
return pgettext('person_name_salutation', value)
return value
return order.invoice_address.name_parts.get(key, '') if getattr(order, 'invoice_address', None) else ''
def get_images(event):
@@ -646,7 +643,7 @@ class Renderer:
self.event = event
if self.background_file:
self.bg_bytes = self.background_file.read()
self.bg_pdf = PdfReader(BytesIO(self.bg_bytes), strict=False)
self.bg_pdf = PdfFileReader(BytesIO(self.bg_bytes), strict=False)
else:
self.bg_bytes = None
self.bg_pdf = None
@@ -734,6 +731,7 @@ class Renderer:
text = o['text']
def replace(x):
print(x.group(1))
if x.group(1).startswith('itemmeta:'):
return op.item.meta_data.get(x.group(1)[9:]) or ''
elif x.group(1).startswith('meta:'):
@@ -831,10 +829,9 @@ class Renderer:
textColor=Color(o['color'][0] / 255, o['color'][1] / 255, o['color'][2] / 255),
alignment=align_map[o['align']]
)
# add an almost-invisible space &hairsp; after hyphens as word-wrap in ReportLab only works on space chars
text = conditional_escape(
self._get_text_content(op, order, o) or "",
).replace("\n", "<br/>\n").replace("-", "-&hairsp;")
).replace("\n", "<br/>\n")
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
# to resolve all ligatures and python-bidi to switch RTL texts.
@@ -861,7 +858,7 @@ class Renderer:
canvas.restoreState()
def draw_page(self, canvas: Canvas, order: Order, op: OrderPosition, show_page=True, only_page=None):
page_count = len(self.bg_pdf.pages)
page_count = self.bg_pdf.getNumPages()
if not only_page and not show_page:
raise ValueError("only_page=None and show_page=False cannot be combined")
@@ -881,7 +878,7 @@ class Renderer:
elif o['type'] == "poweredby":
self._draw_poweredby(canvas, op, o)
if self.bg_pdf:
canvas.setPageSize((self.bg_pdf.pages[0].mediabox[2], self.bg_pdf.pages[0].mediabox[3]))
canvas.setPageSize((self.bg_pdf.getPage(page).mediaBox[2], self.bg_pdf.getPage(page).mediaBox[3]))
if show_page:
canvas.showPage()
@@ -905,17 +902,17 @@ class Renderer:
with open(os.path.join(d, 'out.pdf'), 'rb') as f:
return BytesIO(f.read())
else:
from PyPDF2 import PdfReader, PdfWriter
from PyPDF2 import PdfFileReader, PdfFileWriter
buffer.seek(0)
new_pdf = PdfReader(buffer)
output = PdfWriter()
new_pdf = PdfFileReader(buffer)
output = PdfFileWriter()
for i, page in enumerate(new_pdf.pages):
bg_page = copy.copy(self.bg_pdf.pages[i])
bg_page.merge_page(page)
output.add_page(bg_page)
bg_page = copy.copy(self.bg_pdf.getPage(i))
bg_page.mergePage(page)
output.addPage(bg_page)
output.add_metadata({
output.addMetadata({
'/Title': str(title),
'/Creator': 'pretix',
})

View File

@@ -143,8 +143,8 @@ class Sig1TicketSecretGenerator(BaseTicketSecretGenerator):
The resulting string is REVERSED, to avoid all secrets of same length beginning with the same 10
characters, which would make it impossible to search for secrets manually.
"""
verbose_name = _('pretix signature scheme 1 (for very large events, changes semantics of offline scanning '
'please refer to documentation or support for details)')
verbose_name = _('pretix signature scheme 1 (for very large events, does not work with pretixSCAN on iOS and '
'changes semantics of offline scanning please refer to documentation or support for details)')
identifier = 'pretix_sig1'
use_revocation_list = True

View File

@@ -447,10 +447,7 @@ class CartManager:
try:
voucher = self.event.vouchers.get(code__iexact=voucher_code.strip())
except Voucher.DoesNotExist:
if self.event.organizer.accepted_gift_cards.filter(secret__iexact=voucher_code).exists():
raise CartError(error_messages['gift_card'])
else:
raise CartError(error_messages['voucher_invalid'])
raise CartError(error_messages['voucher_invalid'])
voucher_use_diff = Counter()
ops = []
@@ -947,11 +944,11 @@ class CartManager:
if voucher_available_count < 1:
if op.voucher in self._voucher_depend_on_cart:
err = err or _(error_messages['voucher_redeemed_cart']) % self.event.settings.reservation_time
err = err or error_messages['voucher_redeemed_cart'] % self.event.settings.reservation_time
else:
err = err or error_messages['voucher_redeemed']
elif voucher_available_count < requested_count:
err = err or _(error_messages['voucher_redeemed_partial']) % voucher_available_count
err = err or error_messages['voucher_redeemed_partial'] % voucher_available_count
available_count = min(quota_available_count, voucher_available_count)

View File

@@ -796,7 +796,6 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
gate=device.gate if device else None,
nonce=nonce,
forced=force and (not entry_allowed or from_revoked_secret),
force_sent=force,
raw_barcode=raw_barcode,
)
op.order.log_action('pretix.event.checkin', data={

View File

@@ -412,9 +412,8 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
logger.exception('Could not attach invoice to email')
pass
if attach_size < settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT - 1:
# Do not attach more than (limit - 1 MB) in tickets (1MB space for invoice, email itself, …),
# it will bounce way to often.
if attach_size < settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT:
# Do not attach more than 4MB, it will bounce way to often.
for a in args:
try:
email.attach(*a)

View File

@@ -2304,11 +2304,6 @@ class OrderChangeManager:
# Do nothing
return
# Clear prefetched objects cache of order. We're going to modify the positions and fees and we have no guarantee
# that every operation tuple points to a position/fee instance that has been fetched from the same object cache,
# so it's dangerous to keep the cache around.
self.order._prefetched_objects_cache = {}
# finally, incorporate difference in payment fees
self._payment_fee_diff()

View File

@@ -132,7 +132,6 @@ def generate_seats(event, subevent, plan, mapping, blocked_guids=None):
'already used in a voucher.', s.name))
Seat.objects.bulk_create(create_seats)
CartPosition.objects.filter(addon_to__seat__in=[s.pk for s in current_seats.values()]).delete()
CartPosition.objects.filter(seat__in=[s.pk for s in current_seats.values()]).delete()
OrderPosition.all.filter(
Q(canceled=True) | Q(order__status__in=(Order.STATUS_CANCELED, Order.STATUS_EXPIRED)),

View File

@@ -51,7 +51,7 @@ from pretix.celery_app import app
@app.task(base=ProfiledEventTask)
def export(event: Event, shredders: List[str], session_key=None, cfid=None) -> None:
def export(event: Event, shredders: List[str], session_key=None) -> None:
known_shredders = event.get_data_shredders()
with NamedTemporaryFile() as rawfile:
@@ -85,16 +85,13 @@ def export(event: Event, shredders: List[str], session_key=None, cfid=None) -> N
rawfile.seek(0)
if cfid:
cf = CachedFile.objects.get(pk=cfid)
else:
cf = CachedFile()
cf.date = now()
cf.session_key = session_key
cf.web_download = True
cf.expires = now() + timedelta(hours=1)
cf = CachedFile()
cf.date = now()
cf.filename = event.slug + '.zip'
cf.type = 'application/zip'
cf.session_key = session_key
cf.web_download = True
cf.expires = now() + timedelta(hours=1)
cf.save()
cf.file.save(cachedfile_name(cf, cf.filename), rawfile)
@@ -118,7 +115,7 @@ def shred(event: Event, fileid: str, confirm_code: str) -> None:
if not shredder:
continue
shredders.append(shredder)
if confirm_code is not True and any(shredder.require_download_confirmation for shredder in shredders):
if any(shredder.require_download_confirmation for shredder in shredders):
if indexdata['confirm_code'] != confirm_code:
raise ShredError(_("The confirm code you entered was incorrect."))
if event.logentry_set.filter(datetime__gte=parse(indexdata['time'])):

View File

@@ -57,7 +57,6 @@ from django_countries.fields import Country
from hierarkey.models import GlobalSettingsBase, Hierarkey
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
from i18nfield.strings import LazyI18nString
from phonenumbers import PhoneNumber, parse
from rest_framework import serializers
from pretix.api.serializers.fields import (
@@ -1515,15 +1514,6 @@ DEFAULTS = {
"before the order is canceled and a refund is issued."),
)
},
'cancel_allow_user_paid_require_approval_fee_unknown': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Do not show the cancellation fee to users when they request cancellation."),
)
},
'cancel_allow_user_paid_refund_as_giftcard': {
'default': 'off',
'type': str,
@@ -2595,9 +2585,7 @@ Your {organizer} team"""))
},
'name_scheme': {
'default': 'full', # default for new events is 'given_family'
'type': str,
'serializer_class': serializers.ChoiceField,
'serializer_kwargs': {},
'type': str
},
'giftcard_length': {
'default': settings.ENTROPY['giftcard_secret'],
@@ -2992,9 +2980,6 @@ PERSON_NAME_SCHEMES = OrderedDict([
},
}),
])
DEFAULTS['name_scheme']['serializer_kwargs']['choices'] = ((k, k) for k in PERSON_NAME_SCHEMES)
COUNTRIES_WITH_STATE_IN_ADDRESS = {
# Source: http://www.bitboost.com/ref/international-address-formats.html
# This is not a list of countries that *have* states, this is a list of countries where states
@@ -3031,7 +3016,6 @@ settings_hierarkey.add_type(LazyI18nStringList,
settings_hierarkey.add_type(RelativeDateWrapper,
serialize=lambda rdw: rdw.to_string(),
unserialize=lambda s: RelativeDateWrapper.from_string(s))
settings_hierarkey.add_type(PhoneNumber, lambda pn: pn.as_international, lambda s: parse(s))
@settings_hierarkey.set_global(cache_namespace='global')

View File

@@ -1,52 +0,0 @@
{% extends "error.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Unknown host" %}{% endblock %}
{% block content %}
<i class="fa fa-question-circle-o fa-fw big-icon"></i>
<div class="error-details">
<h1>{% trans "Unknown host" %}</h1>
<p>
{% blocktrans trimmed with host=header_host %}
Your browser told us that you want to access "{{ header_host }}". Unfortunately, we don't have
any content for this domain.
{% endblocktrans %}
</p>
{% if is_fresh_install %}
<p>
{% blocktrans trimmed %}
It looks like this is a fresh installation of pretix. This error message is probably caused due to
the fact that either your configuration includes the wrong site URL or your reverse proxy is sending
the wrong header.
{% endblocktrans %}
</p>
<dl>
<dt>{% trans "Expected host according to configuration" %}</dt>
<dd><code>{{ site_host }}</code></dd>
<dt>{% trans "Received headers" %}</dt>
<dd>
<code>Host: {{ request.headers.Host }}</code>
{% if xfh %}
<br>
<code>X-Forwarded-For: {{ xfh }}</code>
{% if not settings.USE_X_FORWARDED_HOST %}({% trans "ignored" %}){% endif %}
{% endif %}
</dd>
<dt>{% trans "Derived host from headers" %}</dt>
<dd><code>{{ header_host }}</code></dd>
</dl>
{% else %}
<p>
{% blocktrans trimmed %}
If you just configured this as a domain for your ticket shop, you now need to set this up as a "custom domain"
in your organizer account.
{% endblocktrans %}
</p>
{% endif %}
<p class="links">
<a id='goback' href='#'>{% trans "Take a step back" %}</a>
&middot; <a id='reload' href='#'>{% trans "Try again" %}</a>
</p>
<img src="{% static "pretixbase/img/pretix-logo.svg" %}" class="logo"/>
</div>
{% endblock %}

View File

@@ -96,7 +96,6 @@ ALLOWED_ATTRIBUTES = {
'div': ['class'],
'p': ['class'],
'span': ['class', 'title'],
'ol': ['start'],
# Update doc/user/markdown.rst if you change this!
}

View File

@@ -84,17 +84,13 @@ def timeline_for_event(event, subevent=None):
edit_url=ev_edit_url
))
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=(
ev.presale_end or ev.date_to or ev.date_from.astimezone(ev.timezone).replace(hour=23, minute=59, second=59)
),
description='{}{}'.format(
pgettext_lazy('timeline', 'End of ticket sales'),
f" ({pgettext_lazy('timeline', 'automatically because the event is over and no end of presale has been configured')})" if not ev.presale_end else ""
),
edit_url=ev_edit_url
))
if ev.presale_end:
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=ev.presale_end,
description=pgettext_lazy('timeline', 'End of ticket sales'),
edit_url=ev_edit_url
))
rd = event.settings.get('last_order_modification_date', as_type=RelativeDateWrapper)
if rd:

View File

@@ -29,15 +29,13 @@ from celery.result import AsyncResult
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import ValidationError
from django.http import HttpResponse, JsonResponse, QueryDict
from django.http import JsonResponse, QueryDict
from django.shortcuts import redirect, render
from django.test import RequestFactory
from django.utils import timezone, translation
from django.utils.timezone import get_current_timezone
from django.utils.translation import get_language, gettext as _
from django.views import View
from django.views.generic import FormView
from redis import ResponseError
from pretix.base.models import User
from pretix.base.services.tasks import ProfiledEventTask
@@ -70,11 +68,6 @@ class AsyncMixin:
res.get(timeout=timeout, propagate=False)
except celery.exceptions.TimeoutError:
pass
except ResponseError:
# There is a long-standing concurrency issue in either celery or redis-py that hasn't been fixed
# yet. Instead of crashing, we can ignore it and the client will retry their request and hopefully
# it is fixed next time.
logger.warning('Ignored ResponseError in AsyncResult.get()')
except ConnectionError:
# Redis probably just restarted, let's just report not ready and retry next time
data = self._ajax_response_data()
@@ -313,94 +306,3 @@ class AsyncFormView(AsyncMixin, FormView):
else:
return self.error(res.info)
return redirect(self.get_check_url(res.id, False))
class AsyncPostView(AsyncMixin, View):
"""
View variant in which instead of ``post``, an ``async_post`` is executed in a celery task.
Note that this places some severe limitations on the form and the view, e.g. ``async_post`` may not
depend on the request object unless specifically supported by this class. File upload is currently also
not supported.
"""
known_errortypes = ['ValidationError']
expected_exceptions = (ValidationError,)
task_base = ProfiledEventTask
def __init_subclass__(cls):
def async_execute(self, *, request_path, url_args, url_kwargs, query_string, post_data, locale, tz,
organizer=None, event=None, user=None, session_key=None):
view_instance = cls()
req = RequestFactory().post(
request_path + '?' + query_string,
data=post_data,
content_type='application/x-www-form-urlencoded'
)
view_instance.request = req
if event:
view_instance.request.event = event
view_instance.request.organizer = event.organizer
elif organizer:
view_instance.request.organizer = organizer
if user:
view_instance.request.user = User.objects.get(pk=user) if isinstance(user, int) else user
if session_key:
engine = import_module(settings.SESSION_ENGINE)
self.SessionStore = engine.SessionStore
view_instance.request.session = self.SessionStore(session_key)
with translation.override(locale), timezone.override(pytz.timezone(tz)):
return view_instance.async_post(view_instance.request, *url_args, **url_kwargs)
cls.async_execute = app.task(
base=cls.task_base,
bind=True,
name=cls.__module__ + '.' + cls.__name__ + '.async_execute',
throws=cls.expected_exceptions
)(async_execute)
def async_post(self, request, *args, **kwargs):
pass
def get(self, request, *args, **kwargs):
if 'async_id' in request.GET and settings.HAS_CELERY:
return self.get_result(request)
return HttpResponse(status=405)
def post(self, request, *args, **kwargs):
if request.FILES:
raise TypeError('File upload currently not supported in AsyncPostView')
kwargs = {
'request_path': self.request.path,
'query_string': self.request.GET.urlencode(),
'post_data': self.request.POST.urlencode(),
'locale': get_language(),
'url_args': args,
'url_kwargs': kwargs,
'tz': get_current_timezone().zone,
}
if hasattr(self.request, 'organizer'):
kwargs['organizer'] = self.request.organizer.pk
if self.request.user.is_authenticated:
kwargs['user'] = self.request.user.pk
if hasattr(self.request, 'event'):
kwargs['event'] = self.request.event.pk
if hasattr(self.request, 'session'):
kwargs['session_key'] = self.request.session.session_key
try:
res = type(self).async_execute.apply_async(kwargs=kwargs)
except ConnectionError:
# Task very likely not yet sent, due to redis restarting etc. Let's try once again
res = type(self).async_execute.apply_async(kwargs=kwargs)
if 'ajax' in self.request.GET or 'ajax' in self.request.POST:
data = self._return_ajax_result(res)
data['check_url'] = self.get_check_url(res.id, True)
return JsonResponse(data)
else:
if res.ready():
if res.successful() and not isinstance(res.info, Exception):
return self.success(res.info)
else:
return self.error(res.info)
return redirect(self.get_check_url(res.id, False))

View File

@@ -71,7 +71,7 @@ def _default_context(request):
except Resolver404:
return {}
if not request.path.startswith(get_script_prefix() + 'control') or not hasattr(request, 'user'):
if not request.path.startswith(get_script_prefix() + 'control'):
return {}
ctx = {
'url_name': url.url_name,

View File

@@ -22,10 +22,9 @@
from datetime import datetime, timedelta
from django import forms
from django.core.exceptions import ValidationError
from django.urls import reverse
from django.utils.timezone import get_current_timezone, make_aware, now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django.utils.translation import pgettext_lazy
from django_scopes.forms import (
SafeModelChoiceField, SafeModelMultipleChoiceField,
)
@@ -110,7 +109,6 @@ class CheckinListForm(forms.ModelForm):
'rules',
'gates',
'exit_all_at',
'addon_match',
]
widgets = {
'limit_products': forms.CheckboxSelectMultiple(attrs={
@@ -132,12 +130,6 @@ class CheckinListForm(forms.ModelForm):
def clean(self):
d = super().clean()
d['rules'] = CheckinList.validate_rules(d.get('rules'))
if d.get('addon_match') and d.get('all_products'):
raise ValidationError(_('If you allow checking in add-on tickets by scanning the main ticket, you must '
'select a specific set of products for this check-in list, only including the '
'possible add-on products.'))
return d

View File

@@ -39,11 +39,9 @@ from urllib.parse import urlencode, urlparse
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, validate_email
from django.core.validators import validate_email
from django.db.models import Prefetch, Q, prefetch_related_objects
from django.forms import (
CheckboxSelectMultiple, formset_factory, inlineformset_factory,
)
from django.forms import CheckboxSelectMultiple, formset_factory
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.html import escape
@@ -60,7 +58,7 @@ from pretix.base.channels import get_all_sales_channels
from pretix.base.email import get_available_placeholders
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
from pretix.base.models import Event, Organizer, TaxRule, Team
from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent
from pretix.base.models.event import EventMetaValue, SubEvent
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from pretix.base.settings import (
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, validate_event_settings,
@@ -674,7 +672,6 @@ class CancelSettingsForm(SettingsForm):
'cancel_allow_user_paid_adjust_fees_step',
'cancel_allow_user_paid_refund_as_giftcard',
'cancel_allow_user_paid_require_approval',
'cancel_allow_user_paid_require_approval_fee_unknown',
'change_allow_user_variation',
'change_allow_user_price',
'change_allow_user_until',
@@ -848,7 +845,6 @@ class InvoiceSettingsForm(SettingsForm):
self.fields['invoice_generate_sales_channels'].choices = (
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
)
self.fields['invoice_numbers_counter_length'].validators.append(MaxValueValidator(15))
def clean(self):
data = super().clean()
@@ -1488,25 +1484,3 @@ ConfirmTextFormset = formset_factory(
formset=BaseConfirmTextFormSet,
can_order=True, can_delete=True, extra=0
)
class EventFooterLinkForm(I18nModelForm):
class Meta:
model = EventFooterLink
fields = ('label', 'url')
class BaseEventFooterLinkFormSet(I18nFormSetMixin, forms.BaseInlineFormSet):
def __init__(self, *args, **kwargs):
event = kwargs.pop('event', None)
if event:
kwargs['locales'] = event.settings.get('locales')
super().__init__(*args, **kwargs)
EventFooterLinkFormset = inlineformset_factory(
Event, EventFooterLink,
EventFooterLinkForm,
formset=BaseEventFooterLinkFormSet,
can_order=False, can_delete=True, extra=0
)

View File

@@ -32,7 +32,6 @@
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import copy
import os
from decimal import Decimal
from urllib.parse import urlencode
@@ -424,10 +423,9 @@ class ItemCreateForm(I18nModelForm):
if self.cleaned_data.get('has_variations'):
if self.cleaned_data.get('copy_from') and self.cleaned_data.get('copy_from').has_variations:
for variation in self.cleaned_data['copy_from'].variations.all():
v = copy.copy(variation)
v.pk = None
v.item = instance
v.save()
ItemVariation.objects.create(item=instance, value=variation.value, active=variation.active,
position=variation.position, default_price=variation.default_price,
description=variation.description, original_price=variation.original_price)
else:
ItemVariation.objects.create(
item=instance, value=__('Standard')

View File

@@ -39,13 +39,12 @@ from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.forms import inlineformset_factory
from django.forms.utils import ErrorDict
from django.utils.crypto import get_random_string
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes.forms import SafeModelMultipleChoiceField
from i18nfield.forms import I18nFormField, I18nFormSetMixin, I18nTextarea
from i18nfield.forms import I18nFormField, I18nTextarea
from phonenumber_field.formfields import PhoneNumberField
from pytz import common_timezones
@@ -61,7 +60,6 @@ from pretix.base.models import (
Customer, Device, EventMetaProperty, Gate, GiftCard, Membership,
MembershipType, Organizer, Team,
)
from pretix.base.models.organizer import OrganizerFooterLink
from pretix.base.settings import PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS
from pretix.control.forms import ExtFileField, SplitDateTimeField
from pretix.control.forms.event import (
@@ -684,25 +682,3 @@ class MembershipUpdateForm(forms.ModelForm):
titles=self.instance.customer.organizer.settings.name_scheme_titles,
label=_('Attendee name'),
)
class OrganizerFooterLinkForm(I18nModelForm):
class Meta:
model = OrganizerFooterLink
fields = ('label', 'url')
class BaseOrganizerFooterLinkFormSet(I18nFormSetMixin, forms.BaseInlineFormSet):
def __init__(self, *args, **kwargs):
organizer = kwargs.pop('organizer', None)
if organizer:
kwargs['locales'] = organizer.settings.get('locales')
super().__init__(*args, **kwargs)
OrganizerFooterLinkFormset = inlineformset_factory(
Organizer, OrganizerFooterLink,
OrganizerFooterLinkForm,
formset=BaseOrganizerFooterLinkFormSet,
can_order=False, can_delete=True, extra=0
)

View File

@@ -314,7 +314,6 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.object.cloned': _('This object has been created by cloning.'),
'pretix.organizer.changed': _('The organizer has been changed.'),
'pretix.organizer.settings': _('The organizer settings have been changed.'),
'pretix.organizer.footerlinks.changed': _('The footer links have been changed.'),
'pretix.giftcards.acceptance.added': _('Gift card acceptance for another organizer has been added.'),
'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'),
'pretix.webhook.created': _('The webhook has been created.'),
@@ -366,7 +365,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.order.payment.changed': _('A new payment {local_id} has been started instead of the previous one.'),
'pretix.event.order.email.sent': _('An unidentified type email has been sent.'),
'pretix.event.order.email.error': _('Sending of an email has failed.'),
'pretix.event.order.email.attachments.skipped': _('The email has been sent without attached tickets since they '
'pretix.event.order.email.attachments.skipped': _('The email has been sent without attachments since they '
'would have been too large to be likely to arrive.'),
'pretix.event.order.email.custom_sent': _('A custom email has been sent.'),
'pretix.event.order.position.email.custom_sent': _('A custom email has been sent to an attendee.'),
@@ -469,7 +468,6 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.testmode.deactivated': _('The test mode has been disabled.'),
'pretix.event.added': _('The event has been created.'),
'pretix.event.changed': _('The event details have been changed.'),
'pretix.event.footerlinks.changed': _('The footer links have been changed.'),
'pretix.event.question.option.added': _('An answer option has been added to the question.'),
'pretix.event.question.option.deleted': _('An answer option has been removed from the question.'),
'pretix.event.question.option.changed': _('An answer option has been changed.'),

View File

@@ -72,31 +72,5 @@
</dd>
<dt>{% trans "Authorization Code" %}</dt>
<dd>{{ payment_info.payment_data.authorizationCode }}</dd>
{% elif payment_info.payment_type == "izettle_qrc" %}
<dt>{% trans "Payment provider" %}</dt>
<dd>PayPal QRC via Zettle</dd>
<dt>{% trans "Payment reference" %}</dt>
<dd>{{ payment_info.payment_data.reference }}</dd>
<dt>{% trans "Transaction ID" %}</dt>
<dd>{{ payment_info.payment_data.transactionId }}</dd>
{% elif payment_info.payment_type == "adyen_legacy" %}
<dt>{% trans "Payment provider" %}</dt>
<dd>Adyen POS</dd>
<dt>{% trans "Reference" %}</dt>
<dd>{{ payment_info.payment_data.pspReference }}</dd>
<dt>{% trans "Terminal ID" %}</dt>
<dd>{{ payment_info.payment_data.terminalId }}</dd>
<dt>{% trans "Payment method" %}</dt>
<dd>{{ payment_info.payment_data.paymentMethod }} ({{ payment_info.payment_data.cardType }} / {{ payment_info.payment_data.cardScheme }} / {{ payment_info.payment_data.paymentMethodVariant }})</dd>
<dt>{% trans "Card holder" %}</dt>
<dd>{{ payment_info.payment_data.cardHolderName }}</dd>
<dt>{% trans "Card number" %}</dt>
<dd>{{ payment_info.payment_data.cardBin }} **** {{ payment_info.payment_data.cardSummary }}</dd>
<dt>{% trans "Card expiration" %}</dt>
<dd>{{ payment_info.payment_data.expiryMonth }} / {{ payment_info.payment_data.expiryYear }}</dd>
<dt>{% trans "Card Entry Mode" %}</dt>
<dd>{{ payment_info.payment_data.posEntryMode }}</dd>
<dt>{% trans "Result Code" %}</dt>
<dd>{{ payment_info.payment_data.posResultCode }}</dd>
{% endif %}
</dl>

View File

@@ -80,17 +80,13 @@
{% elif c.forced and c.successful %}
<span class="fa fa-fw fa-warning" data-toggle="tooltip"
title="{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Additional entry scan: {{ date }}{% endblocktrans %}"></span>
{% elif c.force_sent %}
<span class="fa fa-fw fa-cloud-upload" data-toggle="tooltip"
title="{% blocktrans trimmed with date=c.created|date:'SHORT_DATETIME_FORMAT' %}Offline scan. Upload time: {{ date }}{% endblocktrans %}"></span>
{% elif c.forced and not c.successful %}
<br>
<small class="text-muted">{% trans "Failed in offline mode" %}</small>
{% elif c.auto_checked_in %}
<span class="fa fa-fw fa-magic" data-toggle="tooltip"
title="{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically checked in: {{ date }}{% endblocktrans %}"></span>
{% endif %}
{% if c.forced and not c.successful %}
<br>
<small class="text-muted">{% trans "Failed in offline mode" %}</small>
{% endif %}
</td>
<td>
{% if c.type == "exit" %}<span class="fa fa-fw fa-sign-out"></span>{% endif %}

View File

@@ -71,20 +71,13 @@
</p>
</div>
{% else %}
<form method="post" action="{% url "control:event.orders.checkinlists.bulk_action" event=request.event.slug organizer=request.event.organizer.slug list=checkinlist.pk %}" data-asynctask>
<div class="hidden">
{{ filter_form.as_p }}
<input name="returnquery" type="hidden" value="{{ request.META.QUERY_STRING }}">
</div>
<form method="post" action="">
{% csrf_token %}
<div class="table-responsive">
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>
<label aria-label="{% trans "select all rows for batch-operation" %}"
class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
</th>
<th></th>
<th>{% trans "Order code" %} <a href="?{% url_replace request 'ordering' '-code'%}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'code'%}"><i class="fa fa-caret-up"></i></a></th>
<th>{% trans "Item" %} <a href="?{% url_replace request 'ordering' '-item'%}"><i class="fa fa-caret-down"></i></a>
@@ -107,19 +100,6 @@
<th>{% trans "Timestamp" %} <a href="?{% url_replace request 'ordering' '-timestamp'%}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'timestamp'%}"><i class="fa fa-caret-up"></i></a></th>
</tr>
{% if page_obj.paginator.num_pages > 1 %}
<tr class="table-select-all warning hidden">
<td>
<input type="checkbox" name="__ALL" id="__all"
data-results-total="{{ page_obj.paginator.count }}">
</td>
<td colspan="8">
<label for="__all">
{% trans "Select all results on other pages as well" %}
</label>
</td>
</tr>
{% endif %}
</thead>
<tbody>
{% for e in entries %}
@@ -200,16 +180,13 @@
</div>
{% if "can_change_orders" in request.eventpermset %}
<button type="submit" class="btn btn-primary btn-save">
<span class="fa fa-sign-in" aria-hidden="true"></span>
{% trans "Check-In selected attendees" %}
</button>
<button type="submit" class="btn btn-default btn-save" name="checkout" value="true">
<span class="fa fa-sign-out" aria-hidden="true"></span>
{% trans "Check-Out selected attendees" %}
</button>
<button type="submit" class="btn btn-danger btn-save" name="revert" value="true">
<span class="fa fa-trash" aria-hidden="true"></span>
{% trans "Delete all check-ins of selected attendees" %}
<button type="submit" class="btn btn-default btn-save" name="revert" value="true">
{% trans "Revert selected check-ins" %}
</button>
{% endif %}
</form>

View File

@@ -48,14 +48,16 @@
Make sure to always use the latest version of our scanning apps for these options to work.
{% endblocktrans %}
<br>
{% blocktrans trimmed %}
If you make use of these advanced options, we recommend using our Android and Desktop apps.
{% endblocktrans %}
<strong>
{% blocktrans trimmed %}
If you make use of these advanced options, we recommend using our Android and Desktop apps.
Custom check-in rules do not work offline with our iOS scanning app.
{% endblocktrans %}
</strong>
</div>
{% bootstrap_field form.allow_multiple_entries layout="control" %}
{% bootstrap_field form.allow_entry_after_exit layout="control" %}
{% bootstrap_field form.addon_match layout="control" %}
{% bootstrap_field form.exit_all_at layout="control" %}
{% bootstrap_field form.auto_checkin_sales_channels layout="control" %}
{% if form.gates %}

View File

@@ -16,9 +16,6 @@
<legend>{% trans "Paid orders" %}</legend>
{% bootstrap_field form.cancel_allow_user_paid layout="control" %}
{% bootstrap_field form.cancel_allow_user_paid_require_approval layout="control" %}
<div data-display-dependency="#id_cancel_allow_user_paid_require_approval">
{% bootstrap_field form.cancel_allow_user_paid_require_approval_fee_unknown layout="control" %}
</div>
{% bootstrap_field form.cancel_allow_user_paid_keep layout="control" %}
{% bootstrap_field form.cancel_allow_user_paid_keep_percentage layout="control" %}
{% bootstrap_field form.cancel_allow_user_paid_keep_fees layout="control" %}

View File

@@ -69,8 +69,7 @@
</label>
<div class="col-md-9">
<div class="checkbox">
<label><input type="checkbox" checked="checked"
disabled="disabled"> {% trans "Ask and require input" %}</label>
<label><input type="checkbox" checked="checked" disabled="disabled"> {% trans "Ask and require input" %}</label>
</div>
</div>
</div>
@@ -82,8 +81,7 @@
</label>
<div class="col-md-9 static-form-row">
<p>
<a href="{% url "control:event.settings.invoice" event=request.event.slug organizer=request.organizer.slug %}#tab-0-1-open"
target="_blank">
<a href="{% url "control:event.settings.invoice" event=request.event.slug organizer=request.organizer.slug %}#tab-0-1-open" target="_blank">
{% trans "See invoice settings" %}
</a>
</p>
@@ -103,8 +101,7 @@
</label>
<div class="col-md-9 static-form-row">
<p>
<a href="{% url "control:event.items.questions" event=request.event.slug organizer=request.organizer.slug %}"
target="_blank">
<a href="{% url "control:event.items.questions" event=request.event.slug organizer=request.organizer.slug %}" target="_blank">
{% trans "Manage questions" %}
</a>
</p>
@@ -235,74 +232,10 @@
{% bootstrap_field sform.display_net_prices layout="control" %}
{% bootstrap_field sform.show_variations_expanded layout="control" %}
{% bootstrap_field sform.hide_sold_out layout="control" %}
<div class="form-group">
<label class="col-md-3 control-label">
{% trans "Footer links" %}<br>
<span class="optional">{% trans "Optional" %}</span>
</label>
<div class="col-md-9">
<p class="help-block">
{% blocktrans trimmed %}
These links will be shown in the footer of your ticket shop. You could
for example link your terms of service here. Your contact address, imprint, and privacy
policy will be linked automatically (if you configured them), so you do not need to add
them here.
{% endblocktrans %}
</p>
<div class="formset" data-formset data-formset-prefix="{{ footer_links_formset.prefix }}">
{{ footer_links_formset.management_form }}
{% bootstrap_formset_errors footer_links_formset %}
<div data-formset-body>
{% for form in footer_links_formset %}
<div class="row formset-row" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-md-5">
{% bootstrap_form_errors form %}
{% bootstrap_field form.label layout='inline' form_group_class="" %}
</div>
<div class="col-md-5">
{% bootstrap_field form.url layout='inline' form_group_class="" %}
</div>
<div class="col-md-2 text-right flip">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="row formset-row" data-formset-form>
<div class="sr-only">
{{ footer_links_formset.empty_form.id }}
{% bootstrap_field footer_links_formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-md-5">
{% bootstrap_field footer_links_formset.empty_form.label layout='inline' form_group_class="" %}
</div>
<div class="col-md-5">
{% bootstrap_field footer_links_formset.empty_form.url layout='inline' form_group_class="" %}
</div>
<div class="col-md-2 text-right flip">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add link" %}</button>
</p>
</div>
</div>
</div>
{% url "control:organizer.edit" organizer=request.organizer.slug as org_url %}
{% propagated request.event org_url "meta_noindex" %}
{% bootstrap_field sform.meta_noindex layout="control" %}
{% endpropagated %}
{% if sform.frontpage_subevent_ordering %}
{% bootstrap_field sform.frontpage_subevent_ordering layout="control" %}
{% endif %}
@@ -312,11 +245,6 @@
{% if sform.event_list_available_only %}
{% bootstrap_field sform.event_list_available_only layout="control" %}
{% endif %}
{% url "control:organizer.edit" organizer=request.organizer.slug as org_url %}
{% propagated request.event org_url "meta_noindex" %}
{% bootstrap_field sform.meta_noindex layout="control" %}
{% endpropagated %}
</fieldset>
<fieldset>
<legend>{% trans "Cart" %}</legend>
@@ -334,15 +262,13 @@
</div>
<div class="alert alert-info">
{% blocktrans trimmed %}
The waiting list determines availability mainly based on quotas. If you use a seating plan and
your
The waiting list determines availability mainly based on quotas. If you use a seating plan and your
number of available seats is less than the available quota, you might run into situations where
people are sent an email from the waiting list but still are unable to book a seat.
{% endblocktrans %}
<strong>
{% blocktrans trimmed %}
Specifically, this means the waiting list is not safe to use together with the minimum
distance
Specifically, this means the waiting list is not safe to use together with the minimum distance
feature of our seating plan module.
{% endblocktrans %}
</strong>

View File

@@ -10,7 +10,7 @@
<p>{% blocktrans %}You cannot delete the discount <strong>{{ discount }}</strong> because it already has
been used as part of an order.{% endblocktrans %}</p>
<div class="form-group submit-group">
<a href="{% url "control:event.items.discounts" organizer=request.event.organizer.slug event=request.event.slug %}"
<a href="{% url "control:event.discounts" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>

View File

@@ -153,7 +153,7 @@
{% endif %}
</td>
<td>
<span class="fa fa-fw fa-{{ o.sales_channel_obj.icon }} text-muted"
<span class="fa fa-{{ o.sales_channel_obj.icon }} text-muted"
data-toggle="tooltip" title="{% trans o.sales_channel_obj.verbose_name %}"></span>
{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}
</td>

View File

@@ -50,77 +50,6 @@
{% bootstrap_field sform.event_list_availability layout="control" %}
{% bootstrap_field sform.organizer_link_back layout="control" %}
{% bootstrap_field sform.meta_noindex layout="control" %}
<div class="form-group">
<label class="col-md-3 control-label">
{% trans "Footer links" %}<br>
<span class="optional">{% trans "Optional" %}</span>
</label>
<div class="col-md-9">
<p class="help-block">
{% blocktrans trimmed %}
These links will be shown in the footer of your ticket shop. You could
for example link your terms of service here. Your contact address, imprint, and privacy
policy will be linked automatically (if you configured them), so you do not need to add
them here.
{% endblocktrans %}
</p>
<p class="help-block">
{% blocktrans trimmed %}
The links you configure here will also be shown on all of your events.
{% endblocktrans %}
</p>
<div class="formset" data-formset data-formset-prefix="{{ footer_links_formset.prefix }}">
{{ footer_links_formset.management_form }}
{% bootstrap_formset_errors footer_links_formset %}
<div data-formset-body>
{% for form in footer_links_formset %}
<div class="row formset-row" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-md-5">
{% bootstrap_form_errors form %}
{% bootstrap_field form.label layout='inline' form_group_class="" %}
</div>
<div class="col-md-5">
{% bootstrap_field form.url layout='inline' form_group_class="" %}
</div>
<div class="col-md-2 text-right flip">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="row formset-row" data-formset-form>
<div class="sr-only">
{{ footer_links_formset.empty_form.id }}
{% bootstrap_field footer_links_formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-md-5">
{% bootstrap_field footer_links_formset.empty_form.label layout='inline' form_group_class="" %}
</div>
<div class="col-md-5">
{% bootstrap_field footer_links_formset.empty_form.url layout='inline' form_group_class="" %}
</div>
<div class="col-md-2 text-right flip">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add link" %}</button>
</p>
</div>
</div>
</div>
</fieldset>
<fieldset>
<legend>{% trans "Localization" %}</legend>

View File

@@ -16,12 +16,6 @@
</li>
{% endfor %}
</ul>
<div class="alert-info alert">
{% blocktrans trimmed %}
It is possible that some of the above dates can't be deleted if a plugin has data attached to them.
In that case, they will be disabled instead.
{% endblocktrans %}
</div>
{% endif %}
{% if forbidden %}
<p>{% blocktrans trimmed %}The following dates can't be deleted as they already have orders, but
@@ -36,8 +30,7 @@
</ul>
{% endif %}
<div class="form-group submit-group">
<a href="{% url "control:event.subevents" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-default btn-cancel">
<a href="{% url "control:event.subevents" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger btn-save" value="delete_confirm" name="action">

View File

@@ -392,7 +392,6 @@ urlpatterns = [
re_path(r'^checkinlists/add$', checkin.CheckinListCreate.as_view(), name='event.orders.checkinlists.add'),
re_path(r'^checkinlists/select2$', typeahead.checkinlist_select2, name='event.orders.checkinlists.select2'),
re_path(r'^checkinlists/(?P<list>\d+)/$', checkin.CheckInListShow.as_view(), name='event.orders.checkinlists.show'),
re_path(r'^checkinlists/(?P<list>\d+)/bulk_action$', checkin.CheckInListBulkActionView.as_view(), name='event.orders.checkinlists.bulk_action'),
re_path(r'^checkinlists/(?P<list>\d+)/change$', checkin.CheckinListUpdate.as_view(),
name='event.orders.checkinlists.edit'),
re_path(r'^checkinlists/(?P<list>\d+)/delete$', checkin.CheckinListDelete.as_view(),

View File

@@ -37,7 +37,7 @@ from django.contrib import messages
from django.db import transaction
from django.db.models import Exists, Max, OuterRef, Prefetch, Subquery
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.timezone import is_aware, make_aware, now
@@ -49,7 +49,6 @@ from pretix.base.channels import get_all_sales_channels
from pretix.base.models import Checkin, Order, OrderPosition
from pretix.base.models.checkin import CheckinList
from pretix.base.signals import checkin_created
from pretix.base.views.tasks import AsyncPostView
from pretix.control.forms.checkin import CheckinListForm
from pretix.control.forms.filter import (
CheckinFilterForm, CheckinListAttendeeFilterForm,
@@ -59,13 +58,11 @@ from pretix.control.views import CreateView, PaginationMixin, UpdateView
from pretix.helpers.models import modelcopy
class CheckInListQueryMixin:
@cached_property
def request_data(self):
if self.request.method == "POST":
return self.request.POST
return self.request.GET
class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, ListView):
model = Checkin
context_object_name = 'entries'
template_name = 'pretixcontrol/checkin/index.html'
permission = 'can_view_orders'
def get_queryset(self, filter=True):
cqs = Checkin.objects.filter(
@@ -108,28 +105,16 @@ class CheckInListQueryMixin:
if filter and self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)
if 'checkin' in self.request_data and '__ALL' not in self.request_data:
qs = qs.filter(
id__in=self.request_data.getlist('checkin')
)
return qs
@cached_property
def filter_form(self):
return CheckinListAttendeeFilterForm(
data=self.request_data,
data=self.request.GET,
event=self.request.event,
list=self.list
)
class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, CheckInListQueryMixin, ListView):
model = Checkin
context_object_name = 'entries'
template_name = 'pretixcontrol/checkin/index.html'
permission = 'can_view_orders'
def dispatch(self, request, *args, **kwargs):
self.list = get_object_or_404(self.request.event.checkin_lists.all(), pk=kwargs.get("list"))
return super().dispatch(request, *args, **kwargs)
@@ -168,23 +153,18 @@ class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, CheckInList
e.last_exit_aware = e.last_exit
return ctx
def post(self, request, *args, **kwargs):
if "can_change_orders" not in request.eventpermset:
messages.error(request, _('You do not have permission to perform this action.'))
return redirect(reverse('control:event.orders.checkins', kwargs={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug
}) + '?' + request.GET.urlencode())
class CheckInListBulkActionView(CheckInListQueryMixin, EventPermissionRequiredMixin, AsyncPostView):
template_name = 'pretixcontrol/organizers/device_bulk_edit.html'
permission = 'can_change_orders'
context_object_name = 'device'
positions = self.get_queryset(filter=False).filter(
pk__in=request.POST.getlist('checkin')
)
def dispatch(self, request, *args, **kwargs):
self.list = get_object_or_404(self.request.event.checkin_lists.all(), pk=kwargs.get("list"))
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return super().get_queryset().prefetch_related(None).order_by()
@transaction.atomic()
def async_post(self, request, *args, **kwargs):
self.list = get_object_or_404(request.event.checkin_lists.all(), pk=kwargs.get("list"))
positions = self.get_queryset()
if request.POST.get('revert') == 'true':
for op in positions:
if op.order.status == Order.STATUS_PAID or (self.list.include_pending and op.order.status == Order.STATUS_PENDING):
@@ -197,11 +177,12 @@ class CheckInListBulkActionView(CheckInListQueryMixin, EventPermissionRequiredMi
}, user=request.user)
op.order.touch()
return 'reverted', request.POST.get('returnquery')
messages.success(request, _('The selected check-ins have been reverted.'))
else:
t = Checkin.TYPE_EXIT if request.POST.get('checkout') == 'true' else Checkin.TYPE_ENTRY
for op in positions:
if op.order.status == Order.STATUS_PAID or (self.list.include_pending and op.order.status == Order.STATUS_PENDING):
t = Checkin.TYPE_EXIT if request.POST.get('checkout') == 'true' else Checkin.TYPE_ENTRY
lci = op.checkins.filter(list=self.list).first()
if self.list.allow_multiple_entries or t != Checkin.TYPE_ENTRY or (lci and lci.type != Checkin.TYPE_ENTRY):
ci = Checkin.objects.create(position=op, list=self.list, datetime=now(), type=t)
@@ -225,22 +206,14 @@ class CheckInListBulkActionView(CheckInListQueryMixin, EventPermissionRequiredMi
'web': True
}, user=request.user)
checkin_created.send(op.order.event, checkin=ci)
return 'checked-out' if t == Checkin.TYPE_EXIT else 'checked-in', request.POST.get('returnquery')
def get_success_message(self, value):
if value[0] == 'reverted':
return _('The selected check-ins have been reverted.')
elif value[0] == 'checked-out':
return _('The selected tickets have been marked as checked out.')
else:
return _('The selected tickets have been marked as checked in.')
messages.success(request, _('The selected tickets have been marked as checked in.'))
def get_success_url(self, value):
return reverse('control:event.orders.checkinlists.show', kwargs={
return redirect(reverse('control:event.orders.checkinlists.show', kwargs={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug,
'list': self.list.pk
}) + ('?' + value[1] if value[1] else '')
}) + '?' + request.GET.urlencode())
class CheckinListList(EventPermissionRequiredMixin, PaginationMixin, ListView):
@@ -429,9 +402,9 @@ class CheckinListView(EventPermissionRequiredMixin, PaginationMixin, ListView):
qs = Checkin.all.filter(
list__event=self.request.event,
).select_related(
'position', 'position__order', 'position__item', 'position__variation', 'position__subevent'
'position', 'position', 'position__item', 'position__variation', 'position__subevent'
).prefetch_related(
'list', 'gate', 'device'
'list', 'gate'
)
if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)

View File

@@ -74,9 +74,9 @@ from pretix.base.signals import register_ticket_outputs
from pretix.base.templatetags.rich_text import markdown_compile_email
from pretix.control.forms.event import (
CancelSettingsForm, CommentForm, ConfirmTextFormset, EventDeleteForm,
EventFooterLinkFormset, EventMetaValueForm, EventSettingsForm,
EventUpdateForm, InvoiceSettingsForm, ItemMetaPropertyForm,
MailSettingsForm, PaymentSettingsForm, ProviderForm, QuickSetupForm,
EventMetaValueForm, EventSettingsForm, EventUpdateForm,
InvoiceSettingsForm, ItemMetaPropertyForm, MailSettingsForm,
PaymentSettingsForm, ProviderForm, QuickSetupForm,
QuickSetupProductFormSet, TaxRuleForm, TaxRuleLineFormSet,
TicketSettingsForm, WidgetCodeForm,
)
@@ -186,7 +186,6 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
context['meta_forms'] = self.meta_forms
context['item_meta_property_formset'] = self.item_meta_property_formset
context['confirm_texts_formset'] = self.confirm_texts_formset
context['footer_links_formset'] = self.footer_links_formset
return context
@transaction.atomic
@@ -196,7 +195,6 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
self.save_meta()
self.save_item_meta_property_formset(self.object)
self.save_confirm_texts_formset(self.object)
self.save_footer_links_formset(self.object)
change_css = False
if self.sform.has_changed() or self.confirm_texts_formset.has_changed():
@@ -206,10 +204,6 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
self.request.event.log_action('pretix.event.settings', user=self.request.user, data=data)
if any(p in self.sform.changed_data for p in SETTINGS_AFFECTING_CSS):
change_css = True
if self.footer_links_formset.has_changed():
self.request.event.log_action('pretix.event.footerlinks.changed', user=self.request.user, data={
'data': self.footer_links_formset.cleaned_data
})
if form.has_changed():
self.request.event.log_action('pretix.event.changed', user=self.request.user, data={
k: (form.cleaned_data.get(k).name
@@ -244,8 +238,7 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
def post(self, request, *args, **kwargs):
form = self.get_form()
if form.is_valid() and self.sform.is_valid() and all([f.is_valid() for f in self.meta_forms]) and \
self.item_meta_property_formset.is_valid() and self.confirm_texts_formset.is_valid() and \
self.footer_links_formset.is_valid():
self.item_meta_property_formset.is_valid() and self.confirm_texts_formset.is_valid():
# reset timezone
zone = timezone(self.sform.cleaned_data['timezone'])
event = form.instance
@@ -299,18 +292,10 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
def save_confirm_texts_formset(self, obj):
obj.settings.confirm_texts = LazyI18nStringList(
form_data['text'].data
for form_data in sorted((d for d in self.confirm_texts_formset.cleaned_data if d), key=operator.itemgetter("ORDER"))
if form_data and not form_data.get("DELETE", False)
for form_data in sorted(self.confirm_texts_formset.cleaned_data, key=operator.itemgetter("ORDER"))
if not form_data.get("DELETE", False)
)
@cached_property
def footer_links_formset(self):
return EventFooterLinkFormset(self.request.POST if self.request.method == "POST" else None, event=self.object,
prefix="footer-links", instance=self.object)
def save_footer_links_formset(self, obj):
self.footer_links_formset.save()
class EventPlugins(EventSettingsViewMixin, EventPermissionRequiredMixin, TemplateView, SingleObjectMixin):
model = Event

View File

@@ -1344,36 +1344,22 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
def form_valid(self, form):
self.save_meta()
messages.success(self.request, _('Your changes have been saved.'))
change_data = {
k: form.cleaned_data.get(k)
for k in form.changed_data
}
for f in self.plugin_forms:
change_data.update({
k: (f.cleaned_data.get(k).name
if isinstance(f.cleaned_data.get(k), File)
else f.cleaned_data.get(k))
for k in f.changed_data
})
meta_changed = {}
for f in self.meta_forms:
meta_changed.update({
k: (f.cleaned_data.get(k).name
if isinstance(f.cleaned_data.get(k), File)
else f.cleaned_data.get(k))
for k in f.changed_data
})
if meta_changed:
change_data['meta_data'] = meta_changed
if change_data:
if form.has_changed() or any(f.has_changed() for f in self.plugin_forms):
data = {
k: form.cleaned_data.get(k)
for k in form.changed_data
}
for f in self.plugin_forms:
data.update({
k: (f.cleaned_data.get(k).name
if isinstance(f.cleaned_data.get(k), File)
else f.cleaned_data.get(k))
for k in f.changed_data
})
self.object.log_action(
'pretix.event.item.changed', user=self.request.user, data=change_data
'pretix.event.item.changed', user=self.request.user, data=data
)
invalidate_cache.apply_async(kwargs={'event': self.request.event.pk, 'item': self.object.pk})
for f in self.plugin_forms:
f.save()

View File

@@ -540,8 +540,10 @@ class OrderComment(OrderView):
self.order.log_action('pretix.event.order.checkin_attention', user=self.request.user, data={
'new_value': form.cleaned_data.get('checkin_attention')
})
print(self.order.custom_followup_at)
self.order.save(update_fields=['checkin_attention', 'comment', 'custom_followup_at'])
self.order.refresh_from_db()
print(self.order.custom_followup_at)
messages.success(self.request, _('The comment has been updated.'))
else:
messages.error(self.request, _('Could not update the comment.'))
@@ -1186,17 +1188,13 @@ class OrderTransition(OrderView):
if ps == Decimal('0.00') and self.order.pending_sum <= Decimal('0.00'):
p = self.order.payments.filter(state=OrderPayment.PAYMENT_STATE_CONFIRMED).last()
if p:
try:
p._mark_order_paid(
user=self.request.user,
send_mail=self.mark_paid_form.cleaned_data['send_email'],
force=self.mark_paid_form.cleaned_data.get('force', False),
payment_refund_sum=self.order.payment_refund_sum,
)
except Quota.QuotaExceededException as e:
messages.error(self.request, str(e))
else:
messages.success(self.request, _('The order has been marked as paid.'))
p._mark_order_paid(
user=self.request.user,
send_mail=self.mark_paid_form.cleaned_data['send_email'],
force=self.mark_paid_form.cleaned_data.get('force', False),
payment_refund_sum=self.order.payment_refund_sum,
)
messages.success(self.request, _('The order has been marked as paid.'))
return redirect(self.get_order_url())
try:

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