forked from CGM_Public/pretix_original
Compare commits
61 Commits
v4.11.0
...
invoice-gi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a75f53ec3 | ||
|
|
adfe2764e3 | ||
|
|
0d407ce36f | ||
|
|
f3a77d8154 | ||
|
|
932a2b16ac | ||
|
|
8502387af8 | ||
|
|
157484b42a | ||
|
|
839585a3a9 | ||
|
|
9101b5b69d | ||
|
|
f6fa9b4b16 | ||
|
|
826f1fcfa8 | ||
|
|
ede8d5bc60 | ||
|
|
ffde690d9e | ||
|
|
319b95c3fb | ||
|
|
76cf9c4c54 | ||
|
|
c40e5cca80 | ||
|
|
4f7c7800b0 | ||
|
|
f0922c42d1 | ||
|
|
aa56675594 | ||
|
|
19045a86b4 | ||
|
|
15d8613a0e | ||
|
|
19de9bf22f | ||
|
|
84ae93be26 | ||
|
|
d628acc62a | ||
|
|
4cc249e20e | ||
|
|
0d1ebf4e58 | ||
|
|
748ea38e15 | ||
|
|
50ab762905 | ||
|
|
b72d6478d2 | ||
|
|
87cea200a9 | ||
|
|
32ab7c3d4f | ||
|
|
8c63659050 | ||
|
|
23e5af13ad | ||
|
|
09eb14fe37 | ||
|
|
129e831e06 | ||
|
|
1ffe87ee18 | ||
|
|
b1b4177947 | ||
|
|
fe28a8f539 | ||
|
|
86be7b7934 | ||
|
|
5ded99c74a | ||
|
|
a52cee2c45 | ||
|
|
c2cb968b82 | ||
|
|
52fafa115c | ||
|
|
39f7bfe16f | ||
|
|
3c0ba3c8e8 | ||
|
|
db1c480905 | ||
|
|
96b57f9a50 | ||
|
|
0faf245290 | ||
|
|
cee72b5a6d | ||
|
|
76e8cc42c2 | ||
|
|
d22feada57 | ||
|
|
c792621bcb | ||
|
|
d9a58cf27f | ||
|
|
79ba2185fd | ||
|
|
fcf4750d5f | ||
|
|
5c56139b56 | ||
|
|
1f8da968ba | ||
|
|
6ee034784d | ||
|
|
1ab701c100 | ||
|
|
f0661fb11c | ||
|
|
443283de66 |
@@ -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 {
|
||||
.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 {
|
||||
padding: 12px;
|
||||
line-height: 24px;
|
||||
margin-bottom: 24px;
|
||||
background: #e7f2fa
|
||||
}
|
||||
|
||||
.wy-alert-title, .rst-content .admonition-title {
|
||||
.wy-alert-title, .rst-content .admonition-title, .rst-content .deprecated .versionmodified {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
|
||||
346
doc/api/resources/checkin.rst
Normal file
346
doc/api/resources/checkin.rst
Normal file
@@ -0,0 +1,346 @@
|
||||
.. 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.
|
||||
@@ -1,5 +1,7 @@
|
||||
.. spelling:: checkin
|
||||
|
||||
.. _rest-checkinlists:
|
||||
|
||||
Check-in lists
|
||||
==============
|
||||
|
||||
@@ -34,6 +36,7 @@ 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
|
||||
@@ -53,6 +56,10 @@ exit_all_at datetime Automatically c
|
||||
|
||||
The ``ends_after`` and ``expand`` query parameters have been added.
|
||||
|
||||
.. versionchanged:: 4.12
|
||||
|
||||
The ``addon_match`` attribute has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -94,6 +101,7 @@ Endpoints
|
||||
"allow_entry_after_exit": true,
|
||||
"exit_all_at": null,
|
||||
"rules": {},
|
||||
"addon_match": false,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
@@ -146,6 +154,7 @@ Endpoints
|
||||
"allow_entry_after_exit": true,
|
||||
"exit_all_at": null,
|
||||
"rules": {},
|
||||
"addon_match": false,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
@@ -245,6 +254,7 @@ Endpoints
|
||||
"subevent": null,
|
||||
"allow_multiple_entries": false,
|
||||
"allow_entry_after_exit": true,
|
||||
"addon_match": false,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
@@ -269,6 +279,7 @@ Endpoints
|
||||
"subevent": null,
|
||||
"allow_multiple_entries": false,
|
||||
"allow_entry_after_exit": true,
|
||||
"addon_match": false,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
@@ -323,6 +334,7 @@ Endpoints
|
||||
"subevent": null,
|
||||
"allow_multiple_entries": false,
|
||||
"allow_entry_after_exit": true,
|
||||
"addon_match": false,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
@@ -415,6 +427,9 @@ 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
|
||||
@@ -604,15 +619,23 @@ 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.
|
||||
**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.
|
||||
|
||||
.. 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
|
||||
@@ -733,12 +756,15 @@ Order position endpoints
|
||||
|
||||
Possible error reasons:
|
||||
|
||||
* ``unpaid`` - Ticket is not paid for
|
||||
* ``canceled`` – Ticket is canceled or expired. This reason is only sent when your request sets
|
||||
* ``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.
|
||||
``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
|
||||
* ``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.
|
||||
|
||||
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``.
|
||||
@@ -752,3 +778,6 @@ 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/
|
||||
@@ -131,7 +131,9 @@ Endpoints
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/customers/
|
||||
|
||||
Creates a new customer
|
||||
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``).
|
||||
|
||||
**Example request**:
|
||||
|
||||
@@ -143,7 +145,8 @@ Endpoints
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"email": "test@example.org"
|
||||
"email": "test@example.org",
|
||||
"send_email": true
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -173,8 +176,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``,
|
||||
and ``last_modified`` fields.
|
||||
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.
|
||||
|
||||
**Example request**:
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ at :ref:`plugin-docs`.
|
||||
invoices
|
||||
vouchers
|
||||
discounts
|
||||
checkin
|
||||
checkinlists
|
||||
waitinglist
|
||||
customers
|
||||
|
||||
@@ -89,7 +89,8 @@ 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: 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 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
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 236 KiB After Width: | Height: | Size: 274 KiB |
@@ -2,8 +2,25 @@
|
||||
|
||||
|
||||
partition "data-based check" {
|
||||
"Check based on local database" --> "Is the order in status PAID or PENDING\nand is the position not canceled?"
|
||||
"Check based on local database" -down-> "Is addon_match set to true?"
|
||||
--> 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: 147 KiB After Width: | Height: | Size: 175 KiB |
@@ -19,8 +19,25 @@ else
|
||||
endif
|
||||
|
||||
|
||||
===CHECK=== -down-> "Is the order in status PAID or PENDING\nand is the position not canceled?"
|
||||
===CHECK=== -down-> "Is addon_match set to true?"
|
||||
--> 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?"
|
||||
|
||||
@@ -532,6 +532,7 @@ 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
|
||||
@@ -567,6 +568,7 @@ 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"
|
||||
},
|
||||
|
||||
@@ -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 Android, Desktop
|
||||
Scenario supported on platforms Android, Desktop, iOS Android, Desktop, iOS Android, Desktop Android, Desktop, iOS Android, Desktop, iOS
|
||||
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,6 +90,7 @@ 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
|
||||
|
||||
@@ -135,6 +135,10 @@ 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
|
||||
---------------------
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ 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 *
|
||||
|
||||
@@ -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.11.0"
|
||||
__version__ = "4.12.0.dev1"
|
||||
|
||||
@@ -68,6 +68,8 @@ 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'),
|
||||
)
|
||||
|
||||
|
||||
@@ -98,6 +100,8 @@ 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'),
|
||||
)
|
||||
|
||||
|
||||
@@ -129,6 +133,8 @@ 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'),
|
||||
)
|
||||
|
||||
|
||||
@@ -194,6 +200,8 @@ 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'),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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 CheckinList
|
||||
from pretix.base.models import Checkin, 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')
|
||||
'rules', 'exit_all_at', 'addon_match')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -78,3 +78,31 @@ 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)
|
||||
|
||||
@@ -184,8 +184,9 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
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()
|
||||
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()
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
@@ -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['request'].event)
|
||||
self.context['vars'] = get_variables(self.context['event'])
|
||||
|
||||
if 'vars_images' not in self.context:
|
||||
self.context['vars_images'] = get_images(self.context['request'].event)
|
||||
self.context['vars_images'] = get_images(self.context['event'])
|
||||
|
||||
for k, f in self.context['vars'].items():
|
||||
try:
|
||||
@@ -422,7 +422,14 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
request = self.context.get('request')
|
||||
if request and (not request.query_params.get('pdf_data', 'false') == 'true' or 'can_view_orders' not in request.eventpermset):
|
||||
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:
|
||||
self.fields.pop('pdf_data', None)
|
||||
|
||||
def validate(self, data):
|
||||
@@ -481,13 +488,13 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if 'subevent' in self.context['request'].query_params.getlist('expand'):
|
||||
if 'subevent' in self.context['expand']:
|
||||
self.fields['subevent'] = SubEventSerializer(read_only=True)
|
||||
|
||||
if 'item' in self.context['request'].query_params.getlist('expand'):
|
||||
if 'item' in self.context['expand']:
|
||||
self.fields['item'] = ItemSerializer(read_only=True, context=self.context)
|
||||
|
||||
if 'variation' in self.context['request'].query_params.getlist('expand'):
|
||||
if 'variation' in self.context['expand']:
|
||||
self.fields['variation'] = InlineItemVariationSerializer(read_only=True)
|
||||
|
||||
|
||||
@@ -590,10 +597,10 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not self.context['request'].query_params.get('pdf_data', 'false') == 'true':
|
||||
if not self.context['pdf_data']:
|
||||
self.fields['positions'].child.fields.pop('pdf_data', None)
|
||||
|
||||
for exclude_field in self.context['request'].query_params.getlist('exclude'):
|
||||
for exclude_field in self.context['exclude']:
|
||||
p = exclude_field.split('.')
|
||||
if p[0] in self.fields:
|
||||
if len(p) == 1:
|
||||
|
||||
@@ -396,7 +396,6 @@ 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'])
|
||||
|
||||
@@ -75,6 +75,14 @@ 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:
|
||||
|
||||
@@ -112,6 +112,10 @@ 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)),
|
||||
|
||||
@@ -19,12 +19,16 @@
|
||||
# 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
|
||||
from django.core.exceptions import ValidationError as BaseValidationError
|
||||
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
|
||||
@@ -34,13 +38,18 @@ 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 viewsets
|
||||
from rest_framework import views, 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
|
||||
from pretix.api.serializers.checkin import (
|
||||
CheckinListSerializer, CheckinRPCRedeemInputSerializer,
|
||||
MiniCheckinListSerializer,
|
||||
)
|
||||
from pretix.api.serializers.item import QuestionSerializer
|
||||
from pretix.api.serializers.order import (
|
||||
CheckinListOrderPositionSerializer, FailedCheckinSerializer,
|
||||
@@ -50,7 +59,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,
|
||||
Question, RevokedTicketSecret, TeamAPIToken,
|
||||
)
|
||||
from pretix.base.services.checkin import (
|
||||
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
|
||||
@@ -265,6 +274,399 @@ 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)
|
||||
@@ -280,7 +682,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = CheckinListOrderPositionSerializer
|
||||
queryset = OrderPosition.all.none()
|
||||
filter_backends = (ExtendedBackend, RichOrderingFilter)
|
||||
ordering = ('attendee_name_cached', 'positionid')
|
||||
ordering = (F('attendee_name_cached').asc(nulls_last=True), 'positionid')
|
||||
ordering_fields = (
|
||||
'order__code', 'order__datetime', 'positionid', 'attendee_name',
|
||||
'last_checked_in', 'order__email',
|
||||
@@ -309,6 +711,8 @@ 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):
|
||||
@@ -319,80 +723,18 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
@cached_property
|
||||
def checkinlist(self):
|
||||
try:
|
||||
return get_object_or_404(CheckinList, event=self.request.event, pk=self.kwargs.get("list"))
|
||||
return get_object_or_404(self.request.event.checkin_lists, pk=self.kwargs.get("list"))
|
||||
except ValueError:
|
||||
raise Http404()
|
||||
|
||||
def get_queryset(self, ignore_status=False, ignore_products=False):
|
||||
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')
|
||||
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'),
|
||||
)
|
||||
|
||||
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:
|
||||
@@ -403,217 +745,153 @@ 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))
|
||||
type = self.request.data.get('type', None) or Checkin.TYPE_ENTRY
|
||||
if type not in dict(Checkin.CHECKIN_TYPES):
|
||||
checkin_type = self.request.data.get('type', None) or Checkin.TYPE_ENTRY
|
||||
if checkin_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()
|
||||
|
||||
common_checkin_args = dict(
|
||||
raw_barcode=self.kwargs['pk'],
|
||||
type=type,
|
||||
list=self.checkinlist,
|
||||
answers_data = self.request.data.get('answers')
|
||||
return _redeem_process(
|
||||
checkinlists=[self.checkinlist],
|
||||
raw_barcode=kwargs['pk'],
|
||||
answers_data=answers_data,
|
||||
datetime=dt,
|
||||
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,
|
||||
force=force,
|
||||
checkin_type=checkin_type,
|
||||
ignore_unpaid=ignore_unpaid,
|
||||
nonce=nonce,
|
||||
forced=force,
|
||||
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,
|
||||
)
|
||||
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)
|
||||
|
||||
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:"):],
|
||||
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
|
||||
)
|
||||
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))
|
||||
else:
|
||||
raise ValueError("unknown authentication method")
|
||||
|
||||
allowed_types = (
|
||||
'image/png', 'image/jpeg', 'image/gif', 'application/pdf'
|
||||
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,
|
||||
)
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
@@ -461,7 +461,9 @@ with scopes_disabled():
|
||||
class QuotaFilter(FilterSet):
|
||||
class Meta:
|
||||
model = Quota
|
||||
fields = ['subevent']
|
||||
fields = {
|
||||
'subevent': ['exact', 'in'],
|
||||
}
|
||||
|
||||
|
||||
class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
|
||||
@@ -63,9 +63,10 @@ from pretix.api.serializers.orderchange import (
|
||||
)
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedCombinedTicket, CachedTicket, Checkin, Device, Event, Invoice,
|
||||
InvoiceAddress, Order, OrderFee, OrderPayment, OrderPosition, OrderRefund,
|
||||
Quota, SubEvent, TaxRule, TeamAPIToken, generate_secret,
|
||||
CachedCombinedTicket, CachedTicket, Checkin, Device, EventMetaValue,
|
||||
Invoice, InvoiceAddress, ItemMetaValue, Order, OrderFee, OrderPayment,
|
||||
OrderPosition, OrderRefund, Quota, SubEvent, SubEventMetaValue, TaxRule,
|
||||
TeamAPIToken, generate_secret,
|
||||
)
|
||||
from pretix.base.models.orders import QuestionAnswer, RevokedTicketSecret
|
||||
from pretix.base.payment import PaymentException
|
||||
@@ -187,6 +188,8 @@ 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):
|
||||
@@ -213,14 +216,29 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
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()),
|
||||
'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||
'item__category', 'addon_to', 'seat',
|
||||
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(
|
||||
@@ -932,6 +950,7 @@ 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):
|
||||
@@ -942,25 +961,49 @@ 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('order', Order.objects.select_related('invoice_address').prefetch_related(
|
||||
Prefetch(
|
||||
'event',
|
||||
Event.objects.select_related('organizer')
|
||||
),
|
||||
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(
|
||||
'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(
|
||||
'item', 'variation', 'item__category', 'addon_to', 'seat'
|
||||
'addon_to', 'seat', 'addon_to__seat'
|
||||
)
|
||||
else:
|
||||
qs = qs.prefetch_related(
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
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
|
||||
@@ -38,8 +39,8 @@ from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from pretix.api.models import OAuthAccessToken
|
||||
from pretix.api.serializers.organizer import (
|
||||
CustomerSerializer, DeviceSerializer, GiftCardSerializer,
|
||||
GiftCardTransactionSerializer, MembershipSerializer,
|
||||
CustomerCreateSerializer, CustomerSerializer, DeviceSerializer,
|
||||
GiftCardSerializer, GiftCardTransactionSerializer, MembershipSerializer,
|
||||
MembershipTypeSerializer, OrganizerSerializer, OrganizerSettingsSerializer,
|
||||
SeatingPlanSerializer, TeamAPITokenSerializer, TeamInviteSerializer,
|
||||
TeamMemberSerializer, TeamSerializer,
|
||||
@@ -514,15 +515,24 @@ class CustomerViewSet(viewsets.ModelViewSet):
|
||||
raise MethodNotAllowed("Customers cannot be deleted.")
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
inst = serializer.save(organizer=self.request.organizer)
|
||||
def perform_create(self, serializer, send_email=False):
|
||||
customer = serializer.save(organizer=self.request.organizer, password=make_password(None))
|
||||
serializer.instance.log_action(
|
||||
'pretix.customer.created',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data,
|
||||
)
|
||||
return inst
|
||||
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)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
|
||||
@@ -475,8 +475,11 @@ def base_placeholders(sender, **kwargs):
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'event_admission_time', ['event_or_subevent'],
|
||||
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 '',
|
||||
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 '',
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'subevent', ['waiting_list_entry', 'event'],
|
||||
|
||||
@@ -23,6 +23,7 @@ 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
|
||||
|
||||
222
src/pretix/base/exporters/items.py
Normal file
222
src/pretix/base/exporters/items.py
Normal file
@@ -0,0 +1,222 @@
|
||||
#
|
||||
# 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
|
||||
@@ -28,6 +28,7 @@ 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 (
|
||||
@@ -47,7 +48,7 @@ from reportlab.platypus import (
|
||||
)
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models import Event, Invoice, Order
|
||||
from pretix.base.models import Event, Invoice, Order, OrderPayment
|
||||
from pretix.base.signals import register_invoice_renderers
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.helpers.reportlab import ThumbnailingImageReader
|
||||
@@ -589,15 +590,33 @@ 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 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)
|
||||
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)
|
||||
])
|
||||
tdata.append([pgettext('invoice', 'Outstanding payments')] + (['', '', ''] if has_taxes else ['']) + [
|
||||
money_filter(pending_sum, self.invoice.event.currency)
|
||||
tdata.append([pgettext('invoice', 'Remaining amount')] + (['', '', ''] if has_taxes else ['']) + [
|
||||
money_filter(total - giftcard_sum, self.invoice.event.currency)
|
||||
])
|
||||
tstyledata += [
|
||||
('FONTNAME', (0, len(tdata) - 3), (-1, len(tdata) - 3), self.font_bold),
|
||||
|
||||
18
src/pretix/base/migrations/0218_checkinlist_addon_match.py
Normal file
18
src/pretix/base/migrations/0218_checkinlist_addon_match.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
@@ -34,7 +34,9 @@
|
||||
|
||||
import binascii
|
||||
import json
|
||||
import operator
|
||||
from datetime import timedelta
|
||||
from functools import reduce
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import webauthn
|
||||
@@ -491,11 +493,14 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
if request and self.has_active_staff_session(request.session.session_key):
|
||||
return Event.objects.all()
|
||||
|
||||
kwargs = {permission: True}
|
||||
if isinstance(permission, (tuple, list)):
|
||||
q = reduce(operator.or_, [Q(**{p: True}) for p in permission])
|
||||
else:
|
||||
q = Q(**{permission: True})
|
||||
|
||||
return Event.objects.filter(
|
||||
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))
|
||||
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))
|
||||
)
|
||||
|
||||
@scopes_disabled()
|
||||
|
||||
@@ -56,6 +56,12 @@ 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 "
|
||||
@@ -258,6 +264,7 @@ 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')),
|
||||
@@ -268,6 +275,7 @@ 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')),
|
||||
)
|
||||
|
||||
|
||||
@@ -216,6 +216,27 @@ 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(
|
||||
|
||||
@@ -255,7 +255,9 @@ class Device(LoggedModel):
|
||||
:param request: Ignored, for compatibility with User model
|
||||
:return: Iterable of Events
|
||||
"""
|
||||
if permission in self.permission_set():
|
||||
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()):
|
||||
return self.get_events_with_any_permission()
|
||||
else:
|
||||
return self.organizer.events.none()
|
||||
|
||||
@@ -1448,7 +1448,10 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
@property
|
||||
def meta_data(self):
|
||||
data = self.event.meta_data
|
||||
data.update({v.property.name: v.value for v in self.meta_values.select_related('property').all()})
|
||||
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()})
|
||||
return data
|
||||
|
||||
@property
|
||||
|
||||
@@ -843,7 +843,7 @@ class Order(LockModel, LoggedModel):
|
||||
if terms:
|
||||
term_last = min(terms)
|
||||
else:
|
||||
term_last = None
|
||||
return None
|
||||
else:
|
||||
term_last = term_last.datetime(self.event).date()
|
||||
term_last = make_aware(datetime.combine(
|
||||
@@ -1588,7 +1588,7 @@ class OrderPayment(models.Model):
|
||||
if status_change:
|
||||
self.order.create_transactions()
|
||||
|
||||
def fail(self, info=None, user=None, auth=None):
|
||||
def fail(self, info=None, user=None, auth=None, log_data=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,
|
||||
@@ -1616,6 +1616,7 @@ 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='',
|
||||
|
||||
@@ -461,7 +461,9 @@ class TeamAPIToken(models.Model):
|
||||
:param request: Ignored, for compatibility with User model
|
||||
:return: Iterable of Events
|
||||
"""
|
||||
if getattr(self.team, permission, False):
|
||||
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)):
|
||||
return self.get_events_with_any_permission()
|
||||
else:
|
||||
return self.team.organizer.events.none()
|
||||
|
||||
@@ -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, does not work with pretixSCAN on iOS and '
|
||||
'changes semantics of offline scanning – please refer to documentation or support for details)')
|
||||
verbose_name = _('pretix signature scheme 1 (for very large events, changes semantics of offline scanning – '
|
||||
'please refer to documentation or support for details)')
|
||||
identifier = 'pretix_sig1'
|
||||
use_revocation_list = True
|
||||
|
||||
|
||||
@@ -947,11 +947,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)
|
||||
|
||||
|
||||
@@ -96,6 +96,7 @@ ALLOWED_ATTRIBUTES = {
|
||||
'div': ['class'],
|
||||
'p': ['class'],
|
||||
'span': ['class', 'title'],
|
||||
'ol': ['start'],
|
||||
# Update doc/user/markdown.rst if you change this!
|
||||
}
|
||||
|
||||
|
||||
@@ -22,9 +22,10 @@
|
||||
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 pgettext_lazy
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_scopes.forms import (
|
||||
SafeModelChoiceField, SafeModelMultipleChoiceField,
|
||||
)
|
||||
@@ -109,6 +110,7 @@ class CheckinListForm(forms.ModelForm):
|
||||
'rules',
|
||||
'gates',
|
||||
'exit_all_at',
|
||||
'addon_match',
|
||||
]
|
||||
widgets = {
|
||||
'limit_products': forms.CheckboxSelectMultiple(attrs={
|
||||
@@ -130,6 +132,12 @@ 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
|
||||
|
||||
|
||||
|
||||
@@ -366,7 +366,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 attachments since they '
|
||||
'pretix.event.order.email.attachments.skipped': _('The email has been sent without attached tickets 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.'),
|
||||
|
||||
@@ -48,16 +48,14 @@
|
||||
Make sure to always use the latest version of our scanning apps for these options to work.
|
||||
{% endblocktrans %}
|
||||
<br>
|
||||
<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>
|
||||
{% blocktrans trimmed %}
|
||||
If you make use of these advanced options, we recommend using our Android and Desktop apps.
|
||||
{% endblocktrans %}
|
||||
</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 %}
|
||||
|
||||
@@ -540,10 +540,8 @@ 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.'))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-07-01 07:49+0000\n"
|
||||
"PO-Revision-Date: 2022-07-01 08:03+0000\n"
|
||||
"PO-Revision-Date: 2022-07-22 11:11+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/de/"
|
||||
">\n"
|
||||
@@ -14,7 +14,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 4.12.2\n"
|
||||
"X-Generator: Weblate 4.13.1\n"
|
||||
"X-Poedit-Bookmarks: -1,-1,904,-1,-1,-1,-1,-1,-1,-1\n"
|
||||
|
||||
#: htmlcov/pretix_control_views_dashboards_py.html:963
|
||||
@@ -62,11 +62,12 @@ msgstr "pretixSCAN"
|
||||
|
||||
#: pretix/api/auth/devicesecurity.py:76
|
||||
msgid "pretixSCAN (kiosk mode, no order sync, no search)"
|
||||
msgstr "pretixSCAN (Kiosk-Modus, keine Bestell-Synchronisierung, keine Suche)"
|
||||
msgstr ""
|
||||
"pretixSCAN (Kiosk-Modus, keine Bestellungs-Synchronisierung, keine Suche)"
|
||||
|
||||
#: pretix/api/auth/devicesecurity.py:106
|
||||
msgid "pretixSCAN (online only, no order sync)"
|
||||
msgstr "pretixSCAN (nur online, keine Bestellsynchronisierung)"
|
||||
msgstr "pretixSCAN (nur online, keine Bestellungs-Synchronisierung)"
|
||||
|
||||
#: pretix/api/auth/devicesecurity.py:137
|
||||
msgid "pretixPOS"
|
||||
@@ -78,11 +79,11 @@ msgstr "Name der Applikation"
|
||||
|
||||
#: pretix/api/models.py:42
|
||||
msgid "Redirection URIs"
|
||||
msgstr "URLs zur Weiterleitung"
|
||||
msgstr "Weiterleitungs-URIs"
|
||||
|
||||
#: pretix/api/models.py:43
|
||||
msgid "Allowed URIs list, space separated"
|
||||
msgstr "Liste erlaubter URLs, mit Leerzeichen getrennt"
|
||||
msgstr "Liste erlaubter URIs, mit Leerzeichen getrennt"
|
||||
|
||||
#: pretix/api/models.py:46 pretix/plugins/paypal/payment.py:112
|
||||
#: pretix/plugins/paypal2/payment.py:105
|
||||
@@ -120,7 +121,7 @@ msgstr "Das Produkt \"{}\" ist keinem Kontingent zugeordnet."
|
||||
msgid ""
|
||||
"There is not enough quota available on quota \"{}\" to perform the operation."
|
||||
msgstr ""
|
||||
"Das Kontingent \"{name}\" hat nicht genug freie Kapazität für diese Änderung."
|
||||
"Das Kontingent \"{}\" hat nicht genug freie Kapazität für diese Änderung."
|
||||
|
||||
#: pretix/api/serializers/cart.py:168 pretix/api/serializers/order.py:1091
|
||||
#: pretix/base/services/orders.py:1263
|
||||
|
||||
@@ -41,6 +41,7 @@ Bestätigungs
|
||||
Bestellbestätigungs
|
||||
Bestellungsänderungen
|
||||
Bestellungsstatus
|
||||
Bestellungs
|
||||
bez
|
||||
BezahlCode
|
||||
Bezahlmethode
|
||||
@@ -323,6 +324,7 @@ VIP
|
||||
WebAuthn
|
||||
Webhook
|
||||
Webhooks
|
||||
Weiterleitungs
|
||||
WeChat
|
||||
WhatsApp
|
||||
Widget
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-07-01 07:49+0000\n"
|
||||
"PO-Revision-Date: 2022-07-01 08:03+0000\n"
|
||||
"PO-Revision-Date: 2022-07-22 11:11+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
||||
"pretix/pretix/de_Informal/>\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 4.12.2\n"
|
||||
"X-Generator: Weblate 4.13.1\n"
|
||||
|
||||
#: htmlcov/pretix_control_views_dashboards_py.html:963
|
||||
#: pretix/control/templates/pretixcontrol/events/index.html:140
|
||||
@@ -64,11 +64,12 @@ msgstr "pretixSCAN"
|
||||
|
||||
#: pretix/api/auth/devicesecurity.py:76
|
||||
msgid "pretixSCAN (kiosk mode, no order sync, no search)"
|
||||
msgstr "pretixSCAN (Kiosk-Modus, keine Bestell-Synchronisierung, keine Suche)"
|
||||
msgstr ""
|
||||
"pretixSCAN (Kiosk-Modus, keine Bestellungs-Synchronisierung, keine Suche)"
|
||||
|
||||
#: pretix/api/auth/devicesecurity.py:106
|
||||
msgid "pretixSCAN (online only, no order sync)"
|
||||
msgstr "pretixSCAN (nur online, keine Bestellsynchronisierung)"
|
||||
msgstr "pretixSCAN (nur online, keine Bestellungs-Synchronisierung)"
|
||||
|
||||
#: pretix/api/auth/devicesecurity.py:137
|
||||
msgid "pretixPOS"
|
||||
@@ -80,11 +81,11 @@ msgstr "Name der Applikation"
|
||||
|
||||
#: pretix/api/models.py:42
|
||||
msgid "Redirection URIs"
|
||||
msgstr "URLs zur Weiterleitung"
|
||||
msgstr "Weiterleitungs-URIs"
|
||||
|
||||
#: pretix/api/models.py:43
|
||||
msgid "Allowed URIs list, space separated"
|
||||
msgstr "Liste erlaubter URLs, mit Leerzeichen getrennt"
|
||||
msgstr "Liste erlaubter URIs, mit Leerzeichen getrennt"
|
||||
|
||||
#: pretix/api/models.py:46 pretix/plugins/paypal/payment.py:112
|
||||
#: pretix/plugins/paypal2/payment.py:105
|
||||
@@ -122,7 +123,7 @@ msgstr "Das Produkt \"{}\" ist keinem Kontingent zugeordnet."
|
||||
msgid ""
|
||||
"There is not enough quota available on quota \"{}\" to perform the operation."
|
||||
msgstr ""
|
||||
"Das Kontingent \"{name}\" hat nicht genug freie Kapazität für diese Änderung."
|
||||
"Das Kontingent \"{}\" hat nicht genug freie Kapazität für diese Änderung."
|
||||
|
||||
#: pretix/api/serializers/cart.py:168 pretix/api/serializers/order.py:1091
|
||||
#: pretix/base/services/orders.py:1263
|
||||
|
||||
@@ -41,6 +41,7 @@ Bestätigungs
|
||||
Bestellbestätigungs
|
||||
Bestellungsänderungen
|
||||
Bestellungsstatus
|
||||
Bestellungs
|
||||
bez
|
||||
BezahlCode
|
||||
Bezahlmethode
|
||||
@@ -323,6 +324,7 @@ VIP
|
||||
WebAuthn
|
||||
Webhook
|
||||
Webhooks
|
||||
Weiterleitungs
|
||||
WeChat
|
||||
WhatsApp
|
||||
Widget
|
||||
|
||||
@@ -4,8 +4,8 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-07-01 07:49+0000\n"
|
||||
"PO-Revision-Date: 2022-06-26 18:00+0000\n"
|
||||
"Last-Translator: Hari Har Wolfer <harihar@sri-ma.de>\n"
|
||||
"PO-Revision-Date: 2022-07-22 07:00+0000\n"
|
||||
"Last-Translator: Julius Rickert <pretix@juliusrickert.de>\n"
|
||||
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/fr/"
|
||||
">\n"
|
||||
"Language: fr\n"
|
||||
@@ -13,7 +13,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n > 1;\n"
|
||||
"X-Generator: Weblate 4.12.2\n"
|
||||
"X-Generator: Weblate 4.13.1\n"
|
||||
|
||||
#: htmlcov/pretix_control_views_dashboards_py.html:963
|
||||
#: pretix/control/templates/pretixcontrol/events/index.html:140
|
||||
@@ -53,10 +53,8 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/api/auth/devicesecurity.py:44
|
||||
#, fuzzy
|
||||
#| msgid "1. Download pretixdesk"
|
||||
msgid "pretixSCAN"
|
||||
msgstr "1. Télécharger pretixdesk"
|
||||
msgstr "pretixSCANNER"
|
||||
|
||||
#: pretix/api/auth/devicesecurity.py:76
|
||||
msgid "pretixSCAN (kiosk mode, no order sync, no search)"
|
||||
@@ -2200,7 +2198,7 @@ msgid ""
|
||||
"Invalid placeholder syntax: You used a different number of \"{\" than of "
|
||||
"\"}\"."
|
||||
msgstr ""
|
||||
"Syntaxe de substitution invalide: vous devriez utiliser des numéros "
|
||||
"Syntaxe de substitution invalide : vous devriez utiliser des numéros "
|
||||
"différents de \"{\" ou de \"}\"."
|
||||
|
||||
#: pretix/base/forms/validators.py:73
|
||||
|
||||
@@ -7,16 +7,16 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-07-01 07:49+0000\n"
|
||||
"PO-Revision-Date: 2021-11-15 00:00+0000\n"
|
||||
"Last-Translator: Maarten van den Berg <maartenberg1@gmail.com>\n"
|
||||
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/"
|
||||
">\n"
|
||||
"PO-Revision-Date: 2022-07-06 21:00+0000\n"
|
||||
"Last-Translator: Hans Fraiponts <fraiponts@gmail.com>\n"
|
||||
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/>"
|
||||
"\n"
|
||||
"Language: nl\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 4.8\n"
|
||||
"X-Generator: Weblate 4.12.2\n"
|
||||
|
||||
#: htmlcov/pretix_control_views_dashboards_py.html:963
|
||||
#: pretix/control/templates/pretixcontrol/events/index.html:140
|
||||
@@ -1367,10 +1367,8 @@ msgstr "E-mailadres gecontroleerd"
|
||||
#: pretix/base/exporters/orderlist.py:297
|
||||
#: pretix/base/exporters/orderlist.py:464
|
||||
#: pretix/base/exporters/orderlist.py:618
|
||||
#, fuzzy
|
||||
#| msgid "Customer ID"
|
||||
msgid "External customer ID"
|
||||
msgstr "Klantnummer"
|
||||
msgstr "Externe Klantnummer"
|
||||
|
||||
#: pretix/base/exporters/orderlist.py:302
|
||||
#, python-brace-format
|
||||
@@ -1977,10 +1975,8 @@ msgid "Repeat password"
|
||||
msgstr "Herhaal wachtwoord"
|
||||
|
||||
#: pretix/base/forms/questions.py:198
|
||||
#, fuzzy
|
||||
#| msgid "Please enter a shorter name."
|
||||
msgid "Please do not use special characters in names."
|
||||
msgstr "Vul alstublieft een kortere naam in."
|
||||
msgstr "Gelieve geen bijzondere tekens te gebruiken in namen."
|
||||
|
||||
#: pretix/base/forms/questions.py:257
|
||||
msgid "Please enter a shorter name."
|
||||
@@ -2095,7 +2091,7 @@ msgstr "Het huidige wachtwoord dat u heeft ingevoerd is niet correct."
|
||||
|
||||
#: pretix/base/forms/user.py:58
|
||||
msgid "Please choose a password different to your current one."
|
||||
msgstr ""
|
||||
msgstr "Kies een wachtwoord dat niet hetzelfde is als het huidige alstublieft."
|
||||
|
||||
#: pretix/base/forms/user.py:63 pretix/presale/forms/customer.py:386
|
||||
#: pretix/presale/forms/customer.py:455
|
||||
@@ -2386,10 +2382,8 @@ msgid "Date joined"
|
||||
msgstr "Datum toegevoegd"
|
||||
|
||||
#: pretix/base/models/auth.py:254
|
||||
#, fuzzy
|
||||
#| msgid "Request a new password"
|
||||
msgid "Force user to select a new password"
|
||||
msgstr "Nieuw wachtwoord aanvragen"
|
||||
msgstr "Verplicht de gebruiker een nieuw wachtwoord aan te vragen"
|
||||
|
||||
#: pretix/base/models/auth.py:261
|
||||
msgid "Timezone"
|
||||
@@ -2559,6 +2553,8 @@ msgid ""
|
||||
"The identifier may only contain letters, numbers, dots, dashes, and "
|
||||
"underscores. It must start and end with a letter or number."
|
||||
msgstr ""
|
||||
"De unieke code mag enkel letters, cijfers, punten, streepjes en liggende "
|
||||
"streepjes bevatten. Het moet beginnen met een letter of nummer."
|
||||
|
||||
#: pretix/base/models/customers.py:65
|
||||
msgid "Account active"
|
||||
@@ -2582,15 +2578,13 @@ msgstr "Registratiedatum"
|
||||
#: pretix/control/templates/pretixcontrol/organizers/customer.html:31
|
||||
#: pretix/control/templates/pretixcontrol/organizers/customers.html:65
|
||||
#: pretix/control/templates/pretixcontrol/users/form.html:43
|
||||
#, fuzzy
|
||||
#| msgid "Internal identifier"
|
||||
msgid "External identifier"
|
||||
msgstr "Intern kenmerk"
|
||||
msgstr "Externe unieke code"
|
||||
|
||||
#: pretix/base/models/customers.py:75
|
||||
#: pretix/control/templates/pretixcontrol/organizers/customer.html:67
|
||||
msgid "Notes"
|
||||
msgstr ""
|
||||
msgstr "Notas"
|
||||
|
||||
#: pretix/base/models/customers.py:238 pretix/base/models/orders.py:1318
|
||||
#: pretix/base/models/orders.py:2693 pretix/base/settings.py:841
|
||||
@@ -2631,21 +2625,17 @@ msgstr "Initialisatiedatum"
|
||||
#: pretix/base/models/discount.py:44
|
||||
msgctxt "subevent"
|
||||
msgid "Dates can be mixed without limitation"
|
||||
msgstr ""
|
||||
msgstr "Datums kunnen gecombineerd worden zonder beperking"
|
||||
|
||||
#: pretix/base/models/discount.py:45
|
||||
#, fuzzy
|
||||
#| msgid "Multiple matching products were found."
|
||||
msgctxt "subevent"
|
||||
msgid "All matching products must be for the same date"
|
||||
msgstr "Er zijn meerdere overeenkomende producten gevonden."
|
||||
msgstr "Alle overeenkomende producten moeten op dezelfde datum plaats vinden"
|
||||
|
||||
#: pretix/base/models/discount.py:46
|
||||
#, fuzzy
|
||||
#| msgid "Add tickets for a different date"
|
||||
msgctxt "subevent"
|
||||
msgid "Each matching product must be for a different date"
|
||||
msgstr "Voeg tickets voor een andere datum toe"
|
||||
msgstr "Elk toegevoegde ticket moet op een andere datum zijn"
|
||||
|
||||
#: pretix/base/models/discount.py:55 pretix/base/models/event.py:1296
|
||||
#: pretix/base/models/items.py:368 pretix/base/models/items.py:784
|
||||
@@ -2701,16 +2691,12 @@ msgid "Event series handling"
|
||||
msgstr "Evenementenreeks: datum aangepast"
|
||||
|
||||
#: pretix/base/models/discount.py:92
|
||||
#, fuzzy
|
||||
#| msgid "All products (including newly created ones)"
|
||||
msgid "Apply to all products (including newly created ones)"
|
||||
msgstr "Alle producten (inclusief nieuw gemaakte)"
|
||||
msgstr "Pas toe op alle producten (inclusief nieuw gemaakte)"
|
||||
|
||||
#: pretix/base/models/discount.py:96
|
||||
#, fuzzy
|
||||
#| msgid "Apply to products"
|
||||
msgid "Apply to specific products"
|
||||
msgstr "Toepassen op producten"
|
||||
msgstr "Pas toe op specifieke producten"
|
||||
|
||||
#: pretix/base/models/discount.py:101
|
||||
#, fuzzy
|
||||
@@ -2720,11 +2706,11 @@ msgstr "Toepassen op producten"
|
||||
|
||||
#: pretix/base/models/discount.py:102
|
||||
msgid "Discounts never apply to bundled products"
|
||||
msgstr ""
|
||||
msgstr "Kortingen zijn nooit van toepassing op gebundelde producten"
|
||||
|
||||
#: pretix/base/models/discount.py:106
|
||||
msgid "Ignore products discounted by a voucher"
|
||||
msgstr ""
|
||||
msgstr "Negeer producten die korting krijgen door een voucher"
|
||||
|
||||
#: pretix/base/models/discount.py:107
|
||||
msgid ""
|
||||
@@ -2733,6 +2719,10 @@ msgid ""
|
||||
"use a voucher only to e.g. unlock a hidden product or gain access to sold-"
|
||||
"out quota will still receive the discount."
|
||||
msgstr ""
|
||||
"Als deze optie is aangevinkt, worden producten die korting kregen door een "
|
||||
"voucher niet in rekening genomen voor deze korting. Echter producten die een "
|
||||
"voucher alleen gebruiken om bvb een verborgen product te tonen of om toegang "
|
||||
"te krijgen tot uitverkochte producten zullen nog steeds een korting krijgen."
|
||||
|
||||
#: pretix/base/models/discount.py:112
|
||||
#, fuzzy
|
||||
@@ -2750,7 +2740,7 @@ msgstr ""
|
||||
|
||||
#: pretix/base/models/discount.py:130
|
||||
msgid "Apply discount only to this number of matching products"
|
||||
msgstr ""
|
||||
msgstr "Pas de korting enkel toe op dit aantal relevante producten"
|
||||
|
||||
#: pretix/base/models/discount.py:132
|
||||
msgid ""
|
||||
|
||||
@@ -32,7 +32,7 @@ class PayPalHttpClient(VendorPayPalHttpClient):
|
||||
# Cached access tokens are not updated by PayPal to include new Merchants that granted access rights since
|
||||
# the access token was generated. Therefor we increment the cycle count and by that invalidate the cached
|
||||
# token and pull a new one.
|
||||
incr = cache.get('pretix_paypal_token_hash_cycle', default=0)
|
||||
incr = cache.get('pretix_paypal_token_hash_cycle', default=1)
|
||||
|
||||
# Then we get all the items that make up the current credentials and create a hash to detect changes
|
||||
checksum = hashlib.sha256(''.join([
|
||||
|
||||
@@ -354,6 +354,9 @@ class PaypalMethod(BasePaymentProvider):
|
||||
|
||||
@property
|
||||
def is_enabled(self) -> bool:
|
||||
if self.settings.connect_client_id and self.settings.connect_secret_key and not self.settings.secret:
|
||||
if not self.settings.isu_merchant_id:
|
||||
return False
|
||||
return self.settings.get('_enabled', as_type=bool) and self.settings.get('method_{}'.format(self.method),
|
||||
as_type=bool)
|
||||
|
||||
@@ -588,6 +591,9 @@ class PaypalMethod(BasePaymentProvider):
|
||||
raise PaymentException(_('We were unable to process your payment. See below for details on how to '
|
||||
'proceed.'))
|
||||
|
||||
if self.settings.connect_client_id and self.settings.connect_secret_key and not self.settings.secret:
|
||||
if not self.settings.isu_merchant_id:
|
||||
raise PaymentException('Payment method misconfigured')
|
||||
self.init_api()
|
||||
try:
|
||||
req = OrdersGetRequest(request.session.get('payment_paypal_oid'))
|
||||
|
||||
@@ -199,7 +199,7 @@ def isu_return(request, *args, **kwargs):
|
||||
if not any(k in request.GET for k in getparams) or not any(k in request.session for k in sessionparams):
|
||||
messages.error(request, _('An error occurred returning from PayPal: request parameters missing. Please try again.'))
|
||||
missing_getparams = set(getparams) - set(request.GET)
|
||||
missing_sessionparams = set(sessionparams) - set(request.session)
|
||||
missing_sessionparams = {p for p in sessionparams if p not in request.session}
|
||||
logger.exception('PayPal2 - Missing params in GET {} and/or Session {}'.format(missing_getparams, missing_sessionparams))
|
||||
return redirect(reverse('control:index'))
|
||||
|
||||
@@ -211,7 +211,7 @@ def isu_return(request, *args, **kwargs):
|
||||
try:
|
||||
cache.incr('pretix_paypal_token_hash_cycle')
|
||||
except ValueError:
|
||||
cache.set('pretix_paypal_token_hash_cycle', 0)
|
||||
cache.set('pretix_paypal_token_hash_cycle', 1, None)
|
||||
|
||||
gs = GlobalSettingsObject()
|
||||
prov = Paypal(event)
|
||||
@@ -376,7 +376,7 @@ def webhook(request, *args, **kwargs):
|
||||
prov.init_api()
|
||||
|
||||
try:
|
||||
if rso:
|
||||
if rso and 'id' in rso.payment.info_data:
|
||||
payloadid = rso.payment.info_data['id']
|
||||
sale = prov.client.execute(pp_orders.OrdersGetRequest(payloadid)).result
|
||||
except IOError:
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary pull-right" @click="check(checkResult.position.secret, true, false, false)">
|
||||
<button type="button" class="btn btn-primary pull-right" @click="check(checkResult.position.secret, true, false, false, true)">
|
||||
{{ $root.strings['modal.continue'] }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-default" @click="showUnpaidModal = false">
|
||||
@@ -188,7 +188,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary pull-right" @click="check(checkResult.position.secret, true, true)">
|
||||
<button type="button" class="btn btn-primary pull-right" @click="check(checkResult.position.secret, true, true, true)">
|
||||
{{ $root.strings['modal.continue'] }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-default" @click="showQuestionsModal = false">
|
||||
@@ -296,7 +296,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
selectResult(res) {
|
||||
this.check(res.id, false, false, false)
|
||||
this.check(res.id, false, false, false, false)
|
||||
},
|
||||
answerSetM(qid, opid, checked) {
|
||||
let arr = this.answers[qid] ? this.answers[qid].split(',') : [];
|
||||
@@ -320,7 +320,7 @@ export default {
|
||||
this.showQuestionsModal = false
|
||||
this.answers = {}
|
||||
},
|
||||
check(id, ignoreUnpaid, keepAnswers, fallbackToSearch) {
|
||||
check(id, ignoreUnpaid, keepAnswers, fallbackToSearch, untrusted) {
|
||||
if (!keepAnswers) {
|
||||
this.answers = {}
|
||||
} else if (this.showQuestionsModal) {
|
||||
@@ -339,7 +339,11 @@ export default {
|
||||
this.$refs.input.blur()
|
||||
})
|
||||
|
||||
fetch(this.$root.api.lists + this.checkinlist.id + '/positions/' + encodeURIComponent(id) + '/redeem/?expand=item&expand=subevent&expand=variation', {
|
||||
let url = this.$root.api.lists + this.checkinlist.id + '/positions/' + encodeURIComponent(id) + '/redeem/?expand=item&expand=subevent&expand=variation'
|
||||
if (untrusted) {
|
||||
url += '&untrusted_input=true'
|
||||
}
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': document.querySelector("input[name=csrfmiddlewaretoken]").value,
|
||||
@@ -439,7 +443,7 @@ export default {
|
||||
startSearch(fallbackToScan) {
|
||||
if (this.query.length >= 32 && fallbackToScan) {
|
||||
// likely a secret, not a search result
|
||||
this.check(this.query, false, false, true)
|
||||
this.check(this.query, false, false, true, true)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ window.vapp = new Vue({
|
||||
'result.rules': gettext('Entry not allowed'),
|
||||
'result.revoked': gettext('Ticket code revoked/changed'),
|
||||
'result.canceled': gettext('Order canceled'),
|
||||
'result.ambiguous': gettext('Ticket code is ambiguous on list'),
|
||||
'status.checkin': gettext('Checked-in Tickets'),
|
||||
'status.position': gettext('Valid Tickets'),
|
||||
'status.inside': gettext('Currently inside'),
|
||||
|
||||
@@ -41,9 +41,7 @@ from pretix.base.forms.questions import (
|
||||
)
|
||||
from pretix.base.i18n import get_language_without_region
|
||||
from pretix.base.models import Customer
|
||||
from pretix.base.services.mail import mail
|
||||
from pretix.helpers.http import get_client_ip
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
|
||||
|
||||
class TokenGenerator(PasswordResetTokenGenerator):
|
||||
@@ -268,19 +266,7 @@ class RegistrationForm(forms.Form):
|
||||
customer.set_unusable_password()
|
||||
customer.save()
|
||||
customer.log_action('pretix.customer.created', {})
|
||||
ctx = customer.get_email_context()
|
||||
token = TokenGenerator().make_token(customer)
|
||||
ctx['url'] = build_absolute_uri(self.request.organizer,
|
||||
'presale:organizer.customer.activate') + '?id=' + customer.identifier + '&token=' + token
|
||||
mail(
|
||||
customer.email,
|
||||
_('Activate your account at {organizer}').format(organizer=self.request.organizer.name),
|
||||
self.request.organizer.settings.mail_text_customer_registration,
|
||||
ctx,
|
||||
locale=customer.locale,
|
||||
customer=customer,
|
||||
organizer=self.request.organizer,
|
||||
)
|
||||
customer.send_activation_mail()
|
||||
return customer
|
||||
|
||||
|
||||
|
||||
@@ -127,28 +127,29 @@
|
||||
</del>
|
||||
<ins><span class="sr-only">{% trans "New price:" %}</span>
|
||||
{% endif %}
|
||||
{% if item.free_price %}
|
||||
<div class="input-group input-group-price">
|
||||
<span class="input-group-addon">{{ event.currency }}</span>
|
||||
<input type="number" class="form-control input-item-price"
|
||||
id="price-variation-{{form.pos.pk}}-{{ item.pk }}-{{ var.pk }}"
|
||||
placeholder="0"
|
||||
min="{% if event.settings.display_net_prices %}{{ var.display_price.net|money_numberfield:event.currency }}{% else %}{{ var.display_price.gross|money_numberfield:event.currency }}{% endif %}"
|
||||
name="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}_price"
|
||||
title="{% blocktrans trimmed with item=var.value %}Modify price for {{ item }}{% endblocktrans %}"
|
||||
step="any"
|
||||
value="{% if event.settings.display_net_prices %}{{ var.initial_price.net|money_numberfield:event.currency }}{% else %}{{ var.initial_price.gross|money_numberfield:event.currency }}{% endif %}"
|
||||
>
|
||||
</div>
|
||||
{% elif not var.display_price.gross %}
|
||||
{% elif event.settings.display_net_prices %}
|
||||
{{ var.display_price.net|money:event.currency }}
|
||||
{% else %}
|
||||
{{ var.display_price.gross|money:event.currency }}
|
||||
{% endif %}
|
||||
{% if item.original_price or var.original_price %}
|
||||
</ins>
|
||||
{% endif %}
|
||||
{% if item.free_price %}
|
||||
<div class="input-group input-group-price">
|
||||
<span class="input-group-addon">{{ event.currency }}</span>
|
||||
<input type="number" class="form-control input-item-price"
|
||||
id="price-variation-{{form.pos.pk}}-{{ item.pk }}-{{ var.pk }}"
|
||||
placeholder="0"
|
||||
min="{% if event.settings.display_net_prices %}{{ var.display_price.net|money_numberfield:event.currency }}{% else %}{{ var.display_price.gross|money_numberfield:event.currency }}{% endif %}"
|
||||
name="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}_price"
|
||||
title="{% blocktrans trimmed with item=var.value %}Modify price for {{ item }}{% endblocktrans %}"
|
||||
step="any"
|
||||
value="{% if event.settings.display_net_prices %}{{ var.initial_price.net|money_numberfield:event.currency }}{% else %}{{ var.initial_price.gross|money_numberfield:event.currency }}{% endif %}"
|
||||
>
|
||||
</div>
|
||||
{% elif not var.display_price.gross %}
|
||||
<span class="text-uppercase">{% trans "free" context "price" %}</span>
|
||||
{% elif event.settings.display_net_prices %}
|
||||
{{ var.display_price.net|money:event.currency }}
|
||||
{% else %}
|
||||
{{ var.display_price.gross|money:event.currency }}
|
||||
{% endif %}
|
||||
{% if item.original_price or var.original_price %}
|
||||
</ins>
|
||||
{% endif %}
|
||||
{% if item.includes_mixed_tax_rate %}
|
||||
{% if event.settings.display_net_prices %}
|
||||
<small>{% trans "plus taxes" %}</small>
|
||||
@@ -181,6 +182,7 @@
|
||||
name="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}"
|
||||
data-exclusive-prefix="cp_{{ form.pos.pk }}_variation_{{ item.id }}_"
|
||||
aria-label="{% blocktrans with item=item.name var=var %}Add {{ item }}, {{ var }} to cart{% endblocktrans %}">
|
||||
<i class="fa fa-cart-plus fa-lg" aria-hidden="true"></i>
|
||||
</label>
|
||||
{% else %}
|
||||
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
|
||||
@@ -248,23 +250,24 @@
|
||||
</del>
|
||||
<ins><span class="sr-only">{% trans "New price:" %}</span>
|
||||
{% endif %}
|
||||
{% if item.free_price %}
|
||||
<div class="input-group input-group-price">
|
||||
<span class="input-group-addon">{{ event.currency }}</span>
|
||||
<input type="number" class="form-control input-item-price" placeholder="0"
|
||||
id="price-item-{{ form.pos.pk }}-{{ item.pk }}"
|
||||
min="{% if event.settings.display_net_prices %}{{ item.display_price.net|money_numberfield:event.currency }}{% else %}{{ item.display_price.gross|money_numberfield:event.currency }}{% endif %}"
|
||||
name="cp_{{ form.pos.pk }}_item_{{ item.id }}_price"
|
||||
title="{% blocktrans trimmed with item=item.name %}Modify price for {{ item }}{% endblocktrans %}"
|
||||
value="{% if event.settings.display_net_prices %}{{ item.initial_price.net|money_numberfield:event.currency }}{% else %}{{ item.initial_price.gross|money_numberfield:event.currency }}{% endif %}"
|
||||
step="any">
|
||||
</div>
|
||||
{% elif not item.display_price.gross %}
|
||||
{% elif event.settings.display_net_prices %}
|
||||
{{ item.display_price.net|money:event.currency }}
|
||||
{% else %}
|
||||
{{ item.display_price.gross|money:event.currency }}
|
||||
{% endif %}
|
||||
{% if item.free_price %}
|
||||
<div class="input-group input-group-price">
|
||||
<span class="input-group-addon">{{ event.currency }}</span>
|
||||
<input type="number" class="form-control input-item-price" placeholder="0"
|
||||
id="price-item-{{ form.pos.pk }}-{{ item.pk }}"
|
||||
min="{% if event.settings.display_net_prices %}{{ item.display_price.net|money_numberfield:event.currency }}{% else %}{{ item.display_price.gross|money_numberfield:event.currency }}{% endif %}"
|
||||
name="cp_{{ form.pos.pk }}_item_{{ item.id }}_price"
|
||||
title="{% blocktrans trimmed with item=item.name %}Modify price for {{ item }}{% endblocktrans %}"
|
||||
value="{% if event.settings.display_net_prices %}{{ item.initial_price.net|money_numberfield:event.currency }}{% else %}{{ item.initial_price.gross|money_numberfield:event.currency }}{% endif %}"
|
||||
step="any">
|
||||
</div>
|
||||
{% elif not item.display_price.gross %}
|
||||
<span class="text-uppercase">{% trans "free" context "price" %}</span>
|
||||
{% elif event.settings.display_net_prices %}
|
||||
{{ item.display_price.net|money:event.currency }}
|
||||
{% else %}
|
||||
{{ item.display_price.gross|money:event.currency }}
|
||||
{% endif %}
|
||||
{% if item.original_price %}
|
||||
</ins>
|
||||
{% endif %}
|
||||
@@ -306,6 +309,7 @@
|
||||
id="cp_{{ form.pos.pk }}_item_{{ item.id }}"
|
||||
aria-label="{% blocktrans with item=item.name %}Add {{ item }} to cart{% endblocktrans %}"
|
||||
{% if item.description %} aria-describedby="cp-{{ form.pos.pk }}-item-{{ item.id }}-description"{% endif %}>
|
||||
<i class="fa fa-cart-plus fa-lg" aria-hidden="true"></i>
|
||||
</label>
|
||||
{% else %}
|
||||
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
|
||||
|
||||
@@ -276,7 +276,7 @@
|
||||
</div>
|
||||
<p>
|
||||
{% elif not item.display_price.gross %}
|
||||
{% trans "FREE" context "price" %}
|
||||
<span class="text-uppercase">{% trans "free" context "price" %}</span>
|
||||
{% elif event.settings.display_net_prices %}
|
||||
{{ item.display_price.net|money:event.currency }}
|
||||
{% else %}
|
||||
|
||||
@@ -54,7 +54,9 @@ from lxml import html
|
||||
|
||||
from pretix.base.context import get_powered_by
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import CartPosition, Event, Quota, SubEvent, Voucher
|
||||
from pretix.base.models import (
|
||||
CartPosition, Event, ItemVariation, Quota, SubEvent, Voucher,
|
||||
)
|
||||
from pretix.base.services.cart import error_messages
|
||||
from pretix.base.settings import GlobalSettingsObject
|
||||
from pretix.base.templatetags.rich_text import rich_text
|
||||
@@ -222,9 +224,18 @@ class WidgetAPIProductList(EventListMixin, View):
|
||||
def _get_items(self):
|
||||
qs = self.request.event.items
|
||||
if 'items' in self.request.GET:
|
||||
qs = qs.filter(pk__in=self.request.GET.get('items').split(","))
|
||||
qs = qs.filter(pk__in=[pk.strip() for pk in self.request.GET.get('items').split(",") if pk.strip().isdigit()])
|
||||
if 'categories' in self.request.GET:
|
||||
qs = qs.filter(category__pk__in=self.request.GET.get('categories').split(","))
|
||||
qs = qs.filter(category__pk__in=[pk.strip() for pk in self.request.GET.get('categories').split(",") if pk.strip().isdigit()])
|
||||
variation_filter = None
|
||||
if 'variations' in self.request.GET:
|
||||
variation_filter = [int(pk.strip()) for pk in self.request.GET.get('variations').split(",") if pk.strip().isdigit()]
|
||||
qs = qs.filter(
|
||||
pk__in=ItemVariation.objects.filter(
|
||||
item__event=self.request.event,
|
||||
pk__in=variation_filter,
|
||||
).values_list('item_id', flat=True)
|
||||
)
|
||||
|
||||
items, display_add_to_cart = get_grouped_items(
|
||||
self.request.event,
|
||||
@@ -295,7 +306,7 @@ class WidgetAPIProductList(EventListMixin, View):
|
||||
var.cached_availability[0],
|
||||
var.cached_availability[1] if item.do_show_quota_left else None
|
||||
],
|
||||
} for var in item.available_variations
|
||||
} for var in item.available_variations if (not variation_filter or var.id in variation_filter)
|
||||
]
|
||||
|
||||
} for item in g
|
||||
|
||||
2411
src/pretix/static/npm_dir/package-lock.json
generated
2411
src/pretix/static/npm_dir/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,13 +4,13 @@
|
||||
"private": true,
|
||||
"scripts": {},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.18.2",
|
||||
"@babel/preset-env": "^7.18.2",
|
||||
"@babel/core": "^7.18.6",
|
||||
"@babel/preset-env": "^7.18.6",
|
||||
"@rollup/plugin-babel": "^5.3.1",
|
||||
"@rollup/plugin-node-resolve": "^13.3.0",
|
||||
"vue": "^2.6.14",
|
||||
"rollup": "^2.75.5",
|
||||
"vue": "^2.7.0",
|
||||
"rollup": "^2.75.7",
|
||||
"rollup-plugin-vue": "^5.0.1",
|
||||
"vue-template-compiler": "^2.6.14"
|
||||
"vue-template-compiler": "^2.7.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,6 +199,7 @@ function setup_basics(el) {
|
||||
});
|
||||
$(".has-error").each(function() {
|
||||
var target = target = $(":input", this);
|
||||
if (!target || !target.attr("aria-describedby")) return;
|
||||
var desc = $("#" + target.attr("aria-describedby").split(' ', 1)[0]);
|
||||
// multi-input fields have a role=group with aria-labelledby
|
||||
var label = this.hasAttribute("aria-labelledby") ? $("#" + this.getAttribute("aria-labelledby")) : $("[for="+target.attr("id")+"]");
|
||||
|
||||
@@ -1398,6 +1398,9 @@ var shared_root_methods = {
|
||||
if (this.$root.category_filter) {
|
||||
url += '&categories=' + encodeURIComponent(this.$root.category_filter);
|
||||
}
|
||||
if (this.$root.variation_filter) {
|
||||
url += '&variations=' + encodeURIComponent(this.$root.variation_filter);
|
||||
}
|
||||
var cart_id = getCookie(this.cookieName);
|
||||
if (this.$root.voucher_code) {
|
||||
url += '&voucher=' + encodeURIComponent(this.$root.voucher_code);
|
||||
@@ -1464,7 +1467,7 @@ var shared_root_methods = {
|
||||
root.error = data.error;
|
||||
root.display_add_to_cart = data.display_add_to_cart;
|
||||
root.waiting_list_enabled = data.waiting_list_enabled;
|
||||
root.show_variations_expanded = data.show_variations_expanded;
|
||||
root.show_variations_expanded = data.show_variations_expanded || !!root.variation_filter;
|
||||
root.cart_id = cart_id;
|
||||
root.cart_exists = data.cart_exists;
|
||||
root.vouchers_exist = data.vouchers_exist;
|
||||
@@ -1665,6 +1668,7 @@ var create_widget = function (element) {
|
||||
var widget_data = JSON.parse(JSON.stringify(window.PretixWidget.widget_data));
|
||||
var filter = element.attributes.filter ? element.attributes.filter.value : null;
|
||||
var items = element.attributes.items ? element.attributes.items.value : null;
|
||||
var variations = element.attributes.variations ? element.attributes.variations.value : null;
|
||||
var categories = element.attributes.categories ? element.attributes.categories.value : null;
|
||||
for (var i = 0; i < element.attributes.length; i++) {
|
||||
var attrib = element.attributes[i];
|
||||
@@ -1696,10 +1700,11 @@ var create_widget = function (element) {
|
||||
filter: filter,
|
||||
item_filter: items,
|
||||
category_filter: categories,
|
||||
variation_filter: variations,
|
||||
voucher_code: voucher,
|
||||
display_net_prices: false,
|
||||
voucher_explanation_text: null,
|
||||
show_variations_expanded: false,
|
||||
show_variations_expanded: !!variations,
|
||||
skip_ssl: skip_ssl,
|
||||
disable_iframe: disable_iframe,
|
||||
style: style,
|
||||
|
||||
@@ -116,6 +116,7 @@ ignore =
|
||||
tests/plugins/badges/*
|
||||
tests/plugins/banktransfer/*
|
||||
tests/plugins/paypal/*
|
||||
tests/plugins/paypal2/*
|
||||
tests/plugins/pretixdroid/*
|
||||
tests/plugins/stripe/*
|
||||
tests/plugins/sendmail/*
|
||||
|
||||
@@ -204,7 +204,7 @@ setup(
|
||||
'packaging',
|
||||
'paypalrestsdk==1.13.*',
|
||||
'paypal-checkout-serversdk==1.0.*',
|
||||
'PyJWT==2.0.*',
|
||||
'PyJWT==2.4.*',
|
||||
'phonenumberslite==8.12.*',
|
||||
'Pillow==9.1.*',
|
||||
'protobuf==3.19.*',
|
||||
@@ -220,7 +220,7 @@ setup(
|
||||
'redis==3.5.*',
|
||||
'reportlab==3.6.*',
|
||||
'requests==2.27.*',
|
||||
'sentry-sdk==1.5.*',
|
||||
'sentry-sdk==1.8.*',
|
||||
'sepaxml==2.4.*,>=2.4.1',
|
||||
'slimit',
|
||||
'static3==0.7.*',
|
||||
@@ -237,7 +237,7 @@ setup(
|
||||
'dev': [
|
||||
'coverage',
|
||||
'coveralls',
|
||||
'django-debug-toolbar==3.2.*',
|
||||
'django-debug-toolbar==3.5.*',
|
||||
'flake8==4.0.*',
|
||||
'freezegun',
|
||||
'isort==5.10.*',
|
||||
|
||||
@@ -70,7 +70,7 @@ def order(event, item, other_item, taxrule):
|
||||
total=46, locale='en'
|
||||
)
|
||||
InvoiceAddress.objects.create(order=o, company="Sample company", country=Country('NZ'))
|
||||
OrderPosition.objects.create(
|
||||
op1 = OrderPosition.objects.create(
|
||||
order=o,
|
||||
positionid=1,
|
||||
item=item,
|
||||
@@ -90,6 +90,16 @@ def order(event, item, other_item, taxrule):
|
||||
secret="sf4HZG73fU6kwddgjg2QOusFbYZwVKpK",
|
||||
pseudonymization_id="BACDEFGHKL",
|
||||
)
|
||||
OrderPosition.objects.create(
|
||||
order=o,
|
||||
positionid=3,
|
||||
item=other_item,
|
||||
addon_to=op1,
|
||||
variation=None,
|
||||
price=Decimal("0"),
|
||||
secret="3u4ez6vrrbgb3wvezxhq446p548dt2wn",
|
||||
pseudonymization_id="FOOBAR12345",
|
||||
)
|
||||
return o
|
||||
|
||||
|
||||
@@ -157,6 +167,38 @@ TEST_ORDERPOSITION2_RES = {
|
||||
"pseudonymization_id": "BACDEFGHKL",
|
||||
}
|
||||
|
||||
TEST_ORDERPOSITION3_RES = {
|
||||
"id": 3,
|
||||
"require_attention": False,
|
||||
"order__status": "p",
|
||||
"order": "FOO",
|
||||
"positionid": 3,
|
||||
"item": 1,
|
||||
"variation": None,
|
||||
"price": "0.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_name_parts": {'full_name': "Peter"},
|
||||
"attendee_email": None,
|
||||
"voucher": None,
|
||||
"tax_rate": "0.00",
|
||||
"tax_value": "0.00",
|
||||
"tax_rule": None,
|
||||
"secret": "3u4ez6vrrbgb3wvezxhq446p548dt2wn",
|
||||
"addon_to": None,
|
||||
"checkins": [],
|
||||
"downloads": [],
|
||||
"answers": [],
|
||||
"seat": None,
|
||||
"company": None,
|
||||
"street": None,
|
||||
"zipcode": None,
|
||||
"city": None,
|
||||
"country": None,
|
||||
"state": None,
|
||||
"subevent": None,
|
||||
"pseudonymization_id": "FOOBAR12345",
|
||||
}
|
||||
|
||||
TEST_LIST_RES = {
|
||||
"name": "Default",
|
||||
"all_products": False,
|
||||
@@ -168,6 +210,7 @@ TEST_LIST_RES = {
|
||||
"allow_entry_after_exit": True,
|
||||
"subevent": None,
|
||||
"exit_all_at": None,
|
||||
"addon_match": False,
|
||||
"rules": {}
|
||||
}
|
||||
|
||||
@@ -186,13 +229,14 @@ def clist_all(event, item):
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_list_list(token_client, organizer, event, clist, item, subevent):
|
||||
def test_list_list(token_client, organizer, event, clist, item, subevent, django_assert_num_queries):
|
||||
res = dict(TEST_LIST_RES)
|
||||
res["id"] = clist.pk
|
||||
res["limit_products"] = [item.pk]
|
||||
res["auto_checkin_sales_channels"] = []
|
||||
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/'.format(organizer.slug, event.slug))
|
||||
with django_assert_num_queries(11):
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/'.format(organizer.slug, event.slug))
|
||||
assert resp.status_code == 200
|
||||
assert [res] == resp.data['results']
|
||||
|
||||
@@ -393,30 +437,35 @@ def test_list_update(token_client, organizer, event, clist):
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_list_all_items_positions(token_client, organizer, event, clist, clist_all, item, other_item, order):
|
||||
def test_list_all_items_positions(token_client, organizer, event, clist, clist_all, item, other_item, order, django_assert_num_queries):
|
||||
with scopes_disabled():
|
||||
p1 = dict(TEST_ORDERPOSITION1_RES)
|
||||
p1["id"] = order.positions.first().pk
|
||||
p1["id"] = order.positions.get(positionid=1).pk
|
||||
p1["item"] = item.pk
|
||||
p2 = dict(TEST_ORDERPOSITION2_RES)
|
||||
p2["id"] = order.positions.last().pk
|
||||
p2["id"] = order.positions.get(positionid=2).pk
|
||||
p2["item"] = other_item.pk
|
||||
p3 = dict(TEST_ORDERPOSITION3_RES)
|
||||
p3["id"] = order.positions.get(positionid=3).pk
|
||||
p3["item"] = other_item.pk
|
||||
p3["addon_to"] = p1["id"]
|
||||
|
||||
# All items
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=positionid'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
with django_assert_num_queries(23):
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=positionid'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [p1, p2] == resp.data['results']
|
||||
assert [p1, p2, p3] == resp.data['results']
|
||||
|
||||
# Check-ins on other list ignored
|
||||
with scopes_disabled():
|
||||
order.positions.first().checkins.create(list=clist)
|
||||
c = order.positions.get(positionid=1).checkins.create(list=clist)
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=positionid'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [p1, p2] == resp.data['results']
|
||||
assert [p1, p2, p3] == resp.data['results']
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?has_checkin=1'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
@@ -425,7 +474,7 @@ def test_list_all_items_positions(token_client, organizer, event, clist, clist_a
|
||||
|
||||
# Only checked in
|
||||
with scopes_disabled():
|
||||
c = order.positions.first().checkins.create(list=clist_all)
|
||||
c = order.positions.get(positionid=1).checkins.create(list=clist_all)
|
||||
p1['checkins'] = [
|
||||
{
|
||||
'id': c.pk,
|
||||
@@ -448,7 +497,7 @@ def test_list_all_items_positions(token_client, organizer, event, clist, clist_a
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [p2] == resp.data['results']
|
||||
assert [p2, p3] == resp.data['results']
|
||||
|
||||
# Order by checkin
|
||||
resp = token_client.get(
|
||||
@@ -456,18 +505,18 @@ def test_list_all_items_positions(token_client, organizer, event, clist, clist_a
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [p1, p2] == resp.data['results']
|
||||
assert resp.data['results'][0] == p1
|
||||
resp = token_client.get(
|
||||
'/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=last_checked_in'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [p2, p1] == resp.data['results']
|
||||
assert resp.data['results'][-1] == p1
|
||||
|
||||
# Order by checkin date
|
||||
time.sleep(1)
|
||||
with scopes_disabled():
|
||||
c = order.positions.last().checkins.create(list=clist_all)
|
||||
c = order.positions.get(positionid=2).checkins.create(list=clist_all)
|
||||
p2['checkins'] = [
|
||||
{
|
||||
'id': c.pk,
|
||||
@@ -480,23 +529,23 @@ def test_list_all_items_positions(token_client, organizer, event, clist, clist_a
|
||||
}
|
||||
]
|
||||
resp = token_client.get(
|
||||
'/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=-last_checked_in'.format(
|
||||
'/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=-last_checked_in,positionid'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [p2, p1] == resp.data['results']
|
||||
assert [p2, p1, p3] == resp.data['results']
|
||||
|
||||
# Order by attendee_name
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=-attendee_name'.format(
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=-attendee_name,positionid'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [p1, p2] == resp.data['results']
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=attendee_name'.format(
|
||||
assert [p1, p3, p2] == resp.data['results']
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=attendee_name,positionid'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [p2, p1] == resp.data['results']
|
||||
assert [p2, p1, p3] == resp.data['results']
|
||||
|
||||
# Paid only
|
||||
order.status = Order.STATUS_PENDING
|
||||
@@ -513,32 +562,41 @@ def test_list_all_items_positions(token_client, organizer, event, clist, clist_a
|
||||
assert resp.status_code == 200
|
||||
p1['order__status'] = 'n'
|
||||
p2['order__status'] = 'n'
|
||||
assert [p2, p1] == resp.data['results']
|
||||
p3['order__status'] = 'n'
|
||||
assert [p2, p1, p3] == resp.data['results']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_list_all_items_positions_by_subevent(token_client, organizer, event, clist, clist_all, item, other_item, order, subevent):
|
||||
with scopes_disabled():
|
||||
se2 = event.subevents.create(name="Foobar", date_from=datetime.datetime(2017, 12, 27, 10, 0, 0, tzinfo=UTC))
|
||||
pfirst = order.positions.first()
|
||||
pfirst = order.positions.get(positionid=1)
|
||||
pfirst.subevent = se2
|
||||
pfirst.save()
|
||||
p1 = dict(TEST_ORDERPOSITION1_RES)
|
||||
p1["id"] = pfirst.pk
|
||||
p1["subevent"] = se2.pk
|
||||
p1["item"] = item.pk
|
||||
plast = order.positions.last()
|
||||
plast.subevent = subevent
|
||||
plast.save()
|
||||
psecond = order.positions.get(positionid=2)
|
||||
psecond.subevent = subevent
|
||||
psecond.save()
|
||||
p2 = dict(TEST_ORDERPOSITION2_RES)
|
||||
p2["id"] = plast.pk
|
||||
p2["id"] = psecond.pk
|
||||
p2["item"] = other_item.pk
|
||||
p2["subevent"] = subevent.pk
|
||||
pthird = order.positions.get(positionid=3)
|
||||
pthird.subevent = se2
|
||||
pthird.save()
|
||||
p3 = dict(TEST_ORDERPOSITION3_RES)
|
||||
p3["id"] = pthird.pk
|
||||
p3["addon_to"] = pfirst.pk
|
||||
p3["item"] = other_item.pk
|
||||
p3["subevent"] = se2.pk
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=positionid'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [p1, p2] == resp.data['results']
|
||||
assert [p1, p2, p3] == resp.data['results']
|
||||
|
||||
clist_all.subevent = subevent
|
||||
clist_all.save()
|
||||
@@ -593,7 +651,7 @@ def test_status(token_client, organizer, event, clist_all, item, other_item, ord
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['checkin_count'] == 1
|
||||
assert resp.data['position_count'] == 2
|
||||
assert resp.data['position_count'] == 3
|
||||
assert resp.data['inside_count'] == 1
|
||||
assert resp.data['items'] == [
|
||||
{
|
||||
@@ -622,23 +680,37 @@ def test_status(token_client, organizer, event, clist_all, item, other_item, ord
|
||||
'id': other_item.pk,
|
||||
'checkin_count': 0,
|
||||
'admission': False,
|
||||
'position_count': 1,
|
||||
'position_count': 2,
|
||||
'variations': []
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def _redeem(token_client, org, clist, p, body=None):
|
||||
return token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
org.slug, clist.event.slug, clist.pk, p
|
||||
), body or {}, format='json')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_query_load(token_client, organizer, clist, event, order, django_assert_max_num_queries):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first().pk
|
||||
with django_assert_max_num_queries(30):
|
||||
resp = _redeem(token_client, organizer, clist, p)
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_custom_datetime(token_client, organizer, clist, event, order):
|
||||
dt = now() - datetime.timedelta(days=1)
|
||||
dt = dt.replace(microsecond=0)
|
||||
with scopes_disabled():
|
||||
p = order.positions.first().pk
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p
|
||||
), {
|
||||
resp = _redeem(token_client, organizer, clist, p, {
|
||||
'datetime': dt.isoformat()
|
||||
}, format='json')
|
||||
})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
with scopes_disabled():
|
||||
@@ -654,9 +726,7 @@ def test_name_fallback(token_client, organizer, clist, event, order):
|
||||
op.attendee_name_cached = None
|
||||
op.attendee_name_parts = {}
|
||||
op.save()
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, op.pk
|
||||
), {}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, op.pk, {})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
assert resp.data['position']['attendee_name'] == 'Paul'
|
||||
@@ -667,9 +737,7 @@ def test_name_fallback(token_client, organizer, clist, event, order):
|
||||
def test_by_secret(token_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.secret
|
||||
), {}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
|
||||
@@ -680,9 +748,7 @@ def test_by_secret_special_chars(token_client, organizer, clist, event, order):
|
||||
p = order.positions.first()
|
||||
p.secret = "abc+-/=="
|
||||
p.save()
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, urlquote(p.secret, safe='')
|
||||
), {}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, urlquote(p.secret, safe=''), {})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
|
||||
@@ -693,9 +759,7 @@ def test_by_secret_special_chars_space_fallback(token_client, organizer, clist,
|
||||
p = order.positions.first()
|
||||
p.secret = "foo bar"
|
||||
p.save()
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, "foo+bar"
|
||||
), {}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, "foo+bar", {})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
|
||||
@@ -704,14 +768,10 @@ def test_by_secret_special_chars_space_fallback(token_client, organizer, clist,
|
||||
def test_only_once(token_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'error'
|
||||
assert resp.data['reason'] == 'already_redeemed'
|
||||
@@ -721,14 +781,10 @@ def test_only_once(token_client, organizer, clist, event, order):
|
||||
def test_reupload_same_nonce(token_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {'nonce': 'foobar'}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {'nonce': 'foobar'})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {'nonce': 'foobar'}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {'nonce': 'foobar'})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
|
||||
@@ -739,14 +795,10 @@ def test_allow_multiple(token_client, organizer, clist, event, order):
|
||||
clist.save()
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
with scopes_disabled():
|
||||
@@ -759,14 +811,10 @@ def test_allow_multiple_reupload_same_nonce(token_client, organizer, clist, even
|
||||
clist.save()
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {'nonce': 'foobar'}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {'nonce': 'foobar'})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {'nonce': 'foobar'}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {'nonce': 'foobar'})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
with scopes_disabled():
|
||||
@@ -777,14 +825,10 @@ def test_allow_multiple_reupload_same_nonce(token_client, organizer, clist, even
|
||||
def test_multiple_different_list(token_client, organizer, clist, clist_all, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {'nonce': 'foobar'}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {'nonce': 'foobar'})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist_all.pk, p.pk
|
||||
), {'nonce': 'baz'}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist_all, p.pk, {'nonce': 'baz'})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
|
||||
@@ -793,14 +837,10 @@ def test_multiple_different_list(token_client, organizer, clist, clist_all, even
|
||||
def test_forced_multiple(token_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {'force': True}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {'force': True})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
|
||||
@@ -809,17 +849,13 @@ def test_forced_multiple(token_client, organizer, clist, event, order):
|
||||
def test_forced_flag_set_if_required(token_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {'force': True}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {'force': True})
|
||||
with scopes_disabled():
|
||||
assert not p.checkins.order_by('pk').last().forced
|
||||
assert p.checkins.order_by('pk').last().force_sent
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {'force': True}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {'force': True})
|
||||
with scopes_disabled():
|
||||
assert p.checkins.order_by('pk').last().forced
|
||||
assert p.checkins.order_by('pk').last().force_sent
|
||||
@@ -833,9 +869,7 @@ def test_require_product(token_client, organizer, clist, event, order):
|
||||
clist.limit_products.clear()
|
||||
p = order.positions.first()
|
||||
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'error'
|
||||
assert resp.data['reason'] == 'product'
|
||||
@@ -848,32 +882,24 @@ def test_require_paid(token_client, organizer, clist, event, order):
|
||||
|
||||
order.status = Order.STATUS_CANCELED
|
||||
order.save()
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'error'
|
||||
assert resp.data['reason'] == 'unpaid'
|
||||
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {'canceled_supported': True}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {'canceled_supported': True})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'error'
|
||||
assert resp.data['reason'] == 'canceled'
|
||||
|
||||
order.status = Order.STATUS_PENDING
|
||||
order.save()
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'error'
|
||||
assert resp.data['reason'] == 'unpaid'
|
||||
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {'ignore_unpaid': True}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {'ignore_unpaid': True})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'error'
|
||||
assert resp.data['reason'] == 'unpaid'
|
||||
@@ -881,16 +907,12 @@ def test_require_paid(token_client, organizer, clist, event, order):
|
||||
clist.include_pending = True
|
||||
clist.save()
|
||||
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'error'
|
||||
assert resp.data['reason'] == 'unpaid'
|
||||
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {'ignore_unpaid': True}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {'ignore_unpaid': True})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
|
||||
@@ -912,17 +934,13 @@ def test_question_number(token_client, organizer, clist, event, order, question)
|
||||
question[0].type = 'N'
|
||||
question[0].save()
|
||||
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'incomplete'
|
||||
with scopes_disabled():
|
||||
assert resp.data['questions'] == [QuestionSerializer(question[0]).data]
|
||||
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {'answers': {question[0].pk: "3.24"}}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {'answers': {question[0].pk: "3.24"}})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
with scopes_disabled():
|
||||
@@ -933,17 +951,13 @@ def test_question_number(token_client, organizer, clist, event, order, question)
|
||||
def test_question_choice(token_client, organizer, clist, event, order, question):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'incomplete'
|
||||
with scopes_disabled():
|
||||
assert resp.data['questions'] == [QuestionSerializer(question[0]).data]
|
||||
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {'answers': {question[0].pk: str(question[1].pk)}}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {'answers': {question[0].pk: str(question[1].pk)}})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
with scopes_disabled():
|
||||
@@ -955,17 +969,13 @@ def test_question_choice(token_client, organizer, clist, event, order, question)
|
||||
def test_question_choice_identifier(token_client, organizer, clist, event, order, question):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'incomplete'
|
||||
with scopes_disabled():
|
||||
assert resp.data['questions'] == [QuestionSerializer(question[0]).data]
|
||||
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {'answers': {question[0].pk: str(question[1].identifier)}}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {'answers': {question[0].pk: str(question[1].identifier)}})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
with scopes_disabled():
|
||||
@@ -977,9 +987,7 @@ def test_question_choice_identifier(token_client, organizer, clist, event, order
|
||||
def test_question_invalid(token_client, organizer, clist, event, order, question):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {'answers': {question[0].pk: "A"}}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {'answers': {question[0].pk: "A"}})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'incomplete'
|
||||
with scopes_disabled():
|
||||
@@ -993,17 +1001,13 @@ def test_question_required(token_client, organizer, clist, event, order, questio
|
||||
question[0].required = True
|
||||
question[0].save()
|
||||
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'incomplete'
|
||||
with scopes_disabled():
|
||||
assert resp.data['questions'] == [QuestionSerializer(question[0]).data]
|
||||
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {'answers': {question[0].pk: ""}}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {'answers': {question[0].pk: ""}})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'incomplete'
|
||||
with scopes_disabled():
|
||||
@@ -1017,17 +1021,13 @@ def test_question_optional(token_client, organizer, clist, event, order, questio
|
||||
question[0].required = False
|
||||
question[0].save()
|
||||
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {'answers': {}})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'incomplete'
|
||||
with scopes_disabled():
|
||||
assert resp.data['questions'] == [QuestionSerializer(question[0]).data]
|
||||
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {'answers': {question[0].pk: ""}}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {'answers': {question[0].pk: ""}})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
|
||||
@@ -1039,17 +1039,13 @@ def test_question_multiple_choice(token_client, organizer, clist, event, order,
|
||||
question[0].type = 'M'
|
||||
question[0].save()
|
||||
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'incomplete'
|
||||
with scopes_disabled():
|
||||
assert resp.data['questions'] == [QuestionSerializer(question[0]).data]
|
||||
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {'answers': {question[0].pk: "{},{}".format(question[1].pk, question[2].pk)}}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {'answers': {question[0].pk: "{},{}".format(question[1].pk, question[2].pk)}})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
with scopes_disabled():
|
||||
@@ -1076,23 +1072,17 @@ def test_question_upload(token_client, organizer, clist, event, order, question)
|
||||
question[0].type = 'F'
|
||||
question[0].save()
|
||||
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'incomplete'
|
||||
with scopes_disabled():
|
||||
assert resp.data['questions'] == [QuestionSerializer(question[0]).data]
|
||||
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {'answers': {question[0].pk: "invalid"}}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {'answers': {question[0].pk: "invalid"}})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'incomplete'
|
||||
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {'answers': {question[0].pk: file_id_png}}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {'answers': {question[0].pk: file_id_png}})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
with scopes_disabled():
|
||||
@@ -1144,11 +1134,7 @@ def test_store_failed(token_client, organizer, clist, event, order):
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_unknown(token_client, organizer, clist, event, order):
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, 'unknown_secret'
|
||||
), {
|
||||
'force': True
|
||||
}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, 'unknown_secret', {'force': True})
|
||||
assert resp.status_code == 404
|
||||
assert resp.data["status"] == "error"
|
||||
assert resp.data["reason"] == "invalid"
|
||||
@@ -1161,10 +1147,7 @@ def test_redeem_unknown_revoked(token_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
event.revoked_secrets.create(position=p, secret='revoked_secret')
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, 'revoked_secret'
|
||||
), {
|
||||
}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, 'revoked_secret', {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data["status"] == "error"
|
||||
assert resp.data["reason"] == "revoked"
|
||||
@@ -1177,29 +1160,22 @@ def test_redeem_unknown_revoked_force(token_client, organizer, clist, event, ord
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
event.revoked_secrets.create(position=p, secret='revoked_secret')
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, 'revoked_secret'
|
||||
), {
|
||||
'force': True
|
||||
}, format='json')
|
||||
resp = _redeem(token_client, organizer, clist, 'revoked_secret', {'force': True})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data["status"] == "ok"
|
||||
with scopes_disabled():
|
||||
assert Checkin.objects.last().forced
|
||||
assert Checkin.objects.last().force_sent
|
||||
ci = Checkin.objects.last()
|
||||
assert ci.forced
|
||||
assert ci.force_sent
|
||||
assert ci.position == p
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_unknown_legacy_device_bug(device, device_client, organizer, clist, event, order):
|
||||
def test_redeem_unknown_legacy_device_bug(device, device_client, organizer, clist, event):
|
||||
device.software_brand = "pretixSCAN"
|
||||
device.software_version = "1.11.1"
|
||||
device.save()
|
||||
resp = device_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, 'unknown_secret'
|
||||
), {
|
||||
'force': True
|
||||
}, format='json')
|
||||
print(resp.data)
|
||||
resp = _redeem(device_client, organizer, clist, 'unknown_secret', {'force': True})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data["status"] == "error"
|
||||
assert resp.data["reason"] == "already_redeemed"
|
||||
@@ -1209,13 +1185,139 @@ def test_redeem_unknown_legacy_device_bug(device, device_client, organizer, clis
|
||||
device.software_brand = "pretixSCAN"
|
||||
device.software_version = "1.11.2"
|
||||
device.save()
|
||||
resp = device_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, 'unknown_secret'
|
||||
), {
|
||||
'force': True
|
||||
}, format='json')
|
||||
resp = _redeem(device_client, organizer, clist, 'unknown_secret', {'force': True})
|
||||
assert resp.status_code == 404
|
||||
assert resp.data["status"] == "error"
|
||||
assert resp.data["reason"] == "invalid"
|
||||
with scopes_disabled():
|
||||
assert not Checkin.objects.last()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_by_id_not_allowed_if_pretixscan(device, device_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
device.software_brand = "pretixSCAN"
|
||||
device.software_version = "1.14.2"
|
||||
device.save()
|
||||
resp = _redeem(device_client, organizer, clist, p.pk, {'force': True})
|
||||
assert resp.status_code == 404
|
||||
resp = _redeem(device_client, organizer, clist, p.secret, {'force': True})
|
||||
assert resp.status_code == 201
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_by_id_not_allowed_if_untrusted(device, device_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = device_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/?untrusted_input=true'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {
|
||||
'force': True
|
||||
}, format='json')
|
||||
assert resp.status_code == 404
|
||||
resp = device_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/?untrusted_input=true'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.secret
|
||||
), {
|
||||
'force': True
|
||||
}, format='json')
|
||||
assert resp.status_code == 201
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_addon_if_match_disabled(token_client, organizer, clist, other_item, event, order):
|
||||
with scopes_disabled():
|
||||
clist.all_products = False
|
||||
clist.save()
|
||||
clist.limit_products.set([other_item])
|
||||
resp = _redeem(token_client, organizer, clist, 'z3fsn8jyufm5kpk768q69gkbyr5f4h6w', {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data["status"] == "error"
|
||||
assert resp.data["reason"] == "product"
|
||||
with scopes_disabled():
|
||||
assert not Checkin.objects.last()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_addon_if_match_enabled(token_client, organizer, clist, other_item, event, order):
|
||||
with scopes_disabled():
|
||||
clist.all_products = False
|
||||
clist.addon_match = True
|
||||
clist.save()
|
||||
clist.limit_products.set([other_item])
|
||||
p = order.positions.first().addons.all().first()
|
||||
resp = _redeem(token_client, organizer, clist, 'z3fsn8jyufm5kpk768q69gkbyr5f4h6w', {})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
assert resp.data['position']['attendee_name'] == 'Peter' # test propagation of names
|
||||
assert resp.data['position']['item'] == other_item.pk
|
||||
with scopes_disabled():
|
||||
ci = Checkin.objects.last()
|
||||
assert ci.position == p
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_addon_if_match_ambiguous(token_client, organizer, clist, item, other_item, event, order):
|
||||
with scopes_disabled():
|
||||
clist.all_products = False
|
||||
clist.addon_match = True
|
||||
clist.save()
|
||||
clist.limit_products.set([item, other_item])
|
||||
resp = _redeem(token_client, organizer, clist, 'z3fsn8jyufm5kpk768q69gkbyr5f4h6w', {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data["status"] == "error"
|
||||
assert resp.data["reason"] == "ambiguous"
|
||||
with scopes_disabled():
|
||||
assert not Checkin.objects.last()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_addon_if_match_and_revoked_force(token_client, organizer, clist, other_item, event, order):
|
||||
with scopes_disabled():
|
||||
event.revoked_secrets.create(position=order.positions.get(positionid=1), secret='revoked_secret')
|
||||
clist.all_products = False
|
||||
clist.addon_match = True
|
||||
clist.save()
|
||||
clist.limit_products.set([other_item])
|
||||
p = order.positions.first().addons.all().first()
|
||||
resp = _redeem(token_client, organizer, clist, 'revoked_secret', {'force': True})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data["status"] == "ok"
|
||||
with scopes_disabled():
|
||||
ci = Checkin.objects.last()
|
||||
assert ci.forced
|
||||
assert ci.force_sent
|
||||
assert ci.position == p
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_search(token_client, organizer, event, clist, clist_all, item, other_item, order, django_assert_max_num_queries):
|
||||
with scopes_disabled():
|
||||
p1 = dict(TEST_ORDERPOSITION1_RES)
|
||||
p1["id"] = order.positions.get(positionid=1).pk
|
||||
p1["item"] = item.pk
|
||||
|
||||
with django_assert_max_num_queries(17):
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?search=z3fsn8jyu'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [p1] == resp.data['results']
|
||||
assert not resp.data['results'][0].get('pdf_data')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_checkin_pdf_data_requires_permission(token_client, event, team, organizer, clist_all, order):
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?search=z3fsn8jyu&pdf_data=true'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.data['results'][0].get('pdf_data')
|
||||
with scopes_disabled():
|
||||
team.can_view_orders = False
|
||||
team.can_change_orders = False
|
||||
team.can_checkin_orders = True
|
||||
team.save()
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?search=z3fsn8jyu&pdf_data=true'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert not resp.data['results'][0].get('pdf_data')
|
||||
|
||||
911
src/tests/api/test_checkinrpc.py
Normal file
911
src/tests/api/test_checkinrpc.py
Normal file
@@ -0,0 +1,911 @@
|
||||
#
|
||||
# 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 datetime
|
||||
from decimal import Decimal
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from django.core.files.base import ContentFile
|
||||
from django.utils.timezone import now
|
||||
from django_countries.fields import Country
|
||||
from django_scopes import scopes_disabled
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from pytz import UTC
|
||||
|
||||
from pretix.api.serializers.item import QuestionSerializer
|
||||
from pretix.base.models import Checkin, InvoiceAddress, Order, OrderPosition
|
||||
|
||||
# Lots of this code is overlapping with test_checkin.py, and some of it is arguably redundant since it's triggering
|
||||
# the same backend code paths (for now). However, this is SUCH a critical part of pretix that we don't want to take
|
||||
# the risk of some day having differing implementations and missing vital test coverage.
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def item(event):
|
||||
return event.items.create(name="Budget Ticket", default_price=23)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def item_on_event2(event2):
|
||||
return event2.items.create(name="Budget Ticket", default_price=23)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def other_item(event):
|
||||
return event.items.create(name="Budget Ticket", default_price=23)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def order(event, item, other_item, taxrule):
|
||||
testtime = datetime.datetime(2017, 12, 1, 10, 0, 0, tzinfo=UTC)
|
||||
|
||||
with mock.patch('django.utils.timezone.now') as mock_now:
|
||||
mock_now.return_value = testtime
|
||||
o = Order.objects.create(
|
||||
code='FOO', event=event, email='dummy@dummy.test',
|
||||
status=Order.STATUS_PAID, secret="k24fiuwvu8kxz3y1",
|
||||
datetime=datetime.datetime(2017, 12, 1, 10, 0, 0, tzinfo=UTC),
|
||||
expires=datetime.datetime(2017, 12, 10, 10, 0, 0, tzinfo=UTC),
|
||||
total=46, locale='en'
|
||||
)
|
||||
InvoiceAddress.objects.create(order=o, company="Sample company", country=Country('NZ'))
|
||||
op1 = OrderPosition.objects.create(
|
||||
order=o,
|
||||
positionid=1,
|
||||
item=item,
|
||||
variation=None,
|
||||
price=Decimal("23"),
|
||||
attendee_name_parts={'full_name': "Peter"},
|
||||
secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||
pseudonymization_id="ABCDEFGHKL",
|
||||
)
|
||||
OrderPosition.objects.create(
|
||||
order=o,
|
||||
positionid=2,
|
||||
item=other_item,
|
||||
variation=None,
|
||||
price=Decimal("23"),
|
||||
attendee_name_parts={'full_name': "Michael"},
|
||||
secret="sf4HZG73fU6kwddgjg2QOusFbYZwVKpK",
|
||||
pseudonymization_id="BACDEFGHKL",
|
||||
)
|
||||
OrderPosition.objects.create(
|
||||
order=o,
|
||||
positionid=3,
|
||||
item=other_item,
|
||||
addon_to=op1,
|
||||
variation=None,
|
||||
price=Decimal("0"),
|
||||
secret="3u4ez6vrrbgb3wvezxhq446p548dt2wn",
|
||||
pseudonymization_id="FOOBAR12345",
|
||||
)
|
||||
return o
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def order2(event2, item_on_event2):
|
||||
testtime = datetime.datetime(2017, 12, 1, 10, 0, 0, tzinfo=UTC)
|
||||
|
||||
with mock.patch('django.utils.timezone.now') as mock_now:
|
||||
mock_now.return_value = testtime
|
||||
o = Order.objects.create(
|
||||
code='BAR', event=event2, email='dummy@dummy.test',
|
||||
status=Order.STATUS_PAID, secret="ylptCPNOxTyA",
|
||||
datetime=datetime.datetime(2017, 12, 1, 10, 0, 0, tzinfo=UTC),
|
||||
expires=datetime.datetime(2017, 12, 10, 10, 0, 0, tzinfo=UTC),
|
||||
total=46, locale='en'
|
||||
)
|
||||
InvoiceAddress.objects.create(order=o, company="Sample company", country=Country('NZ'))
|
||||
OrderPosition.objects.create(
|
||||
order=o,
|
||||
positionid=1,
|
||||
item=item_on_event2,
|
||||
variation=None,
|
||||
price=Decimal("23"),
|
||||
attendee_name_parts={'full_name': "John"},
|
||||
secret="y8tPmyc5BEK2G9pifSNumwp4NXAaIE4P",
|
||||
pseudonymization_id="A23456789",
|
||||
)
|
||||
OrderPosition.objects.create(
|
||||
order=o,
|
||||
positionid=2,
|
||||
item=item_on_event2,
|
||||
variation=None,
|
||||
price=Decimal("23"),
|
||||
attendee_name_parts={'full_name': "Paul"},
|
||||
secret="xrahgLCfodoNOIZ4uxn75gNBM1bb6m1h",
|
||||
pseudonymization_id="B23456797345",
|
||||
)
|
||||
return o
|
||||
|
||||
|
||||
TEST_ORDERPOSITION1_RES = {
|
||||
"id": 1,
|
||||
"require_attention": False,
|
||||
"order__status": "p",
|
||||
"order": "FOO",
|
||||
"positionid": 1,
|
||||
"item": 1,
|
||||
"variation": None,
|
||||
"price": "23.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_name_parts": {'full_name': "Peter"},
|
||||
"attendee_email": None,
|
||||
"voucher": None,
|
||||
"tax_rate": "0.00",
|
||||
"tax_value": "0.00",
|
||||
"tax_rule": None,
|
||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||
"addon_to": None,
|
||||
"checkins": [],
|
||||
"downloads": [],
|
||||
"answers": [],
|
||||
"seat": None,
|
||||
"company": None,
|
||||
"street": None,
|
||||
"zipcode": None,
|
||||
"city": None,
|
||||
"country": None,
|
||||
"state": None,
|
||||
"subevent": None,
|
||||
"pseudonymization_id": "ABCDEFGHKL",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def clist(event, item):
|
||||
c = event.checkin_lists.create(name="Default", all_products=False)
|
||||
c.limit_products.add(item)
|
||||
return c
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def clist_all(event, item):
|
||||
c = event.checkin_lists.create(name="Default", all_products=True)
|
||||
return c
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def clist_event2(event2):
|
||||
c = event2.checkin_lists.create(name="Event 2", all_products=True)
|
||||
return c
|
||||
|
||||
|
||||
def _redeem(token_client, org, clist, p, body=None, query=''):
|
||||
body = body or {}
|
||||
if isinstance(clist, list):
|
||||
body['lists'] = [c.pk for c in clist]
|
||||
else:
|
||||
body['lists'] = [clist.pk]
|
||||
body['secret'] = p
|
||||
return token_client.post('/api/v1/organizers/{}/checkinrpc/redeem/{}'.format(
|
||||
org.slug, query,
|
||||
), body, format='json')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_query_load(token_client, organizer, clist, event, order, django_assert_max_num_queries):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
with django_assert_max_num_queries(30):
|
||||
resp = _redeem(token_client, organizer, clist, p.secret)
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_custom_datetime(token_client, organizer, clist, event, order):
|
||||
dt = now() - datetime.timedelta(days=1)
|
||||
dt = dt.replace(microsecond=0)
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {
|
||||
'datetime': dt.isoformat()
|
||||
})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
with scopes_disabled():
|
||||
assert Checkin.objects.last().datetime == dt
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_name_fallback(token_client, organizer, clist, event, order):
|
||||
order.invoice_address.name_parts = {'_legacy': 'Paul'}
|
||||
order.invoice_address.save()
|
||||
with scopes_disabled():
|
||||
op = order.positions.first()
|
||||
op.attendee_name_cached = None
|
||||
op.attendee_name_parts = {}
|
||||
op.save()
|
||||
resp = _redeem(token_client, organizer, clist, op.secret, {})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
assert resp.data['position']['attendee_name'] == 'Paul'
|
||||
assert resp.data['position']['attendee_name_parts'] == {'_legacy': 'Paul'}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_by_pk_not_allowed(token_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {})
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_by_secret(token_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_by_secret_special_chars(token_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
p.secret = "abc+-/=="
|
||||
p.save()
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_only_once(token_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'error'
|
||||
assert resp.data['reason'] == 'already_redeemed'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_reupload_same_nonce(token_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {'nonce': 'foobar'})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {'nonce': 'foobar'})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_allow_multiple(token_client, organizer, clist, event, order):
|
||||
clist.allow_multiple_entries = True
|
||||
clist.save()
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
with scopes_disabled():
|
||||
assert p.checkins.count() == 2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_allow_multiple_reupload_same_nonce(token_client, organizer, clist, event, order):
|
||||
clist.allow_multiple_entries = True
|
||||
clist.save()
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {'nonce': 'foobar'})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {'nonce': 'foobar'})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
with scopes_disabled():
|
||||
assert p.checkins.count() == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_multiple_different_list(token_client, organizer, clist, clist_all, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {'nonce': 'foobar'})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
resp = _redeem(token_client, organizer, clist_all, p.secret, {'nonce': 'baz'})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_forced_multiple(token_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {'force': True})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_forced_flag_set_if_required(token_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {'force': True})
|
||||
with scopes_disabled():
|
||||
assert not p.checkins.order_by('pk').last().forced
|
||||
assert p.checkins.order_by('pk').last().force_sent
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {'force': True})
|
||||
with scopes_disabled():
|
||||
assert p.checkins.order_by('pk').last().forced
|
||||
assert p.checkins.order_by('pk').last().force_sent
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_require_product(token_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
clist.limit_products.clear()
|
||||
p = order.positions.first()
|
||||
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'error'
|
||||
assert resp.data['reason'] == 'product'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_require_paid(token_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
|
||||
order.status = Order.STATUS_CANCELED
|
||||
order.save()
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'error'
|
||||
assert resp.data['reason'] == 'canceled'
|
||||
|
||||
order.status = Order.STATUS_PENDING
|
||||
order.save()
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'error'
|
||||
assert resp.data['reason'] == 'unpaid'
|
||||
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {'ignore_unpaid': True})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'error'
|
||||
assert resp.data['reason'] == 'unpaid'
|
||||
|
||||
clist.include_pending = True
|
||||
clist.save()
|
||||
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'error'
|
||||
assert resp.data['reason'] == 'unpaid'
|
||||
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {'ignore_unpaid': True})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def question(event, item):
|
||||
q = event.questions.create(question=LazyI18nString('Size'), type='C', required=True, ask_during_checkin=True)
|
||||
a1 = q.options.create(answer=LazyI18nString("M"))
|
||||
a2 = q.options.create(answer=LazyI18nString("L"))
|
||||
q.items.add(item)
|
||||
return q, a1, a2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_question_number(token_client, organizer, clist, event, order, question):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
question[0].options.all().delete()
|
||||
question[0].type = 'N'
|
||||
question[0].save()
|
||||
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'incomplete'
|
||||
with scopes_disabled():
|
||||
assert resp.data['questions'] == [QuestionSerializer(question[0]).data]
|
||||
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {'answers': {question[0].pk: "3.24"}})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
with scopes_disabled():
|
||||
assert order.positions.first().answers.get(question=question[0]).answer == '3.24'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_question_choice(token_client, organizer, clist, event, order, question):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'incomplete'
|
||||
with scopes_disabled():
|
||||
assert resp.data['questions'] == [QuestionSerializer(question[0]).data]
|
||||
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {'answers': {question[0].pk: str(question[1].pk)}})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
with scopes_disabled():
|
||||
assert order.positions.first().answers.get(question=question[0]).answer == 'M'
|
||||
assert list(order.positions.first().answers.get(question=question[0]).options.all()) == [question[1]]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_question_choice_identifier(token_client, organizer, clist, event, order, question):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'incomplete'
|
||||
with scopes_disabled():
|
||||
assert resp.data['questions'] == [QuestionSerializer(question[0]).data]
|
||||
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {'answers': {question[0].pk: str(question[1].identifier)}})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
with scopes_disabled():
|
||||
assert order.positions.first().answers.get(question=question[0]).answer == 'M'
|
||||
assert list(order.positions.first().answers.get(question=question[0]).options.all()) == [question[1]]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_question_invalid(token_client, organizer, clist, event, order, question):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {'answers': {question[0].pk: "A"}})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'incomplete'
|
||||
with scopes_disabled():
|
||||
assert resp.data['questions'] == [QuestionSerializer(question[0]).data]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_question_required(token_client, organizer, clist, event, order, question):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
question[0].required = True
|
||||
question[0].save()
|
||||
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'incomplete'
|
||||
with scopes_disabled():
|
||||
assert resp.data['questions'] == [QuestionSerializer(question[0]).data]
|
||||
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {'answers': {question[0].pk: ""}})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'incomplete'
|
||||
with scopes_disabled():
|
||||
assert resp.data['questions'] == [QuestionSerializer(question[0]).data]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_question_optional(token_client, organizer, clist, event, order, question):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
question[0].required = False
|
||||
question[0].save()
|
||||
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {'answers': {}})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'incomplete'
|
||||
with scopes_disabled():
|
||||
assert resp.data['questions'] == [QuestionSerializer(question[0]).data]
|
||||
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {'answers': {question[0].pk: ""}})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_question_multiple_choice(token_client, organizer, clist, event, order, question):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
question[0].type = 'M'
|
||||
question[0].save()
|
||||
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'incomplete'
|
||||
with scopes_disabled():
|
||||
assert resp.data['questions'] == [QuestionSerializer(question[0]).data]
|
||||
|
||||
resp = _redeem(token_client, organizer, clist, p.secret,
|
||||
{'answers': {question[0].pk: "{},{}".format(question[1].pk, question[2].pk)}})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
with scopes_disabled():
|
||||
assert order.positions.first().answers.get(question=question[0]).answer == 'M, L'
|
||||
assert set(order.positions.first().answers.get(question=question[0]).options.all()) == {question[1],
|
||||
question[2]}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_question_upload(token_client, organizer, clist, event, order, question):
|
||||
r = token_client.post(
|
||||
'/api/v1/upload',
|
||||
data={
|
||||
'media_type': 'image/png',
|
||||
'file': ContentFile('file.png', 'invalid png content')
|
||||
},
|
||||
format='upload',
|
||||
HTTP_CONTENT_DISPOSITION='attachment; filename="file.png"',
|
||||
)
|
||||
assert r.status_code == 201
|
||||
file_id_png = r.data['id']
|
||||
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
question[0].type = 'F'
|
||||
question[0].save()
|
||||
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'incomplete'
|
||||
with scopes_disabled():
|
||||
assert resp.data['questions'] == [QuestionSerializer(question[0]).data]
|
||||
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {'answers': {question[0].pk: "invalid"}})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'incomplete'
|
||||
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {'answers': {question[0].pk: file_id_png}})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
with scopes_disabled():
|
||||
assert order.positions.first().answers.get(question=question[0]).answer.startswith('file://')
|
||||
assert order.positions.first().answers.get(question=question[0]).file
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_store_failed(token_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/failed_checkins/'.format(
|
||||
organizer.slug, event.slug, clist.pk,
|
||||
), {
|
||||
'raw_barcode': '123456',
|
||||
'error_reason': 'invalid'
|
||||
}, format='json')
|
||||
assert resp.status_code == 201
|
||||
with scopes_disabled():
|
||||
assert Checkin.all.filter(successful=False).exists()
|
||||
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/failed_checkins/'.format(
|
||||
organizer.slug, event.slug, clist.pk,
|
||||
), {
|
||||
'raw_barcode': '123456',
|
||||
'position': p.pk,
|
||||
'error_reason': 'unpaid'
|
||||
}, format='json')
|
||||
assert resp.status_code == 201
|
||||
with scopes_disabled():
|
||||
assert p.all_checkins.filter(successful=False).count() == 1
|
||||
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/failed_checkins/'.format(
|
||||
organizer.slug, event.slug, clist.pk,
|
||||
), {
|
||||
'position': p.pk,
|
||||
'error_reason': 'unpaid'
|
||||
}, format='json')
|
||||
assert resp.status_code == 400
|
||||
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/failed_checkins/'.format(
|
||||
organizer.slug, event.slug, clist.pk,
|
||||
), {
|
||||
'raw_barcode': '123456',
|
||||
'error_reason': 'unknown'
|
||||
}, format='json')
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_unknown(token_client, organizer, clist, event, order):
|
||||
resp = _redeem(token_client, organizer, clist, 'unknown_secret', {'force': True})
|
||||
assert resp.status_code == 404
|
||||
assert resp.data["status"] == "error"
|
||||
assert resp.data["reason"] == "invalid"
|
||||
with scopes_disabled():
|
||||
assert not Checkin.objects.last()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_unknown_revoked(token_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
event.revoked_secrets.create(position=p, secret='revoked_secret')
|
||||
resp = _redeem(token_client, organizer, clist, 'revoked_secret', {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data["status"] == "error"
|
||||
assert resp.data["reason"] == "revoked"
|
||||
with scopes_disabled():
|
||||
assert not Checkin.objects.last()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_unknown_revoked_force(token_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
event.revoked_secrets.create(position=p, secret='revoked_secret')
|
||||
resp = _redeem(token_client, organizer, clist, 'revoked_secret', {'force': True})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data["status"] == "ok"
|
||||
with scopes_disabled():
|
||||
ci = Checkin.objects.last()
|
||||
assert ci.forced
|
||||
assert ci.force_sent
|
||||
assert ci.position == p
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_addon_if_match_disabled(token_client, organizer, clist, other_item, event, order):
|
||||
with scopes_disabled():
|
||||
clist.all_products = False
|
||||
clist.save()
|
||||
clist.limit_products.set([other_item])
|
||||
resp = _redeem(token_client, organizer, clist, 'z3fsn8jyufm5kpk768q69gkbyr5f4h6w', {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data["status"] == "error"
|
||||
assert resp.data["reason"] == "product"
|
||||
with scopes_disabled():
|
||||
assert not Checkin.objects.last()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_addon_if_match_enabled(token_client, organizer, clist, other_item, event, order):
|
||||
with scopes_disabled():
|
||||
clist.all_products = False
|
||||
clist.addon_match = True
|
||||
clist.save()
|
||||
clist.limit_products.set([other_item])
|
||||
p = order.positions.first().addons.all().first()
|
||||
resp = _redeem(token_client, organizer, clist, 'z3fsn8jyufm5kpk768q69gkbyr5f4h6w', {})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
assert resp.data['position']['attendee_name'] == 'Peter' # test propagation of names
|
||||
assert resp.data['position']['item'] == other_item.pk
|
||||
with scopes_disabled():
|
||||
ci = Checkin.objects.last()
|
||||
assert ci.position == p
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_addon_if_match_ambiguous(token_client, organizer, clist, item, other_item, event, order):
|
||||
with scopes_disabled():
|
||||
clist.all_products = False
|
||||
clist.addon_match = True
|
||||
clist.save()
|
||||
clist.limit_products.set([item, other_item])
|
||||
resp = _redeem(token_client, organizer, clist, 'z3fsn8jyufm5kpk768q69gkbyr5f4h6w', {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data["status"] == "error"
|
||||
assert resp.data["reason"] == "ambiguous"
|
||||
with scopes_disabled():
|
||||
assert not Checkin.objects.last()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_addon_if_match_and_revoked_force(token_client, organizer, clist, other_item, event, order):
|
||||
with scopes_disabled():
|
||||
event.revoked_secrets.create(position=order.positions.get(positionid=1), secret='revoked_secret')
|
||||
clist.all_products = False
|
||||
clist.addon_match = True
|
||||
clist.save()
|
||||
clist.limit_products.set([other_item])
|
||||
p = order.positions.first().addons.all().first()
|
||||
resp = _redeem(token_client, organizer, clist, 'revoked_secret', {'force': True})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data["status"] == "ok"
|
||||
with scopes_disabled():
|
||||
ci = Checkin.objects.last()
|
||||
assert ci.forced
|
||||
assert ci.force_sent
|
||||
assert ci.position == p
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_multi_list(token_client, organizer, clist, clist_event2, order, order2):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
p2 = order2.positions.first()
|
||||
resp = _redeem(token_client, organizer, [clist, clist_event2], p.secret)
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['position']['id'] == p.pk
|
||||
assert resp.data['list'] == {'id': clist.pk, 'name': 'Default', 'event': 'dummy', 'subevent': None, 'include_pending': False}
|
||||
resp = _redeem(token_client, organizer, [clist, clist_event2], p2.secret)
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['position']['id'] == p2.pk
|
||||
assert resp.data['list'] == {'id': clist_event2.pk, 'name': 'Event 2', 'event': 'dummy2', 'subevent': None, 'include_pending': False}
|
||||
resp = _redeem(token_client, organizer, [clist], p2.secret)
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_no_list(token_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = _redeem(token_client, organizer, [], p.secret)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == ['No check-in list passed.']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_conflicting_lists(token_client, organizer, clist, clist_all, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = _redeem(token_client, organizer, [clist_all, clist], p.secret)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == ['Selecting two check-in lists from the same event is unsupported.']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_search(token_client, organizer, event, clist, clist_all, item, other_item, order,
|
||||
django_assert_max_num_queries):
|
||||
with scopes_disabled():
|
||||
p1 = dict(TEST_ORDERPOSITION1_RES)
|
||||
p1["id"] = order.positions.get(positionid=1).pk
|
||||
p1["item"] = item.pk
|
||||
|
||||
with django_assert_max_num_queries(17):
|
||||
resp = token_client.get(
|
||||
'/api/v1/organizers/{}/checkinrpc/search/?list={}&search=z3fsn8jyu'.format(organizer.slug, clist_all.pk))
|
||||
assert resp.status_code == 200
|
||||
assert [p1] == resp.data['results']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_search_no_list(token_client, organizer, event, clist, clist_all, item, other_item, order):
|
||||
resp = token_client.get(
|
||||
'/api/v1/organizers/{}/checkinrpc/search/?search=z3fsn8jyu'.format(organizer.slug))
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == ['No check-in list passed.']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_search_conflicting_lists(token_client, organizer, event, clist, clist_all, item, other_item, order):
|
||||
resp = token_client.get(
|
||||
'/api/v1/organizers/{}/checkinrpc/search/?search=z3fsn8jyu&list={}&list={}'.format(organizer.slug, clist.pk, clist_all.pk))
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == ['Selecting two check-in lists from the same event is unsupported.']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_search_multiple_lists(token_client, organizer, clist_all, clist_event2, order2, order):
|
||||
resp = token_client.get(
|
||||
'/api/v1/organizers/{}/checkinrpc/search/?list={}&list={}&search=dummy.test&ordering=attendee_name'.format(
|
||||
organizer.slug, clist_all.pk, clist_event2.pk
|
||||
)
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
with scopes_disabled():
|
||||
assert resp.data['results'][0]['id'] == order2.positions.get(positionid=1).pk
|
||||
assert resp.data['results'][1]['id'] == order.positions.get(positionid=2).pk
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_without_permission(token_client, event, team, organizer, clist_all, order):
|
||||
with scopes_disabled():
|
||||
team.can_view_orders = False
|
||||
team.can_change_orders = False
|
||||
team.can_checkin_orders = False
|
||||
team.save()
|
||||
resp = token_client.get(
|
||||
'/api/v1/organizers/{}/checkinrpc/search/?list={}&search=dummy.test&ordering=attendee_name'.format(organizer.slug, clist_all.pk))
|
||||
assert resp.status_code == 403
|
||||
assert resp.data == {
|
||||
"detail": f"You requested lists that do not exist or that you do not have access to: {clist_all.pk}"
|
||||
}
|
||||
|
||||
resp = _redeem(token_client, organizer, [clist_all], "foobar")
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {
|
||||
"lists": [f'Invalid pk "{clist_all.pk}" - object does not exist.']
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_without_permission_for_one_list(token_client, event, team, organizer, clist_all, clist_event2, order2, order):
|
||||
with scopes_disabled():
|
||||
team.all_events = False
|
||||
team.save()
|
||||
team.limit_events.set([event])
|
||||
resp = token_client.get(
|
||||
'/api/v1/organizers/{}/checkinrpc/search/?list={}&list={}&search=dummy.test&ordering=attendee_name'.format(
|
||||
organizer.slug, clist_all.pk, clist_event2.pk
|
||||
)
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
assert resp.data == {
|
||||
"detail": f"You requested lists that do not exist or that you do not have access to: {clist_event2.pk}"
|
||||
}
|
||||
|
||||
resp = _redeem(token_client, organizer, [clist_all, clist_event2], "foobar")
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {
|
||||
"lists": [f'Invalid pk "{clist_event2.pk}" - object does not exist.']
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_checkin_only_permission(token_client, event, team, organizer, clist_all, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
clist_all.allow_multiple_entries = True
|
||||
clist_all.save()
|
||||
|
||||
# With all permissions, I can submit very short search terms
|
||||
resp = token_client.get(
|
||||
'/api/v1/organizers/{}/checkinrpc/search/?list={}&search=du&ordering=attendee_name'.format(organizer.slug, clist_all.pk))
|
||||
assert resp.data['count'] > 0
|
||||
|
||||
# With all permissions, I can request PDF data during checkin
|
||||
resp = _redeem(token_client, organizer, [clist_all], p.secret, {}, '?pdf_data=true')
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['position'].get('pdf_data')
|
||||
|
||||
with scopes_disabled():
|
||||
team.can_view_orders = False
|
||||
team.can_change_orders = False
|
||||
team.can_checkin_orders = True
|
||||
team.save()
|
||||
|
||||
# With limited permissions, I can not search with a 2-character query
|
||||
resp = token_client.get(
|
||||
'/api/v1/organizers/{}/checkinrpc/search/?list={}&search=du&ordering=attendee_name'.format(organizer.slug, clist_all.pk))
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['count'] == 0
|
||||
|
||||
# With limited permissions, I can search with a 4-character query
|
||||
resp = token_client.get(
|
||||
'/api/v1/organizers/{}/checkinrpc/search/?list={}&search=dummy&ordering=attendee_name'.format(organizer.slug, clist_all.pk))
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['count'] > 0
|
||||
|
||||
# With limited permissions, I can not request PDF data during checkin
|
||||
resp = _redeem(token_client, organizer, [clist_all], p.secret, {}, '?pdf_data=true')
|
||||
assert resp.status_code == 201
|
||||
assert not resp.data['position'].get('pdf_data')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_checkin_no_pdf_data(token_client, event, team, organizer, clist_all, order):
|
||||
resp = token_client.get(
|
||||
'/api/v1/organizers/{}/checkinrpc/search/?list={}&search=dummy&pdf_data=true'.format(organizer.slug, clist_all.pk))
|
||||
assert not resp.data['results'][0].get('pdf_data')
|
||||
@@ -20,6 +20,7 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import pytest
|
||||
from django.core import mail as djmail
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
|
||||
@@ -98,6 +99,29 @@ def test_customer_create(token_client, organizer):
|
||||
assert customer.is_active
|
||||
assert customer.name == 'John Doe'
|
||||
assert customer.is_verified
|
||||
assert len(djmail.outbox) == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_customer_create_send_email(token_client, organizer):
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/customers/'.format(organizer.slug),
|
||||
format='json',
|
||||
data={
|
||||
'identifier': 'IGNORED',
|
||||
'email': 'bar@example.com',
|
||||
'name_parts': {
|
||||
"_scheme": "given_family",
|
||||
'given_name': 'John',
|
||||
'family_name': 'Doe',
|
||||
},
|
||||
'is_active': True,
|
||||
'is_verified': True,
|
||||
'send_email': True,
|
||||
}
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
assert len(djmail.outbox) == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
||||
@@ -211,6 +211,7 @@ def test_order_create(token_client, organizer, event, item, quota, question):
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
assert not resp.data['positions'][0].get('pdf_data')
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(code=resp.data['code'])
|
||||
assert o.customer == customer
|
||||
@@ -2549,3 +2550,20 @@ def test_order_create_voucher_block_quota(token_client, organizer, event, item,
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_pdf_data(token_client, organizer, event, item, quota, question):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
with scopes_disabled():
|
||||
customer = organizer.customers.create()
|
||||
res['customer'] = customer.identifier
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/?pdf_data=true'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
assert 'secret' in resp.data['positions'][0]['pdf_data']
|
||||
|
||||
@@ -1654,3 +1654,58 @@ def test_revoked_secret_list(token_client, organizer, event):
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [res] == resp.data['results']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_pdf_data(token_client, organizer, event, order, django_assert_max_num_queries):
|
||||
# order detail
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/{}/?pdf_data=true'.format(
|
||||
organizer.slug, event.slug, order.code
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['positions'][0].get('pdf_data')
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/{}/'.format(
|
||||
organizer.slug, event.slug, order.code
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert not resp.data['positions'][0].get('pdf_data')
|
||||
|
||||
# order list
|
||||
with django_assert_max_num_queries(29):
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?pdf_data=true'.format(
|
||||
organizer.slug, event.slug
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['results'][0]['positions'][0].get('pdf_data')
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert not resp.data['results'][0]['positions'][0].get('pdf_data')
|
||||
|
||||
# position list
|
||||
with django_assert_max_num_queries(32):
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/orderpositions/?pdf_data=true'.format(
|
||||
organizer.slug, event.slug
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['results'][0].get('pdf_data')
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/orderpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert not resp.data['results'][0].get('pdf_data')
|
||||
|
||||
posid = resp.data['results'][0]['id']
|
||||
|
||||
# position detail
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/orderpositions/{}/?pdf_data=true'.format(
|
||||
organizer.slug, event.slug, posid
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert resp.data.get('pdf_data')
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/orderpositions/{}/'.format(
|
||||
organizer.slug, event.slug, posid
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert not resp.data.get('pdf_data')
|
||||
|
||||
@@ -113,7 +113,6 @@ def test_team_update(token_client, organizer, event, second_team):
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
print(resp.data)
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
|
||||
@@ -648,7 +648,6 @@ def test_order_reactivate(client, env):
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
response = client.post('/control/event/dummy/dummy/orders/FOO/reactivate', {
|
||||
}, follow=True)
|
||||
print(response.content.decode())
|
||||
assert 'alert-success' in response.content.decode()
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(id=env[2].id)
|
||||
@@ -1079,7 +1078,6 @@ def test_order_mark_paid_forced(client, env):
|
||||
'amount': str(o.pending_sum),
|
||||
'force': 'on'
|
||||
}, follow=True)
|
||||
print(response.content.decode())
|
||||
assert 'alert-success' in response.content.decode()
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(id=env[2].id)
|
||||
@@ -1366,7 +1364,7 @@ class OrderChangeTests(SoupTest):
|
||||
date_end=self.event.date_from + timedelta(days=1),
|
||||
attendee_name_parts={'_scheme': 'full', 'full_name': 'John Doe'},
|
||||
)
|
||||
r = self.client.post('/control/event/{}/{}/orders/{}/change'.format(
|
||||
self.client.post('/control/event/{}/{}/orders/{}/change'.format(
|
||||
self.event.organizer.slug, self.event.slug, self.order.code
|
||||
), {
|
||||
'add-TOTAL_FORMS': '0',
|
||||
@@ -1377,7 +1375,6 @@ class OrderChangeTests(SoupTest):
|
||||
'op-{}-used_membership'.format(self.op2.pk): str(m_correct1.pk),
|
||||
'op-{}-used_membership'.format(self.op3.pk): str(m_correct1.pk),
|
||||
}, follow=True)
|
||||
print(r.content)
|
||||
self.op1.refresh_from_db()
|
||||
self.order.refresh_from_db()
|
||||
assert self.op1.used_membership == m_correct1
|
||||
|
||||
@@ -63,7 +63,6 @@ def compare_ignoring_order(data1, data2):
|
||||
try:
|
||||
assert set(data1) == set(data2)
|
||||
except:
|
||||
print(data1, data2)
|
||||
assert len(data1) == len(data2) and all(data1.count(i) == data2.count(i) for i in data1)
|
||||
elif isinstance(data1, dict) and isinstance(data2, dict):
|
||||
assert set(data1.keys()) == set(data2.keys())
|
||||
|
||||
@@ -137,5 +137,4 @@ def test_payment(env, monkeypatch):
|
||||
'payment_paypal_wallet_oid': '04F89033701558004',
|
||||
'payment_paypal_wallet_payer': 'Q739JNKWH67HE',
|
||||
})
|
||||
print(response.content.decode())
|
||||
assert response['Location'] == '/ccc/30c3/checkout/confirm/'
|
||||
|
||||
@@ -440,7 +440,6 @@ def test_sendmail_attendee_checkin_filter(logged_in_client, sendmail_url, event,
|
||||
},
|
||||
follow=True)
|
||||
assert response.status_code == 200
|
||||
print(response.rendered_content)
|
||||
assert 'alert-success' in response.rendered_content
|
||||
assert len(djmail.outbox) == 1
|
||||
assert djmail.outbox[0].to == ['attendee1@dummy.test']
|
||||
|
||||
@@ -328,7 +328,6 @@ class ItemDisplayTest(EventTestMixin, SoupTest):
|
||||
self.event.subevents.create(name='Foo SE2', date_from=now() + datetime.timedelta(days=12),
|
||||
active=True)
|
||||
resp = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug))
|
||||
print(resp.rendered_content)
|
||||
self.assertIn("Foo SE2", resp.rendered_content)
|
||||
self.assertNotIn("Foo SE1", resp.rendered_content)
|
||||
resp = self.client.get('/%s/%s/?date=%d-W%d' % (self.orga.slug, self.event.slug, se1.date_from.isocalendar()[0], se1.date_from.isocalendar()[1]))
|
||||
|
||||
@@ -290,6 +290,50 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
data = json.loads(response.content.decode())
|
||||
assert len(data['items_by_category']) == 0
|
||||
|
||||
def test_product_list_view_variation_filter(self):
|
||||
response = self.client.get('/%s/%s/widget/product_list?variations=%s' % (self.orga.slug, self.event.slug,
|
||||
self.shirt_red.pk))
|
||||
assert response['Access-Control-Allow-Origin'] == '*'
|
||||
data = json.loads(response.content.decode())
|
||||
assert data['items_by_category'] == [
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"require_voucher": False,
|
||||
"order_min": None,
|
||||
"max_price": "14.00",
|
||||
"price": None,
|
||||
"picture": None,
|
||||
"has_variations": 4,
|
||||
"allow_waitinglist": True,
|
||||
"description": None,
|
||||
"min_price": "12.00",
|
||||
"avail": None,
|
||||
"variations": [
|
||||
{
|
||||
"value": "Red",
|
||||
"id": self.shirt_red.pk,
|
||||
'original_price': None,
|
||||
"price": {"gross": "14.00", "net": "11.76", "tax": "2.24", "name": "",
|
||||
"rate": "19.00", "includes_mixed_tax_rate": False},
|
||||
"description": None,
|
||||
"avail": [100, None],
|
||||
"order_max": 2
|
||||
}
|
||||
],
|
||||
"id": self.shirt.pk,
|
||||
"free_price": False,
|
||||
"original_price": None,
|
||||
"name": "T-Shirt",
|
||||
"order_max": None
|
||||
}
|
||||
],
|
||||
"description": None,
|
||||
"id": self.category.pk,
|
||||
"name": "Everything"
|
||||
}
|
||||
]
|
||||
|
||||
def test_product_list_view_with_voucher(self):
|
||||
with scopes_disabled():
|
||||
self.event.vouchers.create(item=self.ticket, code="ABCDE")
|
||||
|
||||
Reference in New Issue
Block a user