diff --git a/doc/_templates/index.html b/doc/_templates/index.html index c6964b43d..6fa23386e 100644 --- a/doc/_templates/index.html +++ b/doc/_templates/index.html @@ -54,6 +54,23 @@

+
+
+ + + +
+
+ + Storefront API + +

+ Documentation and reference of the headless shopping API exposed by pretix for building a custom + storefront. +

+
+
+
@@ -68,7 +85,6 @@ pretix.

-
@@ -82,19 +98,6 @@

Documentation and details on plugins that ship with pretix or are officially supported.

-
-
- - - -
-
- - Table of contents - -

Detailled overview of everything contained in this documentation.

-
-

Useful links

diff --git a/doc/api/fundamentals.rst b/doc/api/fundamentals.rst index 00b2d261c..fb84e80ef 100644 --- a/doc/api/fundamentals.rst +++ b/doc/api/fundamentals.rst @@ -156,6 +156,8 @@ Field specific input errors include the name of the offending fields as keys in If you see errors of type ``429 Too Many Requests``, you should read our documentation on :ref:`rest-ratelimit`. +.. _`rest-types`: + Data types ---------- diff --git a/doc/contents.rst b/doc/contents.rst index d8544d0eb..929d1e6f0 100644 --- a/doc/contents.rst +++ b/doc/contents.rst @@ -7,6 +7,7 @@ Table of contents user/index admin/index api/index + storefrontapi/index development/index plugins/index license/faq diff --git a/setup.py b/setup.py index cb98090f7..1c714bfb9 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,6 @@ from pathlib import Path import setuptools - sys.path.append(str(Path.cwd() / 'src')) diff --git a/src/pretix/_base_settings.py b/src/pretix/_base_settings.py index b687a3ce3..4eb336e97 100644 --- a/src/pretix/_base_settings.py +++ b/src/pretix/_base_settings.py @@ -44,6 +44,7 @@ INSTALLED_APPS = [ 'pretix.presale', 'pretix.multidomain', 'pretix.api', + 'pretix.storefrontapi', 'pretix.helpers', 'rest_framework', 'djangoformsetjs', diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index 1e8f44d61..dd8a35bfe 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -3063,6 +3063,38 @@ class Transaction(models.Model): return self.tax_value * self.count +class CheckoutSession(models.Model): + """ + A checkout session optionally bundles cart positions with additional information. This is historically + not required in pretix and currently only used in the Storefront API. + """ + event = models.ForeignKey( + Event, + verbose_name=_("Event"), + on_delete=models.CASCADE + ) + cart_id = models.CharField( + max_length=255, unique=True, + verbose_name=_("Cart ID (e.g. session key)") + ) + created = models.DateTimeField( + verbose_name=_("Date"), + auto_now_add=True + ) + customer = models.ForeignKey( + Customer, + related_name='checkout_sessions', + null=True, blank=True, + on_delete=models.SET_NULL, + ) + sales_channel = models.ForeignKey( + "SalesChannel", + on_delete=models.CASCADE, + ) + testmode = models.BooleanField(default=False) + session_data = models.JSONField(default=dict) + + class CartPosition(AbstractPosition): """ A cart position is similar to an order line, except that it is not @@ -3245,6 +3277,13 @@ class CartPosition(AbstractPosition): class InvoiceAddress(models.Model): last_modified = models.DateTimeField(auto_now=True) + checkout_session = models.OneToOneField( + CheckoutSession, + null=True, + blank=True, + related_name='invoice_address', + on_delete=models.CASCADE + ) order = models.OneToOneField(Order, null=True, blank=True, related_name='invoice_address', on_delete=models.CASCADE) customer = models.ForeignKey( Customer, diff --git a/src/pretix/base/services/cross_selling.py b/src/pretix/base/services/cross_selling.py index e81abd665..81feff634 100644 --- a/src/pretix/base/services/cross_selling.py +++ b/src/pretix/base/services/cross_selling.py @@ -29,7 +29,7 @@ from typing import List from django.utils.functional import cached_property from pretix.base.models import CartPosition, ItemCategory, SalesChannel -from pretix.presale.views.event import get_grouped_items +from pretix.base.storelogic.products import get_items_for_product_list class DummyCategory: @@ -161,7 +161,7 @@ class CrossSellingService: ] def _prepare_items(self, subevent, items_qs, discount_info): - items, _btn = get_grouped_items( + items, _btn = get_items_for_product_list( self.event, subevent=subevent, voucher=None, diff --git a/src/pretix/base/validators.py b/src/pretix/base/validators.py index 254e6778a..e4d153fee 100644 --- a/src/pretix/base/validators.py +++ b/src/pretix/base/validators.py @@ -67,6 +67,7 @@ class EventSlugBanlistValidator(BanlistValidator): '_global', '__debug__', 'api', + 'storefrontapi', 'events', 'csp_report', 'widget', @@ -91,6 +92,7 @@ class OrganizerSlugBanlistValidator(BanlistValidator): '__debug__', 'about', 'api', + 'storefrontapi', 'csp_report', 'widget', 'lead', diff --git a/src/pretix/control/apps.py b/src/pretix/control/apps.py index 1b2148618..50614649f 100644 --- a/src/pretix/control/apps.py +++ b/src/pretix/control/apps.py @@ -40,5 +40,5 @@ class PretixControlConfig(AppConfig): label = 'pretixcontrol' def ready(self): - from .views import dashboards # noqa from . import logdisplay # noqa + from .views import dashboards # noqa diff --git a/src/pretix/presale/checkoutflow.py b/src/pretix/presale/checkoutflow.py index 3a1ea6ff2..849a38c17 100644 --- a/src/pretix/presale/checkoutflow.py +++ b/src/pretix/presale/checkoutflow.py @@ -72,6 +72,7 @@ from pretix.base.services.orders import perform_order from pretix.base.services.tasks import EventTask from pretix.base.settings import PERSON_NAME_SCHEMES from pretix.base.signals import validate_cart_addons +from pretix.base.storelogic.products import get_items_for_product_list from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.phone_format import phone_format from pretix.base.templatetags.rich_text import rich_text_snippet @@ -98,7 +99,6 @@ from pretix.presale.views.cart import ( _items_from_post_data, cart_session, create_empty_cart_id, get_or_create_cart_id, ) -from pretix.presale.views.event import get_grouped_items from pretix.presale.views.questions import QuestionsViewMixin @@ -560,7 +560,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep): if ckey not in item_cache: # Get all items to possibly show - items, _btn = get_grouped_items( + items, _btn = get_items_for_product_list( self.request.event, subevent=cartpos.subevent, voucher=None, diff --git a/src/pretix/presale/views/cart.py b/src/pretix/presale/views/cart.py index 35ec6304b..c75894150 100644 --- a/src/pretix/presale/views/cart.py +++ b/src/pretix/presale/views/cart.py @@ -64,6 +64,9 @@ from pretix.base.services.cart import ( CartError, add_items_to_cart, apply_voucher, clear_cart, error_messages, remove_cart_position, ) +from pretix.base.storelogic.products import ( + get_items_for_product_list, item_group_by_category, +) from pretix.base.timemachine import time_machine_now from pretix.base.views.tasks import AsyncAction from pretix.helpers.http import redirect_to_url @@ -72,9 +75,6 @@ from pretix.presale.views import ( CartMixin, EventViewMixin, allow_cors_if_namespaced, allow_frame_if_namespaced, iframe_entry_view_wrapper, ) -from pretix.presale.views.event import ( - get_grouped_items, item_group_by_category, -) from pretix.presale.views.robots import NoSearchIndexViewMixin try: @@ -613,7 +613,7 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, CartMixin, TemplateView context['max_times'] = self.voucher.max_usages - self.voucher.redeemed # Fetch all items - items, display_add_to_cart = get_grouped_items( + items, display_add_to_cart = get_items_for_product_list( self.request.event, subevent=self.subevent, voucher=self.voucher, diff --git a/src/pretix/presale/views/event.py b/src/pretix/presale/views/event.py index d84e8ca96..c12b776ef 100644 --- a/src/pretix/presale/views/event.py +++ b/src/pretix/presale/views/event.py @@ -34,7 +34,6 @@ import calendar import hashlib -import sys from collections import defaultdict from datetime import date, datetime, timedelta from decimal import Decimal @@ -47,10 +46,7 @@ from django import forms from django.conf import settings from django.contrib import messages from django.core.exceptions import PermissionDenied -from django.db.models import ( - Count, Exists, IntegerField, OuterRef, Prefetch, Q, Value, -) -from django.db.models.lookups import Exact +from django.db.models import Count from django.http import Http404, HttpResponse from django.shortcuts import get_object_or_404, redirect, render from django.utils.decorators import method_decorator @@ -64,15 +60,9 @@ from django.views.decorators.csrf import csrf_exempt from django.views.generic import TemplateView from pretix.base.forms.widgets import SplitDateTimePickerWidget -from pretix.base.models import ( - ItemVariation, Quota, SalesChannel, SeatCategoryMapping, Voucher, -) +from pretix.base.models import Quota, Voucher from pretix.base.models.event import Event, SubEvent -from pretix.base.models.items import ( - ItemAddOn, ItemBundle, SubEventItem, SubEventItemVariation, -) from pretix.base.services.placeholders import PlaceholderContext -from pretix.base.services.quotas import QuotaAvailability from pretix.base.timemachine import ( has_time_machine_permission, time_machine_now, ) @@ -83,12 +73,15 @@ from pretix.helpers.formats.en.formats import ( from pretix.helpers.http import redirect_to_url from pretix.multidomain.urlreverse import eventreverse from pretix.presale.ical import get_public_ical -from pretix.presale.signals import item_description, seatingframe_html_head +from pretix.presale.signals import seatingframe_html_head from pretix.presale.views.organizer import ( EventListMixin, add_subevents_for_days, days_for_template, filter_qs_by_attr, has_before_after, weeks_for_template, ) +from ...base.storelogic.products import ( + get_items_for_product_list, item_group_by_category, +) from . import ( CartMixin, EventViewMixin, allow_frame_if_namespaced, get_cart, iframe_entry_view_wrapper, @@ -97,386 +90,6 @@ from . import ( SessionStore = import_module(settings.SESSION_ENGINE).SessionStore -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_grouped_items(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 += ("
" 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 += ("
" 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 - - @method_decorator(allow_frame_if_namespaced, 'dispatch') @method_decorator(iframe_entry_view_wrapper, 'dispatch') class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView): @@ -571,7 +184,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView): if not self.request.event.has_subevents or self.subevent: # Fetch all items - items, display_add_to_cart = get_grouped_items( + items, display_add_to_cart = get_items_for_product_list( self.request.event, subevent=self.subevent, filter_items=self.request.GET.getlist('item'), diff --git a/src/pretix/presale/views/order.py b/src/pretix/presale/views/order.py index eb6bd8512..8cc6c4d53 100644 --- a/src/pretix/presale/views/order.py +++ b/src/pretix/presale/views/order.py @@ -82,6 +82,7 @@ from pretix.base.services.orders import ( from pretix.base.services.pricing import get_price from pretix.base.services.tickets import generate, invalidate_cache from pretix.base.signals import order_modified, register_ticket_outputs +from pretix.base.storelogic.products import get_items_for_product_list from pretix.base.templatetags.money import money_filter from pretix.base.views.mixins import OrderQuestionsViewMixin from pretix.base.views.tasks import AsyncAction @@ -95,7 +96,6 @@ from pretix.presale.signals import question_form_fields_overrides from pretix.presale.views import ( CartMixin, EventViewMixin, iframe_entry_view_wrapper, ) -from pretix.presale.views.event import get_grouped_items from pretix.presale.views.robots import NoSearchIndexViewMixin @@ -1372,7 +1372,7 @@ class OrderChangeMixin: if ckey not in item_cache: # Get all items to possibly show - items, _btn = get_grouped_items( + items, _btn = get_items_for_product_list( self.request.event, subevent=p.subevent, voucher=None, diff --git a/src/pretix/presale/views/waiting.py b/src/pretix/presale/views/waiting.py index b72888f82..0685c4795 100644 --- a/src/pretix/presale/views/waiting.py +++ b/src/pretix/presale/views/waiting.py @@ -39,9 +39,9 @@ from pretix.presale.views import EventViewMixin, iframe_entry_view_wrapper from ...base.i18n import get_language_without_region from ...base.models import Voucher, WaitingListEntry +from ...base.storelogic.products import get_items_for_product_list from ..forms.waitinglist import WaitingListForm from . import allow_frame_if_namespaced -from .event import get_grouped_items @method_decorator(allow_frame_if_namespaced, 'dispatch') @@ -53,7 +53,7 @@ class WaitingView(EventViewMixin, FormView): @cached_property def itemvars(self): customer = getattr(self.request, 'customer', None) - items, display_add_to_cart = get_grouped_items( + items, display_add_to_cart = get_items_for_product_list( self.request.event, subevent=self.subevent, require_seat=None, diff --git a/src/pretix/presale/views/widget.py b/src/pretix/presale/views/widget.py index 8ae618a6c..0dc0f5c0b 100644 --- a/src/pretix/presale/views/widget.py +++ b/src/pretix/presale/views/widget.py @@ -61,6 +61,9 @@ from pretix.base.models import ( from pretix.base.services.cart import error_messages from pretix.base.services.placeholders import PlaceholderContext from pretix.base.settings import GlobalSettingsObject +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.helpers.daterange import daterange from pretix.helpers.thumb import get_thumbnail @@ -68,9 +71,6 @@ from pretix.multidomain.urlreverse import build_absolute_uri from pretix.presale.forms.organizer import meta_filtersets from pretix.presale.style import get_theme_vars_css from pretix.presale.views.cart import get_or_create_cart_id -from pretix.presale.views.event import ( - get_grouped_items, item_group_by_category, -) from pretix.presale.views.organizer import ( EventListMixin, add_events_for_days, add_subevents_for_days, days_for_template, filter_qs_by_attr, weeks_for_template, @@ -270,7 +270,7 @@ class WidgetAPIProductList(EventListMixin, View): ).values_list('item_id', flat=True) ) - items, display_add_to_cart = get_grouped_items( + items, display_add_to_cart = get_items_for_product_list( self.request.event, subevent=self.subevent, voucher=self.voucher, diff --git a/src/pretix/urls.py b/src/pretix/urls.py index f557fc9ab..8e215e3be 100644 --- a/src/pretix/urls.py +++ b/src/pretix/urls.py @@ -57,6 +57,7 @@ base_patterns = [ re_path(r'^csp_report/$', csp.csp_report, name='csp.report'), re_path(r'^agpl_source$', source.get_source, name='source'), re_path(r'^js_helpers/states/$', js_helpers.states, name='js_helpers.states'), + re_path(r'^storefrontapi/v1/', include(('pretix.storefrontapi.urls', 'pretixstorefrontapi'), namespace='storefrontapi-v1')), re_path(r'^api/v1/', include(('pretix.api.urls', 'pretixapi'), namespace='api-v1')), re_path(r'^api/$', RedirectView.as_view(url='/api/v1/'), name='redirect-api-version'), re_path(r'^.well-known/apple-developer-merchantid-domain-association$',