more work

This commit is contained in:
Raphael Michel
2025-06-24 15:01:56 +02:00
parent a7edb16fc0
commit 5139eeb03b
17 changed files with 243 additions and 116 deletions

View File

@@ -152,6 +152,8 @@ Endpoints
and ``null`` for "status unknown". These values might be served from a cache. This parameter can make the response
slow.
: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
:statuscode 200: no error
:statuscode 401: Authentication failure
@@ -223,6 +225,8 @@ Endpoints
:param organizer: The ``slug`` field of the organizer 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 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.

View File

@@ -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
default_price money (string) The item price that is applied if the price is not
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``).
active boolean If ``false``, the item is hidden from all public lists
and will not be sold.
@@ -33,7 +33,7 @@ free_price_suggestion money (string) A suggested p
``free_price`` is set (or ``null``).
tax_rate decimal (string) The VAT rate to be applied for this item (read-only,
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
(such as primary tickets) and ``false`` for others
(such as add-ons or merchandise).
@@ -390,6 +390,9 @@ Endpoints
will be returned.
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id`` and ``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 event: The ``slug`` field of the event to fetch
:statuscode 200: no error
@@ -531,6 +534,9 @@ Endpoints
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event 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 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.

View File

@@ -157,8 +157,8 @@ order string Order code of t
positionid integer Number of the position within the order
canceled boolean Whether or not this position has been canceled. Note that
by default, only non-canceled positions are shown.
item integer ID of the purchased item
variation integer ID of the purchased variation (or ``null``)
item integer (expandable) ID of the purchased item
variation integer (expandable) ID of the purchased variation (or ``null``)
price money (string) Price of this position
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)
@@ -170,7 +170,7 @@ city string Attendee city (
country string Attendee country code (or ``null``)
state string Attendee state (ISO 3166-2 code). Only supported in
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
to how much of the ``budget`` of the voucher is consumed.
**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``)
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``)
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``).
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``.

View File

@@ -93,6 +93,8 @@ Endpoints
}
: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 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.

View File

@@ -146,10 +146,70 @@ Endpoints
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
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 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
.. http:get:: /api/v1/organizers/(organizer)/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/
Creates a new subevent.
@@ -237,63 +297,6 @@ Endpoints
:statuscode 401: Authentication failure
: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)/
Updates a sub-event, identified by its ID. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to

View File

@@ -132,7 +132,7 @@ class SalesChannelMigrationMixin:
s.identifier for s in
self.organizer.sales_channels.all()
])
else:
elif "limit_sales_channels" in value:
value["sales_channels"] = value["limit_sales_channels"]
return value
@@ -145,6 +145,9 @@ class ConfigurableSerializerMixin:
# 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:
@@ -156,6 +159,9 @@ class ConfigurableSerializerMixin:
# 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:
@@ -167,6 +173,9 @@ class ConfigurableSerializerMixin:
# 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:
@@ -231,6 +240,13 @@ class ConfigurableSerializerMixin:
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,
@@ -241,7 +257,7 @@ class ConfigurableSerializerMixin:
expanded = False
for expand in sorted(list(self.get_expand_requests())):
expanded = expanded or self._expand_field(self, expand.split('.'), expand)
expanded = self._expand_field(self, expand.split('.'), expand) or expanded
includes = set(self.get_include_requests())
if includes:

View File

@@ -48,7 +48,8 @@ from rest_framework.fields import ChoiceField, Field
from rest_framework.relations import SlugRelatedField
from pretix.api.serializers import (
CompatibleJSONField, SalesChannelMigrationMixin,
CompatibleJSONField, ConfigurableSerializerMixin,
SalesChannelMigrationMixin,
)
from pretix.api.serializers.i18n import I18nAwareModelSerializer
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='*')
item_meta_properties = MetaPropertyField(required=False, source='*')
plugins = PluginsField(required=False, source='*')
@@ -198,10 +199,11 @@ class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
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:
self.fields.pop('best_availability_state')
self.fields['limit_sales_channels'].child_relation.queryset = self.context['organizer'].sales_channels.all()
self.fields.pop('best_availability_state', None)
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):
data = super().validate(data)
@@ -483,7 +485,7 @@ class SubEventItemVariationSerializer(I18nAwareModelSerializer):
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)
variation_price_overrides = SubEventItemVariationSerializer(source='subeventitemvariation_set', many=True, required=False)
seat_category_mapping = SeatCategoryMappingField(source='*', required=False)
@@ -502,7 +504,7 @@ class SubEventSerializer(I18nAwareModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
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):
data = super().validate(data)

View File

@@ -42,8 +42,10 @@ from django.utils.functional import cached_property, lazy
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from pretix.api.serializers import SalesChannelMigrationMixin
from pretix.api.serializers.event import MetaDataField
from pretix.api.serializers import (
ConfigurableSerializerMixin, SalesChannelMigrationMixin,
)
from pretix.api.serializers.event import MetaDataField, TaxRuleSerializer
from pretix.api.serializers.fields import UploadedFileField
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import (
@@ -246,7 +248,29 @@ class ItemTaxRateField(serializers.Field):
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)
bundles = InlineItemBundleSerializer(many=True, required=False)
variations = InlineItemVariationSerializer(many=True, required=False)
@@ -262,6 +286,16 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
allow_empty=True,
many=True,
)
expand_fields = {
"category": {
"serializer": ItemCategorySerializer,
"prefetch": ["category"],
},
"tax_rule": {
"serializer": TaxRuleSerializer,
"prefetch": ["tax_rule"],
},
}
class Meta:
model = Item
@@ -284,13 +318,18 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['default_price'].allow_null = False
self.fields['default_price'].required = True
if 'default_price' in self.fields:
self.fields['default_price'].allow_null = False
self.fields['default_price'].required = True
if not self.read_only:
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
self.fields['grant_membership_type'].queryset = self.context['event'].organizer.membership_types.all()
self.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
self.fields['variations'].child.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
if 'require_membership_types' in self.fields:
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
if 'grant_membership_type' in self.fields:
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):
data = super().validate(data)
@@ -437,28 +476,6 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
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):
identifier = serializers.CharField(allow_null=True)

View File

@@ -782,6 +782,33 @@ class OrderSerializer(ConfigurableSerializerMixin, I18nAwareModelSerializer):
"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",
],
},
}

View File

@@ -31,7 +31,7 @@ from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from pretix.api.auth.devicesecurity import get_all_security_profiles
from pretix.api.serializers import AsymmetricField, ConfigurableSerializer
from pretix.api.serializers import AsymmetricField, ConfigurableSerializerMixin
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.order import CompatibleJSONField
from pretix.api.serializers.settings import SettingsSerializer
@@ -51,7 +51,7 @@ from pretix.multidomain.urlreverse import build_absolute_uri
logger = logging.getLogger(__name__)
class OrganizerSerializer(ConfigurableSerializer, I18nAwareModelSerializer):
class OrganizerSerializer(ConfigurableSerializerMixin, I18nAwareModelSerializer):
public_url = serializers.SerializerMethodField('get_organizer_url', read_only=True)
def get_organizer_url(self, organizer):

View File

@@ -121,6 +121,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['request'] = self.request
return ctx
def perform_update(self, serializer):

View File

@@ -44,6 +44,7 @@ from django.utils.timezone import now
from django_countries.fields import Country
from django_scopes import scope, scopes_disabled
from tests import assert_num_queries
from tests.api.utils import _test_configurable_serializer
from tests.const import SAMPLE_PNG
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.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
def test_event_list_name_filter(token_client, organizer, event):

View File

@@ -42,6 +42,7 @@ from django.conf import settings
from django.core.files.base import ContentFile
from django_countries.fields import Country
from django_scopes import scopes_disabled
from tests.api.utils import _test_configurable_serializer
from tests.const import SAMPLE_PNG
from pretix.base.models import (
@@ -359,10 +360,17 @@ TEST_ITEM_RES = {
@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")
cat2 = event.categories.create(name="bar")
item.category = cat2
item.tax_rule = taxrule
item.save()
res = dict(TEST_ITEM_RES)
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))
assert resp.status_code == 200
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.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 [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.data['results']
@@ -419,6 +427,15 @@ def test_item_list(token_client, organizer, event, team, item):
assert resp.status_code == 200
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
def test_item_detail(token_client, organizer, event, team, item):

View File

@@ -533,8 +533,8 @@ def test_order_list(token_client, organizer, event, order, item, team, taxrule,
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 == 200
assert resp.data == "No permission to expand field positions.voucher"
assert resp.status_code == 403
assert resp.data["detail"] == "No permission to expand field positions.voucher"
@pytest.mark.django_db
@@ -543,6 +543,7 @@ def test_order_detail(token_client, organizer, event, order, item, taxrule, ques
with scopes_disabled():
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]["device_id"] = order.positions.first().print_logs.first().device_id
res["fees"][0]["id"] = order.fees.first().pk
res["positions"][0]["item"] = item.pk
res["fees"][0]["tax_rule"] = taxrule.pk

View File

@@ -21,6 +21,7 @@
#
import pytest
from django.core.files.base import ContentFile
from tests.api.utils import _test_configurable_serializer
from tests.const import SAMPLE_PNG
TEST_ORGANIZER_RES = {
@@ -36,6 +37,15 @@ def test_organizer_list(token_client, organizer):
assert resp.status_code == 200
assert TEST_ORGANIZER_RES in resp.data['results']
_test_configurable_serializer(
token_client,
"/api/v1/organizers/",
[
"name", "public_url"
],
expands=[],
)
@pytest.mark.django_db
def test_organizer_detail(token_client, organizer):

View File

@@ -26,6 +26,7 @@ from unittest import mock
import pytest
from django_countries.fields import Country
from django_scopes import scopes_disabled
from tests.api.utils import _test_configurable_serializer
from pretix.base.models import (
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.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
def test_subevent_list_filter(token_client, organizer, event, subevent):

View File

@@ -33,12 +33,11 @@ def _add_params(url, params):
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:
elif isinstance(v, list) and len(v) > 0 and isinstance(v[0], dict):
names |= _find_field_names(v[0], path=(*path, k))
else:
names.add(".".join([*path, k]))
return names
@@ -54,10 +53,12 @@ def _test_configurable_serializer(client, url, field_name_samples, expands):
# 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))
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
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]))
@@ -85,4 +86,4 @@ def _test_configurable_serializer(client, url, field_name_samples, expands):
if isinstance(obj, list):
obj = obj[0]
path = path[1:]
assert isinstance(obj[path[0]], dict)
assert isinstance(obj[path[0]], dict), f"{e} is not a dictionary, but {type(obj[path[0]])}"