Compare commits

...
Author SHA1 Message Date
Lukas Bockstallerandpajowu b05de915bd Update src/pretix/control/templates/pretixcontrol/giftcards/payment.html
Co-authored-by: pajowu <engelhardt@pretix.eu>
2026-03-13 13:58:54 +01:00
Lukas Bockstallerandpajowu 384c83fffb Apply suggestion from @pajowu
Co-authored-by: pajowu <engelhardt@pretix.eu>
2026-03-13 13:58:07 +01:00
Lukas Bockstaller da97be5194 handle unknown gift cards 2026-03-10 18:16:19 +01:00
Lukas Bockstaller a21e67a9ba make payment_provider and use_gift_cards exclusive 2026-03-10 17:34:50 +01:00
Lukas Bockstaller 353a08e094 reject duplicate gift card secrets 2026-03-10 16:59:51 +01:00
Lukas Bockstaller 0c05b532bf documentation lectoring 2026-03-10 10:59:36 +01:00
Lukas Bockstaller 9b4495c491 provide more context for failed transactions 2026-03-10 10:48:30 +01:00
Lukas Bockstaller 46d18dd489 docs 2026-03-10 10:21:42 +01:00
Lukas Bockstaller b5cf122a2a let create_transactions() handle all the mailing 2026-03-09 18:06:04 +01:00
Lukas Bockstaller 4602db2bff styling 2026-03-09 17:24:44 +01:00
Lukas Bockstaller ba8dbad733 implement giftcard payment via order create 2026-03-06 16:59:56 +01:00
Lukas Bockstaller 3b76ae48fd adds safeguard to prevent empty giftcard transactions on giftcards of value 0.00 2026-03-06 16:56:15 +01:00
Lukas BockstallerandRichard Schreiber c07ba31307 API: add organizer-level orderpositions endpoint (#5848)
* initial implementation

* handle permissions

* split out organizer list endpoint

* remove left over empty lines

* revert import changes

* tidying up

* revert no longer needed test changes

* revert no longer needed test changes

* Apply suggestions from code review

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>

* add event to api response

* prefetch

* handle auth

* document event

* bump querycounts for prefetches

* Use existing Permission Denied Error Message

---------

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
2026-03-06 11:55:38 +01:00
Ruud Hendrickx 87b3e0c417 Translations: Update Dutch (Belgium)
Currently translated at 71.0% (4446 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/nl_BE/

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud Hendrickx d3fd031639 Translations: Update Dutch (Belgium)
Currently translated at 69.6% (4355 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/nl_BE/

powered by weblate
2026-03-05 07:34:01 +01:00
Renne Rocha 9253327334 Translations: Update Portuguese (Brazil)
Currently translated at 92.9% (5813 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/pt_BR/

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud Hendrickx 080b9cacaf Translations: Update Dutch (Belgium)
Currently translated at 63.6% (3982 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/nl_BE/

powered by weblate
2026-03-05 07:34:01 +01:00
CVZ-es 9c2cc02df1 Translations: Update Spanish
Currently translated at 100.0% (6257 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/es/

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud Hendrickx fceae0a2fe Translations: Update Dutch
Currently translated at 100.0% (6257 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/nl/

powered by weblate
2026-03-05 07:34:01 +01:00
CVZ-es 9fc3fdf751 Translations: Update French
Currently translated at 100.0% (6257 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/fr/

powered by weblate
2026-03-05 07:34:01 +01:00
André Almeida 04f79b7014 Translations: Update Portuguese (Brazil)
Currently translated at 92.8% (5811 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/pt_BR/

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud Hendrickx 9d0b9387e6 Translations: Update Dutch (Belgium)
Currently translated at 57.2% (3581 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/nl_BE/

powered by weblate
2026-03-05 07:34:01 +01:00
Sandra Rial Pérez b25e6f598d Translations: Update Galician
Currently translated at 100.0% (256 of 256 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/gl/

powered by weblate
2026-03-05 07:34:01 +01:00
Sandra Rial Pérez e8e2648f7e Translations: Update Galician
Currently translated at 17.5% (1095 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/gl/

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud Hendrickx e0fac42225 Translations: Update Dutch (Belgium)
Currently translated at 53.1% (3326 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/nl_BE/

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud Hendrickx 3e9bc7675b Translations: Update Dutch (Belgium)
Currently translated at 50.7% (3176 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/nl_BE/

powered by weblate
2026-03-05 07:34:01 +01:00
Hijiri Umemoto 1541033467 Translations: Update Japanese
Currently translated at 100.0% (6257 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/ja/

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud Hendrickx 6b8c3ef15c Translations: Update Dutch (informal) (nl_Informal)
Currently translated at 100.0% (6257 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/nl_Informal/

powered by weblate
2026-03-05 07:34:01 +01:00
Alberto Ortega 135e07c183 Translations: Update Spanish
Currently translated at 99.9% (6256 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/es/

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud Hendrickx fe97915b36 Translations: Update Dutch
Currently translated at 100.0% (6257 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/nl/

powered by weblate
2026-03-05 07:34:01 +01:00
Hijiri Umemoto 233281cea4 Translations: Update Japanese
Currently translated at 99.9% (6255 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/ja/

powered by weblate
2026-03-05 07:34:01 +01:00
Renne Rocha 0300a44634 Translations: Update Portuguese (Brazil)
Currently translated at 92.6% (5797 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/pt_BR/

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud Hendrickx 449d930565 Translations: Update Dutch (Belgium)
Currently translated at 46.7% (2927 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/nl_BE/

powered by weblate
2026-03-05 07:34:01 +01:00
Sandra Rial Pérez 49f49bd8a6 Translations: Update Galician
Currently translated at 16.7% (1048 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/gl/

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud Hendrickx e896704fe0 Translations: Update Dutch (Belgium)
Currently translated at 42.9% (2689 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/nl_BE/

powered by weblate
2026-03-05 07:34:01 +01:00
Sandra Rial Pérez cfee402a27 Translations: Update Galician
Currently translated at 16.3% (1026 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/gl/

powered by weblate
2026-03-05 07:34:01 +01:00
David Ibáñez Cerdeira f8878e53a3 Translations: Update Galician
Currently translated at 16.3% (1026 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/gl/

powered by weblate
2026-03-05 07:34:01 +01:00
Renne Rocha fd6a342bc6 Translations: Update Portuguese (Brazil)
Currently translated at 92.6% (5797 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/pt_BR/

powered by weblate
2026-03-05 07:34:01 +01:00
Pedro Orlando 865433276e Translations: Update Portuguese (Brazil)
Currently translated at 92.6% (5797 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/pt_BR/

powered by weblate
2026-03-05 07:34:01 +01:00
André Almeida f616f64f47 Translations: Update Portuguese (Brazil)
Currently translated at 92.6% (5797 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/pt_BR/

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud Hendrickx 26550887b7 Translations: Update Dutch (Belgium)
Currently translated at 30.7% (1924 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/nl_BE/

powered by weblate
2026-03-05 07:34:01 +01:00
David Ibáñez Cerdeira 0f3de911b8 Translations: Update Galician
Currently translated at 100.0% (256 of 256 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/gl/

powered by weblate
2026-03-05 07:34:01 +01:00
David Ibáñez Cerdeira b648390dbf Translations: Update Galician
Currently translated at 15.7% (986 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/gl/

powered by weblate
2026-03-05 07:34:01 +01:00
David Ibáñez Cerdeira 50fec0b31c Translations: Update Greek
Currently translated at 43.8% (2743 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud Hendrickx e44af04e43 Translations: Update Dutch (informal) (nl_Informal)
Currently translated at 100.0% (6257 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/nl_Informal/

powered by weblate
2026-03-05 07:34:01 +01:00
André Almeida 276c3177f5 Translations: Update Portuguese (Brazil)
Currently translated at 89.7% (5616 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/pt_BR/

powered by weblate
2026-03-05 07:34:01 +01:00
Pedro Orlando 27ac004a0b Translations: Update Portuguese (Brazil)
Currently translated at 89.7% (5616 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/pt_BR/

powered by weblate
2026-03-05 07:34:01 +01:00
André Almeida 6d517d4e8d Translations: Update Portuguese (Brazil)
Currently translated at 89.7% (5616 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/pt_BR/

powered by weblate
2026-03-05 07:34:01 +01:00
Ruud Hendrickx d9c3deda8a Translations: Update Dutch
Currently translated at 100.0% (6257 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/nl/

powered by weblate
2026-03-05 07:34:01 +01:00
CVZ-es fe6add618a Translations: Update Spanish
Currently translated at 100.0% (6257 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/es/

powered by weblate
2026-03-05 07:34:01 +01:00
CVZ-es 3615a52cc4 Translations: Update French
Currently translated at 100.0% (6257 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/fr/

powered by weblate
2026-03-05 07:34:01 +01:00
Kara Engelhardt e3ae3b08bd Handle PlainHtmlAlternativeString in placeholder help text 2026-03-04 18:57:25 +02:00
Richard SchreiberandRaphael Michel 959e926a67 API: validate payment_info (#5944)
* API: validate payment_info

* improve dict-check

* Apply suggestions from code review

Co-authored-by: Raphael Michel <michel@pretix.eu>

---------

Co-authored-by: Raphael Michel <michel@pretix.eu>
2026-03-02 12:28:47 +01:00
Raphael Michel 876ddf1321 Add a log entry on manual VAT ID validation (Z#23223874) (#5939) 2026-02-27 15:22:50 +01:00
Richard Schreiber 005b1d54d3 add missing licenseheaders 2026-02-27 09:09:27 +01:00
AnanyaandRichard Schreiber 2066471086 Fix #1907 – Obfuscate contact email addresses in public HTML (#5477)
* Include nix development enviornment

* Obfuscate contact email addresses in shop HTML and deanonymize via JavaScript

This change addresses #1907: "hide contact e-mail address in source code
of a shop".

- Contact email addresses rendered in public-facing templates are now
obfuscated in the HTML source (e.g., replacing "@" with "[at]" and "."
with "[dot]").
- A new JavaScript file is included in the relevant templates to
automatically rewrite and restore the email address for users after the
page loads.
- This approach helps protect email addresses from basic harvesting bots
and reduces spam, while keeping them accessible and user-friendly for
human visitors.
- The obfuscation and deanonymization logic is only applied to web
templates, not to emails sent via pretix.

This implementation follows the recommendations discussed in #1907,
using a standardized, maintainable approach that’s compatible with
pretix's asset pipeline and template structure.

* Undo nix development environment for merge into main

* convert complete mailto-link to HTML entities

* remove gitignore noise

* Update .gitignore

* fix gitignore noise

* Update .gitignore

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2026-02-27 08:50:33 +01:00
Richard Schreiber a25bca7471 Fix static instance name in emails (Z#23224360) (#5914) 2026-02-25 13:19:53 +01:00
luelista da43984ad2 Add datasync logging (Z#23225588) (#5928)
* Fix inconsistent log messages

* Add logging for successfully synced orders

(debugging orders that might get silently skipped)
2026-02-25 09:49:52 +01:00
Martin Gross 7cce1c9219 PPv2: Handle paypal-payments/oders in 'created' status (Z#23225625) (#5929) 2026-02-25 09:21:58 +01:00
Martin Gross cb9c4466f9 Revert "PPv2: Do not put payments in pending-state if no capture has occured yet."
This reverts commit e5c8f19984.
2026-02-24 16:55:57 +01:00
Martin Gross 3398cda74b PPv2: properly check for pending-payments in pending-renderer 2026-02-24 16:16:22 +01:00
Martin Gross e5c8f19984 PPv2: Do not put payments in pending-state if no capture has occured yet. 2026-02-24 16:07:16 +01:00
Raphael Michel 5027f6dd59 Bump version to 2026.3.0.dev0 2026-02-24 13:37:15 +01:00
Raphael Michel 787db18d72 Bump version to 2026.2.0 2026-02-24 13:37:09 +01:00
Raphael Michel aadce7be00 Remove print statement from debugging (Z#23225586)
This was reported as a security issue, but we see no security impact or
exploitation path, as the security of PKCE relies on keeping the
verifier secret, not the challenge.
2026-02-24 13:36:52 +01:00
Raphael Michel 26f296bc11 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (6257 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de_Informal/

powered by weblate
2026-02-24 13:10:57 +01:00
Raphael Michel 6ae80cdd4b Translations: Update German
Currently translated at 100.0% (6257 of 6257 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de/

powered by weblate
2026-02-24 13:10:57 +01:00
Raphael Michel cb3956c994 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@pretix.eu>
2026-02-24 12:50:51 +01:00
Hijiri Umemoto b9f350bf3a Translations: Update Japanese
Currently translated at 100.0% (6247 of 6247 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/ja/

powered by weblate
2026-02-24 12:50:18 +01:00
Raphael Michel ab447bb85f Fix HTML injection in error message (Z#23225396) (#5921)
We're not treating it as a security issue as there is no vector to
inject the HTML into other people's browser, only one's own.
2026-02-24 12:48:43 +01:00
Raphael Michel bf33a42ae8 Validate request_id_header not to be misunderstood (Z#23225356) (#5920) 2026-02-24 12:48:25 +01:00
Lukas Bockstaller 081f975ff9 add missing slug fields (#5925) 2026-02-24 10:39:03 +01:00
89 changed files with 22098 additions and 16559 deletions
+64 -2
View File
@@ -117,6 +117,8 @@ cancellation_date datetime Time of order c
reliable for orders that have been cancelled,
reactivated and cancelled again.
plugin_data object Additional data added by plugins.
use_gift_cards list of strings List of unique gift card secrets that are used to pay
for this order.
===================================== ========================== =======================================================
@@ -156,6 +158,10 @@ plugin_data object Additional data
The ``tax_rounding_mode`` attribute has been added.
.. versionchanged:: 2026.03
The ``use_gift_cards `` attribute has been added.
.. _order-position-resource:
Order position resource
@@ -987,8 +993,6 @@ Creating orders
* does not support file upload questions
* does not support redeeming gift cards
* does not support or validate memberships
@@ -1095,6 +1099,14 @@ Creating orders
whether these emails are enabled for certain sales channels. If set to ``null``, behavior will be controlled by pretix'
settings based on the sales channels (added in pretix 4.7). Defaults to ``false``.
Used to be ``send_mail`` before pretix 3.14.
* ``use_gift_cards`` (optional) The provided gift cards will be used to pay for this order. They will be debited and
all the necessary payment records for these transactions will be created. The gift cards will be used in sequence to
pay for the order. Processing of the gift cards stops as soon as the order is payed for. All gift card transactions
are listed under ``payments`` in the response.
This option can only be used with orders that are in the pending state.
The ``use_gift_cards`` attribute can not be combined with ``payment_info`` and ``payment_provider`` fields. If the
order isn't completely paid after its creation with ``use_gift_cards``, then a subsequent request to the payment
endpoint is needed.
If you want to use add-on products, you need to set the ``positionid`` fields of all positions manually
to incrementing integers starting with ``1``. Then, you can reference one of these
@@ -1719,6 +1731,56 @@ List of all order positions
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/orderpositions/
Returns a list of all order positions within all events of a given organizer (with sufficient access permissions).
The supported query parameters and output format of this endpoint are almost identical to those of the list endpoint
within an event.
The only changes are that responses also contain the ``event`` attribute in each result and that the 'pdf_data'
parameter is not supported.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/orderpositions/ 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
X-Page-Generated: 2017-12-01T10:00:00Z
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id:": 23442
"event": "sampleconf",
"order": "ABC12",
"positionid": 1,
"canceled": false,
"item": 1345,
...
}
]
}
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
Fetching individual positions
-----------------------------
+1 -1
View File
@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "2026.2.0.dev0"
__version__ = "2026.3.0.dev0"
+72 -2
View File
@@ -19,6 +19,7 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import json
import logging
import os
from collections import Counter, defaultdict
@@ -52,7 +53,7 @@ from pretix.base.decimal import round_decimal
from pretix.base.i18n import language
from pretix.base.invoicing.transmission import get_transmission_types
from pretix.base.models import (
CachedFile, Checkin, Customer, Device, Invoice, InvoiceAddress,
CachedFile, Checkin, Customer, Device, GiftCard, Invoice, InvoiceAddress,
InvoiceLine, Item, ItemVariation, Order, OrderPosition, Question,
QuestionAnswer, ReusableMedium, SalesChannel, Seat, SubEvent, TaxRule,
Voucher,
@@ -61,6 +62,7 @@ from pretix.base.models.orders import (
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
PrintLog, RevokedTicketSecret, Transaction,
)
from pretix.base.payment import GiftCardPayment, PaymentException
from pretix.base.pdf import get_images, get_variables
from pretix.base.services.cart import error_messages
from pretix.base.services.locking import LOCK_TRUST_WINDOW, lock_objects
@@ -636,6 +638,14 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
return entry
class OrganizerOrderPositionSerializer(OrderPositionSerializer):
event = SlugRelatedField(slug_field='slug', read_only=True)
class Meta(OrderPositionSerializer.Meta):
fields = OrderPositionSerializer.Meta.fields + ('event',)
read_only_fields = OrderPositionSerializer.Meta.read_only_fields + ('event',)
class RequireAttentionField(serializers.Field):
def to_representation(self, instance: OrderPosition):
return instance.require_checkin_attention
@@ -1191,6 +1201,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
)
tax_rounding_mode = serializers.ChoiceField(choices=ROUNDING_MODES, allow_null=True, required=False,)
locale = serializers.ChoiceField(choices=[], required=False, allow_null=True)
use_gift_cards = serializers.ListField(child=serializers.CharField(required=False), required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -1206,7 +1217,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date',
'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at',
'require_approval', 'valid_if_pending', 'expires', 'api_meta', 'tax_rounding_mode')
'require_approval', 'valid_if_pending', 'expires', 'api_meta', 'tax_rounding_mode', 'use_gift_cards')
def validate_payment_provider(self, pp):
if pp is None:
@@ -1215,6 +1226,18 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
raise ValidationError('The given payment provider is not known.')
return pp
def validate_payment_info(self, info):
if info:
try:
obj = json.loads(info)
except ValueError:
raise ValidationError('payment_info must be valid JSON.')
if not isinstance(obj, dict):
# only objects are allowed
raise ValidationError('payment_info must be a JSON object.')
return info
def validate_expires(self, expires):
if expires < now():
raise ValidationError('Expiration date must be in the future.')
@@ -1289,6 +1312,14 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
payment_date = validated_data.pop('payment_date', now())
force = validated_data.pop('force', False)
simulate = validated_data.pop('simulate', False)
gift_card_secrets = validated_data.pop('use_gift_cards') if 'use_gift_cards' in validated_data else []
if (payment_provider is not None or payment_info != '{}') and len(gift_card_secrets) > 0:
raise ValidationError({"use_gift_cards": ['The attribute use_gift_cards is not compatible with payment_provider or payment_info']})
if validated_data.get('status') != Order.STATUS_PENDING and len(gift_card_secrets) > 0:
raise ValidationError({"use_gift_cards": ['The attribute use_gift_cards is only supported for orders that are created as pending']})
if len(set(gift_card_secrets)) != len(gift_card_secrets):
raise ValidationError({"use_gift_cards": ['Multiple copies of the same gift card secret are not allowed']})
if not validated_data.get("sales_channel"):
validated_data["sales_channel"] = self.context['event'].organizer.sales_channels.get(identifier="web")
@@ -1773,6 +1804,45 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
if order.total != Decimal('0.00') and order.event.currency == "XXX":
raise ValidationError('Paid products not supported without a valid currency.')
for gift_card_secret in gift_card_secrets:
try:
if order.status != Order.STATUS_PAID:
gift_card_payment_provider = GiftCardPayment(event=order.event)
gc = order.event.organizer.accepted_gift_cards.get(
secret=gift_card_secret
)
payment = order.payments.create(
amount=min(order.pending_sum, gc.value),
provider=gift_card_payment_provider.identifier,
info_data={
'gift_card': gc.pk,
'gift_card_secret': gc.secret,
'retry': True
},
state=OrderPayment.PAYMENT_STATE_CREATED
)
gift_card_payment_provider.execute_payment(request=None, payment=payment, is_early_special_case=True)
if order.pending_sum <= Decimal('0.00'):
order.status = Order.STATUS_PAID
except PaymentException:
pass
except GiftCard.DoesNotExist as e:
payment = order.payments.create(
amount=order.pending_sum,
provider=GiftCardPayment.identifier,
info_data={
'gift_card_secret': gift_card_secret,
},
state=OrderPayment.PAYMENT_STATE_CREATED
)
payment.fail(info={**payment.info_data, 'error': str(e)},
send_mail=False)
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID and not validated_data.get('require_approval'):
order.status = Order.STATUS_PAID
order.save()
+2 -1
View File
@@ -365,9 +365,10 @@ class TeamInviteSerializer(serializers.ModelSerializer):
def _send_invite(self, instance):
mail(
instance.email,
_('pretix account invitation'),
_('Account invitation'),
'pretixcontrol/email/invitation.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': self,
'organizer': self.context['organizer'].name,
'team': instance.team.name,
+2 -1
View File
@@ -67,6 +67,7 @@ orga_router.register(r'invoices', order.InvoiceViewSet)
orga_router.register(r'scheduled_exports', exporters.ScheduledOrganizerExportViewSet)
orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters')
orga_router.register(r'transactions', order.OrganizerTransactionViewSet)
orga_router.register(r'orderpositions', order.OrganizerOrderPositionViewSet, basename='orderpositions')
team_router = routers.DefaultRouter()
team_router.register(r'members', organizer.TeamMemberViewSet)
@@ -83,7 +84,7 @@ event_router.register(r'discounts', discount.DiscountViewSet)
event_router.register(r'quotas', item.QuotaViewSet)
event_router.register(r'vouchers', voucher.VoucherViewSet)
event_router.register(r'orders', order.EventOrderViewSet)
event_router.register(r'orderpositions', order.OrderPositionViewSet)
event_router.register(r'orderpositions', order.EventOrderPositionViewSet)
event_router.register(r'transactions', order.TransactionViewSet)
event_router.register(r'invoices', order.InvoiceViewSet)
event_router.register(r'revokedsecrets', order.RevokedSecretViewSet, basename='revokedsecrets')
+49 -12
View File
@@ -57,9 +57,10 @@ from pretix.api.serializers.order import (
BlockedTicketSecretSerializer, InvoiceSerializer, OrderCreateSerializer,
OrderPaymentCreateSerializer, OrderPaymentSerializer,
OrderPositionSerializer, OrderRefundCreateSerializer,
OrderRefundSerializer, OrderSerializer, OrganizerTransactionSerializer,
PriceCalcSerializer, PrintLogSerializer, RevokedTicketSecretSerializer,
SimulatedOrderSerializer, TransactionSerializer,
OrderRefundSerializer, OrderSerializer, OrganizerOrderPositionSerializer,
OrganizerTransactionSerializer, PriceCalcSerializer, PrintLogSerializer,
RevokedTicketSecretSerializer, SimulatedOrderSerializer,
TransactionSerializer,
)
from pretix.api.serializers.orderchange import (
BlockNameSerializer, OrderChangeOperationSerializer,
@@ -1065,8 +1066,7 @@ with scopes_disabled():
}
class OrderPositionViewSet(viewsets.ModelViewSet):
serializer_class = OrderPositionSerializer
class OrderPositionViewSetMixin:
queryset = OrderPosition.all.none()
filter_backends = (DjangoFilterBackend, RichOrderingFilter)
ordering = ('order__datetime', 'positionid')
@@ -1087,8 +1087,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').lower() == 'true'
ctx['pdf_data'] = False
ctx['check_quotas'] = self.request.query_params.get('check_quotas', 'true').lower() == 'true'
return ctx
@@ -1097,9 +1096,8 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
qs = OrderPosition.all
else:
qs = OrderPosition.objects
qs = qs.filter(order__event=self.request.event)
if self.request.query_params.get('pdf_data', 'false').lower() == 'true':
qs = qs.filter(order__event__organizer=self.request.organizer)
if self.request.query_params.get('pdf_data', 'false').lower() == 'true' and getattr(self.request, 'event', None):
prefetch_related_objects([self.request.organizer], 'meta_properties')
prefetch_related_objects(
[self.request.event],
@@ -1154,9 +1152,9 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
qs = qs.prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'answers', 'answers__options', 'answers__question',
'answers', 'answers__options', 'answers__question', 'order__event', 'order__event__organizer'
).select_related(
'item', 'order', 'order__event', 'order__event__organizer', 'seat'
'item', 'order', 'seat'
)
return qs
@@ -1168,6 +1166,45 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
return prov
raise NotFound('Unknown output provider.')
class OrganizerOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = OrganizerOrderPositionSerializer
def get_queryset(self):
qs = super().get_queryset()
perm = self.permission if self.request.method in SAFE_METHODS else self.write_permission
if isinstance(self.request.auth, (TeamAPIToken, Device)):
auth_obj = self.request.auth
elif self.request.user.is_authenticated:
auth_obj = self.request.user
else:
raise PermissionDenied("Unknown authentication scheme")
qs = qs.filter(
order__event__in=auth_obj.get_events_with_permission(perm, request=self.request).filter(
organizer=self.request.organizer
)
)
return qs
class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet):
serializer_class = OrderPositionSerializer
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true'
return ctx
def get_queryset(self):
qs = super().get_queryset()
qs = qs.filter(order__event=self.request.event)
return qs
@action(detail=True, methods=['POST'], url_name='price_calc')
def price_calc(self, request, *args, **kwargs):
"""
+2
View File
@@ -183,6 +183,7 @@ class ParametrizedGiftcardWebhookEvent(ParametrizedWebhookEvent):
return {
'notification_id': logentry.pk,
'issuer_id': logentry.organizer_id,
'issuer_slug': logentry.organizer.slug,
'giftcard': giftcard.pk,
'action': logentry.action_type,
}
@@ -197,6 +198,7 @@ class ParametrizedGiftcardTransactionWebhookEvent(ParametrizedWebhookEvent):
return {
'notification_id': logentry.pk,
'issuer_id': logentry.organizer_id,
'issuer_slug': logentry.organizer.slug,
'acceptor_id': logentry.parsed_data.get('acceptor_id'),
'acceptor_slug': logentry.parsed_data.get('acceptor_slug'),
'giftcard': giftcard.pk,
+5 -2
View File
@@ -216,7 +216,10 @@ class OutboundSyncProvider:
try:
mapped_objects = self.sync_order(sq.order)
if not all(all(not res or res.sync_info.get("action", "") == "nothing_to_do" for res in res_list) for res_list in mapped_objects.values()):
actions_taken = [res and res.sync_info.get("action", "") for res_list in mapped_objects.values() for res in res_list]
should_write_logentry = any(action not in (None, "nothing_to_do") for action in actions_taken)
logger.info('Synced order %s to %s, actions: %r, log: %r', sq.order.code, sq.sync_provider, actions_taken, should_write_logentry)
if should_write_logentry:
sq.order.log_action("pretix.event.order.data_sync.success", {
"provider": self.identifier,
"objects": {
@@ -237,7 +240,7 @@ class OutboundSyncProvider:
sq.set_sync_error("exceeded", e.messages, e.full_message)
else:
logger.info(
f"Could not sync order {sq.order.code} to {type(self).__name__} "
f"Could not sync order {sq.order.code} to {sq.sync_provider} "
f"(transient error, attempt #{sq.failed_attempts}, next {sq.not_before})",
exc_info=True,
)
+10 -1
View File
@@ -42,6 +42,8 @@ from django.utils.html import escape
from django.utils.timezone import get_current_timezone, now
from django.utils.translation import gettext_lazy as _
from pretix.helpers.format import PlainHtmlAlternativeString
def replace_arabic_numbers(inp):
if not isinstance(inp, str):
@@ -61,11 +63,18 @@ def replace_arabic_numbers(inp):
return inp.translate(table)
def format_placeholder_help_text(placeholder_name, sample_value):
if isinstance(sample_value, PlainHtmlAlternativeString):
sample_value = sample_value.plain
title = (_("Sample: %s") % sample_value) if sample_value else ""
return ('<button type="button" class="content-placeholder" title="%s">{%s}</button>' % (escape(title), escape(placeholder_name)))
def format_placeholders_help_text(placeholders, event=None):
placeholders = [(k, v.render_sample(event) if event else v) for k, v in placeholders.items()]
placeholders.sort(key=lambda x: x[0])
phs = [
'<button type="button" class="content-placeholder" title="%s">{%s}</button>' % (escape(_("Sample: %s") % v) if v else "", escape(k))
format_placeholder_help_text(k, v)
for k, v in placeholders
]
return _('Available placeholders: {list}').format(
+4 -1
View File
@@ -346,7 +346,8 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
{
'user': self,
'messages': msg,
'url': build_absolute_uri('control:user.settings')
'url': build_absolute_uri('control:user.settings'),
'instance': settings.PRETIX_INSTANCE_NAME,
},
event=None,
user=self,
@@ -391,6 +392,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
'user': self,
'reason': msg,
'code': code,
'instance': settings.PRETIX_INSTANCE_NAME,
},
event=None,
user=self,
@@ -430,6 +432,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
mail(
self.email, _('Password recovery'), 'pretixcontrol/email/forgot.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': self,
'url': (build_absolute_uri('control:auth.forgot.recover')
+ '?id=%d&token=%s' % (self.id, default_token_generator.make_token(self)))
+1 -1
View File
@@ -86,7 +86,7 @@ class OrderSyncQueue(models.Model):
def set_sync_error(self, failure_mode, messages, full_message):
logger.exception(
f"Could not sync order {self.order.code} to {type(self).__name__} ({failure_mode})"
f"Could not sync order {self.order.code} to {self.sync_provider} ({failure_mode})"
)
self.order.log_action(f"pretix.event.order.data_sync.failed.{failure_mode}", {
"provider": self.sync_provider,
+24 -8
View File
@@ -1525,16 +1525,26 @@ class GiftCardPayment(BasePaymentProvider):
def payment_control_render(self, request, payment) -> str:
from .models import GiftCard
if 'gift_card' in payment.info_data:
gc = GiftCard.objects.get(pk=payment.info_data.get('gift_card'))
if any(key in payment.info_data for key in ('gift_card', 'error')):
template = get_template('pretixcontrol/giftcards/payment.html')
ctx = {
'request': request,
'event': self.event,
'gc': gc,
**({'error': payment.info_data[
'error']} if 'error' in payment.info_data else {}),
**({'gift_card_secret': payment.info_data[
'gift_card_secret']} if 'gift_card_secret' in payment.info_data else {})
}
return template.render(ctx)
try:
gc = GiftCard.objects.get(pk=payment.info_data.get('gift_card'))
ctx = {
'gc': gc,
}
except GiftCard.DoesNotExist:
pass
finally:
return template.render(ctx)
def payment_control_render_short(self, payment: OrderPayment) -> str:
d = payment.info_data
@@ -1549,12 +1559,16 @@ class GiftCardPayment(BasePaymentProvider):
try:
gc = GiftCard.objects.get(pk=payment.info_data.get('gift_card'))
except GiftCard.DoesNotExist:
return {}
return {
**({'error': payment.info_data[
'error']} if 'error' in payment.info_data else {})
}
return {
'gift_card': {
'id': gc.pk,
'secret': gc.secret,
'organizer': gc.issuer.slug
'organizer': gc.issuer.slug,
** ({'error': payment.info_data['error']} if 'error' in payment.info_data else {})
}
}
@@ -1626,6 +1640,8 @@ class GiftCardPayment(BasePaymentProvider):
raise PaymentException(_("This gift card does not support this currency."))
if not gc.accepted_by(self.event.organizer):
raise PaymentException(_("This gift card is not accepted by this event organizer."))
if gc.value <= Decimal("0.00"):
raise PaymentException(_("All credit on this gift card has been used."))
if payment.amount > gc.value:
raise PaymentException(_("This gift card was used in the meantime. Please try again."))
if gc.testmode and not payment.order.testmode:
@@ -1655,7 +1671,7 @@ class GiftCardPayment(BasePaymentProvider):
}
)
except PaymentException as e:
payment.fail(info={'error': str(e)})
payment.fail(info={**payment.info_data, 'error': str(e)}, send_mail=not is_early_special_case)
raise e
def payment_is_valid_session(self, request: HttpRequest) -> bool:
+1
View File
@@ -176,6 +176,7 @@ def shred(self, event: Event, fileid: str, confirm_code: str, user: int=None, lo
_('Data shredding completed'),
'pretixbase/email/shred_completed.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': user,
'organizer': event.organizer.name,
'event': str(event.name),
@@ -13,5 +13,5 @@ Start time: {{ start_time }} (new data added after this time might not have been
Best regards,
Your pretix team
Your {{ instance }} team
{% endblocktrans %}
@@ -0,0 +1,34 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from django import template
from django.utils.html import mark_safe
register = template.Library()
@register.filter("anon_email")
def anon_email(value):
"""Replaces @ with [at] and . with [dot] for anonymization."""
if not isinstance(value, str):
return value
value = value.replace("@", "[at]").replace(".", "[dot]")
return mark_safe(''.join(['&#{0};'.format(ord(char)) for char in value]))
+1
View File
@@ -518,6 +518,7 @@ def pretixcontrol_orderposition_blocked_display(sender: Event, orderposition, bl
'The order requires approval before it can continue to be processed.'),
'pretix.event.order.approved': _('The order has been approved.'),
'pretix.event.order.denied': _('The order has been denied (comment: "{comment}").'),
'pretix.event.order.vatid.validated': _('The customer VAT ID has been verified.'),
'pretix.event.order.contact.changed': _('The email address has been changed from "{old_email}" '
'to "{new_email}".'),
'pretix.event.order.contact.confirmed': _(
@@ -9,5 +9,5 @@ Please do never give this code to another person. Our support team will never as
If this code was not requested by you, please contact us immediately.
Best regards,
Your pretix team
Your {{ instance }} team
{% endblocktrans %}
@@ -5,5 +5,5 @@ you requested a new password. Please go to the following page to reset your pass
{{ url }}
Best regards,
Your pretix team
{% endblocktrans %}
Your {{ instance }} team
{% endblocktrans %}
@@ -1,6 +1,6 @@
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
you have been invited to a team on pretix, a platform to perform event
you have been invited to a team on {{ instance }}, a platform to perform event
ticket sales.
Organizer: {{ organizer }}
@@ -13,5 +13,5 @@ If you do not want to join, you can safely ignore or delete this email.
Best regards,
Your pretix team
Your {{ instance }} team
{% endblocktrans %}
@@ -1,6 +1,6 @@
{% load i18n %}{% blocktrans with url=url|safe messages=messages|safe %}Hello,
this is to inform you that the account information of your pretix account has been
this is to inform you that the account information of your {{ instance }} account has been
changed. In particular, the following changes have been performed:
{{ messages }}
@@ -12,5 +12,5 @@ You can review and change your account settings here:
{{ url }}
Best regards,
Your pretix team
Your {{ instance }} team
{% endblocktrans %}
@@ -3,16 +3,26 @@
<dl class="dl-horizontal">
<dt>{% trans "Gift card code" %}</dt>
<dd>
<a href="{% url "control:organizer.giftcard" organizer=gc.issuer.slug giftcard=gc.pk %}">
{{ gc.secret }}
</a>
{% if gc.issuer != request.organizer %}
<span class="text-muted">
<br>
<span class="fa fa-group"></span> {{ gc.issuer }}
</span>
{% if gc %}
<a href="{% url "control:organizer.giftcard" organizer=gc.issuer.slug giftcard=gc.pk %}">
{{ gc.secret }}
</a>
{% if gc.issuer.slug != request.organizer %}
<span class="text-muted">
<br>
<span class="fa fa-group"></span> {{ gc.issuer }}
</span>
{% endif %}
{% elif gift_card_secret %}
{{ gift_card_secret }}
{% endif %}
</dd>
<dt>{% trans "Issuer" %}</dt>
<dd>{{ gc.issuer }}</dd>
{% if gc %}
<dt>{% trans "Issuer" %}</dt>
<dd>{{ gc.issuer }}</dd>
{% endif %}
{% if error %}
<dt>{% trans "Error" %}</dt>
<dd>{{ error }}</dd>
{% endif %}
</dl>
+8 -4
View File
@@ -870,11 +870,15 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
)
except ValueError:
msgs[self.supported_locale[idx]] = '<div class="alert alert-danger">{}</div>'.format(
PlaceholderValidator.error_message)
msgs[self.supported_locale[idx]] = format_html(
'<div class="alert alert-danger">{}</div>',
PlaceholderValidator.error_message
)
except KeyError as e:
msgs[self.supported_locale[idx]] = '<div class="alert alert-danger">{}</div>'.format(
_('Invalid placeholder: {%(value)s}') % {'value': e.args[0]})
msgs[self.supported_locale[idx]] = format_html(
'<div class="alert alert-danger">{}</div>',
_('Invalid placeholder: {%(value)s}') % {'value': e.args[0]}
)
return JsonResponse({
'item': preview_item,
+11 -3
View File
@@ -1641,9 +1641,17 @@ class OrderCheckVATID(OrderView):
try:
normalized_id = validate_vat_id(ia.vat_id, str(ia.country))
ia.vat_id_validated = True
ia.vat_id = normalized_id
ia.save()
with transaction.atomic():
ia.vat_id_validated = True
ia.vat_id = normalized_id
ia.save()
self.order.log_action(
'pretix.event.order.vatid.validated',
data={
'vat_id': normalized_id,
},
user=self.request.user,
)
except VATIDFinalError as e:
messages.error(self.request, e.message)
except VATIDTemporaryError:
+2 -1
View File
@@ -1039,9 +1039,10 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
def _send_invite(self, instance):
mail(
instance.email,
_('pretix account invitation'),
_('Account invitation'),
'pretixcontrol/email/invitation.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': self,
'organizer': self.request.organizer.name,
'team': instance.team.name,
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-20 13:01+0000\n"
"POT-Creation-Date: 2026-02-24 11:50+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 09:10+0000\n"
"PO-Revision-Date: 2025-12-03 23:00+0000\n"
"Last-Translator: sandra r <sandrarial@gestiontickets.online>\n"
"PO-Revision-Date: 2026-03-02 21:00+0000\n"
"Last-Translator: Sandra Rial Pérez <sandrarial@gestiontickets.online>\n"
"Language-Team: Galician <https://translate.pretix.eu/projects/pretix/pretix-"
"js/gl/>\n"
"Language: gl\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.14.3\n"
"X-Generator: Weblate 5.16.1\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -162,12 +162,12 @@ msgstr "Pedidos pagados"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Attendees (ordered)"
msgstr ""
msgstr "Asistentes (ordenados)"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Attendees (paid)"
msgstr ""
msgstr "Asistentes (de pago)"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:51
msgid "Total revenue"
@@ -732,8 +732,8 @@ msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr ""
"Os artigos da túa cesta xa non están reservados para ti. Aínda podes "
"completar o teu pedido mentres estean dispoñibles."
"Os artigos do teu carro xa non están reservados para ti. Podes completar o "
"teu pedido sempre que estean dispoñibles."
#: pretix/static/pretixpresale/js/ui/cart.js:49
msgid "Cart expired"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+23 -17
View File
@@ -802,31 +802,37 @@ class PaypalMethod(BasePaymentProvider):
all_captures_completed = False
else:
any_captures = True
if not (any_captures and all_captures_completed):
# Payment has at least one capture, but it is not yet completed
if any_captures and not all_captures_completed:
messages.warning(request, _('PayPal has not yet approved the payment. We will inform you as '
'soon as the payment completed.'))
payment.info = json.dumps(pp_captured_order.dict())
payment.state = OrderPayment.PAYMENT_STATE_PENDING
payment.save()
return
# Payment has at least one capture and all captures are completed
elif any_captures and all_captures_completed:
if pp_captured_order.status != 'COMPLETED':
payment.fail(info=pp_captured_order.dict())
logger.error('Invalid state: %s' % repr(pp_captured_order.dict()))
raise PaymentException(
_('We were unable to process your payment. See below for details on how to proceed.')
)
if pp_captured_order.status != 'COMPLETED':
payment.fail(info=pp_captured_order.dict())
logger.error('Invalid state: %s' % repr(pp_captured_order.dict()))
raise PaymentException(
_('We were unable to process your payment. See below for details on how to proceed.')
)
if payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED:
logger.warning('PayPal success event even though order is already marked as paid')
return
if payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED:
logger.warning('PayPal success event even though order is already marked as paid')
try:
payment.info = json.dumps(pp_captured_order.dict())
payment.save(update_fields=['info'])
payment.confirm()
except Quota.QuotaExceededException as e:
raise PaymentException(str(e))
# Payment has not any captures yet - so it's probably in created status
else:
return
try:
payment.info = json.dumps(pp_captured_order.dict())
payment.save(update_fields=['info'])
payment.confirm()
except Quota.QuotaExceededException as e:
raise PaymentException(str(e))
finally:
if 'payment_paypal_oid' in request.session:
del request.session['payment_paypal_oid']
@@ -836,7 +842,7 @@ class PaypalMethod(BasePaymentProvider):
try:
if (
payment.info
and payment.info_data['purchase_units'][0]['payments']['captures'][0]['status'] == 'pending'
and payment.info_data['purchase_units'][0]['payments']['captures'][0]['status'] == 'PENDING'
):
retry = False
except (KeyError, IndexError):
@@ -6,6 +6,7 @@
{% load eventurl %}
{% load safelink %}
{% load rich_text %}
{% load anonymize_email %}
{% block thetitle %}
{% if messages %}
{{ messages|join:" " }} ::
@@ -219,7 +220,7 @@
{% endblock %}
{% block footernav %}
{% if request.event.settings.contact_mail %}
<li><a href="mailto:{{ request.event.settings.contact_mail }}" target="_blank" rel="noopener">{% trans "Contact" %}</a></li>
<li><a href="{{ 'mailto:'|add:request.event.settings.contact_mail|anon_email }}" target="_blank" rel="noopener">{% trans "Contact" %}</a></li>
{% endif %}
{% if request.event.settings.privacy_url %}
<li><a href="{% safelink request.event.settings.privacy_url %}" target="_blank" rel="noopener">{% trans "Privacy policy" %}</a></li>
@@ -21,4 +21,5 @@
<script type="text/javascript" src="{% static "pretixpresale/js/ui/cart.js" %}"></script>
<script type="text/javascript" src="{% static "pretixpresale/js/ui/iframe.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/addressform.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/deanonymize_email.js" %}"></script>
{% endcompress %}
@@ -5,6 +5,7 @@
{% load thumb %}
{% load eventurl %}
{% load safelink %}
{% load anonymize_email %}
{% block thetitle %}
{% block title %}{% endblock %}{% if url_name != "organizer.index" %} :: {% endif %}{{ organizer.name }}
{% endblock %}
@@ -97,7 +98,7 @@
{% endblock %}
{% block footernav %}
{% if not request.event and request.organizer.settings.contact_mail %}
<li><a href="mailto:{{ request.organizer.settings.contact_mail }}" target="_blank" rel="noopener">{% trans "Contact" %}</a></li>
<li><a href="{{ 'mailto:'|add:request.organizer.settings.contact_mail|anon_email }}" target="_blank" rel="noopener">{% trans "Contact" %}</a></li>
{% endif %}
{% if not request.event and request.organizer.settings.privacy_url %}
<li><a href="{% safelink request.organizer.settings.privacy_url %}" target="_blank" rel="noopener">{% trans "Privacy policy" %}</a></li>
-1
View File
@@ -393,7 +393,6 @@ class TokenView(View):
if grant.code_challenge_method == "S256":
expected_challenge = base64.urlsafe_b64encode(hashlib.sha256(request.POST["code_verifier"].encode()).digest()).decode().rstrip("=")
print(grant.code_challenge, expected_challenge)
if expected_challenge != grant.code_challenge:
return JsonResponse({
"error": "invalid_grant",
+4
View File
@@ -211,6 +211,10 @@ USE_X_FORWARDED_HOST = config.getboolean('pretix', 'trust_x_forwarded_host', fal
REQUEST_ID_HEADER = config.get('pretix', 'request_id_header', fallback=False)
if REQUEST_ID_HEADER in config.cp.BOOLEAN_STATES:
raise ImproperlyConfigured(
"request_id_header should be set to a header name, not a boolean value."
)
if config.getboolean('pretix', 'trust_x_forwarded_proto', fallback=False):
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
@@ -0,0 +1,7 @@
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('a[href^="mailto:"]').forEach(function(link) {
// Replace [at] with @ and the [dot] with . in both the href and the displayed text (if needed)
link.href = link.href.replace('[at]', '@').replace('[dot]', '.');
link.textContent = link.textContent.replace('[at]', '@').replace('[dot]', '.');
});
});
+285 -2
View File
@@ -35,8 +35,8 @@ from django_scopes import scopes_disabled
from tests.const import SAMPLE_PNG
from pretix.base.models import (
InvoiceAddress, Item, Order, OrderPosition, Organizer, Question,
SeatingPlan,
GiftCard, InvoiceAddress, Item, Order, OrderPayment, OrderPosition,
Organizer, Question, SeatingPlan,
)
from pretix.base.models.orders import CartPosition, OrderFee, QuestionAnswer
@@ -895,6 +895,41 @@ def test_order_create_payment_info_optional(token_client, organizer, event, item
assert json.loads(p.info) == res['payment_info']
@pytest.mark.django_db
def test_order_create_payment_info_valid_object(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
res["payment_info"] = [{"should": "fail"}]
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 400
res['payment_info'] = {
'foo': {
'bar': [1, 2],
'test': False
}
}
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
with scopes_disabled():
o = Order.objects.get(code=resp.data['code'])
p = o.payments.first()
assert p.provider == "banktransfer"
assert p.amount == o.total
assert json.loads(p.info) == res['payment_info']
@pytest.mark.django_db
def test_order_create_position_secret_optional(token_client, organizer, event, item, quota, question):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
@@ -3336,3 +3371,251 @@ def test_order_create_rounding_default_pretixpos_fallback(device, device_client,
assert resp.data["total"] == "500.00"
assert resp.data["positions"][0]["price"] == "100.00"
assert resp.data["positions"][-1]["price"] == "100.00"
@pytest.mark.parametrize(
"order_status,status_code",
[
(
Order.STATUS_PENDING, 201
),
(
Order.STATUS_PAID, 400
),
],
)
@pytest.mark.django_db
def test_order_create_use_gift_cards_only_pending(token_client, organizer, event, item, quota, question, order_status, status_code):
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
res['api_meta'] = {
'test': 1
}
gc = GiftCard.objects.create(issuer=organizer, currency='EUR')
gc.transactions.create(value=Decimal("100.00"), acceptor=organizer).save()
res['status'] = order_status
del res['payment_provider']
res['use_gift_cards'] = [gc.secret]
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == status_code
if status_code != 201:
assert resp.data == {'use_gift_cards': ['The attribute use_gift_cards is only supported for orders that are created as pending']}
@pytest.mark.django_db
@pytest.mark.parametrize(
"send_mail,mail_amount",
[
(
False, 0
),
(
True, 2
),
],
)
def test_order_create_use_gift_card(token_client, organizer, event, item, quota, question, send_mail, mail_amount):
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
res['api_meta'] = {
'test': 1
}
if send_mail:
res['send_email'] = True
gc = GiftCard.objects.create(issuer=organizer, currency='EUR')
gc.transactions.create(value=Decimal("100.00"), acceptor=organizer).save()
del res['payment_provider']
res['use_gift_cards'] = [gc.secret]
djmail.outbox = []
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
with scopes_disabled():
o = Order.objects.get(code=resp.data['code'])
assert o.status == Order.STATUS_PAID
assert gc.transactions.count() == 2
assert -gc.transactions.last().value == o.total
assert len(djmail.outbox) == mail_amount
@pytest.mark.django_db
def test_order_create_use_multiple_gift_cards(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
res['api_meta'] = {
'test': 1
}
del res['payment_provider']
gc_one_eur = GiftCard.objects.create(issuer=organizer, currency='EUR')
gc_one_eur.transactions.create(value=Decimal("1.00"), acceptor=organizer).save()
gc_empty = GiftCard.objects.create(issuer=organizer, currency='EUR')
gc_wrong_currency = GiftCard.objects.create(issuer=organizer, currency='USD')
gc_wrong_currency.transactions.create(value=Decimal("100.00"), acceptor=organizer).save()
gc_enough_eur = GiftCard.objects.create(issuer=organizer, currency='EUR')
gc_enough_eur.transactions.create(value=Decimal("100.00"), acceptor=organizer).save()
res['use_gift_cards'] = [gc_one_eur.secret, gc_empty.secret, gc_wrong_currency.secret, gc_enough_eur.secret]
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
with scopes_disabled():
o = Order.objects.get(code=resp.data['code'])
# order has a payment entry per giftcard
assert o.status == Order.STATUS_PAID
assert o.payments.count() == 4
assert gc_one_eur.transactions.count() == 2 # +1€ charge and -1€ payment
assert o.payments.all()[0].state == OrderPayment.PAYMENT_STATE_CONFIRMED
assert Decimal(-1.00) == gc_one_eur.transactions.last().value
assert gc_empty.transactions.count() == 0 # no charge and no payment transaction
assert o.payments.all()[1].state == OrderPayment.PAYMENT_STATE_FAILED
assert gc_wrong_currency.transactions.count() == 1 # charge transaction
assert o.payments.all()[2].state == OrderPayment.PAYMENT_STATE_FAILED
assert gc_enough_eur.transactions.count() == 2 # +100€ charge and -remainder € payment
assert o.payments.all()[3].state == OrderPayment.PAYMENT_STATE_CONFIRMED
assert -(o.total - Decimal(1.00)) == gc_enough_eur.transactions.last().value
@pytest.mark.django_db
def test_order_create_use_gift_card_exclusive_with_payment_provider(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
res['api_meta'] = {
'test': 1
}
gc_value = Decimal("1.00")
gc = GiftCard.objects.create(issuer=organizer, currency='EUR')
gc.transactions.create(value=gc_value, acceptor=organizer).save()
res['use_gift_cards'] = [gc.secret]
res_with_payment_provider = copy.deepcopy(res)
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res_with_payment_provider
)
assert resp.status_code == 400
assert resp.json() == {"use_gift_cards": ["The attribute use_gift_cards is not compatible with payment_provider or payment_info"]}
res_with_payment_info = copy.deepcopy(res)
res_with_payment_info['payment_info'] = {"a": "b"}
del res_with_payment_info['payment_provider']
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res_with_payment_info
)
assert resp.status_code == 400
assert resp.json() == {"use_gift_cards": ["The attribute use_gift_cards is not compatible with payment_provider or payment_info"]}
@pytest.mark.django_db
def test_order_create_use_gift_card_repeated(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
res['api_meta'] = {
'test': 1
}
del res['payment_provider']
gc_one_eur = GiftCard.objects.create(issuer=organizer, currency='EUR')
gc_one_eur.transactions.create(value=Decimal("1.00"), acceptor=organizer).save()
gc_enough_eur = GiftCard.objects.create(issuer=organizer, currency='EUR')
gc_enough_eur.transactions.create(value=Decimal("100.00"), acceptor=organizer).save()
res['use_gift_cards'] = [gc_one_eur.secret, gc_one_eur.secret, gc_enough_eur.secret]
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 400
assert resp.json() == {'use_gift_cards': ['Multiple copies of the same gift card secret are not allowed']}
@pytest.mark.django_db
def test_order_create_use_gift_card_invalid_secret(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
res['api_meta'] = {
'test': 1
}
del res['payment_provider']
gc_enough_eur = GiftCard.objects.create(issuer=organizer, currency='EUR')
gc_enough_eur.transactions.create(value=Decimal("100.00"),
acceptor=organizer).save()
res['use_gift_cards'] = ["INVALID", gc_enough_eur.secret]
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
with scopes_disabled():
o = Order.objects.get(code=resp.data['code'])
assert o.status == Order.STATUS_PAID
assert o.payments.count() == 2
assert o.payments.all()[0].state == OrderPayment.PAYMENT_STATE_FAILED
assert o.payments.all()[1].state == OrderPayment.PAYMENT_STATE_CONFIRMED
+129 -67
View File
@@ -34,7 +34,7 @@ from stripe import error
from tests.plugins.stripe.test_checkout import apple_domain_create
from tests.plugins.stripe.test_provider import MockedCharge
from pretix.base.models import InvoiceAddress, Order, OrderPosition
from pretix.base.models import InvoiceAddress, Order, OrderPosition, Team
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
@@ -180,6 +180,41 @@ def order2(event2, item2):
return o
@pytest.fixture
@scopes_disabled()
def team2(organizer, event2):
team2 = Team.objects.create(
organizer=organizer,
name="Test-Team 2",
can_change_teams=True,
can_manage_gift_cards=True,
can_change_items=True,
can_create_events=True,
can_change_event_settings=True,
can_change_vouchers=True,
can_view_vouchers=True,
can_change_orders=True,
can_manage_customers=True,
can_manage_reusable_media=True,
can_change_organizer_settings=True,
)
team2.limit_events.add(event2)
team2.save()
return team2
@pytest.fixture
@scopes_disabled()
def limited_token_client(client, team2):
team2.can_view_orders = True
team2.can_view_vouchers = True
team2.save()
t = team2.tokens.create(name='Foo')
client.credentials(HTTP_AUTHORIZATION='Token ' + t.token)
return client
TEST_ORDERPOSITION_RES = {
"id": 1,
"order": "FOO",
@@ -987,8 +1022,64 @@ def test_refund_cancel(token_client, organizer, event, order):
assert resp.status_code == 400
@pytest.mark.parametrize(
"endpoint_template, response_code",
[('/api/v1/organizers/{}/events/{}/orderpositions/', 403), ('/api/v1/organizers/{}/orderpositions/', 200)]
)
@pytest.mark.django_db
def test_orderposition_list(token_client, organizer, device, event, order, item, subevent, subevent2, question, django_assert_num_queries):
def test_orderposition_list_limited_read(
endpoint_template, response_code, limited_token_client, organizer, device, event, order, item, subevent, subevent2, question
):
endpoint = endpoint_template.format(organizer.slug, event.slug)
i2 = copy.copy(item)
i2.pk = None
i2.save()
with scopes_disabled():
var = item.variations.create(value="Children")
res = copy.copy(TEST_ORDERPOSITION_RES)
op = order.positions.first()
op.variation = var
op.save()
res["id"] = op.pk
res["item"] = item.pk
res["variation"] = var.pk
res["answers"][0]["question"] = question.pk
res["print_logs"][0]["id"] = op.print_logs.first().pk
res["print_logs"][0]["device_id"] = device.device_id
resp = limited_token_client.get(endpoint)
assert resp.status_code == response_code
if response_code == 200:
assert resp.json() == {'count': 0, 'next': None, 'previous': None, 'results': []}
else:
assert resp.json() == {'detail': 'You do not have permission to perform this action.'}
@pytest.mark.parametrize(
("endpoint_template", "endpoint_type"),
[
('/api/v1/organizers/{}/events/{}/orderpositions/', "event"),
('/api/v1/organizers/{}/orderpositions/', "organizer")
],
)
@pytest.mark.django_db
def test_orderposition_list(
endpoint_template,
endpoint_type,
token_client,
organizer,
device,
event,
order,
item,
subevent,
subevent2,
question,
django_assert_num_queries
):
endpoint = endpoint_template.format(organizer.slug, event.slug)
i2 = copy.copy(item)
i2.pk = None
i2.save()
@@ -1005,88 +1096,64 @@ def test_orderposition_list(token_client, organizer, device, event, order, item,
res["answers"][0]["question"] = question.pk
res["print_logs"][0]["id"] = op.print_logs.first().pk
res["print_logs"][0]["device_id"] = device.device_id
if endpoint_type == "organizer":
res["event"] = event.slug
resp = token_client.get('/api/v1/organizers/{}/events/{}/orderpositions/'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint)
assert resp.status_code == 200
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?order__status=n'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?order__status=n')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?order__status=p'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?order__status=p')
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?item={}'.format(organizer.slug, event.slug, item.pk))
resp = token_client.get(endpoint + '?item={}'.format(item.pk))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?item__in={},{}'.format(
organizer.slug, event.slug, item.pk, i2.pk
))
resp = token_client.get(endpoint + '?item__in={},{}'.format(item.pk, i2.pk))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?item={}'.format(organizer.slug, event.slug, i2.pk))
resp = token_client.get(endpoint + '?item={}'.format(i2.pk))
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?variation={}'.format(organizer.slug, event.slug, var.pk))
resp = token_client.get(endpoint + '?variation={}'.format(var.pk))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?variation={}'.format(organizer.slug, event.slug, var2.pk))
resp = token_client.get(endpoint + '?variation={}'.format(var2.pk))
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?attendee_name=Peter'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?attendee_name=Peter')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?attendee_name=peter'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?attendee_name=peter')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?attendee_name=Mark'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?attendee_name=Mark')
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?secret=z3fsn8jyufm5kpk768q69gkbyr5f4h6w'.format(
organizer.slug, event.slug))
resp = token_client.get(endpoint + '?secret=z3fsn8jyufm5kpk768q69gkbyr5f4h6w')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?secret=abc123'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?secret=abc123')
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?pseudonymization_id=ABCDEFGHKL'.format(
organizer.slug, event.slug))
resp = token_client.get(endpoint + '?pseudonymization_id=ABCDEFGHKL')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?pseudonymization_id=FOO'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?pseudonymization_id=FOO')
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?search=FO'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?search=FO')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?search=z3fsn8j'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?search=z3fsn8j')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?search=Peter'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?search=Peter')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?search=5f4h6w'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?search=5f4h6w')
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?order=FOO'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?order=FOO')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?order=BAR'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?order=BAR')
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?has_checkin=false'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?has_checkin=false')
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?has_checkin=true'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?has_checkin=true')
assert [] == resp.data['results']
with scopes_disabled():
@@ -1103,33 +1170,28 @@ def test_orderposition_list(token_client, organizer, device, event, order, item,
'gate': None,
'type': 'entry'
}]
with django_assert_num_queries(16):
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?has_checkin=true'.format(organizer.slug, event.slug)
)
if '/events/' in endpoint:
with django_assert_num_queries(18):
resp = token_client.get(endpoint + '?has_checkin=true')
else:
with django_assert_num_queries(17):
resp = token_client.get(endpoint + '?has_checkin=true')
assert [res] == resp.data['results']
op.subevent = subevent
op.save()
res['subevent'] = subevent.pk
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?subevent={}'.format(organizer.slug, event.slug, subevent.pk))
resp = token_client.get(endpoint + '?subevent={}'.format(subevent.pk))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?subevent__in={},{}'.format(organizer.slug, event.slug,
subevent.pk, subevent2.pk))
resp = token_client.get(endpoint + '?subevent__in={},{}'.format(subevent.pk, subevent2.pk))
assert [res] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?subevent={}'.format(organizer.slug, event.slug,
subevent.pk + 1))
resp = token_client.get(endpoint + '?subevent={}'.format(subevent.pk + 1))
assert [] == resp.data['results']
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?include_canceled_positions=false'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?include_canceled_positions=false')
assert len(resp.data['results']) == 1
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?include_canceled_positions=true'.format(organizer.slug, event.slug))
resp = token_client.get(endpoint + '?include_canceled_positions=true')
assert len(resp.data['results']) == 2