mirror of
https://github.com/pretix/pretix.git
synced 2026-08-12 11:07:02 +00:00
add missing packages
This commit is contained in:
@@ -0,0 +1,396 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db.models import (
|
||||||
|
Count, Exists, IntegerField, OuterRef, Prefetch, Q, Value,
|
||||||
|
)
|
||||||
|
from django.db.models.lookups import Exact
|
||||||
|
|
||||||
|
from pretix.base.models import (
|
||||||
|
ItemVariation, Quota, SalesChannel, SeatCategoryMapping,
|
||||||
|
)
|
||||||
|
from pretix.base.models.items import (
|
||||||
|
ItemAddOn, ItemBundle, SubEventItem, SubEventItemVariation,
|
||||||
|
)
|
||||||
|
from pretix.base.services.quotas import QuotaAvailability
|
||||||
|
from pretix.base.timemachine import time_machine_now
|
||||||
|
from pretix.presale.signals import item_description
|
||||||
|
|
||||||
|
|
||||||
|
def item_group_by_category(items):
|
||||||
|
return sorted(
|
||||||
|
[
|
||||||
|
# a group is a tuple of a category and a list of items
|
||||||
|
(cat, [i for i in items if i.category == cat])
|
||||||
|
for cat in set([i.category for i in items])
|
||||||
|
# insert categories into a set for uniqueness
|
||||||
|
# a set is unsorted, so sort again by category
|
||||||
|
],
|
||||||
|
key=lambda group: (group[0].position, group[0].id) if (group[0] is not None and group[0].id is not None) else (0, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_items_for_product_list(event, *, channel: SalesChannel, subevent=None, voucher=None, require_seat=0,
|
||||||
|
base_qs=None, allow_addons=False, allow_cross_sell=False,
|
||||||
|
quota_cache=None, filter_items=None, filter_categories=None, memberships=None,
|
||||||
|
ignore_hide_sold_out_for_item_ids=None):
|
||||||
|
base_qs_set = base_qs is not None
|
||||||
|
base_qs = base_qs if base_qs is not None else event.items
|
||||||
|
|
||||||
|
requires_seat = Exists(
|
||||||
|
SeatCategoryMapping.objects.filter(
|
||||||
|
product_id=OuterRef('pk'),
|
||||||
|
subevent=subevent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not event.settings.seating_choice:
|
||||||
|
requires_seat = Value(0, output_field=IntegerField())
|
||||||
|
|
||||||
|
variation_q = (
|
||||||
|
Q(Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()) | Q(available_from_mode='info')) &
|
||||||
|
Q(Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()) | Q(available_until_mode='info'))
|
||||||
|
)
|
||||||
|
if not voucher or not voucher.show_hidden_items:
|
||||||
|
variation_q &= Q(hide_without_voucher=False)
|
||||||
|
|
||||||
|
if memberships is not None:
|
||||||
|
prefetch_membership_types = ['require_membership_types']
|
||||||
|
else:
|
||||||
|
prefetch_membership_types = []
|
||||||
|
|
||||||
|
prefetch_var = Prefetch(
|
||||||
|
'variations',
|
||||||
|
to_attr='available_variations',
|
||||||
|
queryset=ItemVariation.objects.using(settings.DATABASE_REPLICA).annotate(
|
||||||
|
subevent_disabled=Exists(
|
||||||
|
SubEventItemVariation.objects.filter(
|
||||||
|
Q(disabled=True)
|
||||||
|
| (Exact(OuterRef('available_from_mode'), 'hide') & Q(available_from__gt=time_machine_now()))
|
||||||
|
| (Exact(OuterRef('available_until_mode'), 'hide') & Q(available_until__lt=time_machine_now())),
|
||||||
|
variation_id=OuterRef('pk'),
|
||||||
|
subevent=subevent,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
).filter(
|
||||||
|
variation_q,
|
||||||
|
Q(all_sales_channels=True) | Q(limit_sales_channels=channel),
|
||||||
|
active=True,
|
||||||
|
quotas__isnull=False,
|
||||||
|
subevent_disabled=False
|
||||||
|
).prefetch_related(
|
||||||
|
*prefetch_membership_types,
|
||||||
|
Prefetch('quotas',
|
||||||
|
to_attr='_subevent_quotas',
|
||||||
|
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(
|
||||||
|
subevent=subevent).select_related("subevent"))
|
||||||
|
).distinct()
|
||||||
|
)
|
||||||
|
prefetch_quotas = Prefetch(
|
||||||
|
'quotas',
|
||||||
|
to_attr='_subevent_quotas',
|
||||||
|
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(subevent=subevent).select_related("subevent")
|
||||||
|
)
|
||||||
|
prefetch_bundles = Prefetch(
|
||||||
|
'bundles',
|
||||||
|
queryset=ItemBundle.objects.using(settings.DATABASE_REPLICA).prefetch_related(
|
||||||
|
Prefetch('bundled_item',
|
||||||
|
queryset=event.items.using(settings.DATABASE_REPLICA).select_related(
|
||||||
|
'tax_rule').prefetch_related(
|
||||||
|
Prefetch('quotas',
|
||||||
|
to_attr='_subevent_quotas',
|
||||||
|
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(
|
||||||
|
subevent=subevent)),
|
||||||
|
)),
|
||||||
|
Prefetch('bundled_variation',
|
||||||
|
queryset=ItemVariation.objects.using(
|
||||||
|
settings.DATABASE_REPLICA
|
||||||
|
).select_related('item', 'item__tax_rule').filter(item__event=event).prefetch_related(
|
||||||
|
Prefetch('quotas',
|
||||||
|
to_attr='_subevent_quotas',
|
||||||
|
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(
|
||||||
|
subevent=subevent)),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
items = base_qs.using(settings.DATABASE_REPLICA).filter_available(
|
||||||
|
channel=channel.identifier, voucher=voucher, allow_addons=allow_addons, allow_cross_sell=allow_cross_sell
|
||||||
|
).select_related(
|
||||||
|
'category', 'tax_rule', # for re-grouping
|
||||||
|
'hidden_if_available',
|
||||||
|
).prefetch_related(
|
||||||
|
*prefetch_membership_types,
|
||||||
|
Prefetch(
|
||||||
|
'hidden_if_item_available',
|
||||||
|
queryset=event.items.annotate(
|
||||||
|
has_variations=Count('variations'),
|
||||||
|
).prefetch_related(
|
||||||
|
prefetch_var,
|
||||||
|
prefetch_quotas,
|
||||||
|
prefetch_bundles,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
prefetch_quotas,
|
||||||
|
prefetch_var,
|
||||||
|
prefetch_bundles,
|
||||||
|
).annotate(
|
||||||
|
quotac=Count('quotas'),
|
||||||
|
has_variations=Count('variations'),
|
||||||
|
subevent_disabled=Exists(
|
||||||
|
SubEventItem.objects.filter(
|
||||||
|
Q(disabled=True)
|
||||||
|
| (Exact(OuterRef('available_from_mode'), 'hide') & Q(available_from__gt=time_machine_now()))
|
||||||
|
| (Exact(OuterRef('available_until_mode'), 'hide') & Q(available_until__lt=time_machine_now())),
|
||||||
|
item_id=OuterRef('pk'),
|
||||||
|
subevent=subevent,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
mandatory_priced_addons=Exists(
|
||||||
|
ItemAddOn.objects.filter(
|
||||||
|
base_item_id=OuterRef('pk'),
|
||||||
|
min_count__gte=1,
|
||||||
|
price_included=False
|
||||||
|
)
|
||||||
|
),
|
||||||
|
requires_seat=requires_seat,
|
||||||
|
).filter(
|
||||||
|
quotac__gt=0, subevent_disabled=False,
|
||||||
|
).order_by('category__position', 'category_id', 'position', 'name')
|
||||||
|
if require_seat:
|
||||||
|
items = items.filter(requires_seat__gt=0)
|
||||||
|
elif require_seat is not None:
|
||||||
|
items = items.filter(requires_seat=0)
|
||||||
|
|
||||||
|
if filter_items:
|
||||||
|
items = items.filter(pk__in=[a for a in filter_items if a.isdigit()])
|
||||||
|
if filter_categories:
|
||||||
|
items = items.filter(category_id__in=[a for a in filter_categories if a.isdigit()])
|
||||||
|
|
||||||
|
display_add_to_cart = False
|
||||||
|
quota_cache_key = f'item_quota_cache:{subevent.id if subevent else 0}:{channel.identifier}:{bool(require_seat)}'
|
||||||
|
quota_cache = quota_cache or event.cache.get(quota_cache_key) or {}
|
||||||
|
quota_cache_existed = bool(quota_cache)
|
||||||
|
|
||||||
|
if subevent:
|
||||||
|
item_price_override = subevent.item_price_overrides
|
||||||
|
var_price_override = subevent.var_price_overrides
|
||||||
|
else:
|
||||||
|
item_price_override = {}
|
||||||
|
var_price_override = {}
|
||||||
|
|
||||||
|
restrict_vars = set()
|
||||||
|
if voucher and voucher.quota_id:
|
||||||
|
# If a voucher is set to a specific quota, we need to filter out on that level
|
||||||
|
restrict_vars = set(voucher.quota.variations.all())
|
||||||
|
|
||||||
|
quotas_to_compute = []
|
||||||
|
for item in items:
|
||||||
|
assert item.event_id == event.pk
|
||||||
|
item.event = event # save a database query if this is looked up
|
||||||
|
if item.has_variations:
|
||||||
|
for v in item.available_variations:
|
||||||
|
for q in v._subevent_quotas:
|
||||||
|
if q.pk not in quota_cache:
|
||||||
|
quotas_to_compute.append(q)
|
||||||
|
else:
|
||||||
|
for q in item._subevent_quotas:
|
||||||
|
if q.pk not in quota_cache:
|
||||||
|
quotas_to_compute.append(q)
|
||||||
|
|
||||||
|
if quotas_to_compute:
|
||||||
|
qa = QuotaAvailability()
|
||||||
|
qa.queue(*quotas_to_compute)
|
||||||
|
qa.compute()
|
||||||
|
quota_cache.update({q.pk: r for q, r in qa.results.items()})
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
if voucher and voucher.item_id and voucher.variation_id:
|
||||||
|
# Restrict variations if the voucher only allows one
|
||||||
|
item.available_variations = [v for v in item.available_variations
|
||||||
|
if v.pk == voucher.variation_id]
|
||||||
|
|
||||||
|
if channel.type_instance.unlimited_items_per_order:
|
||||||
|
max_per_order = sys.maxsize
|
||||||
|
else:
|
||||||
|
max_per_order = item.max_per_order or int(event.settings.max_items_per_order)
|
||||||
|
|
||||||
|
if item.hidden_if_available:
|
||||||
|
q = item.hidden_if_available.availability(_cache=quota_cache)
|
||||||
|
if q[0] == Quota.AVAILABILITY_OK:
|
||||||
|
item._remove = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if item.hidden_if_item_available:
|
||||||
|
if item.hidden_if_item_available.has_variations:
|
||||||
|
dependency_available = any(
|
||||||
|
var.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)[0] == Quota.AVAILABILITY_OK
|
||||||
|
for var in item.hidden_if_item_available.available_variations
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
q = item.hidden_if_item_available.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
|
||||||
|
dependency_available = q[0] == Quota.AVAILABILITY_OK
|
||||||
|
if dependency_available:
|
||||||
|
item._remove = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if item.require_membership and item.require_membership_hidden:
|
||||||
|
if not memberships or not any([m.membership_type in item.require_membership_types.all() for m in memberships]):
|
||||||
|
item._remove = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
item.current_unavailability_reason = item.unavailability_reason(has_voucher=voucher, subevent=subevent)
|
||||||
|
|
||||||
|
item.description = str(item.description)
|
||||||
|
for recv, resp in item_description.send(sender=event, item=item, variation=None, subevent=subevent):
|
||||||
|
if resp:
|
||||||
|
item.description += ("<br/>" if item.description else "") + resp
|
||||||
|
|
||||||
|
if not item.has_variations:
|
||||||
|
item._remove = False
|
||||||
|
if not bool(item._subevent_quotas):
|
||||||
|
item._remove = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if voucher and (voucher.allow_ignore_quota or voucher.block_quota):
|
||||||
|
item.cached_availability = (
|
||||||
|
Quota.AVAILABILITY_OK, voucher.max_usages - voucher.redeemed
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
item.cached_availability = list(
|
||||||
|
item.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not (
|
||||||
|
ignore_hide_sold_out_for_item_ids and item.pk in ignore_hide_sold_out_for_item_ids
|
||||||
|
) and event.settings.hide_sold_out and item.cached_availability[0] < Quota.AVAILABILITY_RESERVED:
|
||||||
|
item._remove = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
item.order_max = min(
|
||||||
|
item.cached_availability[1]
|
||||||
|
if item.cached_availability[1] is not None else sys.maxsize,
|
||||||
|
max_per_order
|
||||||
|
)
|
||||||
|
|
||||||
|
original_price = item_price_override.get(item.pk, item.default_price)
|
||||||
|
voucher_reduced = False
|
||||||
|
if voucher:
|
||||||
|
price = voucher.calculate_price(original_price)
|
||||||
|
voucher_reduced = price < original_price
|
||||||
|
include_bundled = not voucher.all_bundles_included
|
||||||
|
else:
|
||||||
|
price = original_price
|
||||||
|
include_bundled = True
|
||||||
|
|
||||||
|
item.display_price = item.tax(price, currency=event.currency, include_bundled=include_bundled)
|
||||||
|
if item.free_price and item.free_price_suggestion is not None and not voucher_reduced:
|
||||||
|
item.suggested_price = item.tax(max(price, item.free_price_suggestion), currency=event.currency, include_bundled=include_bundled)
|
||||||
|
else:
|
||||||
|
item.suggested_price = item.display_price
|
||||||
|
|
||||||
|
if price != original_price:
|
||||||
|
item.original_price = item.tax(original_price, currency=event.currency, include_bundled=True)
|
||||||
|
else:
|
||||||
|
item.original_price = (
|
||||||
|
item.tax(item.original_price, currency=event.currency, include_bundled=True,
|
||||||
|
base_price_is='net' if event.settings.display_net_prices else 'gross') # backwards-compat
|
||||||
|
if item.original_price else None
|
||||||
|
)
|
||||||
|
if not display_add_to_cart:
|
||||||
|
display_add_to_cart = not item.requires_seat and item.order_max > 0
|
||||||
|
else:
|
||||||
|
for var in item.available_variations:
|
||||||
|
if var.require_membership and var.require_membership_hidden:
|
||||||
|
if not memberships or not any([m.membership_type in var.require_membership_types.all() for m in memberships]):
|
||||||
|
var._remove = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
var.description = str(var.description)
|
||||||
|
for recv, resp in item_description.send(sender=event, item=item, variation=var, subevent=subevent):
|
||||||
|
if resp:
|
||||||
|
var.description += ("<br/>" if var.description else "") + resp
|
||||||
|
|
||||||
|
if voucher and (voucher.allow_ignore_quota or voucher.block_quota):
|
||||||
|
var.cached_availability = (
|
||||||
|
Quota.AVAILABILITY_OK, voucher.max_usages - voucher.redeemed
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
var.cached_availability = list(
|
||||||
|
var.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
var.order_max = min(
|
||||||
|
var.cached_availability[1]
|
||||||
|
if var.cached_availability[1] is not None else sys.maxsize,
|
||||||
|
max_per_order
|
||||||
|
)
|
||||||
|
|
||||||
|
original_price = var_price_override.get(var.pk, var.price)
|
||||||
|
voucher_reduced = False
|
||||||
|
if voucher:
|
||||||
|
price = voucher.calculate_price(original_price)
|
||||||
|
voucher_reduced = price < original_price
|
||||||
|
include_bundled = not voucher.all_bundles_included
|
||||||
|
else:
|
||||||
|
price = original_price
|
||||||
|
include_bundled = True
|
||||||
|
|
||||||
|
var.display_price = var.tax(price, currency=event.currency, include_bundled=include_bundled)
|
||||||
|
|
||||||
|
if item.free_price and var.free_price_suggestion is not None and not voucher_reduced:
|
||||||
|
var.suggested_price = item.tax(max(price, var.free_price_suggestion), currency=event.currency,
|
||||||
|
include_bundled=include_bundled)
|
||||||
|
elif item.free_price and item.free_price_suggestion is not None and not voucher_reduced:
|
||||||
|
var.suggested_price = item.tax(max(price, item.free_price_suggestion), currency=event.currency,
|
||||||
|
include_bundled=include_bundled)
|
||||||
|
else:
|
||||||
|
var.suggested_price = var.display_price
|
||||||
|
|
||||||
|
if price != original_price:
|
||||||
|
var.original_price = var.tax(original_price, currency=event.currency, include_bundled=True)
|
||||||
|
else:
|
||||||
|
var.original_price = (
|
||||||
|
var.tax(var.original_price or item.original_price, currency=event.currency,
|
||||||
|
include_bundled=True,
|
||||||
|
base_price_is='net' if event.settings.display_net_prices else 'gross') # backwards-compat
|
||||||
|
) if var.original_price or item.original_price else None
|
||||||
|
|
||||||
|
if not display_add_to_cart:
|
||||||
|
display_add_to_cart = not item.requires_seat and var.order_max > 0
|
||||||
|
|
||||||
|
var.current_unavailability_reason = var.unavailability_reason(has_voucher=voucher, subevent=subevent)
|
||||||
|
|
||||||
|
item.original_price = (
|
||||||
|
item.tax(item.original_price, currency=event.currency, include_bundled=True,
|
||||||
|
base_price_is='net' if event.settings.display_net_prices else 'gross') # backwards-compat
|
||||||
|
if item.original_price else None
|
||||||
|
)
|
||||||
|
|
||||||
|
item.available_variations = [
|
||||||
|
v for v in item.available_variations if v._subevent_quotas and (
|
||||||
|
not voucher or not voucher.quota_id or v in restrict_vars
|
||||||
|
) and not getattr(v, '_remove', False)
|
||||||
|
]
|
||||||
|
|
||||||
|
if not (ignore_hide_sold_out_for_item_ids and item.pk in ignore_hide_sold_out_for_item_ids) and event.settings.hide_sold_out:
|
||||||
|
item.available_variations = [v for v in item.available_variations
|
||||||
|
if v.cached_availability[0] >= Quota.AVAILABILITY_RESERVED]
|
||||||
|
|
||||||
|
if voucher and voucher.variation_id:
|
||||||
|
item.available_variations = [v for v in item.available_variations
|
||||||
|
if v.pk == voucher.variation_id]
|
||||||
|
|
||||||
|
if len(item.available_variations) > 0:
|
||||||
|
item.min_price = min([v.display_price.net if event.settings.display_net_prices else
|
||||||
|
v.display_price.gross for v in item.available_variations])
|
||||||
|
item.max_price = max([v.display_price.net if event.settings.display_net_prices else
|
||||||
|
v.display_price.gross for v in item.available_variations])
|
||||||
|
item.best_variation_availability = max([v.cached_availability[0] for v in item.available_variations])
|
||||||
|
|
||||||
|
item._remove = not bool(item.available_variations)
|
||||||
|
|
||||||
|
if not quota_cache_existed and not voucher and not allow_addons and not base_qs_set and not filter_items and not filter_categories:
|
||||||
|
event.cache.set(quota_cache_key, quota_cache, 5)
|
||||||
|
items = [item for item in items
|
||||||
|
if (len(item.available_variations) > 0 or not item.has_variations) and not item._remove]
|
||||||
|
return items, display_add_to_cart
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#
|
||||||
|
# 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/>.
|
||||||
|
#
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PretixStorefrontApiConfig(AppConfig):
|
||||||
|
name = "pretix.storefrontapi"
|
||||||
|
label = "pretixstorefrontapi"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from . import signals # noqa
|
||||||
@@ -0,0 +1,400 @@
|
|||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from rest_framework import serializers, viewsets
|
||||||
|
from rest_framework.generics import get_object_or_404
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from pretix.base.models import (
|
||||||
|
Event, Item, ItemCategory, ItemVariation, Quota, SubEvent,
|
||||||
|
)
|
||||||
|
from pretix.base.storelogic.products import (
|
||||||
|
get_items_for_product_list, item_group_by_category,
|
||||||
|
)
|
||||||
|
from pretix.base.templatetags.rich_text import rich_text
|
||||||
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
from pretix.storefrontapi.permission import StorefrontEventPermission
|
||||||
|
from pretix.storefrontapi.serializers import I18nFlattenedModelSerializer
|
||||||
|
|
||||||
|
|
||||||
|
def opt_str(o):
|
||||||
|
if o is None:
|
||||||
|
return None
|
||||||
|
return str(o)
|
||||||
|
|
||||||
|
|
||||||
|
class RichtTextField(serializers.Field):
|
||||||
|
def to_representation(self, value):
|
||||||
|
return rich_text(value)
|
||||||
|
|
||||||
|
|
||||||
|
class DynamicAttrField(serializers.Field):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.attr = kwargs.pop("attr")
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
return getattr(value, self.attr)
|
||||||
|
|
||||||
|
|
||||||
|
class EventURLField(serializers.Field):
|
||||||
|
def to_representation(self, ev):
|
||||||
|
if isinstance(ev, SubEvent):
|
||||||
|
return build_absolute_uri(
|
||||||
|
ev.event, "presale:event.index", kwargs={"subevent": ev.pk}
|
||||||
|
)
|
||||||
|
return build_absolute_uri(ev, "presale:event.index")
|
||||||
|
|
||||||
|
|
||||||
|
class EventSettingsField(serializers.Field):
|
||||||
|
def to_representation(self, ev):
|
||||||
|
event = ev.event if isinstance(ev, SubEvent) else ev
|
||||||
|
return {
|
||||||
|
"display_net_prices": event.settings.display_net_prices,
|
||||||
|
"show_variations_expanded": event.settings.show_variations_expanded,
|
||||||
|
"show_times": event.settings.show_times,
|
||||||
|
"show_dates_on_frontpage": event.settings.show_dates_on_frontpage,
|
||||||
|
"voucher_explanation_text": str(
|
||||||
|
rich_text(event.settings.voucher_explanation_text, safelinks=False)
|
||||||
|
),
|
||||||
|
"frontpage_text": str(
|
||||||
|
rich_text(
|
||||||
|
(
|
||||||
|
ev.frontpage_text
|
||||||
|
if isinstance(ev, SubEvent)
|
||||||
|
else event.settings.frontpage_text
|
||||||
|
),
|
||||||
|
safelinks=False,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CategorySerializer(I18nFlattenedModelSerializer):
|
||||||
|
description = RichtTextField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ItemCategory
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PricingField(serializers.Field):
|
||||||
|
def to_representation(self, item_or_var):
|
||||||
|
if isinstance(item_or_var, Item) and item_or_var.has_variations:
|
||||||
|
return None
|
||||||
|
|
||||||
|
item = item_or_var if isinstance(item_or_var, Item) else item_or_var.item
|
||||||
|
|
||||||
|
return {
|
||||||
|
"display_price": {
|
||||||
|
"net": opt_str(item_or_var.display_price.net),
|
||||||
|
"gross": opt_str(item_or_var.display_price.gross),
|
||||||
|
"tax_rate": opt_str(
|
||||||
|
item_or_var.display_price.rate
|
||||||
|
if not item.includes_mixed_tax_rate
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"tax_name": opt_str(
|
||||||
|
item_or_var.display_price.name
|
||||||
|
if not item.includes_mixed_tax_rate
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"original_price": (
|
||||||
|
{
|
||||||
|
"net": opt_str(item_or_var.original_price.net),
|
||||||
|
"gross": opt_str(item_or_var.original_price.gross),
|
||||||
|
"tax_rate": opt_str(
|
||||||
|
item_or_var.original_price.rate
|
||||||
|
if not item.includes_mixed_tax_rate
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"tax_name": opt_str(
|
||||||
|
item_or_var.original_price.name
|
||||||
|
if not item.includes_mixed_tax_rate
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
}
|
||||||
|
if item_or_var.original_price
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"free_price": item.free_price,
|
||||||
|
"suggested_price": {
|
||||||
|
"net": opt_str(item_or_var.suggested_price.net),
|
||||||
|
"gross": opt_str(item_or_var.suggested_price.gross),
|
||||||
|
"tax_rate": opt_str(
|
||||||
|
item_or_var.suggested_price.rate
|
||||||
|
if not item.includes_mixed_tax_rate
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"tax_name": opt_str(
|
||||||
|
item_or_var.suggested_price.name
|
||||||
|
if not item.includes_mixed_tax_rate
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"mandatory_priced_addons": item.mandatory_priced_addons,
|
||||||
|
"includes_mixed_tax_rate": item.includes_mixed_tax_rate,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AvailabilityField(serializers.Field):
|
||||||
|
def to_representation(self, item_or_var):
|
||||||
|
if isinstance(item_or_var, Item) and item_or_var.has_variations:
|
||||||
|
return None
|
||||||
|
|
||||||
|
item = item_or_var if isinstance(item_or_var, Item) else item_or_var.item
|
||||||
|
|
||||||
|
if (
|
||||||
|
item_or_var.current_unavailability_reason == "require_voucher"
|
||||||
|
or item.current_unavailability_reason == "require_voucher"
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
"available": False,
|
||||||
|
"code": "require_voucher",
|
||||||
|
"message": _("Enter a voucher code below to buy this product."),
|
||||||
|
"waiting_list": False,
|
||||||
|
"max_selection": 0,
|
||||||
|
"quota_left": None,
|
||||||
|
}
|
||||||
|
elif (
|
||||||
|
item_or_var.current_unavailability_reason == "available_from"
|
||||||
|
or item.current_unavailability_reason == "available_from"
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
"available": False,
|
||||||
|
"code": "available_from",
|
||||||
|
"message": _("Not available yet."),
|
||||||
|
"waiting_list": False,
|
||||||
|
"max_selection": 0,
|
||||||
|
"quota_left": None,
|
||||||
|
}
|
||||||
|
elif (
|
||||||
|
item_or_var.current_unavailability_reason == "available_until"
|
||||||
|
or item.current_unavailability_reason == "available_until"
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
"available": False,
|
||||||
|
"code": "available_until",
|
||||||
|
"message": _("Not available any more."),
|
||||||
|
"waiting_list": False,
|
||||||
|
"max_selection": 0,
|
||||||
|
"quota_left": None,
|
||||||
|
}
|
||||||
|
elif item_or_var.cached_availability[0] <= Quota.AVAILABILITY_ORDERED:
|
||||||
|
return {
|
||||||
|
"available": False,
|
||||||
|
"code": "sold_out",
|
||||||
|
"message": _("SOLD OUT"),
|
||||||
|
"waiting_list": self.context["allow_waitinglist"]
|
||||||
|
and item.allow_waitinglist,
|
||||||
|
"max_selection": 0,
|
||||||
|
"quota_left": 0,
|
||||||
|
}
|
||||||
|
elif item_or_var.cached_availability[0] < Quota.AVAILABILITY_OK:
|
||||||
|
return {
|
||||||
|
"available": False,
|
||||||
|
"code": "reserved",
|
||||||
|
"message": _(
|
||||||
|
"All remaining products are reserved but might become available again."
|
||||||
|
),
|
||||||
|
"waiting_list": self.context["allow_waitinglist"]
|
||||||
|
and item.allow_waitinglist,
|
||||||
|
"max_selection": 0,
|
||||||
|
"quota_left": 0,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"available": True,
|
||||||
|
"code": "ok",
|
||||||
|
"message": None,
|
||||||
|
"waiting_list": False,
|
||||||
|
"max_selection": item_or_var.order_max,
|
||||||
|
"quota_left": (
|
||||||
|
item_or_var.cached_availability[1]
|
||||||
|
if item.show_quota_left
|
||||||
|
and item_or_var.cached_availability[1] is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class VariationSerializer(I18nFlattenedModelSerializer):
|
||||||
|
description = RichtTextField()
|
||||||
|
pricing = PricingField(source="*")
|
||||||
|
availability = AvailabilityField(source="*")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ItemVariation
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"value",
|
||||||
|
"description",
|
||||||
|
"pricing",
|
||||||
|
"availability",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ItemSerializer(I18nFlattenedModelSerializer):
|
||||||
|
description = RichtTextField()
|
||||||
|
available_variations = VariationSerializer(many=True, read_only=True)
|
||||||
|
pricing = PricingField(source="*")
|
||||||
|
availability = AvailabilityField(source="*")
|
||||||
|
has_variations = serializers.BooleanField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Item
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"has_variations",
|
||||||
|
"description",
|
||||||
|
"picture",
|
||||||
|
"min_per_order",
|
||||||
|
"free_price",
|
||||||
|
"available_variations",
|
||||||
|
"pricing",
|
||||||
|
"availability",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ProductGroupField(serializers.Field):
|
||||||
|
def to_representation(self, ev):
|
||||||
|
event = ev.event if isinstance(ev, SubEvent) else ev
|
||||||
|
|
||||||
|
items, display_add_to_cart = get_items_for_product_list(
|
||||||
|
event,
|
||||||
|
subevent=ev if isinstance(ev, SubEvent) else None,
|
||||||
|
require_seat=False,
|
||||||
|
channel=self.context["sales_channel"],
|
||||||
|
memberships=(
|
||||||
|
self.context["customer"].usable_memberships(
|
||||||
|
for_event=ev, testmode=event.testmode
|
||||||
|
)
|
||||||
|
if self.context.get("customer")
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"category": (
|
||||||
|
CategorySerializer(cat, context=self.context).data if cat else None
|
||||||
|
),
|
||||||
|
"items": ItemSerializer(items, many=True, context=self.context).data,
|
||||||
|
}
|
||||||
|
for cat, items in item_group_by_category(items)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class BaseEventDetailSerializer(I18nFlattenedModelSerializer):
|
||||||
|
public_url = EventURLField(source="*", read_only=True)
|
||||||
|
settings = EventSettingsField(source="*", read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Event
|
||||||
|
fields = [
|
||||||
|
"name",
|
||||||
|
"has_subevents",
|
||||||
|
"public_url",
|
||||||
|
"currency",
|
||||||
|
"settings",
|
||||||
|
]
|
||||||
|
|
||||||
|
def to_representation(self, ev):
|
||||||
|
r = super().to_representation(ev)
|
||||||
|
event = ev.event if isinstance(ev, SubEvent) else ev
|
||||||
|
|
||||||
|
if not event.settings.presale_start_show_date or event.presale_is_running:
|
||||||
|
r["effective_presale_start"] = None
|
||||||
|
if not event.settings.show_date_to:
|
||||||
|
r["date_to"] = None
|
||||||
|
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
class SubEventDetailSerializer(BaseEventDetailSerializer):
|
||||||
|
testmode = serializers.BooleanField(source="event.testmode")
|
||||||
|
has_subevents = serializers.BooleanField(source="event.has_subevents")
|
||||||
|
product_list = ProductGroupField(source="*")
|
||||||
|
|
||||||
|
# todo: vouchers_exist
|
||||||
|
# todo: date range
|
||||||
|
# todo: waiting list info
|
||||||
|
# todo: has seating
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SubEvent
|
||||||
|
fields = [
|
||||||
|
"name",
|
||||||
|
"testmode",
|
||||||
|
"has_subevents",
|
||||||
|
"public_url",
|
||||||
|
"currency",
|
||||||
|
"settings",
|
||||||
|
"location",
|
||||||
|
"date_from",
|
||||||
|
"date_to",
|
||||||
|
"date_admission",
|
||||||
|
"presale_is_running",
|
||||||
|
"effective_presale_start",
|
||||||
|
"product_list",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class EventDetailSerializer(BaseEventDetailSerializer):
|
||||||
|
# todo: vouchers_exist
|
||||||
|
# todo: date range
|
||||||
|
# todo: waiting list info
|
||||||
|
# todo: has seating
|
||||||
|
product_list = ProductGroupField(source="*")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Event
|
||||||
|
fields = [
|
||||||
|
"name",
|
||||||
|
"testmode",
|
||||||
|
"has_subevents",
|
||||||
|
"public_url",
|
||||||
|
"currency",
|
||||||
|
"settings",
|
||||||
|
"location",
|
||||||
|
"date_from",
|
||||||
|
"date_to",
|
||||||
|
"date_admission",
|
||||||
|
"presale_is_running",
|
||||||
|
"effective_presale_start",
|
||||||
|
"product_list",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class EventViewSet(viewsets.ViewSet):
|
||||||
|
queryset = Event.objects.none()
|
||||||
|
lookup_url_kwarg = "event"
|
||||||
|
lookup_field = "slug"
|
||||||
|
permission_classes = [
|
||||||
|
StorefrontEventPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def retrieve(self, request, *args, **kwargs):
|
||||||
|
event = request.event # Lookup is already done
|
||||||
|
|
||||||
|
ctx = {
|
||||||
|
"sales_channel": request.sales_channel,
|
||||||
|
"customer": None,
|
||||||
|
"event": event,
|
||||||
|
"allow_waitinglist": True,
|
||||||
|
}
|
||||||
|
if event.has_subevents:
|
||||||
|
if "subevent" in request.GET:
|
||||||
|
ctx["event"] = request.event
|
||||||
|
subevent = get_object_or_404(
|
||||||
|
request.event.subevents, pk=request.GET.get("subevent"), active=True
|
||||||
|
)
|
||||||
|
serializer = SubEventDetailSerializer(subevent, context=ctx)
|
||||||
|
else:
|
||||||
|
serializer = BaseEventDetailSerializer(event, context=ctx)
|
||||||
|
else:
|
||||||
|
serializer = EventDetailSerializer(event, context=ctx)
|
||||||
|
return Response(serializer.data)
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
#
|
||||||
|
# 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 logging
|
||||||
|
|
||||||
|
from dateutil.parser import parse
|
||||||
|
from django.http import HttpRequest
|
||||||
|
from django.urls import resolve
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from django_scopes import scope
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from pretix.base.middleware import LocaleMiddleware
|
||||||
|
from pretix.base.models import Event, Organizer
|
||||||
|
from pretix.base.timemachine import timemachine_now_var
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ApiMiddleware:
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request: HttpRequest):
|
||||||
|
if not request.path.startswith("/storefrontapi/"):
|
||||||
|
return self.get_response(request)
|
||||||
|
|
||||||
|
url = resolve(request.path_info)
|
||||||
|
try:
|
||||||
|
request.organizer = Organizer.objects.filter(
|
||||||
|
slug=url.kwargs["organizer"],
|
||||||
|
).first()
|
||||||
|
except Organizer.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"detail": "Organizer not found."},
|
||||||
|
status=404,
|
||||||
|
)
|
||||||
|
|
||||||
|
with scope(organizer=getattr(request, "organizer", None)):
|
||||||
|
# todo: Authorization
|
||||||
|
is_authorized_public = False # noqa
|
||||||
|
is_authorized_private = True
|
||||||
|
sales_channel_id = "web" # todo: get form authorization
|
||||||
|
|
||||||
|
if "event" in url.kwargs:
|
||||||
|
try:
|
||||||
|
request.event = request.organizer.events.get(
|
||||||
|
slug=url.kwargs["event"],
|
||||||
|
organizer=request.organizer,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not request.event.live and not is_authorized_private:
|
||||||
|
return Response(
|
||||||
|
{"detail": "Event not live."},
|
||||||
|
status=403,
|
||||||
|
)
|
||||||
|
|
||||||
|
except Event.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"detail": "Event not found."},
|
||||||
|
status=404,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
request.sales_channel = request.organizer.sales_channels.get(
|
||||||
|
identifier=sales_channel_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
"X-Storefront-Time-Machine-Date" in request.headers
|
||||||
|
and "event" in url.kwargs
|
||||||
|
):
|
||||||
|
if not request.event.testmode:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"detail": "Time machine can only be used for events in test mode."
|
||||||
|
},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
time_machine_date = parse(
|
||||||
|
request.headers["X-Storefront-Time-Machine-Date"]
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
return Response(
|
||||||
|
{"detail": "Invalid time machine header"},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
request.now_dt = time_machine_date
|
||||||
|
request.now_dt_is_fake = True
|
||||||
|
timemachine_now_var.set(
|
||||||
|
request.now_dt if request.now_dt_is_fake else None
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
request.now_dt = now()
|
||||||
|
request.now_dt_is_fake = False
|
||||||
|
|
||||||
|
if (
|
||||||
|
not request.event.all_sales_channels
|
||||||
|
and request.sales_channel.identifier
|
||||||
|
not in (
|
||||||
|
s.identifier for s in request.event.limit_sales_channels.all()
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return Response(
|
||||||
|
{"detail": "Event not available on this sales channel."},
|
||||||
|
status=403,
|
||||||
|
)
|
||||||
|
|
||||||
|
LocaleMiddleware(NotImplementedError).process_request(request)
|
||||||
|
r = self.get_response(request)
|
||||||
|
r["Access-Control-Allow-Origin"] = "*" # todo: allow whitelist?
|
||||||
|
return r
|
||||||
|
finally:
|
||||||
|
timemachine_now_var.set(None)
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#
|
||||||
|
# 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/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
from rest_framework.permissions import BasePermission
|
||||||
|
|
||||||
|
|
||||||
|
class StorefrontEventPermission(BasePermission):
|
||||||
|
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
# TODO: Check middleware results
|
||||||
|
return True
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
from i18nfield.fields import I18nCharField, I18nTextField
|
||||||
|
from rest_framework.fields import Field
|
||||||
|
from rest_framework.serializers import ModelSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class I18nFlattenedField(Field):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.allow_blank = kwargs.pop("allow_blank", False)
|
||||||
|
self.trim_whitespace = kwargs.pop("trim_whitespace", True)
|
||||||
|
self.max_length = kwargs.pop("max_length", None)
|
||||||
|
self.min_length = kwargs.pop("min_length", None)
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
raise TypeError("Input not supported.")
|
||||||
|
|
||||||
|
|
||||||
|
class I18nFlattenedModelSerializer(ModelSerializer):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
I18nFlattenedModelSerializer.serializer_field_mapping[I18nCharField] = (
|
||||||
|
I18nFlattenedField
|
||||||
|
)
|
||||||
|
I18nFlattenedModelSerializer.serializer_field_mapping[I18nTextField] = (
|
||||||
|
I18nFlattenedField
|
||||||
|
)
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#
|
||||||
|
# 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 importlib
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
|
from django.urls import include, re_path
|
||||||
|
from rest_framework import routers
|
||||||
|
|
||||||
|
from .endpoints import event
|
||||||
|
|
||||||
|
storefront_orga_router = routers.DefaultRouter()
|
||||||
|
storefront_orga_router.register(r"events", event.EventViewSet)
|
||||||
|
|
||||||
|
storefront_event_router = routers.DefaultRouter()
|
||||||
|
|
||||||
|
# Force import of all plugins to give them a chance to register URLs with the router
|
||||||
|
for app in apps.get_app_configs():
|
||||||
|
if hasattr(app, "PretixPluginMeta"):
|
||||||
|
if importlib.util.find_spec(app.name + ".urls"):
|
||||||
|
importlib.import_module(app.name + ".urls")
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
re_path(r"^organizers/(?P<organizer>[^/]+)/", include(storefront_orga_router.urls)),
|
||||||
|
re_path(
|
||||||
|
r"^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/",
|
||||||
|
include(storefront_event_router.urls),
|
||||||
|
),
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user