mirror of
https://github.com/pretix/pretix.git
synced 2025-12-16 15:02:28 +00:00
Compare commits
7 Commits
invoice-em
...
api-expand
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5139eeb03b | ||
|
|
a7edb16fc0 | ||
|
|
f6df03c427 | ||
|
|
308eac20b2 | ||
|
|
ab3c03b278 | ||
|
|
161404f152 | ||
|
|
8b119b329c |
@@ -203,9 +203,35 @@ Query parameters
|
|||||||
Most list endpoints allow a filtering of the results using query parameters. In this case, booleans should be passed
|
Most list endpoints allow a filtering of the results using query parameters. In this case, booleans should be passed
|
||||||
as the string values ``true`` and ``false``.
|
as the string values ``true`` and ``false``.
|
||||||
|
|
||||||
|
Ordering
|
||||||
|
--------
|
||||||
|
|
||||||
If the ``ordering`` parameter is documented for a resource, you can use it to sort the result set by one of the allowed
|
If the ``ordering`` parameter is documented for a resource, you can use it to sort the result set by one of the allowed
|
||||||
fields. Prepend a ``-`` to the field name to reverse the sort order.
|
fields. Prepend a ``-`` to the field name to reverse the sort order.
|
||||||
|
|
||||||
|
Filtering and expanding fields
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
On many endpoints, you can modify what fields are being returned:
|
||||||
|
|
||||||
|
- Using the ``include`` query parameter, you can chose which fields will be returned as part of the response.
|
||||||
|
For example, if you pass ``include=code&include=email`` to the list of orders, you will receive a list of only
|
||||||
|
order codes and email addresses.
|
||||||
|
|
||||||
|
- Using the ``exclude`` query parameter, you can chose which fields will not be returned as part of the response.
|
||||||
|
For example, if you pass ``exclude=payments&exclude=refunds`` to the list of orders, you will receive a list
|
||||||
|
without the payment and refund objects.
|
||||||
|
|
||||||
|
- Using the ``expand`` query parameter, you can chose which fields will be expanded into full objects. For example,
|
||||||
|
if you pass ``expand=voucher`` to the list of order positions, the response will contain a full voucher object
|
||||||
|
instead of just the ID. If you do not have permission to view vouchers, a 403 status code is returned.
|
||||||
|
For performance reasons, this option is only available for a limited number of fields that are noted as
|
||||||
|
"expandable" in the documentation of the respective object.
|
||||||
|
|
||||||
|
In all of these, you can use dotted notation to address fields of sub-objects, such as ``positions.checkins.gate``.
|
||||||
|
|
||||||
|
These options are not available everywhere as we are slowly rolling them out throughout the codebase. Please check
|
||||||
|
the individual endpoint documentation for availability.
|
||||||
|
|
||||||
Idempotency
|
Idempotency
|
||||||
-----------
|
-----------
|
||||||
|
|||||||
@@ -152,6 +152,8 @@ Endpoints
|
|||||||
and ``null`` for "status unknown". These values might be served from a cache. This parameter can make the response
|
and ``null`` for "status unknown". These values might be served from a cache. This parameter can make the response
|
||||||
slow.
|
slow.
|
||||||
:query search: Only return events matching a given search query.
|
:query search: Only return events matching a given search query.
|
||||||
|
:query string include: Limit the output to the given field. Can be passed multiple times.
|
||||||
|
:query string exclude: Exclude a field from the output. Can be passed multiple times.
|
||||||
:param organizer: The ``slug`` field of a valid organizer
|
:param organizer: The ``slug`` field of a valid organizer
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
@@ -223,6 +225,8 @@ Endpoints
|
|||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:param event: The ``slug`` field of the event to fetch
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:query string include: Limit the output to the given field. Can be passed multiple times.
|
||||||
|
:query string exclude: Exclude a field from the output. Can be passed multiple times.
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ name multi-lingual string The item's vi
|
|||||||
internal_name string An optional name that is only used in the backend
|
internal_name string An optional name that is only used in the backend
|
||||||
default_price money (string) The item price that is applied if the price is not
|
default_price money (string) The item price that is applied if the price is not
|
||||||
overwritten by variations or other options.
|
overwritten by variations or other options.
|
||||||
category integer The ID of the category this item belongs to
|
category integer (expandable) The ID of the category this item belongs to
|
||||||
(or ``null``).
|
(or ``null``).
|
||||||
active boolean If ``false``, the item is hidden from all public lists
|
active boolean If ``false``, the item is hidden from all public lists
|
||||||
and will not be sold.
|
and will not be sold.
|
||||||
@@ -33,7 +33,7 @@ free_price_suggestion money (string) A suggested p
|
|||||||
``free_price`` is set (or ``null``).
|
``free_price`` is set (or ``null``).
|
||||||
tax_rate decimal (string) The VAT rate to be applied for this item (read-only,
|
tax_rate decimal (string) The VAT rate to be applied for this item (read-only,
|
||||||
set through ``tax_rule``).
|
set through ``tax_rule``).
|
||||||
tax_rule integer The internal ID of the applied tax rule (or ``null``).
|
tax_rule integer (expandable) The internal ID of the applied tax rule (or ``null``).
|
||||||
admission boolean ``true`` for items that grant admission to the event
|
admission boolean ``true`` for items that grant admission to the event
|
||||||
(such as primary tickets) and ``false`` for others
|
(such as primary tickets) and ``false`` for others
|
||||||
(such as add-ons or merchandise).
|
(such as add-ons or merchandise).
|
||||||
@@ -390,6 +390,9 @@ Endpoints
|
|||||||
will be returned.
|
will be returned.
|
||||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id`` and ``position``.
|
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id`` and ``position``.
|
||||||
Default: ``position``
|
Default: ``position``
|
||||||
|
:query string include: Limit the output to the given field. Can be passed multiple times.
|
||||||
|
:query string exclude: Exclude a field from the output. Can be passed multiple times.
|
||||||
|
:query string expand: Expand an object reference with the referenced object. Can be passed multiple times.
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:param event: The ``slug`` field of the event to fetch
|
:param event: The ``slug`` field of the event to fetch
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
@@ -531,6 +534,9 @@ Endpoints
|
|||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:param event: The ``slug`` field of the event to fetch
|
:param event: The ``slug`` field of the event to fetch
|
||||||
:param id: The ``id`` field of the item to fetch
|
:param id: The ``id`` field of the item to fetch
|
||||||
|
:query string include: Limit the output to the given field. Can be passed multiple times.
|
||||||
|
:query string exclude: Exclude a field from the output. Can be passed multiple times.
|
||||||
|
:query string expand: Expand an object reference with the referenced object. Can be passed multiple times.
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||||
|
|||||||
@@ -157,8 +157,8 @@ order string Order code of t
|
|||||||
positionid integer Number of the position within the order
|
positionid integer Number of the position within the order
|
||||||
canceled boolean Whether or not this position has been canceled. Note that
|
canceled boolean Whether or not this position has been canceled. Note that
|
||||||
by default, only non-canceled positions are shown.
|
by default, only non-canceled positions are shown.
|
||||||
item integer ID of the purchased item
|
item integer (expandable) ID of the purchased item
|
||||||
variation integer ID of the purchased variation (or ``null``)
|
variation integer (expandable) ID of the purchased variation (or ``null``)
|
||||||
price money (string) Price of this position
|
price money (string) Price of this position
|
||||||
attendee_name string Specified attendee name for this position (or ``null``)
|
attendee_name string Specified attendee name for this position (or ``null``)
|
||||||
attendee_name_parts object of strings Decomposition of attendee name (i.e. given name, family name)
|
attendee_name_parts object of strings Decomposition of attendee name (i.e. given name, family name)
|
||||||
@@ -170,7 +170,7 @@ city string Attendee city (
|
|||||||
country string Attendee country code (or ``null``)
|
country string Attendee country code (or ``null``)
|
||||||
state string Attendee state (ISO 3166-2 code). Only supported in
|
state string Attendee state (ISO 3166-2 code). Only supported in
|
||||||
AU, BR, CA, CN, MY, MX, and US, otherwise ``null``.
|
AU, BR, CA, CN, MY, MX, and US, otherwise ``null``.
|
||||||
voucher integer Internal ID of the voucher used for this position (or ``null``)
|
voucher integer (expandable) Internal ID of the voucher used for this position (or ``null``)
|
||||||
voucher_budget_use money (string) Amount of money discounted by the voucher, corresponding
|
voucher_budget_use money (string) Amount of money discounted by the voucher, corresponding
|
||||||
to how much of the ``budget`` of the voucher is consumed.
|
to how much of the ``budget`` of the voucher is consumed.
|
||||||
**Important:** Do not rely on this amount to be a useful
|
**Important:** Do not rely on this amount to be a useful
|
||||||
@@ -182,7 +182,7 @@ tax_code string Codified reason
|
|||||||
tax_rule integer The ID of the used tax rule (or ``null``)
|
tax_rule integer The ID of the used tax rule (or ``null``)
|
||||||
secret string Secret code printed on the tickets for validation
|
secret string Secret code printed on the tickets for validation
|
||||||
addon_to integer Internal ID of the position this position is an add-on for (or ``null``)
|
addon_to integer Internal ID of the position this position is an add-on for (or ``null``)
|
||||||
subevent integer ID of the date inside an event series this position belongs to (or ``null``).
|
subevent integer (expandable) ID of the date inside an event series this position belongs to (or ``null``).
|
||||||
discount integer ID of a discount that has been used during the creation of this position in some way (or ``null``).
|
discount integer ID of a discount that has been used during the creation of this position in some way (or ``null``).
|
||||||
blocked list of strings A list of strings, or ``null``. Whenever not ``null``, the ticket may not be used (e.g. for check-in).
|
blocked list of strings A list of strings, or ``null``. Whenever not ``null``, the ticket may not be used (e.g. for check-in).
|
||||||
valid_from datetime The ticket will not be valid before this time. Can be ``null``.
|
valid_from datetime The ticket will not be valid before this time. Can be ``null``.
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ Endpoints
|
|||||||
:query page: The page number in case of a multi-page result set, default is 1
|
:query 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 ``slug`` and
|
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``slug`` and
|
||||||
``name``. Default: ``slug``.
|
``name``. Default: ``slug``.
|
||||||
|
:query string include: Limit the output to the given field. Can be passed multiple times.
|
||||||
|
:query string exclude: Exclude a field from the output. Can be passed multiple times.
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
|
|
||||||
@@ -91,6 +93,8 @@ Endpoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:query string include: Limit the output to the given field. Can be passed multiple times.
|
||||||
|
:query string exclude: Exclude a field from the output. Can be passed multiple times.
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||||
|
|||||||
@@ -146,10 +146,70 @@ Endpoints
|
|||||||
attribute with values of 100 for "tickets available", values less than 100 for "tickets sold out or reserved",
|
attribute with values of 100 for "tickets available", values less than 100 for "tickets sold out or reserved",
|
||||||
and ``null`` for "status unknown". These values might be served from a cache. This parameter can make the response
|
and ``null`` for "status unknown". These values might be served from a cache. This parameter can make the response
|
||||||
slow.
|
slow.
|
||||||
|
:query string include: Limit the output to the given field. Can be passed multiple times.
|
||||||
|
:query string exclude: Exclude a field from the output. Can be passed multiple times.
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/
|
||||||
|
|
||||||
|
Returns information on one sub-event, identified by its ID.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/subevents/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": {"en": "First Sample Conference"},
|
||||||
|
"event": "sampleconf",
|
||||||
|
"active": false,
|
||||||
|
"is_public": true,
|
||||||
|
"date_from": "2017-12-27T10:00:00Z",
|
||||||
|
"date_to": null,
|
||||||
|
"date_admission": null,
|
||||||
|
"presale_start": null,
|
||||||
|
"presale_end": null,
|
||||||
|
"location": null,
|
||||||
|
"geo_lat": null,
|
||||||
|
"geo_lon": null,
|
||||||
|
"seating_plan": null,
|
||||||
|
"seat_category_mapping": {},
|
||||||
|
"item_price_overrides": [
|
||||||
|
{
|
||||||
|
"item": 2,
|
||||||
|
"disabled": false,
|
||||||
|
"available_from": null,
|
||||||
|
"available_until": null,
|
||||||
|
"price": "12.00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variation_price_overrides": [],
|
||||||
|
"meta_data": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of a valid organizer
|
||||||
|
:param event: The ``slug`` field of the main event
|
||||||
|
:param id: The ``id`` field of the sub-event to fetch
|
||||||
|
:query string include: Limit the output to the given field. Can be passed multiple times.
|
||||||
|
:query string exclude: Exclude a field from the output. Can be passed multiple times.
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/subevents/
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/subevents/
|
||||||
|
|
||||||
Creates a new subevent.
|
Creates a new subevent.
|
||||||
@@ -237,63 +297,6 @@ Endpoints
|
|||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
||||||
|
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/
|
|
||||||
|
|
||||||
Returns information on one sub-event, identified by its ID.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /api/v1/organizers/bigevents/events/sampleconf/subevents/1/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"name": {"en": "First Sample Conference"},
|
|
||||||
"event": "sampleconf",
|
|
||||||
"active": false,
|
|
||||||
"is_public": true,
|
|
||||||
"date_from": "2017-12-27T10:00:00Z",
|
|
||||||
"date_to": null,
|
|
||||||
"date_admission": null,
|
|
||||||
"presale_start": null,
|
|
||||||
"presale_end": null,
|
|
||||||
"location": null,
|
|
||||||
"geo_lat": null,
|
|
||||||
"geo_lon": null,
|
|
||||||
"seating_plan": null,
|
|
||||||
"seat_category_mapping": {},
|
|
||||||
"item_price_overrides": [
|
|
||||||
{
|
|
||||||
"item": 2,
|
|
||||||
"disabled": false,
|
|
||||||
"available_from": null,
|
|
||||||
"available_until": null,
|
|
||||||
"price": "12.00"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"variation_price_overrides": [],
|
|
||||||
"meta_data": {}
|
|
||||||
}
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of a valid organizer
|
|
||||||
:param event: The ``slug`` field of the main event
|
|
||||||
:param id: The ``id`` field of the sub-event to fetch
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.
|
|
||||||
|
|
||||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/
|
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/
|
||||||
|
|
||||||
Updates a sub-event, identified by its ID. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to
|
Updates a sub-event, identified by its ID. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ dependencies = [
|
|||||||
"django-oauth-toolkit==2.3.*",
|
"django-oauth-toolkit==2.3.*",
|
||||||
"django-otp==1.6.*",
|
"django-otp==1.6.*",
|
||||||
"django-phonenumber-field==7.3.*",
|
"django-phonenumber-field==7.3.*",
|
||||||
"django-redis==5.4.*",
|
"django-redis==6.0.*",
|
||||||
"django-scopes==2.0.*",
|
"django-scopes==2.0.*",
|
||||||
"django-statici18n==2.6.*",
|
"django-statici18n==2.6.*",
|
||||||
"djangorestframework==3.16.*",
|
"djangorestframework==3.16.*",
|
||||||
@@ -88,10 +88,10 @@ dependencies = [
|
|||||||
"pytz-deprecation-shim==0.1.*",
|
"pytz-deprecation-shim==0.1.*",
|
||||||
"pyuca",
|
"pyuca",
|
||||||
"qrcode==8.2",
|
"qrcode==8.2",
|
||||||
"redis==5.2.*",
|
"redis==6.2.*",
|
||||||
"reportlab==4.4.*",
|
"reportlab==4.4.*",
|
||||||
"requests==2.31.*",
|
"requests==2.31.*",
|
||||||
"sentry-sdk==2.29.*",
|
"sentry-sdk==2.30.*",
|
||||||
"sepaxml==2.6.*",
|
"sepaxml==2.6.*",
|
||||||
"stripe==7.9.*",
|
"stripe==7.9.*",
|
||||||
"text-unidecode==1.*",
|
"text-unidecode==1.*",
|
||||||
@@ -110,7 +110,7 @@ dev = [
|
|||||||
"aiohttp==3.12.*",
|
"aiohttp==3.12.*",
|
||||||
"coverage",
|
"coverage",
|
||||||
"coveralls",
|
"coveralls",
|
||||||
"fakeredis==2.26.*",
|
"fakeredis==2.30.*",
|
||||||
"flake8==7.2.*",
|
"flake8==7.2.*",
|
||||||
"freezegun",
|
"freezegun",
|
||||||
"isort==6.0.*",
|
"isort==6.0.*",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import json
|
|||||||
|
|
||||||
from django.db.models import prefetch_related_objects
|
from django.db.models import prefetch_related_objects
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import PermissionDenied, ValidationError
|
||||||
|
|
||||||
|
|
||||||
class AsymmetricField(serializers.Field):
|
class AsymmetricField(serializers.Field):
|
||||||
@@ -132,6 +132,136 @@ class SalesChannelMigrationMixin:
|
|||||||
s.identifier for s in
|
s.identifier for s in
|
||||||
self.organizer.sales_channels.all()
|
self.organizer.sales_channels.all()
|
||||||
])
|
])
|
||||||
else:
|
elif "limit_sales_channels" in value:
|
||||||
value["sales_channels"] = value["limit_sales_channels"]
|
value["sales_channels"] = value["limit_sales_channels"]
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigurableSerializerMixin:
|
||||||
|
expand_fields = {}
|
||||||
|
|
||||||
|
def get_exclude_requests(self):
|
||||||
|
if hasattr(self, "initial_data"):
|
||||||
|
# Do not support include requests when the serializer is used for writing
|
||||||
|
# TODO: think about this
|
||||||
|
return set()
|
||||||
|
if getattr(self, "parent", None):
|
||||||
|
# Field selection is always handled by top-level serializer
|
||||||
|
return set()
|
||||||
|
if 'exclude' in self.context:
|
||||||
|
return self.context['exclude']
|
||||||
|
elif 'request' in self.context:
|
||||||
|
return self.context['request'].query_params.getlist('exclude')
|
||||||
|
raise TypeError("Could not discover list of fields to exclude")
|
||||||
|
|
||||||
|
def get_include_requests(self):
|
||||||
|
if hasattr(self, "initial_data"):
|
||||||
|
# Do not support include requests when the serializer is used for writing
|
||||||
|
# TODO: think about this
|
||||||
|
return set()
|
||||||
|
if getattr(self, "parent", None):
|
||||||
|
# Field selection is always handled by top-level serializer
|
||||||
|
return set()
|
||||||
|
if 'include' in self.context:
|
||||||
|
return self.context['include']
|
||||||
|
elif 'request' in self.context:
|
||||||
|
return self.context['request'].query_params.getlist('include')
|
||||||
|
raise TypeError("Could not discover list of fields to include")
|
||||||
|
|
||||||
|
def get_expand_requests(self):
|
||||||
|
if hasattr(self, "initial_data"):
|
||||||
|
# Do not support expand requests when the serializer is used for writing
|
||||||
|
# TODO: think about this
|
||||||
|
return set()
|
||||||
|
if getattr(self, "parent", None):
|
||||||
|
# Field selection is always handled by top-level serializer
|
||||||
|
return set()
|
||||||
|
if 'expand' in self.context:
|
||||||
|
return self.context['expand']
|
||||||
|
elif 'request' in self.context:
|
||||||
|
return self.context['request'].query_params.getlist('expand')
|
||||||
|
raise TypeError("Could not discover list of fields to expand")
|
||||||
|
|
||||||
|
def _exclude_field(self, serializer, path):
|
||||||
|
if path[0] not in serializer.fields:
|
||||||
|
return # field does not exist, nothing to do
|
||||||
|
|
||||||
|
if len(path) == 1:
|
||||||
|
del serializer.fields[path[0]]
|
||||||
|
elif len(path) >= 2 and hasattr(serializer.fields[path[0]], "child"):
|
||||||
|
self._exclude_field(serializer.fields[path[0]].child, path[1:])
|
||||||
|
elif len(path) >= 2 and isinstance(serializer.fields[path[0]], serializers.Serializer):
|
||||||
|
self._exclude_field(serializer.fields[path[0]], path[1:])
|
||||||
|
|
||||||
|
def _filter_fields_to_included(self, serializer, includes):
|
||||||
|
any_field_remaining = False
|
||||||
|
for fname, field in list(serializer.fields.items()):
|
||||||
|
if fname in includes:
|
||||||
|
any_field_remaining = True
|
||||||
|
continue
|
||||||
|
elif hasattr(field, 'child'): # Nested list serializers
|
||||||
|
child_includes = {i.removeprefix(f'{fname}.') for i in includes if i.startswith(f'{fname}.')}
|
||||||
|
if child_includes and self._filter_fields_to_included(field.child, child_includes):
|
||||||
|
any_field_remaining = True
|
||||||
|
continue
|
||||||
|
serializer.fields.pop(fname)
|
||||||
|
elif isinstance(field, serializers.Serializer): # Nested serializers
|
||||||
|
child_includes = {i.removeprefix(f'{fname}.') for i in includes if i.startswith(f'{fname}.')}
|
||||||
|
if child_includes and self._filter_fields_to_included(field, child_includes):
|
||||||
|
any_field_remaining = True
|
||||||
|
continue
|
||||||
|
serializer.fields.pop(fname)
|
||||||
|
else:
|
||||||
|
serializer.fields.pop(fname)
|
||||||
|
return any_field_remaining
|
||||||
|
|
||||||
|
def _expand_field(self, serializer, path, original_field):
|
||||||
|
if path[0] not in serializer.fields or not self.is_field_expandable(original_field):
|
||||||
|
return False # field does not exist, nothing to do
|
||||||
|
|
||||||
|
if len(path) == 1:
|
||||||
|
serializer.fields[path[0]] = self.get_expand_serializer(original_field)
|
||||||
|
return True
|
||||||
|
elif len(path) >= 2 and hasattr(serializer.fields[path[0]], "child"):
|
||||||
|
return self._expand_field(serializer.fields[path[0]].child, path[1:], original_field)
|
||||||
|
elif len(path) >= 2 and isinstance(serializer.fields[path[0]], serializers.Serializer):
|
||||||
|
return self._expand_field(serializer.fields[path[0]], path[1:], original_field)
|
||||||
|
|
||||||
|
def is_field_expandable(self, field):
|
||||||
|
return field in self.expand_fields
|
||||||
|
|
||||||
|
def get_expand_serializer(self, field):
|
||||||
|
from pretix.base.models import Device, TeamAPIToken
|
||||||
|
|
||||||
|
ef = self.expand_fields[field]
|
||||||
|
if "permission" in ef:
|
||||||
|
request = self.context["request"]
|
||||||
|
perm_holder = request.auth if isinstance(request.auth, (Device, TeamAPIToken)) else request.user
|
||||||
|
if not perm_holder.has_event_permission(request.organizer, request.event, ef["permission"], request=request):
|
||||||
|
raise PermissionDenied(f"No permission to expand field {field}")
|
||||||
|
|
||||||
|
if hasattr(self, "instance") and "prefetch" in ef:
|
||||||
|
for prefetch in ef["prefetch"]:
|
||||||
|
prefetch_related_objects(
|
||||||
|
self.instance if hasattr(self.instance, '__iter__') else [self.instance],
|
||||||
|
prefetch
|
||||||
|
)
|
||||||
|
|
||||||
|
return ef["serializer"](
|
||||||
|
read_only=True,
|
||||||
|
context=self.context,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
expanded = False
|
||||||
|
for expand in sorted(list(self.get_expand_requests())):
|
||||||
|
expanded = self._expand_field(self, expand.split('.'), expand) or expanded
|
||||||
|
|
||||||
|
includes = set(self.get_include_requests())
|
||||||
|
if includes:
|
||||||
|
self._filter_fields_to_included(self, includes)
|
||||||
|
|
||||||
|
for exclude_field in self.get_exclude_requests():
|
||||||
|
self._exclude_field(self, exclude_field.split('.'))
|
||||||
|
|||||||
@@ -23,15 +23,19 @@ from django.utils.translation import gettext as _
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
|
from pretix.api.serializers import ConfigurableSerializerMixin
|
||||||
from pretix.api.serializers.event import SubEventSerializer
|
from pretix.api.serializers.event import SubEventSerializer
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.base.media import MEDIA_TYPES
|
from pretix.base.media import MEDIA_TYPES
|
||||||
from pretix.base.models import Checkin, CheckinList
|
from pretix.base.models import Checkin, CheckinList
|
||||||
|
|
||||||
|
|
||||||
class CheckinListSerializer(I18nAwareModelSerializer):
|
class CheckinListSerializer(ConfigurableSerializerMixin, I18nAwareModelSerializer):
|
||||||
checkin_count = serializers.IntegerField(read_only=True)
|
checkin_count = serializers.IntegerField(read_only=True)
|
||||||
position_count = serializers.IntegerField(read_only=True)
|
position_count = serializers.IntegerField(read_only=True)
|
||||||
|
expand_fields = {
|
||||||
|
"subevent": SubEventSerializer,
|
||||||
|
}
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CheckinList
|
model = CheckinList
|
||||||
@@ -42,17 +46,6 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
if 'subevent' in self.context['request'].query_params.getlist('expand'):
|
|
||||||
self.fields['subevent'] = SubEventSerializer(read_only=True)
|
|
||||||
|
|
||||||
for exclude_field in self.context['request'].query_params.getlist('exclude'):
|
|
||||||
p = exclude_field.split('.')
|
|
||||||
if p[0] in self.fields:
|
|
||||||
if len(p) == 1:
|
|
||||||
del self.fields[p[0]]
|
|
||||||
elif len(p) == 2:
|
|
||||||
self.fields[p[0]].child.fields.pop(p[1])
|
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
event = self.context['event']
|
event = self.context['event']
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ from rest_framework.fields import ChoiceField, Field
|
|||||||
from rest_framework.relations import SlugRelatedField
|
from rest_framework.relations import SlugRelatedField
|
||||||
|
|
||||||
from pretix.api.serializers import (
|
from pretix.api.serializers import (
|
||||||
CompatibleJSONField, SalesChannelMigrationMixin,
|
CompatibleJSONField, ConfigurableSerializerMixin,
|
||||||
|
SalesChannelMigrationMixin,
|
||||||
)
|
)
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.api.serializers.settings import SettingsSerializer
|
from pretix.api.serializers.settings import SettingsSerializer
|
||||||
@@ -167,7 +168,7 @@ class ValidKeysField(Field):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
class EventSerializer(SalesChannelMigrationMixin, ConfigurableSerializerMixin, I18nAwareModelSerializer):
|
||||||
meta_data = MetaDataField(required=False, source='*')
|
meta_data = MetaDataField(required=False, source='*')
|
||||||
item_meta_properties = MetaPropertyField(required=False, source='*')
|
item_meta_properties = MetaPropertyField(required=False, source='*')
|
||||||
plugins = PluginsField(required=False, source='*')
|
plugins = PluginsField(required=False, source='*')
|
||||||
@@ -198,10 +199,11 @@ class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
if not hasattr(self.context['request'], 'event'):
|
if not hasattr(self.context['request'], 'event'):
|
||||||
self.fields.pop('valid_keys')
|
self.fields.pop('valid_keys', None)
|
||||||
if not self.context.get('request') or 'with_availability_for' not in self.context['request'].GET:
|
if not self.context.get('request') or 'with_availability_for' not in self.context['request'].GET:
|
||||||
self.fields.pop('best_availability_state')
|
self.fields.pop('best_availability_state', None)
|
||||||
self.fields['limit_sales_channels'].child_relation.queryset = self.context['organizer'].sales_channels.all()
|
if 'limit_sales_channels' in self.fields:
|
||||||
|
self.fields['limit_sales_channels'].child_relation.queryset = self.context['organizer'].sales_channels.all()
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
@@ -483,7 +485,7 @@ class SubEventItemVariationSerializer(I18nAwareModelSerializer):
|
|||||||
fields = ('variation', 'price', 'disabled', 'available_from', 'available_until')
|
fields = ('variation', 'price', 'disabled', 'available_from', 'available_until')
|
||||||
|
|
||||||
|
|
||||||
class SubEventSerializer(I18nAwareModelSerializer):
|
class SubEventSerializer(ConfigurableSerializerMixin, I18nAwareModelSerializer):
|
||||||
item_price_overrides = SubEventItemSerializer(source='subeventitem_set', many=True, required=False)
|
item_price_overrides = SubEventItemSerializer(source='subeventitem_set', many=True, required=False)
|
||||||
variation_price_overrides = SubEventItemVariationSerializer(source='subeventitemvariation_set', many=True, required=False)
|
variation_price_overrides = SubEventItemVariationSerializer(source='subeventitemvariation_set', many=True, required=False)
|
||||||
seat_category_mapping = SeatCategoryMappingField(source='*', required=False)
|
seat_category_mapping = SeatCategoryMappingField(source='*', required=False)
|
||||||
@@ -502,7 +504,7 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
if not self.context.get('request') or 'with_availability_for' not in self.context['request'].GET:
|
if not self.context.get('request') or 'with_availability_for' not in self.context['request'].GET:
|
||||||
self.fields.pop('best_availability_state')
|
self.fields.pop('best_availability_state', None)
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
|
|||||||
@@ -42,8 +42,10 @@ from django.utils.functional import cached_property, lazy
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from pretix.api.serializers import SalesChannelMigrationMixin
|
from pretix.api.serializers import (
|
||||||
from pretix.api.serializers.event import MetaDataField
|
ConfigurableSerializerMixin, SalesChannelMigrationMixin,
|
||||||
|
)
|
||||||
|
from pretix.api.serializers.event import MetaDataField, TaxRuleSerializer
|
||||||
from pretix.api.serializers.fields import UploadedFileField
|
from pretix.api.serializers.fields import UploadedFileField
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
@@ -246,7 +248,29 @@ class ItemTaxRateField(serializers.Field):
|
|||||||
return str(Decimal('0.00'))
|
return str(Decimal('0.00'))
|
||||||
|
|
||||||
|
|
||||||
class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
class ItemCategorySerializer(I18nAwareModelSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ItemCategory
|
||||||
|
fields = (
|
||||||
|
'id', 'name', 'internal_name', 'description', 'position',
|
||||||
|
'is_addon', 'cross_selling_mode',
|
||||||
|
'cross_selling_condition', 'cross_selling_match_products'
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
data = super().validate(data)
|
||||||
|
|
||||||
|
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||||
|
full_data.update(data)
|
||||||
|
|
||||||
|
if full_data.get('is_addon') and full_data.get('cross_selling_mode'):
|
||||||
|
raise ValidationError('is_addon and cross_selling_mode are mutually exclusive')
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class ItemSerializer(SalesChannelMigrationMixin, ConfigurableSerializerMixin, I18nAwareModelSerializer):
|
||||||
addons = InlineItemAddOnSerializer(many=True, required=False)
|
addons = InlineItemAddOnSerializer(many=True, required=False)
|
||||||
bundles = InlineItemBundleSerializer(many=True, required=False)
|
bundles = InlineItemBundleSerializer(many=True, required=False)
|
||||||
variations = InlineItemVariationSerializer(many=True, required=False)
|
variations = InlineItemVariationSerializer(many=True, required=False)
|
||||||
@@ -262,6 +286,16 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
allow_empty=True,
|
allow_empty=True,
|
||||||
many=True,
|
many=True,
|
||||||
)
|
)
|
||||||
|
expand_fields = {
|
||||||
|
"category": {
|
||||||
|
"serializer": ItemCategorySerializer,
|
||||||
|
"prefetch": ["category"],
|
||||||
|
},
|
||||||
|
"tax_rule": {
|
||||||
|
"serializer": TaxRuleSerializer,
|
||||||
|
"prefetch": ["tax_rule"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Item
|
model = Item
|
||||||
@@ -284,13 +318,18 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['default_price'].allow_null = False
|
if 'default_price' in self.fields:
|
||||||
self.fields['default_price'].required = True
|
self.fields['default_price'].allow_null = False
|
||||||
|
self.fields['default_price'].required = True
|
||||||
if not self.read_only:
|
if not self.read_only:
|
||||||
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
|
if 'require_membership_types' in self.fields:
|
||||||
self.fields['grant_membership_type'].queryset = self.context['event'].organizer.membership_types.all()
|
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
|
||||||
self.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
if 'grant_membership_type' in self.fields:
|
||||||
self.fields['variations'].child.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
self.fields['grant_membership_type'].queryset = self.context['event'].organizer.membership_types.all()
|
||||||
|
if 'limit_sales_channels' in self.fields:
|
||||||
|
self.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||||
|
if 'variations' in self.fields and 'limit_sales_channels' in self.fields['variations'].child.fields:
|
||||||
|
self.fields['variations'].child.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
@@ -437,28 +476,6 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
class ItemCategorySerializer(I18nAwareModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ItemCategory
|
|
||||||
fields = (
|
|
||||||
'id', 'name', 'internal_name', 'description', 'position',
|
|
||||||
'is_addon', 'cross_selling_mode',
|
|
||||||
'cross_selling_condition', 'cross_selling_match_products'
|
|
||||||
)
|
|
||||||
|
|
||||||
def validate(self, data):
|
|
||||||
data = super().validate(data)
|
|
||||||
|
|
||||||
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
|
||||||
full_data.update(data)
|
|
||||||
|
|
||||||
if full_data.get('is_addon') and full_data.get('cross_selling_mode'):
|
|
||||||
raise ValidationError('is_addon and cross_selling_mode are mutually exclusive')
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class QuestionOptionSerializer(I18nAwareModelSerializer):
|
class QuestionOptionSerializer(I18nAwareModelSerializer):
|
||||||
identifier = serializers.CharField(allow_null=True)
|
identifier = serializers.CharField(allow_null=True)
|
||||||
|
|
||||||
|
|||||||
@@ -40,12 +40,15 @@ from rest_framework.exceptions import ValidationError
|
|||||||
from rest_framework.relations import SlugRelatedField
|
from rest_framework.relations import SlugRelatedField
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
|
|
||||||
from pretix.api.serializers import CompatibleJSONField
|
from pretix.api.serializers import (
|
||||||
|
CompatibleJSONField, ConfigurableSerializerMixin,
|
||||||
|
)
|
||||||
from pretix.api.serializers.event import SubEventSerializer
|
from pretix.api.serializers.event import SubEventSerializer
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.api.serializers.item import (
|
from pretix.api.serializers.item import (
|
||||||
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
|
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
|
||||||
)
|
)
|
||||||
|
from pretix.api.serializers.voucher import VoucherSerializer
|
||||||
from pretix.api.signals import order_api_details, orderposition_api_details
|
from pretix.api.signals import order_api_details, orderposition_api_details
|
||||||
from pretix.base.decimal import round_decimal
|
from pretix.base.decimal import round_decimal
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
@@ -175,7 +178,7 @@ class AnswerSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
r = super().to_representation(instance)
|
r = super().to_representation(instance)
|
||||||
if r['answer'].startswith('file://') and instance.orderposition:
|
if r.get('answer') and r.get('answer').startswith('file://') and instance.orderposition:
|
||||||
r['answer'] = reverse('api-v1:orderposition-answer', kwargs={
|
r['answer'] = reverse('api-v1:orderposition-answer', kwargs={
|
||||||
'organizer': instance.orderposition.order.event.organizer.slug,
|
'organizer': instance.orderposition.order.event.organizer.slug,
|
||||||
'event': instance.orderposition.order.event.slug,
|
'event': instance.orderposition.order.event.slug,
|
||||||
@@ -757,7 +760,7 @@ class OrderPluginDataField(serializers.Field):
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
class OrderSerializer(I18nAwareModelSerializer):
|
class OrderSerializer(ConfigurableSerializerMixin, I18nAwareModelSerializer):
|
||||||
event = SlugRelatedField(slug_field='slug', read_only=True)
|
event = SlugRelatedField(slug_field='slug', read_only=True)
|
||||||
invoice_address = InvoiceAddressSerializer(allow_null=True)
|
invoice_address = InvoiceAddressSerializer(allow_null=True)
|
||||||
positions = OrderPositionSerializer(many=True, read_only=True)
|
positions = OrderPositionSerializer(many=True, read_only=True)
|
||||||
@@ -775,6 +778,39 @@ class OrderSerializer(I18nAwareModelSerializer):
|
|||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
plugin_data = OrderPluginDataField(source='*', allow_null=True, read_only=True)
|
plugin_data = OrderPluginDataField(source='*', allow_null=True, read_only=True)
|
||||||
|
expand_fields = {
|
||||||
|
"positions.voucher": {
|
||||||
|
"serializer": VoucherSerializer,
|
||||||
|
"permission": "can_view_vouchers",
|
||||||
|
"prefetch": ["positions__voucher"],
|
||||||
|
},
|
||||||
|
"positions.item": {
|
||||||
|
"serializer": ItemSerializer,
|
||||||
|
"prefetch": [
|
||||||
|
"positions__item",
|
||||||
|
"positions__item__addons",
|
||||||
|
"positions__item__bundles",
|
||||||
|
"positions__item__meta_values",
|
||||||
|
"positions__item__variations",
|
||||||
|
"positions__item__tax_rule",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"positions.variation": {
|
||||||
|
"serializer": ItemSerializer,
|
||||||
|
"prefetch": ["positions__variation", "positions__variation__meta_values"],
|
||||||
|
},
|
||||||
|
"positions.subevent": {
|
||||||
|
"serializer": SubEventSerializer,
|
||||||
|
"prefetch": [
|
||||||
|
"positions__subevent",
|
||||||
|
"positions__subevent__event",
|
||||||
|
"positions__subevent__subeventitem_set",
|
||||||
|
"positions__subevent__subeventitemvariation_set",
|
||||||
|
"positions__subevent__seat_category_mappings",
|
||||||
|
"positions__subevent__meta_values",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Order
|
model = Order
|
||||||
@@ -793,47 +829,14 @@ class OrderSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
if "organizer" in self.context:
|
if "sales_channel" in self.fields:
|
||||||
self.fields["sales_channel"].queryset = self.context["organizer"].sales_channels.all()
|
if "organizer" in self.context:
|
||||||
else:
|
self.fields["sales_channel"].queryset = self.context["organizer"].sales_channels.all()
|
||||||
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
else:
|
||||||
if not self.context['pdf_data']:
|
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
||||||
|
if not self.context['pdf_data'] and "positions" in self.fields:
|
||||||
self.fields['positions'].child.fields.pop('pdf_data', None)
|
self.fields['positions'].child.fields.pop('pdf_data', None)
|
||||||
|
|
||||||
includes = set(self.context['include'])
|
|
||||||
if includes:
|
|
||||||
for fname, field in list(self.fields.items()):
|
|
||||||
if fname in includes:
|
|
||||||
continue
|
|
||||||
elif hasattr(field, 'child'): # Nested list serializers
|
|
||||||
found_any = False
|
|
||||||
for childfname, childfield in list(field.child.fields.items()):
|
|
||||||
if f'{fname}.{childfname}' not in includes:
|
|
||||||
field.child.fields.pop(childfname)
|
|
||||||
else:
|
|
||||||
found_any = True
|
|
||||||
if not found_any:
|
|
||||||
self.fields.pop(fname)
|
|
||||||
elif isinstance(field, serializers.Serializer): # Nested serializers
|
|
||||||
found_any = False
|
|
||||||
for childfname, childfield in list(field.fields.items()):
|
|
||||||
if f'{fname}.{childfname}' not in includes:
|
|
||||||
field.fields.pop(childfname)
|
|
||||||
else:
|
|
||||||
found_any = True
|
|
||||||
if not found_any:
|
|
||||||
self.fields.pop(fname)
|
|
||||||
else:
|
|
||||||
self.fields.pop(fname)
|
|
||||||
|
|
||||||
for exclude_field in self.context['exclude']:
|
|
||||||
p = exclude_field.split('.')
|
|
||||||
if p[0] in self.fields:
|
|
||||||
if len(p) == 1:
|
|
||||||
del self.fields[p[0]]
|
|
||||||
elif len(p) == 2:
|
|
||||||
self.fields[p[0]].child.fields.pop(p[1])
|
|
||||||
|
|
||||||
def validate_locale(self, l):
|
def validate_locale(self, l):
|
||||||
if l not in set(k for k in self.instance.event.settings.locales):
|
if l not in set(k for k in self.instance.event.settings.locales):
|
||||||
raise ValidationError('"{}" is not a supported locale for this event.'.format(l))
|
raise ValidationError('"{}" is not a supported locale for this event.'.format(l))
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ from rest_framework import serializers
|
|||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
from pretix.api.auth.devicesecurity import get_all_security_profiles
|
from pretix.api.auth.devicesecurity import get_all_security_profiles
|
||||||
from pretix.api.serializers import AsymmetricField
|
from pretix.api.serializers import AsymmetricField, ConfigurableSerializerMixin
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.api.serializers.order import CompatibleJSONField
|
from pretix.api.serializers.order import CompatibleJSONField
|
||||||
from pretix.api.serializers.settings import SettingsSerializer
|
from pretix.api.serializers.settings import SettingsSerializer
|
||||||
@@ -51,7 +51,7 @@ from pretix.multidomain.urlreverse import build_absolute_uri
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class OrganizerSerializer(I18nAwareModelSerializer):
|
class OrganizerSerializer(ConfigurableSerializerMixin, I18nAwareModelSerializer):
|
||||||
public_url = serializers.SerializerMethodField('get_organizer_url', read_only=True)
|
public_url = serializers.SerializerMethodField('get_organizer_url', read_only=True)
|
||||||
|
|
||||||
def get_organizer_url(self, organizer):
|
def get_organizer_url(self, organizer):
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
ctx['event'] = self.request.event
|
ctx['event'] = self.request.event
|
||||||
|
ctx['request'] = self.request
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
|
|||||||
@@ -548,24 +548,23 @@ class OrderDetail(OrderView):
|
|||||||
|
|
||||||
unsent_invoices = [ii.pk for ii in ctx['invoices'] if not ii.sent_to_customer]
|
unsent_invoices = [ii.pk for ii in ctx['invoices'] if not ii.sent_to_customer]
|
||||||
if unsent_invoices:
|
if unsent_invoices:
|
||||||
with language(self.order.locale):
|
ctx['invoices_send_link'] = reverse('control:event.order.sendmail', kwargs={
|
||||||
ctx['invoices_send_link'] = reverse('control:event.order.sendmail', kwargs={
|
'event': self.request.event.slug,
|
||||||
'event': self.request.event.slug,
|
'organizer': self.request.event.organizer.slug,
|
||||||
'organizer': self.request.event.organizer.slug,
|
'code': self.order.code
|
||||||
'code': self.order.code
|
}) + '?' + urlencode({
|
||||||
}) + '?' + urlencode({
|
'subject': ngettext('Your invoice', 'Your invoices', len(unsent_invoices)),
|
||||||
'subject': ngettext('Your invoice', 'Your invoices', len(unsent_invoices)),
|
'message': ngettext(
|
||||||
'message': ngettext(
|
'Hello,\n\nplease find your invoice attached to this email.\n\n'
|
||||||
'Hello,\n\nplease find your invoice attached to this email.\n\n'
|
'Your {event} team',
|
||||||
'Your {event} team',
|
'Hello,\n\nplease find your invoices attached to this email.\n\n'
|
||||||
'Hello,\n\nplease find your invoices attached to this email.\n\n'
|
'Your {event} team',
|
||||||
'Your {event} team',
|
len(unsent_invoices)
|
||||||
len(unsent_invoices)
|
).format(
|
||||||
).format(
|
event="{event}",
|
||||||
event="{event}",
|
),
|
||||||
),
|
'attach_invoices': unsent_invoices
|
||||||
'attach_invoices': unsent_invoices
|
}, doseq=True)
|
||||||
}, doseq=True)
|
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|||||||
13
src/pretix/static/npm_dir/package-lock.json
generated
13
src/pretix/static/npm_dir/package-lock.json
generated
@@ -1887,9 +1887,10 @@
|
|||||||
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
|
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
@@ -5009,9 +5010,9 @@
|
|||||||
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
|
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
|
||||||
},
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ from django.utils.timezone import now
|
|||||||
from django_countries.fields import Country
|
from django_countries.fields import Country
|
||||||
from django_scopes import scope, scopes_disabled
|
from django_scopes import scope, scopes_disabled
|
||||||
from tests import assert_num_queries
|
from tests import assert_num_queries
|
||||||
|
from tests.api.utils import _test_configurable_serializer
|
||||||
from tests.const import SAMPLE_PNG
|
from tests.const import SAMPLE_PNG
|
||||||
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
@@ -215,6 +216,15 @@ def test_event_list_filter(token_client, organizer, event):
|
|||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert resp.data['count'] == 0
|
assert resp.data['count'] == 0
|
||||||
|
|
||||||
|
_test_configurable_serializer(
|
||||||
|
token_client,
|
||||||
|
"/api/v1/organizers/{}/events/".format(organizer.slug),
|
||||||
|
[
|
||||||
|
"slug", "live", "meta_data", "seating_plan", "item_meta_properties"
|
||||||
|
],
|
||||||
|
expands=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_event_list_name_filter(token_client, organizer, event):
|
def test_event_list_name_filter(token_client, organizer, event):
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ from django.conf import settings
|
|||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django_countries.fields import Country
|
from django_countries.fields import Country
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
from tests.api.utils import _test_configurable_serializer
|
||||||
from tests.const import SAMPLE_PNG
|
from tests.const import SAMPLE_PNG
|
||||||
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
@@ -359,10 +360,17 @@ TEST_ITEM_RES = {
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_item_list(token_client, organizer, event, team, item):
|
def test_item_list(token_client, organizer, event, team, item, taxrule):
|
||||||
cat = event.categories.create(name="foo")
|
cat = event.categories.create(name="foo")
|
||||||
|
cat2 = event.categories.create(name="bar")
|
||||||
|
item.category = cat2
|
||||||
|
item.tax_rule = taxrule
|
||||||
|
item.save()
|
||||||
res = dict(TEST_ITEM_RES)
|
res = dict(TEST_ITEM_RES)
|
||||||
res["id"] = item.pk
|
res["id"] = item.pk
|
||||||
|
res["category"] = cat2.pk
|
||||||
|
res["tax_rule"] = taxrule.pk
|
||||||
|
res["tax_rate"] = "19.00"
|
||||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/'.format(organizer.slug, event.slug))
|
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/'.format(organizer.slug, event.slug))
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert [res] == resp.data['results']
|
assert [res] == resp.data['results']
|
||||||
@@ -400,11 +408,11 @@ def test_item_list(token_client, organizer, event, team, item):
|
|||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert [] == resp.data['results']
|
assert [] == resp.data['results']
|
||||||
|
|
||||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/?tax_rate=0'.format(organizer.slug, event.slug))
|
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/?tax_rate=19'.format(organizer.slug, event.slug))
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert [res] == resp.data['results']
|
assert [res] == resp.data['results']
|
||||||
|
|
||||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/?tax_rate=19'.format(organizer.slug, event.slug))
|
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/?tax_rate=0'.format(organizer.slug, event.slug))
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert [] == resp.data['results']
|
assert [] == resp.data['results']
|
||||||
|
|
||||||
@@ -419,6 +427,15 @@ def test_item_list(token_client, organizer, event, team, item):
|
|||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert [] == resp.data['results']
|
assert [] == resp.data['results']
|
||||||
|
|
||||||
|
_test_configurable_serializer(
|
||||||
|
token_client,
|
||||||
|
"/api/v1/organizers/{}/events/{}/items/".format(organizer.slug, event.slug),
|
||||||
|
[
|
||||||
|
"name", "free_price", "variations",
|
||||||
|
],
|
||||||
|
expands=["category", "tax_rule"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_item_detail(token_client, organizer, event, team, item):
|
def test_item_detail(token_client, organizer, event, team, item):
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ from django.utils.timezone import now
|
|||||||
from django_countries.fields import Country
|
from django_countries.fields import Country
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from stripe import error
|
from stripe import error
|
||||||
|
from tests.api.utils import _test_configurable_serializer
|
||||||
from tests.plugins.stripe.test_checkout import apple_domain_create
|
from tests.plugins.stripe.test_checkout import apple_domain_create
|
||||||
from tests.plugins.stripe.test_provider import MockedCharge
|
from tests.plugins.stripe.test_provider import MockedCharge
|
||||||
|
|
||||||
@@ -400,13 +401,18 @@ def test_order_list_filter_subevent_date(token_client, device, organizer, event,
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_order_list(token_client, organizer, event, order, item, taxrule, question, device):
|
def test_order_list(token_client, organizer, event, order, item, team, taxrule, question, device):
|
||||||
res = dict(TEST_ORDER_RES)
|
res = dict(TEST_ORDER_RES)
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
|
voucher = event.vouchers.create(code="FOO")
|
||||||
|
opos = order.positions.first()
|
||||||
|
opos.voucher = voucher
|
||||||
|
opos.save()
|
||||||
res["positions"][0]["id"] = order.positions.first().pk
|
res["positions"][0]["id"] = order.positions.first().pk
|
||||||
res["fees"][0]["id"] = order.fees.first().pk
|
res["fees"][0]["id"] = order.fees.first().pk
|
||||||
res["positions"][0]["print_logs"][0]["id"] = order.positions.first().print_logs.first().pk
|
res["positions"][0]["print_logs"][0]["id"] = order.positions.first().print_logs.first().pk
|
||||||
res["positions"][0]["print_logs"][0]["device_id"] = device.device_id
|
res["positions"][0]["print_logs"][0]["device_id"] = device.device_id
|
||||||
|
res["positions"][0]["voucher"] = voucher.pk
|
||||||
res["positions"][0]["item"] = item.pk
|
res["positions"][0]["item"] = item.pk
|
||||||
res["positions"][0]["answers"][0]["question"] = question.pk
|
res["positions"][0]["answers"][0]["question"] = question.pk
|
||||||
res["last_modified"] = order.last_modified.isoformat().replace('+00:00', 'Z')
|
res["last_modified"] = order.last_modified.isoformat().replace('+00:00', 'Z')
|
||||||
@@ -514,6 +520,22 @@ def test_order_list(token_client, organizer, event, order, item, taxrule, questi
|
|||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert len(resp.data['results'][0]['fees']) == 2
|
assert len(resp.data['results'][0]['fees']) == 2
|
||||||
|
|
||||||
|
_test_configurable_serializer(
|
||||||
|
token_client,
|
||||||
|
"/api/v1/organizers/{}/events/{}/orders/".format(organizer.slug, event.slug),
|
||||||
|
[
|
||||||
|
"status", "invoice_address.company", "fees.value", "payments.state",
|
||||||
|
"positions.print_logs.type", "positions.answers.answer"
|
||||||
|
],
|
||||||
|
expands=["positions.voucher"],
|
||||||
|
)
|
||||||
|
|
||||||
|
team.can_view_vouchers = False
|
||||||
|
team.save()
|
||||||
|
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?expand=positions.voucher'.format(organizer.slug, event.slug))
|
||||||
|
assert resp.status_code == 403
|
||||||
|
assert resp.data["detail"] == "No permission to expand field positions.voucher"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_order_detail(token_client, organizer, event, order, item, taxrule, question):
|
def test_order_detail(token_client, organizer, event, order, item, taxrule, question):
|
||||||
@@ -521,6 +543,7 @@ def test_order_detail(token_client, organizer, event, order, item, taxrule, ques
|
|||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
res["positions"][0]["id"] = order.positions.first().pk
|
res["positions"][0]["id"] = order.positions.first().pk
|
||||||
res["positions"][0]["print_logs"][0]["id"] = order.positions.first().print_logs.first().pk
|
res["positions"][0]["print_logs"][0]["id"] = order.positions.first().print_logs.first().pk
|
||||||
|
res["positions"][0]["print_logs"][0]["device_id"] = order.positions.first().print_logs.first().device_id
|
||||||
res["fees"][0]["id"] = order.fees.first().pk
|
res["fees"][0]["id"] = order.fees.first().pk
|
||||||
res["positions"][0]["item"] = item.pk
|
res["positions"][0]["item"] = item.pk
|
||||||
res["fees"][0]["tax_rule"] = taxrule.pk
|
res["fees"][0]["tax_rule"] = taxrule.pk
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
#
|
#
|
||||||
import pytest
|
import pytest
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
|
from tests.api.utils import _test_configurable_serializer
|
||||||
from tests.const import SAMPLE_PNG
|
from tests.const import SAMPLE_PNG
|
||||||
|
|
||||||
TEST_ORGANIZER_RES = {
|
TEST_ORGANIZER_RES = {
|
||||||
@@ -36,6 +37,15 @@ def test_organizer_list(token_client, organizer):
|
|||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert TEST_ORGANIZER_RES in resp.data['results']
|
assert TEST_ORGANIZER_RES in resp.data['results']
|
||||||
|
|
||||||
|
_test_configurable_serializer(
|
||||||
|
token_client,
|
||||||
|
"/api/v1/organizers/",
|
||||||
|
[
|
||||||
|
"name", "public_url"
|
||||||
|
],
|
||||||
|
expands=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_organizer_detail(token_client, organizer):
|
def test_organizer_detail(token_client, organizer):
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from unittest import mock
|
|||||||
import pytest
|
import pytest
|
||||||
from django_countries.fields import Country
|
from django_countries.fields import Country
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
from tests.api.utils import _test_configurable_serializer
|
||||||
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
InvoiceAddress, ItemVariation, Order, OrderPosition, SeatingPlan, SubEvent,
|
InvoiceAddress, ItemVariation, Order, OrderPosition, SeatingPlan, SubEvent,
|
||||||
@@ -157,6 +158,15 @@ def test_subevent_list(token_client, organizer, event, subevent):
|
|||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert resp.data['results'][0]['best_availability_state'] is None
|
assert resp.data['results'][0]['best_availability_state'] is None
|
||||||
|
|
||||||
|
_test_configurable_serializer(
|
||||||
|
token_client,
|
||||||
|
"/api/v1/organizers/{}/events/{}/subevents/".format(organizer.slug, event.slug),
|
||||||
|
[
|
||||||
|
"name", "active", "item_price_overrides",
|
||||||
|
],
|
||||||
|
expands=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_subevent_list_filter(token_client, organizer, event, subevent):
|
def test_subevent_list_filter(token_client, organizer, event, subevent):
|
||||||
|
|||||||
89
src/tests/api/utils.py
Normal file
89
src/tests/api/utils.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
#
|
||||||
|
# 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 urllib.parse
|
||||||
|
|
||||||
|
|
||||||
|
def _add_params(url, params):
|
||||||
|
url_parts = list(urllib.parse.urlparse(url))
|
||||||
|
query = urllib.parse.parse_qs(url_parts[4])
|
||||||
|
query = [*query, *params]
|
||||||
|
url_parts[4] = urllib.parse.urlencode(query)
|
||||||
|
return urllib.parse.urlunparse(url_parts)
|
||||||
|
|
||||||
|
|
||||||
|
def _find_field_names(d: dict, path):
|
||||||
|
names = set()
|
||||||
|
for k, v in d.items():
|
||||||
|
names.add(".".join([*path, k]))
|
||||||
|
if isinstance(v, dict):
|
||||||
|
names |= _find_field_names(v, path=(*path, k))
|
||||||
|
elif isinstance(v, list) and len(v) > 0 and isinstance(v[0], dict):
|
||||||
|
names |= _find_field_names(v[0], path=(*path, k))
|
||||||
|
return names
|
||||||
|
|
||||||
|
|
||||||
|
def _test_configurable_serializer(client, url, field_name_samples, expands):
|
||||||
|
# Test include
|
||||||
|
resp = client.get(_add_params(url, [("include", f) for f in field_name_samples]))
|
||||||
|
if "results" in resp.data:
|
||||||
|
o = resp.data["results"][0]
|
||||||
|
else:
|
||||||
|
o = resp.data
|
||||||
|
|
||||||
|
found_field_names = _find_field_names(o, tuple())
|
||||||
|
# Assert no unexpected fields
|
||||||
|
for f in found_field_names:
|
||||||
|
depth = f.count(".")
|
||||||
|
assert (f in field_name_samples or
|
||||||
|
any(f.rsplit(".", c)[0] in field_name_samples for c in range(depth + 1)) or
|
||||||
|
any(fn.startswith(f + ".") for fn in field_name_samples))
|
||||||
|
# Assert all fields are there
|
||||||
|
for f in field_name_samples:
|
||||||
|
assert f in found_field_names, f"{f} not in {found_field_names}"
|
||||||
|
|
||||||
|
# Test exclude
|
||||||
|
resp = client.get(_add_params(url, [("exclude", f) for f in field_name_samples]))
|
||||||
|
if "results" in resp.data:
|
||||||
|
o = resp.data["results"][0]
|
||||||
|
else:
|
||||||
|
o = resp.data
|
||||||
|
found_field_names = _find_field_names(o, [])
|
||||||
|
# Assert all fields are not there
|
||||||
|
for f in found_field_names:
|
||||||
|
assert f not in field_name_samples
|
||||||
|
|
||||||
|
# Test expand
|
||||||
|
if expands:
|
||||||
|
resp = client.get(_add_params(url, [("expand", f) for f in expands]))
|
||||||
|
if "results" in resp.data:
|
||||||
|
o = resp.data["results"][0]
|
||||||
|
else:
|
||||||
|
o = resp.data
|
||||||
|
for e in expands:
|
||||||
|
path = e.split(".")
|
||||||
|
obj = o
|
||||||
|
while len(path) > 1:
|
||||||
|
obj = o[path[0]]
|
||||||
|
if isinstance(obj, list):
|
||||||
|
obj = obj[0]
|
||||||
|
path = path[1:]
|
||||||
|
assert isinstance(obj[path[0]], dict), f"{e} is not a dictionary, but {type(obj[path[0]])}"
|
||||||
Reference in New Issue
Block a user