forked from CGM_Public/pretix_original
more work
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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``.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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]])}"
|
||||
|
||||
Reference in New Issue
Block a user