def test_validate_membership_test_mode(event, customer, membership, requiring_ticket, membership_type): with pytest.raises(ValidationError) as excinfo: membership.testmode = True membership.save() validate_memberships_in_order( customer, [CartPosition(item=requiring_ticket, used_membership=membership)], event, lock=False, ignored_order=None, testmode=False, ) assert "test mode" in str(excinfo.value) with pytest.raises(ValidationError) as excinfo: membership.testmode = False membership.save() validate_memberships_in_order( customer, [CartPosition(item=requiring_ticket, used_membership=membership)], event, lock=False, ignored_order=None, testmode=True, ) assert "test mode" in str(excinfo.value)
def test_validate_membership_max_usages(event, customer, membership, requiring_ticket, membership_type): membership_type.max_usages = 1 membership_type.allow_parallel_usage = True membership_type.save() o1 = Order.objects.create( status=Order.STATUS_PENDING, event=event, email='admin@localhost', datetime=now() - timedelta(days=3), expires=now() + timedelta(days=11), total=Decimal("23"), ) OrderPosition.objects.create(order=o1, item=requiring_ticket, used_membership=membership, variation=None, price=Decimal("23"), attendee_name_parts={'full_name': "Peter"}) with pytest.raises(ValidationError) as excinfo: validate_memberships_in_order( customer, [CartPosition(item=requiring_ticket, used_membership=membership)], event, lock=False, ignored_order=None) assert "more than 1 time" in str(excinfo.value) membership_type.max_usages = 2 membership_type.save() validate_memberships_in_order( customer, [CartPosition(item=requiring_ticket, used_membership=membership)], event, lock=False, ignored_order=None) with pytest.raises(ValidationError) as excinfo: validate_memberships_in_order(customer, [ CartPosition(item=requiring_ticket, used_membership=membership), CartPosition(item=requiring_ticket, used_membership=membership), ], event, lock=False, ignored_order=None) assert "more than 2 times" in str(excinfo.value)
def _perform_operations(self): vouchers_ok = self._get_voucher_availability() quotas_ok = self._get_quota_availability() err = None new_cart_positions = [] self._operations.sort(key=lambda a: self.order[type(a)]) for op in self._operations: if isinstance(op, self.RemoveOperation): 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, expires=self._expiry, cart_id=self.cart_id, voucher=op.voucher )) elif isinstance(op, self.ExtendOperation): if available_count == 1: op.position.expires = self._expiry op.position.price = op.price 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 test_validate_membership_required(event, customer, membership, requiring_ticket, membership_type): with pytest.raises(ValidationError) as excinfo: validate_memberships_in_order(customer, [CartPosition(item=requiring_ticket, )], event, lock=False, ignored_order=None) assert "requires an active" in str(excinfo.value)
def test_validate_membership_wrong_type(event, customer, membership, requiring_ticket, membership_type): requiring_ticket.require_membership_types.clear() with pytest.raises(ValidationError) as excinfo: validate_memberships_in_order( customer, [CartPosition(item=requiring_ticket, used_membership=membership)], event, lock=False, ignored_order=None) assert "not allowed for the product" in str(excinfo.value)
def test_validate_membership_wrong_customer(event, customer, membership, requiring_ticket, membership_type): customer2 = event.organizer.customers.create(email="*****@*****.**") with pytest.raises(ValidationError) as excinfo: validate_memberships_in_order( customer2, [CartPosition(item=requiring_ticket, used_membership=membership)], event, lock=False, ignored_order=None) assert "different customer" in str(excinfo.value)
def test_validate_membership_wrong_date(event, customer, membership, requiring_ticket, membership_type): membership.date_start -= timedelta(days=100) membership.date_end -= timedelta(days=100) membership.save() with pytest.raises(ValidationError) as excinfo: validate_memberships_in_order( customer, [CartPosition(item=requiring_ticket, used_membership=membership)], event, lock=False, ignored_order=None) assert "taking place at" in str(excinfo.value)
def test_validate_membership_not_required(event, customer, membership, granting_ticket, membership_type): with pytest.raises(ValidationError) as excinfo: validate_memberships_in_order( customer, [CartPosition( item=granting_ticket, used_membership=membership, )], event, lock=False, ignored_order=None) assert "does not require" in str(excinfo.value)
def test_validate_membership_ensure_locking(event, customer, membership, requiring_ticket, membership_type, django_assert_num_queries): with django_assert_num_queries(4) as captured: validate_memberships_in_order(customer, [ CartPosition( item=requiring_ticket, used_membership=membership, ) ], event, lock=True, ignored_order=None) if 'sqlite' not in settings.DATABASES['default']['ENGINE']: assert any('FOR UPDATE' in s['sql'] for s in captured)
def create_cart(self, sc, expires): with transaction.atomic(): with self.request.event.lock(): positions = [] quotas = Counter() for form in self.formset.forms: if '-' in form.cleaned_data['itemvar']: itemid, varid = form.cleaned_data['itemvar'].split('-') else: itemid, varid = form.cleaned_data['itemvar'], None item = Item.objects.get(pk=itemid, event=self.request.event) variation = ItemVariation.objects.get( pk=varid, item=item) if varid else None price = form.cleaned_data['price'] if not price: price = (variation.default_price if variation and variation.default_price is not None else item.default_price) for quota in item.quotas.all(): quotas[quota] += form.cleaned_data['count'] for i in range(form.cleaned_data['count']): positions.append( CartPosition(item=item, variation=variation, event=self.request.event, cart_id=sc.cart_id, expires=expires, price=price)) for quota, diff in quotas.items(): avail = quota.availability() if avail[0] != Quota.AVAILABILITY_OK or ( avail[1] is not None and avail[1] < diff): raise CartError(self.error_messages['quota'].format( name=quota.name)) sc.expires = expires sc.event = self.request.event sc.total = sum([p.price for p in positions]) sc.save() CartPosition.objects.bulk_create(positions)
def test_validate_membership_parallel(event, customer, membership, subevent, requiring_ticket, membership_type): se2 = event.subevents.create( name='Foo', date_from=TZ.localize(datetime(2021, 4, 28, 10, 0, 0, 0)), ) membership_type.allow_parallel_usage = False membership_type.save() o1 = Order.objects.create( status=Order.STATUS_PENDING, event=event, email='admin@localhost', datetime=now() - timedelta(days=3), expires=now() + timedelta(days=11), total=Decimal("23"), ) OrderPosition.objects.create(order=o1, item=requiring_ticket, used_membership=membership, variation=None, subevent=subevent, price=Decimal("23"), attendee_name_parts={'full_name': "Peter"}) with pytest.raises(ValidationError) as excinfo: validate_memberships_in_order(customer, [ CartPosition(item=requiring_ticket, used_membership=membership, subevent=subevent) ], event, lock=False, ignored_order=None) assert "different ticket at the same time" in str(excinfo.value) validate_memberships_in_order(customer, [ CartPosition( item=requiring_ticket, used_membership=membership, subevent=se2) ], event, lock=False, ignored_order=None) with pytest.raises(ValidationError) as excinfo: validate_memberships_in_order(customer, [ CartPosition(item=requiring_ticket, used_membership=membership, subevent=se2), CartPosition(item=requiring_ticket, used_membership=membership, subevent=se2) ], event, lock=False, ignored_order=None) assert "different ticket at the same time" in str(excinfo.value) membership_type.allow_parallel_usage = True membership_type.save() validate_memberships_in_order(customer, [ CartPosition(item=requiring_ticket, used_membership=membership, subevent=subevent) ], event, lock=False, ignored_order=None)
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) 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 _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) 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): 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 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 op.position.save() elif available_count == 0: op.position.delete() else: raise AssertionError("ExtendOperation cannot affect more than one item") for p in new_cart_positions: if p._answers: p.save() _save_answers(p, {}, p._answers) CartPosition.objects.bulk_create([p for p in new_cart_positions if not p._answers]) 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.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) 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): 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 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 op.position.save() 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 p._answers: p.save() _save_answers(p, {}, p._answers) CartPosition.objects.bulk_create( [p for p in new_cart_positions if not p._answers]) return err
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