mirror of
https://github.com/pretix/pretix.git
synced 2026-01-08 22:02:28 +00:00
Compare commits
2 Commits
api-expand
...
question-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05a0a41e28 | ||
|
|
f8902e8e0a |
@@ -203,35 +203,9 @@ Query parameters
|
||||
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``.
|
||||
|
||||
Ordering
|
||||
--------
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
-----------
|
||||
|
||||
@@ -152,8 +152,6 @@ 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
|
||||
@@ -225,8 +223,6 @@ 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 (expandable) The ID of the category this item belongs to
|
||||
category integer 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 (expandable) The internal ID of the applied tax rule (or ``null``).
|
||||
tax_rule integer 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,9 +390,6 @@ 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
|
||||
@@ -534,9 +531,6 @@ 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 (expandable) ID of the purchased item
|
||||
variation integer (expandable) ID of the purchased variation (or ``null``)
|
||||
item integer ID of the purchased item
|
||||
variation integer 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 (expandable) Internal ID of the voucher used for this position (or ``null``)
|
||||
voucher integer 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 (expandable) ID of the date inside an event series this position belongs to (or ``null``).
|
||||
subevent integer 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``.
|
||||
@@ -1059,10 +1059,9 @@ Creating orders
|
||||
prices. Note that this will not include other fees and is calculated once during order generation and will not
|
||||
be respected automatically when the order changes later.)
|
||||
* ``_split_taxes_like_products`` (Optional convenience flag. If set to ``true``, your ``tax_rule`` will be ignored
|
||||
and the fee will be taxed like the products in the order *unless* the total amount of the positions is zero.
|
||||
If the products have multiple tax rates, multiple fees will be generated with weights adjusted to the net price
|
||||
of the products. Note that this will be calculated once during order generation and is not respected automatically
|
||||
when the order changes later.)
|
||||
and the fee will be taxed like the products in the order. If the products have multiple tax rates, multiple fees
|
||||
will be generated with weights adjusted to the net price of the products. Note that this will be calculated once
|
||||
during order generation and is not respected automatically when the order changes later.)
|
||||
|
||||
* ``force`` (optional). If set to ``true``, quotas will be ignored.
|
||||
* ``send_email`` (optional). If set to ``true``, the same emails will be sent as for a regular order, regardless of
|
||||
|
||||
@@ -61,8 +61,6 @@ Endpoints
|
||||
: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
|
||||
``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 401: Authentication failure
|
||||
|
||||
@@ -93,8 +91,6 @@ 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.
|
||||
|
||||
@@ -81,8 +81,7 @@ Endpoints
|
||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id`` and ``position``.
|
||||
Default: ``position``
|
||||
:query integer subevent: Only return quotas of the sub-event with the given ID.
|
||||
:query integer subevent__in: Only return quotas of sub-events with one of the given IDs (comma-separated).
|
||||
:query integer items__in: Only return quotas that include a product with one of the given IDs (comma-separated).
|
||||
:query integer subevent__in: Only return quotas of sub-events with one the given IDs (comma-separated).
|
||||
:query string with_availability: Set to ``true`` to get availability information. Can lead to increased answer times.
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
|
||||
@@ -146,70 +146,10 @@ 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.
|
||||
@@ -297,6 +237,63 @@ 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
|
||||
|
||||
@@ -51,7 +51,7 @@ dependencies = [
|
||||
"django-oauth-toolkit==2.3.*",
|
||||
"django-otp==1.6.*",
|
||||
"django-phonenumber-field==7.3.*",
|
||||
"django-redis==6.0.*",
|
||||
"django-redis==5.4.*",
|
||||
"django-scopes==2.0.*",
|
||||
"django-statici18n==2.6.*",
|
||||
"djangorestframework==3.16.*",
|
||||
@@ -67,7 +67,7 @@ dependencies = [
|
||||
"markdown==3.8", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
||||
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
|
||||
"mt-940==4.30.*",
|
||||
"oauthlib==3.3.*",
|
||||
"oauthlib==3.2.*",
|
||||
"openpyxl==3.1.*",
|
||||
"packaging",
|
||||
"paypalrestsdk==1.13.*",
|
||||
@@ -88,10 +88,10 @@ dependencies = [
|
||||
"pytz-deprecation-shim==0.1.*",
|
||||
"pyuca",
|
||||
"qrcode==8.2",
|
||||
"redis==6.2.*",
|
||||
"redis==5.2.*",
|
||||
"reportlab==4.4.*",
|
||||
"requests==2.31.*",
|
||||
"sentry-sdk==2.30.*",
|
||||
"sentry-sdk==2.29.*",
|
||||
"sepaxml==2.6.*",
|
||||
"stripe==7.9.*",
|
||||
"text-unidecode==1.*",
|
||||
@@ -110,7 +110,7 @@ dev = [
|
||||
"aiohttp==3.12.*",
|
||||
"coverage",
|
||||
"coveralls",
|
||||
"fakeredis==2.30.*",
|
||||
"fakeredis==2.26.*",
|
||||
"flake8==7.2.*",
|
||||
"freezegun",
|
||||
"isort==6.0.*",
|
||||
|
||||
@@ -23,7 +23,7 @@ import json
|
||||
|
||||
from django.db.models import prefetch_related_objects
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
|
||||
class AsymmetricField(serializers.Field):
|
||||
@@ -132,136 +132,6 @@ class SalesChannelMigrationMixin:
|
||||
s.identifier for s in
|
||||
self.organizer.sales_channels.all()
|
||||
])
|
||||
elif "limit_sales_channels" in value:
|
||||
else:
|
||||
value["sales_channels"] = value["limit_sales_channels"]
|
||||
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,19 +23,15 @@ from django.utils.translation import gettext as _
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers import ConfigurableSerializerMixin
|
||||
from pretix.api.serializers.event import SubEventSerializer
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.media import MEDIA_TYPES
|
||||
from pretix.base.models import Checkin, CheckinList
|
||||
|
||||
|
||||
class CheckinListSerializer(ConfigurableSerializerMixin, I18nAwareModelSerializer):
|
||||
class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
checkin_count = serializers.IntegerField(read_only=True)
|
||||
position_count = serializers.IntegerField(read_only=True)
|
||||
expand_fields = {
|
||||
"subevent": SubEventSerializer,
|
||||
}
|
||||
|
||||
class Meta:
|
||||
model = CheckinList
|
||||
@@ -46,6 +42,17 @@ class CheckinListSerializer(ConfigurableSerializerMixin, I18nAwareModelSerialize
|
||||
def __init__(self, *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):
|
||||
data = super().validate(data)
|
||||
event = self.context['event']
|
||||
|
||||
@@ -48,8 +48,7 @@ from rest_framework.fields import ChoiceField, Field
|
||||
from rest_framework.relations import SlugRelatedField
|
||||
|
||||
from pretix.api.serializers import (
|
||||
CompatibleJSONField, ConfigurableSerializerMixin,
|
||||
SalesChannelMigrationMixin,
|
||||
CompatibleJSONField, SalesChannelMigrationMixin,
|
||||
)
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.settings import SettingsSerializer
|
||||
@@ -168,7 +167,7 @@ class ValidKeysField(Field):
|
||||
}
|
||||
|
||||
|
||||
class EventSerializer(SalesChannelMigrationMixin, ConfigurableSerializerMixin, I18nAwareModelSerializer):
|
||||
class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
item_meta_properties = MetaPropertyField(required=False, source='*')
|
||||
plugins = PluginsField(required=False, source='*')
|
||||
@@ -199,11 +198,10 @@ class EventSerializer(SalesChannelMigrationMixin, ConfigurableSerializerMixin, I
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not hasattr(self.context['request'], 'event'):
|
||||
self.fields.pop('valid_keys', None)
|
||||
self.fields.pop('valid_keys')
|
||||
if not self.context.get('request') or 'with_availability_for' not in self.context['request'].GET:
|
||||
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()
|
||||
self.fields.pop('best_availability_state')
|
||||
self.fields['limit_sales_channels'].child_relation.queryset = self.context['organizer'].sales_channels.all()
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -485,7 +483,7 @@ class SubEventItemVariationSerializer(I18nAwareModelSerializer):
|
||||
fields = ('variation', 'price', 'disabled', 'available_from', 'available_until')
|
||||
|
||||
|
||||
class SubEventSerializer(ConfigurableSerializerMixin, I18nAwareModelSerializer):
|
||||
class SubEventSerializer(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)
|
||||
@@ -504,7 +502,7 @@ class SubEventSerializer(ConfigurableSerializerMixin, 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', None)
|
||||
self.fields.pop('best_availability_state')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
@@ -42,10 +42,8 @@ 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 (
|
||||
ConfigurableSerializerMixin, SalesChannelMigrationMixin,
|
||||
)
|
||||
from pretix.api.serializers.event import MetaDataField, TaxRuleSerializer
|
||||
from pretix.api.serializers import SalesChannelMigrationMixin
|
||||
from pretix.api.serializers.event import MetaDataField
|
||||
from pretix.api.serializers.fields import UploadedFileField
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.models import (
|
||||
@@ -248,29 +246,7 @@ class ItemTaxRateField(serializers.Field):
|
||||
return str(Decimal('0.00'))
|
||||
|
||||
|
||||
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):
|
||||
class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
addons = InlineItemAddOnSerializer(many=True, required=False)
|
||||
bundles = InlineItemBundleSerializer(many=True, required=False)
|
||||
variations = InlineItemVariationSerializer(many=True, required=False)
|
||||
@@ -286,16 +262,6 @@ class ItemSerializer(SalesChannelMigrationMixin, ConfigurableSerializerMixin, I1
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
expand_fields = {
|
||||
"category": {
|
||||
"serializer": ItemCategorySerializer,
|
||||
"prefetch": ["category"],
|
||||
},
|
||||
"tax_rule": {
|
||||
"serializer": TaxRuleSerializer,
|
||||
"prefetch": ["tax_rule"],
|
||||
},
|
||||
}
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
@@ -318,18 +284,13 @@ class ItemSerializer(SalesChannelMigrationMixin, ConfigurableSerializerMixin, I1
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if 'default_price' in self.fields:
|
||||
self.fields['default_price'].allow_null = False
|
||||
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 '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()
|
||||
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()
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -476,6 +437,28 @@ class ItemSerializer(SalesChannelMigrationMixin, ConfigurableSerializerMixin, I1
|
||||
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)
|
||||
|
||||
|
||||
@@ -40,15 +40,12 @@ from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.relations import SlugRelatedField
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from pretix.api.serializers import (
|
||||
CompatibleJSONField, ConfigurableSerializerMixin,
|
||||
)
|
||||
from pretix.api.serializers import CompatibleJSONField
|
||||
from pretix.api.serializers.event import SubEventSerializer
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.item import (
|
||||
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
|
||||
)
|
||||
from pretix.api.serializers.voucher import VoucherSerializer
|
||||
from pretix.api.signals import order_api_details, orderposition_api_details
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.i18n import language
|
||||
@@ -178,7 +175,7 @@ class AnswerSerializer(I18nAwareModelSerializer):
|
||||
|
||||
def to_representation(self, instance):
|
||||
r = super().to_representation(instance)
|
||||
if r.get('answer') and r.get('answer').startswith('file://') and instance.orderposition:
|
||||
if r['answer'].startswith('file://') and instance.orderposition:
|
||||
r['answer'] = reverse('api-v1:orderposition-answer', kwargs={
|
||||
'organizer': instance.orderposition.order.event.organizer.slug,
|
||||
'event': instance.orderposition.order.event.slug,
|
||||
@@ -760,7 +757,7 @@ class OrderPluginDataField(serializers.Field):
|
||||
return d
|
||||
|
||||
|
||||
class OrderSerializer(ConfigurableSerializerMixin, I18nAwareModelSerializer):
|
||||
class OrderSerializer(I18nAwareModelSerializer):
|
||||
event = SlugRelatedField(slug_field='slug', read_only=True)
|
||||
invoice_address = InvoiceAddressSerializer(allow_null=True)
|
||||
positions = OrderPositionSerializer(many=True, read_only=True)
|
||||
@@ -778,39 +775,6 @@ class OrderSerializer(ConfigurableSerializerMixin, I18nAwareModelSerializer):
|
||||
required=False,
|
||||
)
|
||||
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:
|
||||
model = Order
|
||||
@@ -829,14 +793,47 @@ class OrderSerializer(ConfigurableSerializerMixin, I18nAwareModelSerializer):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if "sales_channel" in self.fields:
|
||||
if "organizer" in self.context:
|
||||
self.fields["sales_channel"].queryset = self.context["organizer"].sales_channels.all()
|
||||
else:
|
||||
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
||||
if not self.context['pdf_data'] and "positions" in self.fields:
|
||||
if "organizer" in self.context:
|
||||
self.fields["sales_channel"].queryset = self.context["organizer"].sales_channels.all()
|
||||
else:
|
||||
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
||||
if not self.context['pdf_data']:
|
||||
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):
|
||||
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))
|
||||
@@ -1603,7 +1600,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
self.context['event'].currency)
|
||||
is_split_taxes = fee_data.pop('_split_taxes_like_products', False)
|
||||
|
||||
if is_split_taxes and order.total:
|
||||
if is_split_taxes:
|
||||
d = defaultdict(lambda: Decimal('0.00'))
|
||||
trz = TaxRule.zero()
|
||||
for p in pos_map.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, ConfigurableSerializerMixin
|
||||
from pretix.api.serializers import AsymmetricField
|
||||
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(ConfigurableSerializerMixin, I18nAwareModelSerializer):
|
||||
class OrganizerSerializer(I18nAwareModelSerializer):
|
||||
public_url = serializers.SerializerMethodField('get_organizer_url', read_only=True)
|
||||
|
||||
def get_organizer_url(self, organizer):
|
||||
|
||||
@@ -121,7 +121,6 @@ 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):
|
||||
@@ -486,17 +485,8 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
||||
super().perform_destroy(instance)
|
||||
|
||||
|
||||
class NumberInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
|
||||
pass
|
||||
|
||||
|
||||
with scopes_disabled():
|
||||
class QuotaFilter(FilterSet):
|
||||
items__in = NumberInFilter(
|
||||
field_name='items__id',
|
||||
lookup_expr='in',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Quota
|
||||
fields = {
|
||||
@@ -518,7 +508,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
return self.request.event.quotas.all()
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
queryset = self.filter_queryset(self.get_queryset()).distinct()
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
page = self.paginate_queryset(queryset)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
|
||||
from pretix.base.models import (
|
||||
Discount, Item, ItemCategory, Order, Question, Quota, SubEvent, TaxRule,
|
||||
Voucher, WaitingListEntry,
|
||||
Voucher,
|
||||
)
|
||||
|
||||
from .logentrytype_registry import ( # noqa
|
||||
@@ -145,15 +145,3 @@ class TaxRuleLogEntryType(EventLogEntryType):
|
||||
object_link_viewname = 'control:event.settings.tax.edit'
|
||||
object_link_argname = 'rule'
|
||||
content_type = TaxRule
|
||||
|
||||
|
||||
class WaitingListEntryLogEntryType(EventLogEntryType):
|
||||
object_link_wrapper = _('{val}')
|
||||
object_link_viewname = 'control:event.orders.waitinglist'
|
||||
content_type = WaitingListEntry
|
||||
|
||||
def get_object_link_info(self, logentry) -> Optional[dict]:
|
||||
info = super().get_object_link_info(logentry)
|
||||
if info and 'href' in info:
|
||||
info['href'] += '?status=a&entry=' + str(logentry.content_object.pk)
|
||||
return info
|
||||
|
||||
@@ -793,7 +793,7 @@ class Item(LoggedModel):
|
||||
class Meta:
|
||||
verbose_name = _("Product")
|
||||
verbose_name_plural = _("Products")
|
||||
ordering = ("category__position", "category", "position", "pk")
|
||||
ordering = ("category__position", "category", "position")
|
||||
|
||||
def __str__(self):
|
||||
return str(self.internal_name or self.name)
|
||||
|
||||
@@ -218,6 +218,7 @@ class WaitingListEntry(LoggedModel):
|
||||
'waitinglistentry': self.pk,
|
||||
'subevent': self.subevent.pk if self.subevent else None,
|
||||
}, user=user, auth=auth)
|
||||
self.log_action('pretix.event.orders.waitinglist.voucher_assigned', user=user, auth=auth)
|
||||
self.voucher = v
|
||||
self.save()
|
||||
|
||||
@@ -233,7 +234,6 @@ class WaitingListEntry(LoggedModel):
|
||||
),
|
||||
user=user,
|
||||
auth=auth,
|
||||
log_entry_type='pretix.event.orders.waitinglist.voucher_assigned',
|
||||
)
|
||||
|
||||
def send_mail(self, subject: Union[str, LazyI18nString], template: Union[str, LazyI18nString],
|
||||
|
||||
@@ -96,19 +96,12 @@ class SendMailException(Exception):
|
||||
|
||||
|
||||
def clean_sender_name(sender_name: str) -> str:
|
||||
# Even though we try to properly escape sender names, some characters seem to cause problems when the escaping
|
||||
# fails due to some forwardings, etc.
|
||||
|
||||
# Emails with @ in their sender name are rejected by some mailservers (e.g. Microsoft) because it looks like
|
||||
# a phishing attempt.
|
||||
sender_name = sender_name.replace("@", " ")
|
||||
# Emails with : in their sender name are treated by Microsoft like emails with no From header at all, leading
|
||||
# to a higher spam likelihood.
|
||||
sender_name = sender_name.replace(":", " ")
|
||||
# Emails with , in their sender name look like multiple senders
|
||||
sender_name = sender_name.replace(",", "")
|
||||
# Emails with " in their sender name could be escaped, but somehow create issues in reality
|
||||
sender_name = sender_name.replace("\"", "")
|
||||
|
||||
# Emails with excessively long sender names are rejected by some mailservers
|
||||
if len(sender_name) > 75:
|
||||
|
||||
@@ -665,9 +665,9 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
|
||||
del self.fields['event_list_available_only']
|
||||
del self.fields['event_list_filters']
|
||||
del self.fields['event_calendar_future_only']
|
||||
self.fields['primary_font'].choices = [('Open Sans', 'Open Sans')] + sorted([
|
||||
self.fields['primary_font'].choices += [
|
||||
(a, {"title": a, "data": v}) for a, v in get_fonts(self.event, pdf_support_required=False).items()
|
||||
], key=lambda a: a[0])
|
||||
]
|
||||
|
||||
# create "virtual" fields for better UX when editing <name>_asked and <name>_required fields
|
||||
self.virtual_keys = []
|
||||
|
||||
@@ -50,7 +50,7 @@ from pretix.base.logentrytypes import (
|
||||
DiscountLogEntryType, EventLogEntryType, ItemCategoryLogEntryType,
|
||||
ItemLogEntryType, LogEntryType, OrderLogEntryType, QuestionLogEntryType,
|
||||
QuotaLogEntryType, TaxRuleLogEntryType, VoucherLogEntryType,
|
||||
WaitingListEntryLogEntryType, log_entry_types,
|
||||
log_entry_types,
|
||||
)
|
||||
from pretix.base.models import (
|
||||
Checkin, CheckinList, Event, ItemVariation, LogEntry, OrderPosition,
|
||||
@@ -697,7 +697,11 @@ class CoreUserImpersonatedLogEntryType(UserImpersonatedLogEntryType):
|
||||
'the last request was less than 24 hours ago.'),
|
||||
'pretix.organizer.deleted': _('The organizer "{name}" has been deleted.'),
|
||||
'pretix.waitinglist.voucher': _('A voucher has been sent to a person on the waiting list.'), # legacy
|
||||
'pretix.event.orders.waitinglist.voucher_assigned': _('A voucher has been sent to a person on the waiting list.'),
|
||||
'pretix.event.orders.waitinglist.deleted': _('An entry has been removed from the waiting list.'),
|
||||
'pretix.event.order.waitinglist.transferred': _('An entry has been transferred to another waiting list.'), # legacy
|
||||
'pretix.event.orders.waitinglist.changed': _('An entry has been changed on the waiting list.'),
|
||||
'pretix.event.orders.waitinglist.added': _('An entry has been added to the waiting list.'),
|
||||
'pretix.team.created': _('The team has been created.'),
|
||||
'pretix.team.changed': _('The team settings have been changed.'),
|
||||
'pretix.team.deleted': _('The team has been deleted.'),
|
||||
@@ -899,13 +903,3 @@ class LegacyCheckinLogEntryType(OrderLogEntryType):
|
||||
datetime=dt_formatted,
|
||||
list=checkin_list
|
||||
)
|
||||
|
||||
|
||||
@log_entry_types.new_from_dict({
|
||||
'pretix.event.orders.waitinglist.voucher_assigned': _('A voucher has been sent to a person on the waiting list.'),
|
||||
'pretix.event.orders.waitinglist.deleted': _('An entry has been removed from the waiting list.'),
|
||||
'pretix.event.orders.waitinglist.changed': _('An entry has been changed on the waiting list.'),
|
||||
'pretix.event.orders.waitinglist.added': _('An entry has been added to the waiting list.'),
|
||||
})
|
||||
class CoreWaitingListEntryLogEntryType(WaitingListEntryLogEntryType):
|
||||
pass
|
||||
|
||||
@@ -247,17 +247,6 @@
|
||||
{% bootstrap_field sform.show_variations_expanded layout="control" %}
|
||||
{% bootstrap_field sform.hide_sold_out layout="control" %}
|
||||
|
||||
<div data-display-dependency="#id_settings-waiting_list_enabled">
|
||||
<div data-display-dependency="#id_settings-hide_sold_out">
|
||||
<div class="alert alert-danger dynamic">
|
||||
<h4>{% trans "Incompatible settings" %}</h4>
|
||||
{% blocktrans trimmed %}
|
||||
Customers won't be able to add themselves to the waiting list, because "Hide all products that are sold out" is enabled.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>{% trans "Calendar and list views" context "subevents" %}</h4>
|
||||
{% if sform.frontpage_subevent_ordering %}
|
||||
{% bootstrap_field sform.frontpage_subevent_ordering layout="control" %}
|
||||
@@ -383,16 +372,6 @@
|
||||
</strong>
|
||||
</div>
|
||||
{% bootstrap_field sform.waiting_list_enabled layout="control" %}
|
||||
<div data-display-dependency="#id_settings-hide_sold_out">
|
||||
<div data-display-dependency="#id_settings-waiting_list_enabled">
|
||||
<div class="alert alert-danger dynamic">
|
||||
<h4>{% trans "Incompatible settings" %}</h4>
|
||||
{% blocktrans trimmed %}
|
||||
Customers won't be able to add themselves to the waiting list, because "Hide all products that are sold out" is enabled.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% bootstrap_field sform.waiting_list_auto layout="control" %}
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-3">
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row" id="question-stats">
|
||||
{% if not stats %}
|
||||
<div class="empty-collection col-md-10 col-xs-12">
|
||||
<p>
|
||||
@@ -81,7 +81,7 @@
|
||||
<div class="chart" id="question_chart" data-type="{{ question.type }}">
|
||||
|
||||
</div>
|
||||
{{ stats|json_script:"question-chart-data" }}
|
||||
<script type="application/json" id="question-chart-data">{{ stats_json|escapejson }}</script>
|
||||
</div>
|
||||
<div class="col-md-5 col-xs-12">
|
||||
<table class="table table-bordered table-hover">
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
{% load captureas %}
|
||||
{% load static %}
|
||||
{% load eventsignal %}
|
||||
{% load dialog %}
|
||||
{% block title %}{% trans "Change multiple dates" context "subevent" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>
|
||||
@@ -183,22 +182,21 @@
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Quotas" %}</legend>
|
||||
<div class="bulk-edit-field-group"
|
||||
{% if sampled_quotas|default_if_none:"NONE" == "NONE" %}
|
||||
data-confirm-dialog="#confirm-override-quotas"
|
||||
{% endif %}>
|
||||
{% if sampled_quotas|default_if_none:"NONE" == "NONE" %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
You selected a set of dates that currently have different quota setups. You can therefore
|
||||
not change their quotas in bulk. If you want, you can set up a new set of quotas to
|
||||
<strong>replace</strong> the quota setup of all selected dates.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="bulk-edit-field-group">
|
||||
<label class="field-toggle">
|
||||
<input type="checkbox" name="_bulk" value="__quotas" {% if "__quotas" in bulk_selected %}checked{% endif %}>
|
||||
{% trans "change" context "form_bulk" %}
|
||||
</label>
|
||||
<div class="field-content">
|
||||
{% if sampled_quotas|default_if_none:"NONE" == "NONE" %}
|
||||
<div class="alert alert-warning">
|
||||
{% trans "You selected a set of dates that currently have different quota setups." %}
|
||||
{% trans "Using this option will <strong>delete all current quotas</strong> from <strong>all selected dates</strong>." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
|
||||
{{ formset.management_form }}
|
||||
{% bootstrap_formset_errors formset %}
|
||||
@@ -273,7 +271,7 @@
|
||||
<fieldset>
|
||||
<legend>{% trans "Check-in lists" %}</legend>
|
||||
{% if sampled_lists|default_if_none:"NONE" == "NONE" %}
|
||||
<div class="alert alert-info">
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
You selected a set of dates that currently have different check-in list setups. You can
|
||||
therefore not change their check-in lists in bulk.
|
||||
@@ -369,17 +367,4 @@
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% trans "Delete existing quotas" as dialog_title %}
|
||||
{% trans "Using this option will <strong>delete all current quotas</strong> from <strong>all selected dates</strong>." as dialog_text %}
|
||||
{% trans "This cannot be reverted. Are you sure to proceed?" as dialog_text2 %}
|
||||
{% dialog "confirm-override-quotas" dialog_title dialog_text|add:" "|add:dialog_text2 icon="trash" %}
|
||||
<p class="modal-card-confirm modal-card-confirm-spread">
|
||||
<button class="btn btn-lg btn-default" value="no">
|
||||
{% trans "Cancel" %}
|
||||
</button>
|
||||
<button class="btn btn-lg btn-danger" value="yes">
|
||||
{% trans "Proceed" %}
|
||||
</button>
|
||||
</p>
|
||||
{% enddialog %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -748,6 +748,7 @@ class QuestionView(EventPermissionRequiredMixin, QuestionMixin, ChartContainingV
|
||||
ctx['items'] = self.object.items.all()
|
||||
stats = self.get_answer_statistics()
|
||||
ctx['stats'], ctx['total'] = stats
|
||||
ctx['stats_json'] = json.dumps(stats)
|
||||
return ctx
|
||||
|
||||
def get_object(self, queryset=None) -> Question:
|
||||
|
||||
@@ -5,8 +5,8 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-05-30 10:35+0000\n"
|
||||
"PO-Revision-Date: 2025-06-12 17:00+0000\n"
|
||||
"Last-Translator: Richard Schreiber <schreiber@rami.io>\n"
|
||||
"PO-Revision-Date: 2025-05-30 11:15+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/de/"
|
||||
">\n"
|
||||
"Language: de\n"
|
||||
@@ -33401,7 +33401,7 @@ msgstr "Übersicht über die bestellten Produkte"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart_box.html:50
|
||||
msgid "Continue with order process"
|
||||
msgstr "Fortfahren mit dem Bestellprozess"
|
||||
msgstr "Mit dem Bestellprozess fortfahren"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart_box.html:55
|
||||
#: pretix/presale/templates/pretixpresale/event/index.html:232
|
||||
|
||||
@@ -47,8 +47,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import scope, scopes_disabled
|
||||
|
||||
from pretix.base.logentrytypes import (
|
||||
EventLogEntryType, OrderLogEntryType, WaitingListEntryLogEntryType,
|
||||
log_entry_types,
|
||||
EventLogEntryType, OrderLogEntryType, log_entry_types,
|
||||
)
|
||||
from pretix.base.models import SubEvent
|
||||
from pretix.base.signals import (
|
||||
@@ -131,11 +130,6 @@ class SendmailPluginOrderLogEntryType(OrderLogEntryType):
|
||||
pass
|
||||
|
||||
|
||||
@log_entry_types.new('pretix.plugins.sendmail.waitinglist.email.sent', _('The person on the waiting list received a mass email.'))
|
||||
class SendmailPluginWaitingListLogEntryType(WaitingListEntryLogEntryType):
|
||||
pass
|
||||
|
||||
|
||||
@log_entry_types.new('pretix.plugins.sendmail.rule.added', _('An email rule was created'))
|
||||
@log_entry_types.new('pretix.plugins.sendmail.rule.changed', _('An email rule was updated'))
|
||||
@log_entry_types.new('pretix.plugins.sendmail.rule.order.email.sent', _('A scheduled email was sent to the order'))
|
||||
|
||||
@@ -201,5 +201,4 @@ def send_mails_to_waitinglist(event: Event, user: int, subject: dict, message: d
|
||||
),
|
||||
user=user,
|
||||
attach_cached_files=attachments,
|
||||
log_entry_type='pretix.plugins.sendmail.waitinglist.email.sent',
|
||||
)
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</div>
|
||||
<div role="rowgroup" class="firstchild-in-panel">
|
||||
{% for line in cart.positions %}
|
||||
<div role="row" class="row cart-row {% if hide_prices %}hide-prices{% endif %} {% if download %}has-downloads{% endif %}{% if editable %}editable{% endif %}" data-article-id="item-{{ line.item.id }}{% if line.variation %}-{{ line.variation.id }}{% endif %}">
|
||||
<div role="row" class="row cart-row {% if hide_prices %}hide-prices{% endif %} {% if download %}has-downloads{% endif %}{% if editable %}editable{% endif %}">
|
||||
<div role="cell" class="product">
|
||||
<p>
|
||||
{% if line.addon_to %}
|
||||
|
||||
@@ -64,7 +64,6 @@ from pretix.base.models import (
|
||||
Event, EventMetaValue, Organizer, Quota, SubEvent, SubEventMetaValue,
|
||||
)
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.base.timemachine import time_machine_now
|
||||
from pretix.helpers.compat import date_fromisocalendar
|
||||
from pretix.helpers.daterange import daterange
|
||||
from pretix.helpers.formats.en.formats import (
|
||||
@@ -229,7 +228,7 @@ class EventListMixin:
|
||||
|
||||
def _set_month_to_next_subevent(self):
|
||||
tz = self.request.event.timezone
|
||||
now_dt = time_machine_now()
|
||||
now_dt = now()
|
||||
next_sev = self.request.event.subevents.using(settings.DATABASE_REPLICA).annotate(
|
||||
effective_date=Case(
|
||||
When(date_from__lt=now_dt, date_to__isnull=False, date_to__gte=now_dt, then=Value(now_dt)),
|
||||
@@ -246,8 +245,8 @@ class EventListMixin:
|
||||
self.year = datetime_from.astimezone(tz).year
|
||||
self.month = datetime_from.astimezone(tz).month
|
||||
else:
|
||||
self.year = now_dt.year
|
||||
self.month = now_dt.month
|
||||
self.year = now().year
|
||||
self.month = now().month
|
||||
|
||||
def _set_month_to_next_event(self):
|
||||
now_dt = now()
|
||||
@@ -297,7 +296,7 @@ class EventListMixin:
|
||||
try:
|
||||
date = dateutil.parser.isoparse(self.request.GET.get('date')).date()
|
||||
except ValueError:
|
||||
date = time_machine_now().date()
|
||||
date = now().date()
|
||||
self.year = date.year
|
||||
self.month = date.month
|
||||
else:
|
||||
@@ -307,7 +306,7 @@ class EventListMixin:
|
||||
self._set_month_to_next_event()
|
||||
|
||||
def _set_week_to_next_subevent(self):
|
||||
now_dt = time_machine_now()
|
||||
now_dt = now()
|
||||
tz = self.request.event.timezone
|
||||
next_sev = self.request.event.subevents.using(settings.DATABASE_REPLICA).annotate(
|
||||
effective_date=Case(
|
||||
@@ -325,8 +324,8 @@ class EventListMixin:
|
||||
self.year = datetime_from.astimezone(tz).isocalendar()[0]
|
||||
self.week = datetime_from.astimezone(tz).isocalendar()[1]
|
||||
else:
|
||||
self.year = now_dt.isocalendar()[0]
|
||||
self.week = now_dt.isocalendar()[1]
|
||||
self.year = now().isocalendar()[0]
|
||||
self.week = now().isocalendar()[1]
|
||||
|
||||
def _set_week_to_next_event(self):
|
||||
now_dt = now()
|
||||
@@ -376,7 +375,7 @@ class EventListMixin:
|
||||
try:
|
||||
iso = dateutil.parser.isoparse(self.request.GET.get('date')).isocalendar()
|
||||
except ValueError:
|
||||
iso = time_machine_now().isocalendar()
|
||||
iso = now().isocalendar()
|
||||
self.year = iso[0]
|
||||
self.week = iso[1]
|
||||
else:
|
||||
|
||||
13
src/pretix/static/npm_dir/package-lock.json
generated
13
src/pretix/static/npm_dir/package-lock.json
generated
@@ -1887,10 +1887,9 @@
|
||||
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"license": "MIT",
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
@@ -5010,9 +5009,9 @@
|
||||
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
|
||||
@@ -648,47 +648,18 @@ var form_handlers = function (el) {
|
||||
var $checkbox = $(this).find("input[type=checkbox][name=_bulk]");
|
||||
var $content = $(this).find(".field-content");
|
||||
var $fields = $content.find("input, select, textarea, button");
|
||||
var $dialog = $(this).attr("data-confirm-dialog") ? $($(this).attr("data-confirm-dialog")) : null;
|
||||
var warningShown = false;
|
||||
|
||||
if ($dialog) {
|
||||
$dialog.on("close", function () {
|
||||
if ($dialog.get(0).returnValue === "yes") {
|
||||
$checkbox.prop("checked", true);
|
||||
} else {
|
||||
$checkbox.prop("checked", false);
|
||||
warningShown = false;
|
||||
}
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
var update = function () {
|
||||
var isChecked = $checkbox.prop("checked");
|
||||
|
||||
$content.toggleClass("enabled", isChecked);
|
||||
$fields.attr("tabIndex", isChecked ? 0 : -1);
|
||||
}
|
||||
$content.on("focusin change click", function () {
|
||||
if ($checkbox.prop("checked")) return;
|
||||
if ($dialog && !warningShown) {
|
||||
warningShown = true;
|
||||
$dialog.get(0).showModal();
|
||||
} else {
|
||||
$checkbox.prop("checked", true);
|
||||
update();
|
||||
}
|
||||
});
|
||||
$checkbox.on('change', function () {
|
||||
var isChecked = $checkbox.prop("checked");
|
||||
if (isChecked && $dialog && !warningShown) {
|
||||
warningShown = true;
|
||||
$dialog.get(0).showModal();
|
||||
} else if (!isChecked) {
|
||||
warningShown = false;
|
||||
}
|
||||
$checkbox.prop("checked", true);
|
||||
update();
|
||||
})
|
||||
});
|
||||
$checkbox.on('change', update)
|
||||
update();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
/*global $, Morris, gettext*/
|
||||
$(function () {
|
||||
// Question view
|
||||
if (!$("#question_chart").length) {
|
||||
if (!$("#question-stats").length) {
|
||||
return;
|
||||
}
|
||||
|
||||
$(".chart").css("height", "250px");
|
||||
var data_type = $("#question_chart").attr("data-type"),
|
||||
data = JSON.parse($("#question-chart-data").text() || "[]"),
|
||||
data = JSON.parse($("#question-chart-data").html()),
|
||||
others_sum = 0,
|
||||
max_num = 8;
|
||||
|
||||
data = data.map(function (d) {
|
||||
return {
|
||||
'value': d.count,
|
||||
'label': d.answer.length > 20 ? d.answer.substring(0, 20) + '…' : d.answer,
|
||||
for (var i in data) {
|
||||
data[i].value = data[i].count;
|
||||
data[i].label = data[i].answer;
|
||||
if (data[i].label.length > 20) {
|
||||
data[i].label = data[i].label.substring(0, 20) + '…';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (data_type == 'N') {
|
||||
// Sort
|
||||
@@ -35,7 +36,7 @@ $(function () {
|
||||
// Limit shown options
|
||||
if (data.length > max_num) {
|
||||
for (var i = max_num; i < data.length; i++) {
|
||||
others_sum += data[i].value;
|
||||
others_sum += data[i].count;
|
||||
}
|
||||
data = data.slice(0, max_num);
|
||||
data.push({'value': others_sum, 'label': gettext('Others')});
|
||||
@@ -77,7 +78,7 @@ $(function () {
|
||||
data: data,
|
||||
resize: true,
|
||||
xkey: 'label',
|
||||
ykeys: ['value'],
|
||||
ykeys: ['count'],
|
||||
labels: [gettext('Count')]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ $(function () {
|
||||
.attr("href", "#" + tid)
|
||||
.text($fieldset.find("legend").text())
|
||||
.appendTo($tabli);
|
||||
if ($fieldset.find(".has-error, .alert-danger:not(.dynamic)").length > 0) {
|
||||
if ($fieldset.find(".has-error, .alert-danger").length > 0) {
|
||||
$tablink.append(" ");
|
||||
$tablink.append($("<span>").addClass("fa fa-warning text-danger"));
|
||||
if (preselect === null) {
|
||||
|
||||
@@ -44,7 +44,6 @@ 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 (
|
||||
@@ -216,15 +215,6 @@ 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,7 +42,6 @@ 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 (
|
||||
@@ -360,17 +359,10 @@ TEST_ITEM_RES = {
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_item_list(token_client, organizer, event, team, item, taxrule):
|
||||
def test_item_list(token_client, organizer, event, team, item):
|
||||
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']
|
||||
@@ -408,11 +400,11 @@ def test_item_list(token_client, organizer, event, team, item, taxrule):
|
||||
assert resp.status_code == 200
|
||||
assert [] == 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 [res] == 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.data['results']
|
||||
|
||||
@@ -427,15 +419,6 @@ def test_item_list(token_client, organizer, event, team, item, taxrule):
|
||||
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):
|
||||
@@ -1918,11 +1901,10 @@ TEST_QUOTA_RES = {
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_quota_list(token_client, organizer, event, quota, item, item3, subevent):
|
||||
quota.items.add(item3)
|
||||
def test_quota_list(token_client, organizer, event, quota, item, subevent):
|
||||
res = dict(TEST_QUOTA_RES)
|
||||
res["id"] = quota.pk
|
||||
res["items"] = [item.pk, item3.pk]
|
||||
res["items"] = [item.pk]
|
||||
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/quotas/'.format(organizer.slug, event.slug))
|
||||
assert resp.status_code == 200
|
||||
@@ -1940,13 +1922,6 @@ def test_quota_list(token_client, organizer, event, quota, item, item3, subevent
|
||||
'/api/v1/organizers/{}/events/{}/quotas/?subevent={}'.format(organizer.slug, event.slug, se2.pk))
|
||||
assert [] == resp.data['results']
|
||||
|
||||
resp = token_client.get(
|
||||
'/api/v1/organizers/{}/events/{}/quotas/?items__in={},{},0'.format(organizer.slug, event.slug, item.pk, item3.pk))
|
||||
assert [res] == resp.data['results']
|
||||
resp = token_client.get(
|
||||
'/api/v1/organizers/{}/events/{}/quotas/?items__in=0'.format(organizer.slug, event.slug))
|
||||
assert [] == resp.data['results']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_quota_detail(token_client, organizer, event, quota, item):
|
||||
|
||||
@@ -961,42 +961,6 @@ def test_order_create_fee_as_percentage(token_client, organizer, event, item, qu
|
||||
assert o.total == Decimal('25.30')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_fee_as_percentage_with_zero(token_client, organizer, event, item, quota, question):
|
||||
with scopes_disabled():
|
||||
voucher = event.vouchers.create(price_mode="set", value=Decimal("0.00"))
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['fees'][0]['_treat_value_as_percentage'] = True
|
||||
res['fees'][0]['_split_taxes_like_products'] = True
|
||||
res['fees'][0]['value'] = '10.00'
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
res['positions'][0]['voucher'] = voucher.code
|
||||
del res['positions'][0]['price']
|
||||
|
||||
res['simulate'] = True
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
assert resp.data["total"] == "0.00"
|
||||
|
||||
res['simulate'] = False
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(code=resp.data['code'])
|
||||
fee = o.fees.first()
|
||||
assert fee.value == Decimal('0.00')
|
||||
assert o.total == Decimal('0.00')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_fee_with_auto_tax(token_client, organizer, event, item, quota, question, taxrule):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
|
||||
@@ -31,7 +31,6 @@ from django.utils.timezone import now
|
||||
from django_countries.fields import Country
|
||||
from django_scopes import scopes_disabled
|
||||
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_provider import MockedCharge
|
||||
|
||||
@@ -401,18 +400,13 @@ def test_order_list_filter_subevent_date(token_client, device, organizer, event,
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_list(token_client, organizer, event, order, item, team, taxrule, question, device):
|
||||
def test_order_list(token_client, organizer, event, order, item, taxrule, question, device):
|
||||
res = dict(TEST_ORDER_RES)
|
||||
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["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]["device_id"] = device.device_id
|
||||
res["positions"][0]["voucher"] = voucher.pk
|
||||
res["positions"][0]["item"] = item.pk
|
||||
res["positions"][0]["answers"][0]["question"] = question.pk
|
||||
res["last_modified"] = order.last_modified.isoformat().replace('+00:00', 'Z')
|
||||
@@ -520,22 +514,6 @@ def test_order_list(token_client, organizer, event, order, item, team, taxrule,
|
||||
assert resp.status_code == 200
|
||||
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
|
||||
def test_order_detail(token_client, organizer, event, order, item, taxrule, question):
|
||||
@@ -543,7 +521,6 @@ 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,7 +21,6 @@
|
||||
#
|
||||
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 = {
|
||||
@@ -37,15 +36,6 @@ 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,7 +26,6 @@ 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,
|
||||
@@ -158,15 +157,6 @@ 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):
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
#
|
||||
# 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]])}"
|
||||
@@ -23,7 +23,6 @@ import inspect
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from django.core.cache import cache
|
||||
from django.test import override_settings
|
||||
from django.utils import translation
|
||||
from django_scopes import scopes_disabled
|
||||
@@ -116,7 +115,6 @@ def fakeredis_client(monkeypatch):
|
||||
},
|
||||
}
|
||||
):
|
||||
cache.clear()
|
||||
redis = get_redis_connection("default", True)
|
||||
redis.flushall()
|
||||
monkeypatch.setattr('django_redis.get_redis_connection', get_redis_connection, raising=False)
|
||||
|
||||
Reference in New Issue
Block a user