diff --git a/doc/api/resources/orders.rst b/doc/api/resources/orders.rst index 75bfa97a0..f742801c8 100644 --- a/doc/api/resources/orders.rst +++ b/doc/api/resources/orders.rst @@ -109,6 +109,7 @@ cancellation_date datetime Time of order c Will not be set for partial cancellations and is not reliable for orders that have been cancelled, reactivated and cancelled again. +plugin_data object Additional data added by plugins. ===================================== ========================== ======================================================= @@ -164,6 +165,10 @@ cancellation_date datetime Time of order c The ``tax_code`` attribute has been added. +.. versionchanged:: 2025.2 + + The ``plugin_data`` attribute has been added. + .. _order-position-resource: Order position resource @@ -251,6 +256,7 @@ seat objects The assigned se pdf_data object Data object required for ticket PDF generation. By default, this field is missing. It will be added only if you add the ``pdf_data=true`` query parameter to your request. +plugin_data object Additional data added by plugins. ===================================== ========================== ======================================================= .. versionchanged:: 4.16 @@ -265,6 +271,10 @@ pdf_data object Data object req The ``tax_code`` attribute has been added. +.. versionchanged:: 2025.2 + + The ``plugin_data`` attribute has been added. + .. _order-payment-resource: Order payment resource @@ -461,7 +471,8 @@ List of all orders "output": "pdf", "url": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/pdf/" } - ] + ], + "plugin_data": {} } ], "downloads": [ @@ -483,7 +494,8 @@ List of all orders } ], "refunds": [], - "cancellation_date": null + "cancellation_date": null, + "plugin_data": {} } ] } @@ -702,7 +714,8 @@ Fetching individual orders "output": "pdf", "url": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/pdf/" } - ] + ], + "plugin_data": {} } ], "downloads": [ @@ -724,7 +737,8 @@ Fetching individual orders } ], "refunds": [], - "cancellation_date": null + "cancellation_date": null, + "plugin_data": {} } :param organizer: The ``slug`` field of the organizer to fetch @@ -1671,7 +1685,8 @@ List of all order positions "output": "pdf", "url": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/pdf/" } - ] + ], + "plugin_data": {} } ] } @@ -1798,7 +1813,8 @@ Fetching individual positions "output": "pdf", "url": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/pdf/" } - ] + ], + "plugin_data": {} } :param organizer: The ``slug`` field of the organizer to fetch diff --git a/doc/development/api/general.rst b/doc/development/api/general.rst index fdd34717b..05cc1a267 100644 --- a/doc/development/api/general.rst +++ b/doc/development/api/general.rst @@ -103,4 +103,4 @@ API .. automodule:: pretix.api.signals :no-index: - :members: register_device_security_profile + :members: register_device_security_profile, order_api_details, orderposition_api_details diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py index 6b32a8db5..2f7fd09d8 100644 --- a/src/pretix/api/serializers/order.py +++ b/src/pretix/api/serializers/order.py @@ -46,6 +46,7 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer from pretix.api.serializers.item import ( InlineItemVariationSerializer, ItemSerializer, QuestionSerializer, ) +from pretix.api.signals import order_api_details, orderposition_api_details from pretix.base.decimal import round_decimal from pretix.base.i18n import language from pretix.base.models import ( @@ -494,6 +495,18 @@ class OrderPositionListSerializer(serializers.ListSerializer): return data +class OrderPositionPluginDataField(serializers.Field): + def to_representation(self, value: OrderPosition): + d = {} + if value and value.pk: + for recv, resp in orderposition_api_details.send( + sender=self.context.get("event") or value.order.event, + orderposition=value + ): + d.update(resp) + return d + + class OrderPositionSerializer(I18nAwareModelSerializer): checkins = CheckinSerializer(many=True, read_only=True) print_logs = PrintLogSerializer(many=True, read_only=True) @@ -504,6 +517,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer): seat = InlineSeatSerializer(read_only=True) country = CompatibleCountryField(source='*') attendee_name = serializers.CharField(required=False) + plugin_data = OrderPositionPluginDataField(source='*', allow_null=True, read_only=True) class Meta: list_serializer_class = OrderPositionListSerializer @@ -513,7 +527,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer): 'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins', 'print_logs', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled', 'print_logs', 'downloads', 'answers', 'tax_rule', 'tax_code', 'pseudonymization_id', 'pdf_data', 'seat', - 'canceled', 'valid_from', 'valid_until', 'blocked', 'voucher_budget_use') + 'canceled', 'valid_from', 'valid_until', 'blocked', 'voucher_budget_use', 'plugin_data') read_only_fields = ( 'id', 'order', 'positionid', 'item', 'variation', 'price', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins', 'downloads', 'answers', 'tax_rule', 'tax_code', 'pseudonymization_id', @@ -730,6 +744,18 @@ class OrderListSerializer(serializers.ListSerializer): return data +class OrderPluginDataField(serializers.Field): + def to_representation(self, value: Order): + d = {} + if value and value.pk: + for recv, resp in order_api_details.send( + sender=self.context.get("event") or value.event, + order=value + ): + d.update(resp) + return d + + class OrderSerializer(I18nAwareModelSerializer): event = SlugRelatedField(slug_field='slug', read_only=True) invoice_address = InvoiceAddressSerializer(allow_null=True) @@ -747,6 +773,7 @@ class OrderSerializer(I18nAwareModelSerializer): queryset=SalesChannel.objects.none(), required=False, ) + plugin_data = OrderPluginDataField(source='*', allow_null=True, read_only=True) class Meta: model = Order @@ -755,7 +782,7 @@ class OrderSerializer(I18nAwareModelSerializer): 'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date', 'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads', 'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel', - 'url', 'customer', 'valid_if_pending', 'api_meta', 'cancellation_date' + 'url', 'customer', 'valid_if_pending', 'api_meta', 'cancellation_date', 'plugin_data', ) read_only_fields = ( 'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date', diff --git a/src/pretix/api/signals.py b/src/pretix/api/signals.py index 40383d5d8..22cdd82ee 100644 --- a/src/pretix/api/signals.py +++ b/src/pretix/api/signals.py @@ -26,7 +26,7 @@ from django.utils.timezone import now from django_scopes import scopes_disabled from pretix.api.models import ApiCall, WebHookCall -from pretix.base.signals import periodic_task +from pretix.base.signals import EventPluginSignal, periodic_task from pretix.helpers.periodic import minimum_interval register_webhook_events = Signal() @@ -43,6 +43,28 @@ return an instance of a subclass of ``pretix.api.auth.devicesecurity.BaseSecurit or a list of such instances. """ +order_api_details = EventPluginSignal() +""" +Arguments: ``order`` + +This signal is sent out to fill the ``plugin_details`` field of the order API. Receivers +should return a dictionary that is combined with the dictionaries of all other plugins. +Note that doing database or network queries in receivers to this signal is discouraged +and could cause serious performance issues. The main purpose is to provide information +from e.g. ``meta_info`` to the API consumer, +""" + +orderposition_api_details = EventPluginSignal() +""" +Arguments: ``orderposition`` + +This signal is sent out to fill the ``plugin_details`` field of the order API. Receivers +should return a dictionary that is combined with the dictionaries of all other plugins. +Note that doing database or network queries in receivers to this signal is discouraged +and could cause serious performance issues. The main purpose is to provide information +from e.g. ``meta_info`` to the API consumer, +""" + @receiver(periodic_task) @scopes_disabled() diff --git a/src/tests/api/test_giftcards.py b/src/tests/api/test_giftcards.py index 780b64f73..bbc50fec2 100644 --- a/src/tests/api/test_giftcards.py +++ b/src/tests/api/test_giftcards.py @@ -162,6 +162,7 @@ def test_giftcard_detail_expand(token_client, organizer, event, giftcard): "tax_rule": None, "pseudonymization_id": op.pseudonymization_id, "pdf_data": {}, + "plugin_data": {}, "seat": None, "canceled": False, "valid_from": None, diff --git a/src/tests/api/test_order_create.py b/src/tests/api/test_order_create.py index a164a5008..7f1a18438 100644 --- a/src/tests/api/test_order_create.py +++ b/src/tests/api/test_order_create.py @@ -477,7 +477,8 @@ def test_order_create_simulate(token_client, organizer, event, item, quota, ques 'zipcode': None, 'state': None, 'country': None, - 'canceled': False + 'canceled': False, + 'plugin_data': {}, } ], 'downloads': [], @@ -487,7 +488,8 @@ def test_order_create_simulate(token_client, organizer, event, item, quota, ques 'refunds': [], 'require_approval': False, 'sales_channel': 'web', - 'cancellation_date': None + 'cancellation_date': None, + 'plugin_data': {}, } @@ -533,13 +535,15 @@ def test_order_create_positionids_addons_simulated(token_client, organizer, even 'street': None, 'zipcode': None, 'city': None, 'country': None, 'state': None, 'attendee_email': None, 'voucher': None, 'tax_rate': '19.00', 'tax_code': 'S/standard', 'tax_value': '3.67', 'discount': None, 'voucher_budget_use': None, 'addon_to': None, 'subevent': None, 'checkins': [], 'print_logs': [], 'downloads': [], 'answers': [], 'tax_rule': item.tax_rule_id, - 'pseudonymization_id': 'PREVIEW', 'seat': None, 'canceled': False, 'valid_from': None, 'valid_until': None, 'blocked': None}, + 'pseudonymization_id': 'PREVIEW', 'seat': None, 'canceled': False, 'valid_from': None, 'valid_until': None, 'blocked': None, + 'plugin_data': {}}, {'id': 0, 'order': '', 'positionid': 2, 'item': item.pk, 'variation': None, 'price': '23.00', 'attendee_name': 'Peter', 'attendee_name_parts': {'full_name': 'Peter', '_scheme': 'full'}, 'company': None, 'street': None, 'zipcode': None, 'city': None, 'country': None, 'state': None, 'attendee_email': None, 'voucher': None, 'tax_rate': '19.00', 'tax_code': 'S/standard', 'tax_value': '3.67', 'discount': None, 'voucher_budget_use': None, 'addon_to': 1, 'subevent': None, 'checkins': [], 'print_logs': [], 'downloads': [], 'answers': [], 'tax_rule': item.tax_rule_id, - 'pseudonymization_id': 'PREVIEW', 'seat': None, 'canceled': False, 'valid_from': None, 'valid_until': None, 'blocked': None} + 'pseudonymization_id': 'PREVIEW', 'seat': None, 'canceled': False, 'valid_from': None, 'valid_until': None, 'blocked': None, + 'plugin_data': {}} ] diff --git a/src/tests/api/test_orders.py b/src/tests/api/test_orders.py index 79d12951d..3b59cfd46 100644 --- a/src/tests/api/test_orders.py +++ b/src/tests/api/test_orders.py @@ -236,6 +236,7 @@ TEST_ORDERPOSITION_RES = { ], "subevent": None, "canceled": False, + "plugin_data": {}, } TEST_PAYMENTS_RES = [ { @@ -333,6 +334,7 @@ TEST_ORDER_RES = { "payments": TEST_PAYMENTS_RES, "refunds": TEST_REFUNDS_RES, "cancellation_date": None, + "plugin_data": {}, } diff --git a/src/tests/api/test_reusable_media.py b/src/tests/api/test_reusable_media.py index 4a0ceea01..a415b3f54 100644 --- a/src/tests/api/test_reusable_media.py +++ b/src/tests/api/test_reusable_media.py @@ -203,7 +203,8 @@ def test_medium_detail(token_client, organizer, event, medium, giftcard, custome "canceled": False, "valid_from": None, "valid_until": None, - "blocked": None + "blocked": None, + "plugin_data": {}, } assert resp.data["linked_giftcard"] == { "id": giftcard.pk,