Initial steps

This commit is contained in:
Raphael Michel
2025-01-01 14:38:14 +01:00
parent a56c6ae1e0
commit 8cba60dd93
16 changed files with 87 additions and 426 deletions

View File

@@ -54,6 +54,23 @@
</p> </p>
</div> </div>
</div> </div>
<div class="sectionbox">
<div class="icon">
<a href="storefrontapi/index.html">
<span class="fa fa-shopping-cart fa-fw"></span>
</a>
</div>
<div class="text">
<a href="storefrontapi/index.html">
<strong>Storefront API</strong>
</a>
<p>
Documentation and reference of the headless shopping API exposed by pretix for building a custom
storefront.
</p>
</div>
</div>
<div class="clearfix"></div>
<div class="sectionbox"> <div class="sectionbox">
<div class="icon"> <div class="icon">
<a href="development/index.html"> <a href="development/index.html">
@@ -68,7 +85,6 @@
pretix.</p> pretix.</p>
</div> </div>
</div> </div>
<div class="clearfix"></div>
<div class="sectionbox"> <div class="sectionbox">
<div class="icon"> <div class="icon">
<a href="plugins/index.html"> <a href="plugins/index.html">
@@ -82,19 +98,6 @@
<p>Documentation and details on plugins that ship with pretix or are officially supported.</p> <p>Documentation and details on plugins that ship with pretix or are officially supported.</p>
</div> </div>
</div> </div>
<div class="sectionbox">
<div class="icon">
<a href="contents.html">
<span class="fa fa-list fa-fw"></span>
</a>
</div>
<div class="text">
<a href="contents.html">
<strong>Table of contents</strong>
</a>
<p>Detailled overview of everything contained in this documentation.</p>
</div>
</div>
<div class="clearfix"></div> <div class="clearfix"></div>
<h2>Useful links</h2> <h2>Useful links</h2>

View File

@@ -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`. If you see errors of type ``429 Too Many Requests``, you should read our documentation on :ref:`rest-ratelimit`.
.. _`rest-types`:
Data types Data types
---------- ----------

View File

@@ -7,6 +7,7 @@ Table of contents
user/index user/index
admin/index admin/index
api/index api/index
storefrontapi/index
development/index development/index
plugins/index plugins/index
license/faq license/faq

View File

@@ -24,7 +24,6 @@ from pathlib import Path
import setuptools import setuptools
sys.path.append(str(Path.cwd() / 'src')) sys.path.append(str(Path.cwd() / 'src'))

View File

@@ -44,6 +44,7 @@ INSTALLED_APPS = [
'pretix.presale', 'pretix.presale',
'pretix.multidomain', 'pretix.multidomain',
'pretix.api', 'pretix.api',
'pretix.storefrontapi',
'pretix.helpers', 'pretix.helpers',
'rest_framework', 'rest_framework',
'djangoformsetjs', 'djangoformsetjs',

View File

@@ -3063,6 +3063,38 @@ class Transaction(models.Model):
return self.tax_value * self.count 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): class CartPosition(AbstractPosition):
""" """
A cart position is similar to an order line, except that it is not 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): class InvoiceAddress(models.Model):
last_modified = models.DateTimeField(auto_now=True) 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) order = models.OneToOneField(Order, null=True, blank=True, related_name='invoice_address', on_delete=models.CASCADE)
customer = models.ForeignKey( customer = models.ForeignKey(
Customer, Customer,

View File

@@ -29,7 +29,7 @@ from typing import List
from django.utils.functional import cached_property from django.utils.functional import cached_property
from pretix.base.models import CartPosition, ItemCategory, SalesChannel 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: class DummyCategory:
@@ -161,7 +161,7 @@ class CrossSellingService:
] ]
def _prepare_items(self, subevent, items_qs, discount_info): def _prepare_items(self, subevent, items_qs, discount_info):
items, _btn = get_grouped_items( items, _btn = get_items_for_product_list(
self.event, self.event,
subevent=subevent, subevent=subevent,
voucher=None, voucher=None,

View File

@@ -67,6 +67,7 @@ class EventSlugBanlistValidator(BanlistValidator):
'_global', '_global',
'__debug__', '__debug__',
'api', 'api',
'storefrontapi',
'events', 'events',
'csp_report', 'csp_report',
'widget', 'widget',
@@ -91,6 +92,7 @@ class OrganizerSlugBanlistValidator(BanlistValidator):
'__debug__', '__debug__',
'about', 'about',
'api', 'api',
'storefrontapi',
'csp_report', 'csp_report',
'widget', 'widget',
'lead', 'lead',

View File

@@ -40,5 +40,5 @@ class PretixControlConfig(AppConfig):
label = 'pretixcontrol' label = 'pretixcontrol'
def ready(self): def ready(self):
from .views import dashboards # noqa
from . import logdisplay # noqa from . import logdisplay # noqa
from .views import dashboards # noqa

View File

@@ -72,6 +72,7 @@ from pretix.base.services.orders import perform_order
from pretix.base.services.tasks import EventTask from pretix.base.services.tasks import EventTask
from pretix.base.settings import PERSON_NAME_SCHEMES from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import validate_cart_addons 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.money import money_filter
from pretix.base.templatetags.phone_format import phone_format from pretix.base.templatetags.phone_format import phone_format
from pretix.base.templatetags.rich_text import rich_text_snippet 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, _items_from_post_data, cart_session, create_empty_cart_id,
get_or_create_cart_id, get_or_create_cart_id,
) )
from pretix.presale.views.event import get_grouped_items
from pretix.presale.views.questions import QuestionsViewMixin from pretix.presale.views.questions import QuestionsViewMixin
@@ -560,7 +560,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
if ckey not in item_cache: if ckey not in item_cache:
# Get all items to possibly show # Get all items to possibly show
items, _btn = get_grouped_items( items, _btn = get_items_for_product_list(
self.request.event, self.request.event,
subevent=cartpos.subevent, subevent=cartpos.subevent,
voucher=None, voucher=None,

View File

@@ -64,6 +64,9 @@ from pretix.base.services.cart import (
CartError, add_items_to_cart, apply_voucher, clear_cart, error_messages, CartError, add_items_to_cart, apply_voucher, clear_cart, error_messages,
remove_cart_position, 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.timemachine import time_machine_now
from pretix.base.views.tasks import AsyncAction from pretix.base.views.tasks import AsyncAction
from pretix.helpers.http import redirect_to_url from pretix.helpers.http import redirect_to_url
@@ -72,9 +75,6 @@ from pretix.presale.views import (
CartMixin, EventViewMixin, allow_cors_if_namespaced, CartMixin, EventViewMixin, allow_cors_if_namespaced,
allow_frame_if_namespaced, iframe_entry_view_wrapper, 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 from pretix.presale.views.robots import NoSearchIndexViewMixin
try: try:
@@ -613,7 +613,7 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, CartMixin, TemplateView
context['max_times'] = self.voucher.max_usages - self.voucher.redeemed context['max_times'] = self.voucher.max_usages - self.voucher.redeemed
# Fetch all items # Fetch all items
items, display_add_to_cart = get_grouped_items( items, display_add_to_cart = get_items_for_product_list(
self.request.event, self.request.event,
subevent=self.subevent, subevent=self.subevent,
voucher=self.voucher, voucher=self.voucher,

View File

@@ -34,7 +34,6 @@
import calendar import calendar
import hashlib import hashlib
import sys
from collections import defaultdict from collections import defaultdict
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from decimal import Decimal from decimal import Decimal
@@ -47,10 +46,7 @@ from django import forms
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db.models import ( from django.db.models import Count
Count, Exists, IntegerField, OuterRef, Prefetch, Q, Value,
)
from django.db.models.lookups import Exact
from django.http import Http404, HttpResponse from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.utils.decorators import method_decorator 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 django.views.generic import TemplateView
from pretix.base.forms.widgets import SplitDateTimePickerWidget from pretix.base.forms.widgets import SplitDateTimePickerWidget
from pretix.base.models import ( from pretix.base.models import Quota, Voucher
ItemVariation, Quota, SalesChannel, SeatCategoryMapping, Voucher,
)
from pretix.base.models.event import Event, SubEvent 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.placeholders import PlaceholderContext
from pretix.base.services.quotas import QuotaAvailability
from pretix.base.timemachine import ( from pretix.base.timemachine import (
has_time_machine_permission, time_machine_now, 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.helpers.http import redirect_to_url
from pretix.multidomain.urlreverse import eventreverse from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.ical import get_public_ical 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 ( from pretix.presale.views.organizer import (
EventListMixin, add_subevents_for_days, days_for_template, EventListMixin, add_subevents_for_days, days_for_template,
filter_qs_by_attr, has_before_after, weeks_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 ( from . import (
CartMixin, EventViewMixin, allow_frame_if_namespaced, get_cart, CartMixin, EventViewMixin, allow_frame_if_namespaced, get_cart,
iframe_entry_view_wrapper, iframe_entry_view_wrapper,
@@ -97,386 +90,6 @@ from . import (
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore 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 += ("<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
@method_decorator(allow_frame_if_namespaced, 'dispatch') @method_decorator(allow_frame_if_namespaced, 'dispatch')
@method_decorator(iframe_entry_view_wrapper, 'dispatch') @method_decorator(iframe_entry_view_wrapper, 'dispatch')
class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView): 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: if not self.request.event.has_subevents or self.subevent:
# Fetch all items # Fetch all items
items, display_add_to_cart = get_grouped_items( items, display_add_to_cart = get_items_for_product_list(
self.request.event, self.request.event,
subevent=self.subevent, subevent=self.subevent,
filter_items=self.request.GET.getlist('item'), filter_items=self.request.GET.getlist('item'),

View File

@@ -82,6 +82,7 @@ from pretix.base.services.orders import (
from pretix.base.services.pricing import get_price from pretix.base.services.pricing import get_price
from pretix.base.services.tickets import generate, invalidate_cache from pretix.base.services.tickets import generate, invalidate_cache
from pretix.base.signals import order_modified, register_ticket_outputs 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.templatetags.money import money_filter
from pretix.base.views.mixins import OrderQuestionsViewMixin from pretix.base.views.mixins import OrderQuestionsViewMixin
from pretix.base.views.tasks import AsyncAction 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 ( from pretix.presale.views import (
CartMixin, EventViewMixin, iframe_entry_view_wrapper, CartMixin, EventViewMixin, iframe_entry_view_wrapper,
) )
from pretix.presale.views.event import get_grouped_items
from pretix.presale.views.robots import NoSearchIndexViewMixin from pretix.presale.views.robots import NoSearchIndexViewMixin
@@ -1372,7 +1372,7 @@ class OrderChangeMixin:
if ckey not in item_cache: if ckey not in item_cache:
# Get all items to possibly show # Get all items to possibly show
items, _btn = get_grouped_items( items, _btn = get_items_for_product_list(
self.request.event, self.request.event,
subevent=p.subevent, subevent=p.subevent,
voucher=None, voucher=None,

View File

@@ -39,9 +39,9 @@ from pretix.presale.views import EventViewMixin, iframe_entry_view_wrapper
from ...base.i18n import get_language_without_region from ...base.i18n import get_language_without_region
from ...base.models import Voucher, WaitingListEntry from ...base.models import Voucher, WaitingListEntry
from ...base.storelogic.products import get_items_for_product_list
from ..forms.waitinglist import WaitingListForm from ..forms.waitinglist import WaitingListForm
from . import allow_frame_if_namespaced from . import allow_frame_if_namespaced
from .event import get_grouped_items
@method_decorator(allow_frame_if_namespaced, 'dispatch') @method_decorator(allow_frame_if_namespaced, 'dispatch')
@@ -53,7 +53,7 @@ class WaitingView(EventViewMixin, FormView):
@cached_property @cached_property
def itemvars(self): def itemvars(self):
customer = getattr(self.request, 'customer', None) 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, self.request.event,
subevent=self.subevent, subevent=self.subevent,
require_seat=None, require_seat=None,

View File

@@ -61,6 +61,9 @@ from pretix.base.models import (
from pretix.base.services.cart import error_messages from pretix.base.services.cart import error_messages
from pretix.base.services.placeholders import PlaceholderContext from pretix.base.services.placeholders import PlaceholderContext
from pretix.base.settings import GlobalSettingsObject 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.base.templatetags.rich_text import rich_text
from pretix.helpers.daterange import daterange from pretix.helpers.daterange import daterange
from pretix.helpers.thumb import get_thumbnail 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.forms.organizer import meta_filtersets
from pretix.presale.style import get_theme_vars_css from pretix.presale.style import get_theme_vars_css
from pretix.presale.views.cart import get_or_create_cart_id 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 ( from pretix.presale.views.organizer import (
EventListMixin, add_events_for_days, add_subevents_for_days, EventListMixin, add_events_for_days, add_subevents_for_days,
days_for_template, filter_qs_by_attr, weeks_for_template, days_for_template, filter_qs_by_attr, weeks_for_template,
@@ -270,7 +270,7 @@ class WidgetAPIProductList(EventListMixin, View):
).values_list('item_id', flat=True) ).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, self.request.event,
subevent=self.subevent, subevent=self.subevent,
voucher=self.voucher, voucher=self.voucher,

View File

@@ -57,6 +57,7 @@ base_patterns = [
re_path(r'^csp_report/$', csp.csp_report, name='csp.report'), re_path(r'^csp_report/$', csp.csp_report, name='csp.report'),
re_path(r'^agpl_source$', source.get_source, name='source'), 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'^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/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'^api/$', RedirectView.as_view(url='/api/v1/'), name='redirect-api-version'),
re_path(r'^.well-known/apple-developer-merchantid-domain-association$', re_path(r'^.well-known/apple-developer-merchantid-domain-association$',