def process(self): expiry = now() + timedelta(minutes=self.request.event.settings.get('reservation_time', as_type=int)) self._extend_existing(expiry) self._initial_checks() # Fetch items from the database items_cache = { i.identity: i for i in Item.objects.current.filter( event=self.request.event, identity__in=[i[0] for i in self.items] ).prefetch_related("quotas") } variations_cache = { v.identity: v for v in ItemVariation.objects.current.filter( item__event=self.request.event, identity__in=[i[1] for i in self.items if i[1] is not None] ).select_related("item", "item__event").prefetch_related("quotas", "values", "values__prop") } try: with self.request.event.lock(): # Process the request itself for i in self.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[0] not in items_cache or (i[1] is not None and i[1] not in variations_cache): self.error_message(self.error_messages['not_for_sale']) return redirect(self.get_failure_url()) item = items_cache[i[0]] variation = variations_cache[i[1]] if i[1] is not None else None # Execute restriction plugins to check whether they (a) change the price or # (b) make the item/variation unavailable. If neither is the case, check_restriction # will correctly return the default price price = item.check_restrictions() if variation is None else variation.check_restrictions() # Fetch all quotas. If there are no quotas, this item is not allowed to be sold. quotas = list(item.quotas.all()) if variation is None else list(variation.quotas.all()) if price is False or len(quotas) == 0 or not item.active: self.error_message(self.error_messages['unavailable']) continue # Assume that all quotas allow us to buy i[2] instances of the object quota_ok = i[2] for quota in quotas: avail = quota.availability() if avail[1] < i[2]: # This quota is not available or less than i[2] items are left, so we have to # reduce the number of bought items self.error_message( self.error_messages['unavailable'] if avail[0] != Quota.AVAILABILITY_OK else self.error_messages['in_part'] ) quota_ok = min(quota_ok, avail[1]) # Create a CartPosition for as much items as we can for k in range(quota_ok): if len(i) > 3 and i[2] == 1: # Recreating cp = i[3].clone() cp.expires = expiry cp.price = price cp.save() else: cp = CartPosition( event=self.request.event, item=item, variation=variation, price=price, expires=expiry ) if self.request.user.is_authenticated(): cp.user = self.request.user else: cp.session = self.request.session.session_key cp.save() self._delete_expired() if not self.msg_some_unavailable: messages.success(self.request, _('The products have been successfully added to your cart.')) return redirect(self.get_success_url()) except EventLock.LockTimeoutException: # Is raised when there are too many threads asking for quota locks and we were # unaible to get one self.error_message(self.error_messages['busy'], important=True)
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.addons.all().delete() 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: if op.voucher in self._voucher_depend_on_cart: err = err or error_messages['voucher_redeemed_cart'] % self.event.settings.reservation_time else: 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) if isinstance(op, self.AddOperation): for b in op.bundled: b_quota_available_count = min(available_count * b.count, min(quotas_ok[q] for q in b.quotas)) if b_quota_available_count < b.count: err = err or error_messages['unavailable'] available_count = 0 elif b_quota_available_count < available_count * b.count: err = err or error_messages['in_part'] available_count = b_quota_available_count // b.count for q in b.quotas: quotas_ok[q] -= available_count * b.count # TODO: is this correct? for q in op.quotas: quotas_ok[q] -= available_count if op.voucher: vouchers_ok[op.voucher] -= available_count if any(qa < 0 for qa in quotas_ok.values()): # Safeguard, shouldn't happen err = err or error_messages['unavailable'] available_count = 0 if isinstance(op, self.AddOperation): for k in range(available_count): cp = 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 ) if self.event.settings.attendee_names_asked: scheme = PERSON_NAME_SCHEMES.get(self.event.settings.name_scheme) if 'attendee-name' in self._widget_data: cp.attendee_name_parts = {'_legacy': self._widget_data['attendee-name']} if any('attendee-name-{}'.format(k.replace('_', '-')) in self._widget_data for k, l, w in scheme['fields']): cp.attendee_name_parts = { k: self._widget_data.get('attendee-name-{}'.format(k.replace('_', '-')), '') for k, l, w in scheme['fields'] } if self.event.settings.attendee_emails_asked and 'email' in self._widget_data: cp.attendee_email = self._widget_data.get('email') cp._answers = {} for k, v in self._widget_data.items(): if not k.startswith('question-'): continue q = cp.item.questions.filter(ask_during_checkin=False, identifier__iexact=k[9:]).first() if q: try: cp._answers[q] = q.clean_answer(v) except ValidationError: pass if op.bundled: cp.save() # Needs to be in the database already so we have a PK that we can reference for b in op.bundled: for j in range(b.count): new_cart_positions.append(CartPosition( event=self.event, item=b.item, variation=b.variation, price=b.price.gross, expires=self._expiry, cart_id=self.cart_id, voucher=None, addon_to=cp, subevent=b.subevent, includes_tax=b.includes_tax, is_bundled=True )) new_cart_positions.append(cp) elif isinstance(op, self.ExtendOperation): if available_count == 1: op.position.expires = self._expiry op.position.price = op.price.gross try: op.position.save(force_update=True) except DatabaseError: # Best effort... The position might have been deleted in the meantime! pass elif available_count == 0: op.position.addons.all().delete() op.position.delete() else: raise AssertionError("ExtendOperation cannot affect more than one item") for p in new_cart_positions: if getattr(p, '_answers', None): if not p.pk: # We stored some to the database already before p.save() _save_answers(p, {}, p._answers) CartPosition.objects.bulk_create([p for p in new_cart_positions if not getattr(p, '_answers', None) and not p.pk]) return err