diff --git a/src/pretix/base/services/cross_selling.py b/src/pretix/base/services/cross_selling.py
index f407d2fad1..bce23908ba 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.presale.utils import get_grouped_items
class DummyCategory:
diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py
index dc73363301..c689cdcb18 100644
--- a/src/pretix/base/services/orders.py
+++ b/src/pretix/base/services/orders.py
@@ -110,6 +110,7 @@ from pretix.celery_app import app
from pretix.helpers import OF_SELF
from pretix.helpers.models import modelcopy
from pretix.helpers.periodic import minimum_interval
+from pretix.presale.utils import get_grouped_items
from pretix.testutils.middleware import debugflags_var
@@ -2106,6 +2107,13 @@ class OrderChangeManager:
for a in current_addons[cp][k][:current_num - input_num]:
if a.canceled:
continue
+ items, _ = get_grouped_items(self.order.event,
+ channel=self.order.sales_channel,
+ subevent=a.subevent,
+ has_voucher=True,
+ base_qs=Item.objects.filter(pk=a.item.pk),
+ allow_addons=True
+ )
is_unavailable = (
# If an item is no longer available due to time, it should usually also be no longer
# user-removable, because e.g. the stock has already been ordered.
@@ -2115,10 +2123,11 @@ class OrderChangeManager:
# be part of the input.
(a.variation and a.variation.unavailability_reason(has_voucher=True, subevent=a.subevent))
or (a.variation and not a.variation.all_sales_channels and not a.variation.limit_sales_channels.contains(self.order.sales_channel))
- or a.item.unavailability_reason(has_voucher=True, subevent=a.subevent)
+ or not items
+ or items[0].unavailability_reason(has_voucher=True, subevent=a.subevent)
or (
- not a.item.all_sales_channels and
- not a.item.limit_sales_channels.contains(self.order.sales_channel)
+ not items[0].all_sales_channels and
+ not items[0].limit_sales_channels.contains(self.order.sales_channel)
)
)
if is_unavailable:
diff --git a/src/pretix/presale/checkoutflow.py b/src/pretix/presale/checkoutflow.py
index 4256b07cd9..4cc0acce08 100644
--- a/src/pretix/presale/checkoutflow.py
+++ b/src/pretix/presale/checkoutflow.py
@@ -93,13 +93,12 @@ from pretix.presale.signals import (
order_api_meta_from_request, order_meta_from_request, question_form_fields,
question_form_fields_overrides,
)
-from pretix.presale.utils import customer_login
+from pretix.presale.utils import customer_login, get_grouped_items
from pretix.presale.views import CartMixin, get_cart, get_cart_is_free
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
diff --git a/src/pretix/presale/utils.py b/src/pretix/presale/utils.py
index d0703a439d..e4aa4a7697 100644
--- a/src/pretix/presale/utils.py
+++ b/src/pretix/presale/utils.py
@@ -32,6 +32,7 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import re
+import sys
import time
import warnings
from importlib import import_module
@@ -41,7 +42,10 @@ from django.conf import settings
from django.contrib.auth import (
BACKEND_SESSION_KEY, SESSION_KEY, get_user_model, load_backend,
)
-from django.db.models import Q
+from django.db.models import (
+ Count, Exists, IntegerField, OuterRef, Prefetch, Q, Value,
+)
+from django.db.models.lookups import Exact
from django.http import Http404, HttpResponseForbidden
from django.middleware.csrf import rotate_token
from django.template import loader
@@ -54,14 +58,25 @@ from django.utils.translation import gettext_lazy as _
from django_scopes import scope
from pretix.base.middleware import LocaleMiddleware
-from pretix.base.models import Customer, Event, Organizer
-from pretix.base.timemachine import time_machine_now_assigned_from_request
+from pretix.base.models import (
+ Customer, Event, ItemVariation, Organizer, Quota, SalesChannel,
+ SeatCategoryMapping,
+)
+from pretix.base.models.items import (
+ Item, ItemAddOn, ItemBundle, SubEventItem, SubEventItemVariation,
+)
+from pretix.base.services.quotas import QuotaAvailability
+from pretix.base.timemachine import (
+ time_machine_now, time_machine_now_assigned_from_request,
+)
from pretix.helpers.http import redirect_to_url
from pretix.multidomain.models import KnownDomain
from pretix.multidomain.urlreverse import (
eventreverse_absolute, get_event_domain, get_organizer_domain,
)
-from pretix.presale.signals import process_request, process_response
+from pretix.presale.signals import (
+ item_description, process_request, process_response,
+)
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
@@ -480,3 +495,385 @@ def event_view(function=None, require_live=True):
return fn
return function or noop
+
+
+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, has_voucher=False):
+ 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 has_voucher and (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(
+ 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(
+ Exists(Quota.items.through.objects.filter(quota__subevent_id=subevent, item_id=OuterRef("pk"))),
+ 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 voucher:
+ max_per_order = min(max_per_order, voucher.max_usages - voucher.redeemed)
+
+ 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:
+ item._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)
+ time_available = item.hidden_if_item_available.is_available()
+ item._dependency_available = (q[0] == Quota.AVAILABILITY_OK) and time_available
+ if item._dependency_available and item.hidden_if_item_available_mode == Item.UNAVAIL_MODE_HIDDEN:
+ 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
+
+ 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 item._remove and not display_add_to_cart:
+ display_add_to_cart = not item.requires_seat and any(v.order_max > 0 for v in 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
diff --git a/src/pretix/presale/views/cart.py b/src/pretix/presale/views/cart.py
index 99d027be0d..b2399352de 100644
--- a/src/pretix/presale/views/cart.py
+++ b/src/pretix/presale/views/cart.py
@@ -68,13 +68,11 @@ from pretix.base.timemachine import time_machine_now
from pretix.base.views.tasks import AsyncAction
from pretix.helpers.http import redirect_to_url
from pretix.multidomain.urlreverse import eventreverse
+from pretix.presale.utils import get_grouped_items, item_group_by_category
from pretix.presale.views import (
CartMixin, EventViewMixin, allow_cors_if_namespaced,
allow_frame_if_namespaced, get_cart, 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:
diff --git a/src/pretix/presale/views/event.py b/src/pretix/presale/views/event.py
index 355beed2fa..dcea17c844 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
@@ -65,15 +61,9 @@ from django.views.generic import TemplateView
from pretix.base.auth import has_event_access_permission
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 (
- Item, ItemAddOn, ItemBundle, SubEventItem, SubEventItemVariation,
-)
from pretix.base.services.placeholders import PlaceholderContext
-from pretix.base.services.quotas import QuotaAvailability
from pretix.base.timemachine import time_machine_now
from pretix.helpers.compat import date_fromisocalendar
from pretix.helpers.formats.en.formats import (
@@ -82,7 +72,8 @@ 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.utils import get_grouped_items, item_group_by_category
from pretix.presale.views.organizer import (
EventListMixin, add_subevents_for_days, days_for_template,
filter_qs_by_attr, filter_subevents_with_plugins, has_before_after,
@@ -97,388 +88,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(
- 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(
- Exists(Quota.items.through.objects.filter(quota__subevent_id=subevent, item_id=OuterRef("pk"))),
- 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 voucher:
- max_per_order = min(max_per_order, voucher.max_usages - voucher.redeemed)
-
- 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:
- item._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)
- time_available = item.hidden_if_item_available.is_available()
- item._dependency_available = (q[0] == Quota.AVAILABILITY_OK) and time_available
- if item._dependency_available and item.hidden_if_item_available_mode == Item.UNAVAIL_MODE_HIDDEN:
- 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
-
- 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 item._remove and not display_add_to_cart:
- display_add_to_cart = not item.requires_seat and any(v.order_max > 0 for v in 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):
diff --git a/src/pretix/presale/views/order.py b/src/pretix/presale/views/order.py
index 4fdff85710..97be246648 100644
--- a/src/pretix/presale/views/order.py
+++ b/src/pretix/presale/views/order.py
@@ -93,10 +93,10 @@ from pretix.multidomain.urlreverse import eventreverse, eventreverse_absolute
from pretix.presale.forms.checkout import InvoiceAddressForm, QuestionsForm
from pretix.presale.forms.order import OrderPositionChangeForm
from pretix.presale.signals import question_form_fields_overrides
+from pretix.presale.utils import get_grouped_items
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
logger = logging.getLogger(__name__)
diff --git a/src/pretix/presale/views/waiting.py b/src/pretix/presale/views/waiting.py
index e135e3f338..dcc79ad13a 100644
--- a/src/pretix/presale/views/waiting.py
+++ b/src/pretix/presale/views/waiting.py
@@ -40,8 +40,8 @@ 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 ..forms.waitinglist import WaitingListForm
+from ..utils import get_grouped_items
from . import allow_frame_if_namespaced
-from .event import get_grouped_items
@method_decorator(allow_frame_if_namespaced, 'dispatch')
diff --git a/src/pretix/presale/views/widget.py b/src/pretix/presale/views/widget.py
index 325e2b5853..8574a6d261 100644
--- a/src/pretix/presale/views/widget.py
+++ b/src/pretix/presale/views/widget.py
@@ -68,10 +68,8 @@ from pretix.helpers.thumb import get_thumbnail
from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.presale.forms.organizer import meta_filtersets
from pretix.presale.style import get_theme_vars_css
+from pretix.presale.utils import get_grouped_items, item_group_by_category
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, filter_subevents_with_plugins,