from collections import Counter, defaultdict, namedtuple from datetime import timedelta from decimal import Decimal from typing import List, Optional from celery.exceptions import MaxRetriesExceededError from django.db import transaction from django.db.models import Q from django.dispatch import receiver from django.utils.timezone import now from django.utils.translation import pgettext_lazy, ugettext as _ from pretix.base.i18n import LazyLocaleException, language from pretix.base.models import ( CartPosition, Event, InvoiceAddress, Item, ItemVariation, Voucher, ) from pretix.base.models.event import SubEvent from pretix.base.models.orders import OrderFee from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule from pretix.base.services.async import ProfiledTask from pretix.base.services.locking import LockTimeoutException from pretix.base.services.pricing import get_price from pretix.base.templatetags.rich_text import rich_text from pretix.celery_app import app from pretix.presale.signals import ( checkout_confirm_messages, fee_calculation_for_cart, ) class CartError(LazyLocaleException): pass error_messages = { 'busy': _('We were not able to process your request completely as the ' 'server was too busy. Please try again.'), 'empty': _('You did not select any products.'), 'unknown_position': _('Unknown cart position.'), 'subevent_required': pgettext_lazy('subevent', 'No date was specified.'), 'not_for_sale': _('You selected a product which is not available for sale.'), 'unavailable': _('Some of the products you selected are no longer available. ' 'Please see below for details.'), 'in_part': _('Some of the products you selected are no longer available in ' 'the quantity you selected. Please see below for details.'), 'max_items': _("You cannot select more than %s items per order."), 'max_items_per_product': _("You cannot select more than %(max)s items of the product %(product)s."), 'min_items_per_product': _("You need to select at least %(min)s items of the product %(product)s."), 'min_items_per_product_removed': _("We removed %(product)s from your cart as you can not buy less than " "%(min)s items of it."), 'not_started': _('The presale period for this event has not yet started.'), 'ended': _('The presale period for this event has ended.'), 'some_subevent_not_started': _('The presale period for this event has not yet started. The affected positions ' 'have been removed from your cart.'), 'some_subevent_ended': _('The presale period for one of the events in your cart has ended. The affected ' 'positions have been removed from your cart.'), 'price_too_high': _('The entered price is to high.'), 'voucher_invalid': _('This voucher code is not known in our database.'), 'voucher_redeemed': _('This voucher code has already been used the maximum number of times allowed.'), 'voucher_redeemed_partial': _('This voucher code can only be redeemed %d more times.'), 'voucher_double': _('You already used this voucher code. Remove the associated line from your ' 'cart if you want to use it for a different product.'), 'voucher_expired': _('This voucher is expired.'), 'voucher_invalid_item': _('This voucher is not valid for this product.'), 'voucher_item_not_available': _( 'Your voucher is valid for a product that is currently not for sale.'), 'voucher_invalid_subevent': pgettext_lazy('subevent', 'This voucher is not valid for this event date.'), 'voucher_required': _('You need a valid voucher code to order this product.'), 'inactive_subevent': pgettext_lazy('subevent', 'The selected event date is not active.'), 'addon_invalid_base': _('You can not select an add-on for the selected product.'), 'addon_duplicate_item': _('You can not select two variations of the same add-on product.'), 'addon_max_count': _('You can select at most %(max)s add-ons from the category %(cat)s for the product %(base)s.'), 'addon_min_count': _('You need to select at least %(min)s add-ons from the category %(cat)s for the ' 'product %(base)s.'), 'addon_only': _('One of the products you selected can only be bought as an add-on to another project.'), } class CartManager: AddOperation = namedtuple('AddOperation', ('count', 'item', 'variation', 'price', 'voucher', 'quotas', 'addon_to', 'subevent', 'includes_tax')) RemoveOperation = namedtuple('RemoveOperation', ('position',)) ExtendOperation = namedtuple('ExtendOperation', ('position', 'count', 'item', 'variation', 'price', 'voucher', 'quotas', 'subevent')) order = { RemoveOperation: 10, ExtendOperation: 20, AddOperation: 30 } def __init__(self, event: Event, cart_id: str, invoice_address: InvoiceAddress=None): self.event = event self.cart_id = cart_id self.now_dt = now() self._operations = [] self._quota_diff = Counter() self._voucher_use_diff = Counter() self._items_cache = {} self._subevents_cache = {} self._variations_cache = {} self._expiry = None self.invoice_address = invoice_address @property def positions(self): return CartPosition.objects.filter( Q(cart_id=self.cart_id) & Q(event=self.event) ).select_related('item', 'subevent') def _calculate_expiry(self): self._expiry = self.now_dt + timedelta(minutes=self.event.settings.get('reservation_time', as_type=int)) def _check_presale_dates(self): if self.event.presale_start and self.now_dt < self.event.presale_start: raise CartError(error_messages['not_started']) if self.event.presale_has_ended: raise CartError(error_messages['ended']) def _extend_expiry_of_valid_existing_positions(self): # Extend this user's cart session to ensure all items in the cart expire at the same time # We can extend the reservation of items which are not yet expired without risk self.positions.filter(expires__gt=self.now_dt).update(expires=self._expiry) def _delete_out_of_timeframe(self): err = None for cp in self.positions: if cp.subevent and cp.subevent.presale_start and self.now_dt < cp.subevent.presale_start: err = error_messages['some_subevent_not_started'] cp.delete() if cp.subevent and cp.subevent.presale_end and self.now_dt > cp.subevent.presale_end: err = error_messages['some_subevent_ended'] cp.delete() return err def _update_subevents_cache(self, se_ids: List[int]): self._subevents_cache.update({ i.pk: i for i in self.event.subevents.filter(id__in=[i for i in se_ids if i and i not in self._items_cache]) }) def _update_items_cache(self, item_ids: List[int], variation_ids: List[int]): self._items_cache.update({ i.pk: i for i in self.event.items.select_related('category').prefetch_related( 'addons', 'addons__addon_category', 'quotas' ).filter( id__in=[i for i in item_ids if i and i not in self._items_cache] ) }) self._variations_cache.update({ v.pk: v for v in ItemVariation.objects.filter(item__event=self.event).prefetch_related( 'quotas' ).select_related('item', 'item__event').filter( id__in=[i for i in variation_ids if i and i not in self._variations_cache] ) }) def _check_max_cart_size(self): cartsize = self.positions.filter(addon_to__isnull=True).count() cartsize += sum([op.count for op in self._operations if isinstance(op, self.AddOperation) and not op.addon_to]) cartsize -= len([1 for op in self._operations if isinstance(op, self.RemoveOperation) if not op.position.addon_to_id]) if cartsize > int(self.event.settings.max_items_per_order): # TODO: i18n plurals raise CartError(_(error_messages['max_items']) % (self.event.settings.max_items_per_order,)) def _check_item_constraints(self, op): if isinstance(op, self.AddOperation) or isinstance(op, self.ExtendOperation): if op.item.require_voucher and op.voucher is None: raise CartError(error_messages['voucher_required']) if op.item.hide_without_voucher and (op.voucher is None or op.voucher.item is None or op.voucher.item.pk != op.item.pk): raise CartError(error_messages['voucher_required']) if not op.item.is_available() or (op.variation and not op.variation.active): raise CartError(error_messages['unavailable']) if op.voucher and not op.voucher.applies_to(op.item, op.variation): raise CartError(error_messages['voucher_invalid_item']) if op.voucher and op.voucher.subevent_id and op.voucher.subevent_id != op.subevent.pk: raise CartError(error_messages['voucher_invalid_subevent']) if op.subevent and not op.subevent.active: raise CartError(error_messages['inactive_subevent']) if op.subevent and op.subevent.presale_start and self.now_dt < op.subevent.presale_start: raise CartError(error_messages['not_started']) if op.subevent and op.subevent.presale_has_ended: raise CartError(error_messages['ended']) if isinstance(op, self.AddOperation): if op.item.category and op.item.category.is_addon and not op.addon_to: raise CartError(error_messages['addon_only']) if op.item.max_per_order or op.item.min_per_order: new_total = ( len([1 for p in self.positions if p.item_id == op.item.pk]) + sum([_op.count for _op in self._operations if isinstance(_op, self.AddOperation) and _op.item == op.item]) + op.count - len([1 for _op in self._operations if isinstance(_op, self.RemoveOperation) and _op.position.item_id == op.item.pk]) ) if op.item.max_per_order and new_total > op.item.max_per_order: raise CartError( _(error_messages['max_items_per_product']) % { 'max': op.item.max_per_order, 'product': op.item.name } ) if op.item.min_per_order and new_total < op.item.min_per_order: raise CartError( _(error_messages['min_items_per_product']) % { 'min': op.item.min_per_order, 'product': op.item.name } ) def _get_price(self, item: Item, variation: Optional[ItemVariation], voucher: Optional[Voucher], custom_price: Optional[Decimal], subevent: Optional[SubEvent], cp_is_net: bool=None): return get_price( item, variation, voucher, custom_price, subevent, custom_price_is_net=cp_is_net if cp_is_net is not None else self.event.settings.display_net_prices, invoice_address=self.invoice_address ) def extend_expired_positions(self): expired = self.positions.filter(expires__lte=self.now_dt).select_related( 'item', 'variation', 'voucher' ).prefetch_related('item__quotas', 'variation__quotas') err = None for cp in expired: if not cp.includes_tax: price = self._get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent, cp_is_net=True) price = TaxedPrice(net=price.net, gross=price.net, rate=0, tax=0, name='') else: price = self._get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent) quotas = list(cp.quotas) if not quotas: self._operations.append(self.RemoveOperation(position=cp)) continue err = error_messages['unavailable'] if not cp.voucher or (not cp.voucher.allow_ignore_quota and not cp.voucher.block_quota): for quota in quotas: self._quota_diff[quota] += 1 else: quotas = [] op = self.ExtendOperation( position=cp, item=cp.item, variation=cp.variation, voucher=cp.voucher, count=1, price=price, quotas=quotas, subevent=cp.subevent ) self._check_item_constraints(op) if cp.voucher: self._voucher_use_diff[cp.voucher] += 1 self._operations.append(op) return err def add_new_items(self, items: List[dict]): # Fetch items from the database self._update_items_cache([i['item'] for i in items], [i['variation'] for i in items]) self._update_subevents_cache([i['subevent'] for i in items if i.get('subevent')]) quota_diff = Counter() voucher_use_diff = Counter() operations = [] for i in items: # Check whether the specified items are part of what we just fetched from the database # If they are not, the user supplied item IDs which either do not exist or belong to # a different event if i['item'] not in self._items_cache or (i['variation'] and i['variation'] not in self._variations_cache): raise CartError(error_messages['not_for_sale']) if self.event.has_subevents: if not i.get('subevent'): raise CartError(error_messages['subevent_required']) subevent = self._subevents_cache[int(i.get('subevent'))] else: subevent = None item = self._items_cache[i['item']] variation = self._variations_cache[i['variation']] if i['variation'] is not None else None voucher = None if i.get('voucher'): try: voucher = self.event.vouchers.get(code=i.get('voucher').strip()) except Voucher.DoesNotExist: raise CartError(error_messages['voucher_invalid']) else: voucher_use_diff[voucher] += i['count'] # Fetch all quotas. If there are no quotas, this item is not allowed to be sold. quotas = list(item.quotas.filter(subevent=subevent) if variation is None else variation.quotas.filter(subevent=subevent)) if not quotas: raise CartError(error_messages['unavailable']) if not voucher or (not voucher.allow_ignore_quota and not voucher.block_quota): for quota in quotas: quota_diff[quota] += i['count'] else: quotas = [] price = self._get_price(item, variation, voucher, i.get('price'), subevent) op = self.AddOperation( count=i['count'], item=item, variation=variation, price=price, voucher=voucher, quotas=quotas, addon_to=False, subevent=subevent, includes_tax=bool(price.rate) ) self._check_item_constraints(op) operations.append(op) self._quota_diff.update(quota_diff) self._voucher_use_diff += voucher_use_diff self._operations += operations def remove_item(self, pos_id: int): # TODO: We could calculate quotadiffs and voucherdiffs here, which would lead to more # flexible usages (e.g. a RemoveOperation and an AddOperation in the same transaction # could cancel each other out quota-wise). However, we are not taking this performance # penalty for now as there is currently no outside interface that would allow building # such a transaction. try: cp = self.positions.get(pk=pos_id) except CartPosition.DoesNotExist: raise CartError(error_messages['unknown_position']) self._operations.append(self.RemoveOperation(position=cp)) def clear(self): # TODO: We could calculate quotadiffs and voucherdiffs here, which would lead to more # flexible usages (e.g. a RemoveOperation and an AddOperation in the same transaction # could cancel each other out quota-wise). However, we are not taking this performance # penalty for now as there is currently no outside interface that would allow building # such a transaction. for cp in self.positions.all(): self._operations.append(self.RemoveOperation(position=cp)) def set_addons(self, addons): self._update_items_cache( [a['item'] for a in addons], [a['variation'] for a in addons], ) # Prepare various containers to hold data later current_addons = defaultdict(dict) # CartPos -> currently attached add-ons input_addons = defaultdict(set) # CartPos -> add-ons according to input selected_addons = defaultdict(set) # CartPos -> final desired set of add-ons cpcache = {} # CartPos.pk -> CartPos quota_diff = Counter() # Quota -> Number of usages operations = [] available_categories = defaultdict(set) # CartPos -> Category IDs to choose from price_included = defaultdict(dict) # CartPos -> CategoryID -> bool(price is included) toplevel_cp = self.positions.filter( addon_to__isnull=True ).prefetch_related( 'addons', 'item__addons', 'item__addons__addon_category' ).select_related('item', 'variation') # Prefill some of the cache containers for cp in toplevel_cp: available_categories[cp.pk] = {iao.addon_category_id for iao in cp.item.addons.all()} price_included[cp.pk] = {iao.addon_category_id: iao.price_included for iao in cp.item.addons.all()} cpcache[cp.pk] = cp current_addons[cp] = { (a.item_id, a.variation_id): a for a in cp.addons.all() } # Create operations, perform various checks for a in addons: # Check whether the specified items are part of what we just fetched from the database # If they are not, the user supplied item IDs which either do not exist or belong to # a different event if a['item'] not in self._items_cache or (a['variation'] and a['variation'] not in self._variations_cache): raise CartError(error_messages['not_for_sale']) # Only attach addons to things that are actually in this user's cart if a['addon_to'] not in cpcache: raise CartError(error_messages['addon_invalid_base']) cp = cpcache[a['addon_to']] item = self._items_cache[a['item']] variation = self._variations_cache[a['variation']] if a['variation'] is not None else None if item.category_id not in available_categories[cp.pk]: raise CartError(error_messages['addon_invalid_base']) # Fetch all quotas. If there are no quotas, this item is not allowed to be sold. quotas = list(item.quotas.filter(subevent=cp.subevent) if variation is None else variation.quotas.filter(subevent=cp.subevent)) if not quotas: raise CartError(error_messages['unavailable']) # Every item can be attached to very CartPosition at most once if a['item'] in ([_a[0] for _a in input_addons[cp.id]]): raise CartError(error_messages['addon_duplicate_item']) input_addons[cp.id].add((a['item'], a['variation'])) selected_addons[cp.id, item.category_id].add((a['item'], a['variation'])) if (a['item'], a['variation']) not in current_addons[cp]: # This add-on is new, add it to the cart for quota in quotas: quota_diff[quota] += 1 if price_included[cp.pk].get(item.category_id): price = TAXED_ZERO else: price = self._get_price(item, variation, None, None, cp.subevent) op = self.AddOperation( count=1, item=item, variation=variation, price=price, voucher=None, quotas=quotas, addon_to=cp, subevent=cp.subevent, includes_tax=bool(price.rate) ) self._check_item_constraints(op) operations.append(op) # Check constraints on the add-on combinations for cp in toplevel_cp: item = cp.item for iao in item.addons.all(): selected = selected_addons[cp.id, iao.addon_category_id] if len(selected) > iao.max_count: # TODO: Proper i18n # TODO: Proper pluralization raise CartError( error_messages['addon_max_count'], { 'base': str(item.name), 'max': iao.max_count, 'cat': str(iao.addon_category.name), } ) elif len(selected) < iao.min_count: # TODO: Proper i18n # TODO: Proper pluralization raise CartError( error_messages['addon_min_count'], { 'base': str(item.name), 'min': iao.min_count, 'cat': str(iao.addon_category.name), } ) # Detect removed add-ons and create RemoveOperations for cp, al in current_addons.items(): for k, v in al.items(): if k not in input_addons[cp.id]: if v.expires > self.now_dt: quotas = list(v.quotas) for quota in quotas: quota_diff[quota] -= 1 op = self.RemoveOperation(position=v) operations.append(op) self._quota_diff.update(quota_diff) self._operations += operations def _get_quota_availability(self): quotas_ok = defaultdict(int) for quota, count in self._quota_diff.items(): if count <= 0: quotas_ok[quota] = 0 avail = quota.availability(self.now_dt) if avail[1] is not None and avail[1] < count: quotas_ok[quota] = min(count, avail[1]) else: quotas_ok[quota] = count return quotas_ok def _get_voucher_availability(self): vouchers_ok = {} for voucher, count in self._voucher_use_diff.items(): voucher.refresh_from_db() if voucher.valid_until is not None and voucher.valid_until < self.now_dt: raise CartError(error_messages['voucher_expired']) redeemed_in_carts = CartPosition.objects.filter( Q(voucher=voucher) & Q(event=self.event) & Q(expires__gte=self.now_dt) ).exclude(pk__in=[ op.position.voucher_id for op in self._operations if isinstance(op, self.ExtendOperation) ]) v_avail = voucher.max_usages - voucher.redeemed - redeemed_in_carts.count() vouchers_ok[voucher] = v_avail return vouchers_ok def _check_min_per_product(self): per_product = Counter() min_per_product = {} for p in self.positions: per_product[p.item_id] += 1 min_per_product[p.item.pk] = p.item.min_per_order for op in self._operations: if isinstance(op, self.AddOperation): per_product[op.item.pk] += op.count min_per_product[op.item.pk] = op.item.min_per_order elif isinstance(op, self.RemoveOperation): per_product[op.position.item_id] -= 1 min_per_product[op.position.item.pk] = op.position.item.min_per_order err = None for itemid, num in per_product.items(): min_p = min_per_product[itemid] if min_p and num < min_p: self._operations = [o for o in self._operations if not ( isinstance(o, self.AddOperation) and o.item.pk == itemid )] removals = [o.position.pk for o in self._operations if isinstance(o, self.RemoveOperation)] for p in self.positions: if p.item_id == itemid and p.pk not in removals: self._operations.append(self.RemoveOperation(position=p)) err = _(error_messages['min_items_per_product_removed']) % { 'min': min_p, 'product': p.item.name } return err def _perform_operations(self): vouchers_ok = self._get_voucher_availability() quotas_ok = self._get_quota_availability() err = None new_cart_positions = [] err = err or self._check_min_per_product() self._operations.sort(key=lambda a: self.order[type(a)]) for op in self._operations: if isinstance(op, self.RemoveOperation): if op.position.expires > self.now_dt: for q in op.position.quotas: quotas_ok[q] += 1 op.position.delete() elif isinstance(op, self.AddOperation) or isinstance(op, self.ExtendOperation): # Create a CartPosition for as much items as we can requested_count = quota_available_count = voucher_available_count = op.count if op.quotas: quota_available_count = min(requested_count, min(quotas_ok[q] for q in op.quotas)) if op.voucher: voucher_available_count = min(voucher_available_count, vouchers_ok[op.voucher]) if quota_available_count < 1: err = err or error_messages['unavailable'] elif quota_available_count < requested_count: err = err or error_messages['in_part'] if voucher_available_count < 1: err = err or error_messages['voucher_redeemed'] elif voucher_available_count < requested_count: err = err or error_messages['voucher_redeemed_partial'] % voucher_available_count available_count = min(quota_available_count, voucher_available_count) for q in op.quotas: quotas_ok[q] -= available_count if op.voucher: vouchers_ok[op.voucher] -= available_count if isinstance(op, self.AddOperation): for k in range(available_count): new_cart_positions.append(CartPosition( event=self.event, item=op.item, variation=op.variation, price=op.price.gross, expires=self._expiry, cart_id=self.cart_id, voucher=op.voucher, addon_to=op.addon_to if op.addon_to else None, subevent=op.subevent, includes_tax=op.includes_tax )) elif isinstance(op, self.ExtendOperation): if available_count == 1: op.position.expires = self._expiry op.position.price = op.price.gross op.position.save() elif available_count == 0: op.position.delete() else: raise AssertionError("ExtendOperation cannot affect more than one item") CartPosition.objects.bulk_create(new_cart_positions) return err def commit(self): self._check_presale_dates() self._check_max_cart_size() self._calculate_expiry() with self.event.lock() as now_dt: with transaction.atomic(): self.now_dt = now_dt self._extend_expiry_of_valid_existing_positions() err = self._delete_out_of_timeframe() err = self.extend_expired_positions() or err err = self._perform_operations() or err if err: raise CartError(err) def update_tax_rates(event: Event, cart_id: str, invoice_address: InvoiceAddress): positions = CartPosition.objects.filter( cart_id=cart_id, event=event ).select_related('item', 'item__tax_rule') totaldiff = Decimal('0.00') for pos in positions: if not pos.item.tax_rule: continue charge_tax = pos.item.tax_rule.tax_applicable(invoice_address) if pos.includes_tax and not charge_tax: price = pos.item.tax(pos.price, base_price_is='gross').net totaldiff += price - pos.price pos.price = price pos.includes_tax = False pos.save(update_fields=['price', 'includes_tax']) elif charge_tax and not pos.includes_tax: price = pos.item.tax(pos.price, base_price_is='net').gross totaldiff += price - pos.price pos.price = price pos.includes_tax = True pos.save(update_fields=['price', 'includes_tax']) return totaldiff def get_fees(event, request, total, invoice_address, provider): fees = [] if provider and total != 0: provider = event.get_payment_providers().get(provider) if provider: payment_fee = provider.calculate_fee(total) if payment_fee: payment_fee_tax_rule = event.settings.tax_rate_default or TaxRule.zero() if payment_fee_tax_rule.tax_applicable(invoice_address): payment_fee_tax = payment_fee_tax_rule.tax(payment_fee, base_price_is='gross') fees.append(OrderFee( fee_type=OrderFee.FEE_TYPE_PAYMENT, value=payment_fee, tax_rate=payment_fee_tax.rate, tax_value=payment_fee_tax.tax, tax_rule=payment_fee_tax_rule )) else: fees.append(OrderFee( fee_type=OrderFee.FEE_TYPE_PAYMENT, value=payment_fee, tax_rate=Decimal('0.00'), tax_value=Decimal('0.00'), tax_rule=payment_fee_tax_rule )) for recv, resp in fee_calculation_for_cart.send(sender=event, request=request, invoice_address=invoice_address): fees += resp return fees @app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,)) def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, locale='en', invoice_address: int=None) -> None: """ Adds a list of items to a user's cart. :param event: The event ID in question :param items: A list of dicts with the keys item, variation, number, custom_price, voucher :param cart_id: Session ID of a guest :raises CartError: On any error that occured """ with language(locale): event = Event.objects.get(id=event) ia = False if invoice_address: try: ia = InvoiceAddress.objects.get(pk=invoice_address) except InvoiceAddress.DoesNotExist: pass try: try: cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia) cm.add_new_items(items) cm.commit() except LockTimeoutException: self.retry() except (MaxRetriesExceededError, LockTimeoutException): raise CartError(error_messages['busy']) @app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,)) def remove_cart_position(self, event: int, position: int, cart_id: str=None, locale='en') -> None: """ Removes a list of items from a user's cart. :param event: The event ID in question :param position: A cart position ID :param session: Session ID of a guest """ with language(locale): event = Event.objects.get(id=event) try: try: cm = CartManager(event=event, cart_id=cart_id) cm.remove_item(position) cm.commit() except LockTimeoutException: self.retry() except (MaxRetriesExceededError, LockTimeoutException): raise CartError(error_messages['busy']) @app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,)) def clear_cart(self, event: int, cart_id: str=None, locale='en') -> None: """ Removes a list of items from a user's cart. :param event: The event ID in question :param session: Session ID of a guest """ with language(locale): event = Event.objects.get(id=event) try: try: cm = CartManager(event=event, cart_id=cart_id) cm.clear() cm.commit() except LockTimeoutException: self.retry() except (MaxRetriesExceededError, LockTimeoutException): raise CartError(error_messages['busy']) @app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,)) def set_cart_addons(self, event: int, addons: List[dict], cart_id: str=None, locale='en', invoice_address: int=None) -> None: """ Removes a list of items from a user's cart. :param event: The event ID in question :param addons: A list of dicts with the keys addon_to, item, variation :param session: Session ID of a guest """ with language(locale): event = Event.objects.get(id=event) ia = False if invoice_address: try: ia = InvoiceAddress.objects.get(pk=invoice_address) except InvoiceAddress.DoesNotExist: pass try: try: cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia) cm.set_addons(addons) cm.commit() except LockTimeoutException: self.retry() except (MaxRetriesExceededError, LockTimeoutException): raise CartError(error_messages['busy']) @receiver(checkout_confirm_messages, dispatch_uid="cart_confirm_messages") def confirm_messages(sender, *args, **kwargs): if not sender.settings.confirm_text: return {} return { 'confirm_text': rich_text(str(sender.settings.confirm_text)) }