mirror of
https://github.com/pretix/pretix.git
synced 2026-05-08 15:44:02 +00:00
make cross-selling-applicable more specific and cache state
(only show if really applicable, e.g. don't show if product can't be bought due to order_max)
This commit is contained in:
@@ -150,20 +150,20 @@ class ItemCategory(LoggedModel):
|
|||||||
def cross_sell_visible(self, cart, sales_channel):
|
def cross_sell_visible(self, cart, sales_channel):
|
||||||
"""
|
"""
|
||||||
If this category should be visible in the cross-selling step for a given cart and sales_channel, this method
|
If this category should be visible in the cross-selling step for a given cart and sales_channel, this method
|
||||||
returns a dict describing the items that should be displayed.
|
returns a queryset of the items that should be displayed, as well as a dict giving additional information on them.
|
||||||
|
|
||||||
:returns: (QuerySet<Item>, dict<item_pk: (max_count, discount_rule)>)
|
:returns: (QuerySet<Item>, dict<item_pk: (max_count, discount_rule)>)
|
||||||
max_count is None if the item should not be limited
|
max_count is `inf` if the item should not be limited
|
||||||
discount_rule is None if the item will not be discounted
|
discount_rule is None if the item will not be discounted
|
||||||
"""
|
"""
|
||||||
if self.cross_selling_mode is None:
|
if self.cross_selling_mode is None:
|
||||||
return [], {}
|
return None, {}
|
||||||
if self.cross_selling_condition == 'always':
|
if self.cross_selling_condition == 'always':
|
||||||
return self.items.all(), {}
|
return self.items.all(), {}
|
||||||
if self.cross_selling_condition == 'products':
|
if self.cross_selling_condition == 'products':
|
||||||
# TODO set max_count for products with max_per_order
|
# TODO set max_count for products with max_per_order
|
||||||
match = set(match.pk for match in self.cross_selling_match_products.only('pk')) # TODO prefetch this
|
match = set(match.pk for match in self.cross_selling_match_products.only('pk')) # TODO prefetch this
|
||||||
return (self.items.all(), {}) if any(pos.item.pk in match for pos in cart) else ([], {})
|
return (self.items.all(), {}) if any(pos.item.pk in match for pos in cart) else (None, {})
|
||||||
if self.cross_selling_condition == 'discounts':
|
if self.cross_selling_condition == 'discounts':
|
||||||
potential_discounts_dict = defaultdict(list)
|
potential_discounts_dict = defaultdict(list)
|
||||||
|
|
||||||
@@ -189,10 +189,7 @@ class ItemCategory(LoggedModel):
|
|||||||
infos_for_item = list(infos_for_item)
|
infos_for_item = list(infos_for_item)
|
||||||
return (
|
return (
|
||||||
item,
|
item,
|
||||||
min(
|
sum(max_count for (item, discount_rule, max_count, i) in infos_for_item),
|
||||||
sum(max_count for (item, discount_rule, max_count, i) in infos_for_item),
|
|
||||||
(item.max_per_order - sum(1 for pos in cart if pos.item_id == item.pk)) if item.max_per_order else inf
|
|
||||||
),
|
|
||||||
next(discount_rule for (item, discount_rule, max_count, i) in infos_for_item)
|
next(discount_rule for (item, discount_rule, max_count, i) in infos_for_item)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -488,27 +488,33 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
|
|||||||
label = pgettext_lazy('checkoutflow', 'Add-on products')
|
label = pgettext_lazy('checkoutflow', 'Add-on products')
|
||||||
icon = 'puzzle-piece'
|
icon = 'puzzle-piece'
|
||||||
|
|
||||||
|
def _check_is_applicable(self, request):
|
||||||
|
self.request = request
|
||||||
|
|
||||||
|
# check whether addons are applicable
|
||||||
|
if get_cart(request).filter(item__addons__isnull=False).exists():
|
||||||
|
return True
|
||||||
|
|
||||||
|
# don't re-check whether cross-selling is applicable if we're already past the AddOnsStep
|
||||||
|
cur_step_identifier = request.resolver_match.kwargs.get('step')
|
||||||
|
is_past_this_step = any(step.identifier == cur_step_identifier for step in request._checkout_flow[request._checkout_flow.index(self) + 1:])
|
||||||
|
if is_past_this_step:
|
||||||
|
applicable = self.cart_session.get('_checkoutflow_addons_applicable', None)
|
||||||
|
if applicable is not None:
|
||||||
|
return applicable
|
||||||
|
|
||||||
|
# check whether cross-selling is applicable
|
||||||
|
applicable = self.cross_selling_is_applicable
|
||||||
|
self.cart_session['_checkoutflow_addons_applicable'] = applicable
|
||||||
|
return applicable
|
||||||
|
|
||||||
def is_applicable(self, request):
|
def is_applicable(self, request):
|
||||||
if not hasattr(request, '_checkoutflow_addons_applicable'):
|
if not hasattr(request, '_checkoutflow_addons_applicable'):
|
||||||
cart = get_cart(request)
|
cur_step_identifier = request.resolver_match.kwargs.get('step')
|
||||||
self.request = request
|
request._checkoutflow_addons_applicable = self._check_is_applicable(request) or cur_step_identifier == self.identifier
|
||||||
request._checkoutflow_addons_applicable = (
|
|
||||||
'/addons/' in request.path_info
|
|
||||||
or cart.filter(item__addons__isnull=False).exists()
|
|
||||||
or any(self.cross_selling_applicable_categories))
|
|
||||||
return request._checkoutflow_addons_applicable
|
return request._checkoutflow_addons_applicable
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def cross_selling_applicable_categories(self):
|
|
||||||
cart = get_cart(self.request)
|
|
||||||
return [
|
|
||||||
(c, products_qs, discount_info) for (c, products_qs, discount_info) in
|
|
||||||
(
|
|
||||||
(c, *c.cross_sell_visible(cart, self.request.sales_channel.identifier))
|
|
||||||
for c in self.request.event.categories.filter(cross_selling_mode__isnull=False)
|
|
||||||
)
|
|
||||||
if len(products_qs) > 0
|
|
||||||
]
|
|
||||||
|
|
||||||
def is_completed(self, request, warn=False):
|
def is_completed(self, request, warn=False):
|
||||||
if getattr(self, '_completed', None) is not None:
|
if getattr(self, '_completed', None) is not None:
|
||||||
@@ -624,24 +630,44 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
|
|||||||
formset.append(formsetentry)
|
formset.append(formsetentry)
|
||||||
return formset
|
return formset
|
||||||
|
|
||||||
def get_cross_selling_data(self, ctx):
|
@property
|
||||||
|
def cross_selling_applicable_categories(self):
|
||||||
|
cart = self.positions
|
||||||
|
return [
|
||||||
|
(c, products_qs, discount_info) for (c, products_qs, discount_info) in
|
||||||
|
(
|
||||||
|
(c, *c.cross_sell_visible(cart, self.request.sales_channel.identifier))
|
||||||
|
for c in self.request.event.categories.filter(cross_selling_mode__isnull=False)
|
||||||
|
)
|
||||||
|
if products_qs is not None
|
||||||
|
]
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def cross_selling_is_applicable(self):
|
||||||
|
return any(len(items) > 0 for (category, items) in self.cross_selling_data)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def cross_selling_data(self):
|
||||||
class DummyCategory:
|
class DummyCategory:
|
||||||
def __init__(self, rule, subevent=None):
|
def __init__(self, rule, subevent=None):
|
||||||
self.id = rule.id
|
self.id = rule.id
|
||||||
self.name = rule.name + (f" ({subevent})" if subevent else "")
|
self.name = rule.name + (f" ({subevent})" if subevent else "")
|
||||||
self.description = rule.description
|
self.description = rule.description
|
||||||
|
|
||||||
|
categories = self.cross_selling_applicable_categories
|
||||||
if self.event.has_subevents:
|
if self.event.has_subevents:
|
||||||
return [
|
subevents = set(pos.subevent for pos in self.positions)
|
||||||
|
result = (
|
||||||
(DummyCategory(category, subevent), self._items_for_cross_selling(subevent, items_qs, discount_info), f'subevent_{subevent.pk}_')
|
(DummyCategory(category, subevent), self._items_for_cross_selling(subevent, items_qs, discount_info), f'subevent_{subevent.pk}_')
|
||||||
for (category, items_qs, discount_info) in self.cross_selling_applicable_categories
|
for (category, items_qs, discount_info) in categories
|
||||||
for subevent in set(pos.subevent for pos in ctx['cart']['positions'])
|
for subevent in subevents
|
||||||
]
|
)
|
||||||
else:
|
else:
|
||||||
return [
|
result = (
|
||||||
(category, self._items_for_cross_selling(None, items_qs, discount_info))
|
(category, self._items_for_cross_selling(None, items_qs, discount_info))
|
||||||
for (category, items_qs, discount_info) in self.cross_selling_applicable_categories
|
for (category, items_qs, discount_info) in categories
|
||||||
]
|
)
|
||||||
|
return [(category, items) for (category, items) in result if len(items) > 0]
|
||||||
|
|
||||||
def _items_for_cross_selling(self, subevent, items_qs, discount_info):
|
def _items_for_cross_selling(self, subevent, items_qs, discount_info):
|
||||||
items, _btn = get_grouped_items(
|
items, _btn = get_grouped_items(
|
||||||
@@ -666,8 +692,12 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
|
|||||||
(max_count, discount_rule) = discount_info[item.pk]
|
(max_count, discount_rule) = discount_info[item.pk]
|
||||||
|
|
||||||
# set item.order_max for benefit_only_apply_to_cheapest_n_matches discounted items
|
# set item.order_max for benefit_only_apply_to_cheapest_n_matches discounted items
|
||||||
if max_count and max_count != inf:
|
if not max_count:
|
||||||
item.order_max = min(item.order_max, max_count)
|
max_count = inf
|
||||||
|
item.order_max = min(
|
||||||
|
item.order_max - sum(1 for pos in self.positions if pos.item_id == item.pk),
|
||||||
|
max_count
|
||||||
|
)
|
||||||
|
|
||||||
# calculate discounted price
|
# calculate discounted price
|
||||||
if discount_rule:
|
if discount_rule:
|
||||||
@@ -683,8 +713,8 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data(**kwargs)
|
ctx = super().get_context_data(**kwargs)
|
||||||
ctx['forms'] = self.forms
|
ctx['forms'] = self.forms
|
||||||
ctx['cross_selling_data'] = self.get_cross_selling_data(ctx)
|
|
||||||
ctx['cart'] = self.get_cart()
|
ctx['cart'] = self.get_cart()
|
||||||
|
ctx['cross_selling_data'] = self.cross_selling_data
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def get_success_message(self, value):
|
def get_success_message(self, value):
|
||||||
|
|||||||
@@ -318,14 +318,14 @@ def cart_exists(request):
|
|||||||
|
|
||||||
def get_cart(request):
|
def get_cart(request):
|
||||||
from pretix.presale.views.cart import get_or_create_cart_id
|
from pretix.presale.views.cart import get_or_create_cart_id
|
||||||
qqs = request.event.questions.all()
|
|
||||||
qqs = qqs.filter(ask_during_checkin=False, hidden=False)
|
|
||||||
|
|
||||||
if not hasattr(request, '_cart_cache'):
|
if not hasattr(request, '_cart_cache'):
|
||||||
cart_id = get_or_create_cart_id(request, create=False)
|
cart_id = get_or_create_cart_id(request, create=False)
|
||||||
if not cart_id:
|
if not cart_id:
|
||||||
request._cart_cache = CartPosition.objects.none()
|
request._cart_cache = CartPosition.objects.none()
|
||||||
else:
|
else:
|
||||||
|
qqs = request.event.questions.all()
|
||||||
|
qqs = qqs.filter(ask_during_checkin=False, hidden=False)
|
||||||
request._cart_cache = CartPosition.objects.filter(
|
request._cart_cache = CartPosition.objects.filter(
|
||||||
cart_id=cart_id, event=request.event
|
cart_id=cart_id, event=request.event
|
||||||
).annotate(
|
).annotate(
|
||||||
|
|||||||
Reference in New Issue
Block a user