diff --git a/src/pretix/base/migrations/0047_auto_20161126_1300.py b/src/pretix/base/migrations/0047_auto_20161126_1300.py new file mode 100644 index 0000000000..87e708639b --- /dev/null +++ b/src/pretix/base/migrations/0047_auto_20161126_1300.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2016-11-26 13:00 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + +import pretix.base.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0046_order_meta_info'), + ] + + operations = [ + migrations.AddField( + model_name='voucher', + name='max_usages', + field=models.PositiveIntegerField(default=1, help_text='Number of times this voucher can be redeemed.', verbose_name='Maximum usages'), + ), + migrations.AlterField( + model_name='event', + name='slug', + field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Slug'), + ), + migrations.AlterField( + model_name='organizer', + name='slug', + field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Slug'), + ), + migrations.AlterField( + model_name='voucher', + name='redeemed', + field=models.PositiveIntegerField(default=0, verbose_name='Redeemed'), + ), + ] diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py index 83cc3e623c..b0322271f6 100644 --- a/src/pretix/base/models/items.py +++ b/src/pretix/base/models/items.py @@ -4,8 +4,9 @@ from datetime import datetime from decimal import Decimal from typing import Tuple +from django.conf import settings from django.db import models -from django.db.models import Q +from django.db.models import F, Func, Q, Sum from django.utils.functional import cached_property from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ @@ -577,12 +578,18 @@ class Quota(LoggedModel): from pretix.base.models import Voucher now_dt = now_dt or now() + if 'sqlite3' in settings.DATABASES['default']['ENGINE']: + func = 'MAX' + else: + func = 'GREATEST' + return Voucher.objects.filter( Q(block_quota=True) & - Q(redeemed=False) & Q(Q(valid_until__isnull=True) | Q(valid_until__gte=now_dt)) & Q(Q(self._position_lookup) | Q(quota=self)) - ).values('id').distinct().count() + ).values('id').aggregate( + free=Sum(Func(F('max_usages') - F('redeemed'), 0, function=func)) + )['free'] or 0 def count_in_cart(self, now_dt: datetime=None) -> int: from pretix.base.models import CartPosition @@ -617,9 +624,9 @@ class Quota(LoggedModel): return ( ( # Orders for items which do not have any variations Q(variation__isnull=True) & - Q(item__quotas__in=[self]) + Q(item__quotas=self) ) | ( # Orders for items which do have any variations - Q(variation__quotas__in=[self]) + Q(variation__quotas=self) ) ) diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index 965690f158..0e3a4cf41f 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -7,6 +7,7 @@ from typing import List, Union import pytz from django.conf import settings from django.db import models +from django.db.models import F from django.utils.crypto import get_random_string from django.utils.timezone import make_aware, now from django.utils.translation import ugettext_lazy as _ @@ -459,6 +460,8 @@ class OrderPosition(AbstractPosition): @classmethod def transform_cart_positions(cls, cp: List, order) -> list: + from . import Voucher + ops = [] for cartpos in cp: op = OrderPosition(order=order) @@ -471,8 +474,7 @@ class OrderPosition(AbstractPosition): answ.cartposition = None answ.save() if cartpos.voucher: - cartpos.voucher.redeemed = True - cartpos.voucher.save() + Voucher.objects.filter(pk=cartpos.voucher.pk).update(redeemed=F('redeemed') + 1) cartpos.delete() return ops diff --git a/src/pretix/base/models/vouchers.py b/src/pretix/base/models/vouchers.py index 4da4aa6011..ad1605a035 100644 --- a/src/pretix/base/models/vouchers.py +++ b/src/pretix/base/models/vouchers.py @@ -30,7 +30,9 @@ class Voucher(LoggedModel): :type event: Event :param code: The secret voucher code :type code: str - :param redeemed: Whether or not this voucher has already been redeemed + :param max_usages: The number of times this voucher can be redeemed + :type max_usages: int + :param redeemed: The number of times this voucher already has been redeemed :type redeemed: bool :param valid_until: The expiration date of this voucher (optional) :type valid_until: datetime @@ -68,10 +70,14 @@ class Voucher(LoggedModel): max_length=255, default=generate_code, db_index=True, ) - redeemed = models.BooleanField( + max_usages = models.PositiveIntegerField( + verbose_name=_("Maximum usages"), + help_text=_("Number of times this voucher can be redeemed."), + default=1 + ) + redeemed = models.PositiveIntegerField( verbose_name=_("Redeemed"), - default=False, - db_index=True + default=0 ) valid_until = models.DateTimeField( blank=True, null=True, db_index=True, @@ -197,7 +203,7 @@ class Voucher(LoggedModel): Returns True if a voucher has not yet been redeemed, but is still within its validity (if valid_until is set). """ - if self.redeemed: + if self.redeemed >= self.max_usages: return False if self.valid_until and self.valid_until < now(): return False diff --git a/src/pretix/base/services/cart.py b/src/pretix/base/services/cart.py index be17e12a6a..bc54f2d440 100644 --- a/src/pretix/base/services/cart.py +++ b/src/pretix/base/services/cart.py @@ -33,7 +33,8 @@ error_messages = { 'ended': _('The presale period has ended.'), '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 and can only be used once.'), + '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.'), @@ -114,17 +115,26 @@ def _add_new_items(event: Event, items: List[dict], if i.get('voucher'): try: voucher = Voucher.objects.get(code=i.get('voucher').strip(), event=event) - if voucher.redeemed: + if voucher.redeemed >= voucher.max_usages: return error_messages['voucher_redeemed'] if voucher.valid_until is not None and voucher.valid_until < now_dt: return error_messages['voucher_expired'] if not voucher.applies_to(item, variation): return error_messages['voucher_invalid_item'] - doubleuse = CartPosition.objects.filter(voucher=voucher, cart_id=cart_id, event=event) + + redeemed_in_carts = CartPosition.objects.filter( + Q(voucher=voucher) & Q(event=event) & + (Q(expires__gte=now_dt) | Q(cart_id=cart_id)) + ) if 'cp' in i: - doubleuse = doubleuse.exclude(pk=i['cp'].pk) - if doubleuse.exists(): - return error_messages['voucher_double'] + redeemed_in_carts = redeemed_in_carts.exclude(pk=i['cp'].pk) + v_avail = voucher.max_usages - voucher.redeemed - redeemed_in_carts.count() + + if v_avail < 1: + return error_messages['voucher_redeemed'] + if i['count'] > v_avail: + return error_messages['voucher_redeemed_partial'] % v_avail + except Voucher.DoesNotExist: return error_messages['voucher_invalid'] diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index 46d899b365..b1b76124b0 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -8,6 +8,7 @@ from typing import List, Optional import pytz from celery.exceptions import MaxRetriesExceededError from django.db import transaction +from django.db.models import F, Q from django.dispatch import receiver from django.utils.formats import date_format from django.utils.timezone import make_aware, now @@ -18,7 +19,7 @@ from pretix.base.i18n import ( ) from pretix.base.models import ( CartPosition, Event, Item, ItemVariation, Order, OrderPosition, Quota, - User, + User, Voucher, ) from pretix.base.models.orders import InvoiceAddress from pretix.base.payment import BasePaymentProvider @@ -46,11 +47,15 @@ error_messages = { 'server was too busy. Please try again.'), 'not_started': _('The presale period for this event has not yet started.'), 'ended': _('The presale period has ended.'), - 'voucher_invalid': _('This voucher code is not known in our database.'), - 'voucher_redeemed': _('This voucher code has already been used an can only be used once.'), - 'voucher_expired': _('This voucher is expired.'), - 'voucher_invalid_item': _('This voucher is not valid for this item.'), - 'voucher_required': _('You need a valid voucher code to order one of the products in your cart.'), + 'voucher_invalid': _('The voucher code used for one of the items in your cart is not known in our database.'), + 'voucher_redeemed': _('The voucher code used for one of the items in your cart has already been used the maximum ' + 'number of times allowed. We removed this item from your cart.'), + 'voucher_expired': _('The voucher code used for one of the items in your cart is expired. We removed this item ' + 'from your cart.'), + 'voucher_invalid_item': _('The voucher code used for one of the items in your cart is not valid for this item. We ' + 'removed this item from your cart.'), + 'voucher_required': _('You need a valid voucher code to order one of the products in your cart. We removed this ' + 'item from your cart.'), } logger = logging.getLogger(__name__) @@ -163,8 +168,7 @@ def _cancel_order(order, user=None): for position in order.positions.all(): if position.voucher: - position.voucher.redeemed = False - position.voucher.save() + Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=F('redeemed') - 1) return order @@ -184,7 +188,6 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio err = None _check_date(event, now_dt) - voucherids = set() for i, cp in enumerate(positions): if not cp.item.active or (cp.variation and not cp.variation.active): err = err or error_messages['unavailable'] @@ -193,11 +196,14 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio quotas = list(cp.item.quotas.all()) if cp.variation is None else list(cp.variation.quotas.all()) if cp.voucher: - if cp.voucher.redeemed or cp.voucher_id in voucherids: + redeemed_in_carts = CartPosition.objects.filter( + Q(voucher=cp.voucher) & Q(event=event) & Q(expires__gte=now_dt) + ).exclude(pk=cp.pk) + v_avail = cp.voucher.max_usages - cp.voucher.redeemed - redeemed_in_carts.count() + if v_avail < 1: err = err or error_messages['voucher_redeemed'] - cp.delete() # Sorry! But you should have never gotten into this state at all. + cp.delete() # Sorry! continue - voucherids.add(cp.voucher_id) if cp.item.require_voucher and cp.voucher is None: cp.delete() @@ -225,6 +231,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio if cp.voucher: if cp.voucher.valid_until and cp.voucher.valid_until < now_dt: err = err or error_messages['voucher_expired'] + cp.delete() continue if cp.voucher.price is not None: price = cp.voucher.price diff --git a/src/pretix/control/forms/vouchers.py b/src/pretix/control/forms/vouchers.py index 38f8b8f51f..5ceadeefc4 100644 --- a/src/pretix/control/forms/vouchers.py +++ b/src/pretix/control/forms/vouchers.py @@ -23,7 +23,7 @@ class VoucherForm(I18nModelForm): localized_fields = '__all__' fields = [ 'code', 'valid_until', 'block_quota', 'allow_ignore_quota', 'price', 'tag', - 'comment' + 'comment', 'max_usages' ] widgets = { 'valid_until': forms.DateTimeInput(attrs={'class': 'datetimepicker'}), @@ -81,11 +81,20 @@ class VoucherForm(I18nModelForm): self.instance.item = None self.instance.variation = None + if data['max_usages'] < self.instance.redeemed: + raise ValidationError( + _('This voucher has already been redeemed %(redeemed)s times. You cannot reduce the maximum number of ' + 'usages below this number.'), + params={ + 'redeemed': self.instance.redeemed + } + ) + if 'codes' in data: data['codes'] = [a.strip() for a in data.get('codes', '').strip().split("\n") if a] - cnt = len(data['codes']) + cnt = len(data['codes']) * data['max_usages'] else: - cnt = 1 + cnt = data['max_usages'] if self._clean_quota_needs_checking(data): self._clean_quota_check(data, cnt) @@ -178,11 +187,18 @@ class VoucherBulkForm(VoucherForm): model = Voucher localized_fields = '__all__' fields = [ - 'valid_until', 'block_quota', 'allow_ignore_quota', 'price', 'tag', 'comment' + 'valid_until', 'block_quota', 'allow_ignore_quota', 'price', 'tag', 'comment', + 'max_usages' ] widgets = { 'valid_until': forms.DateTimeInput(attrs={'class': 'datetimepicker'}), } + labels = { + 'max_usages': _('Maximum usages per voucher') + } + help_texts = { + 'max_usages': _('Number of times times EACH of these vouchers can be redeemed.') + } def clean(self): data = super().clean() diff --git a/src/pretix/control/templates/pretixcontrol/vouchers/bulk.html b/src/pretix/control/templates/pretixcontrol/vouchers/bulk.html index a69f70b6dd..91ee271446 100644 --- a/src/pretix/control/templates/pretixcontrol/vouchers/bulk.html +++ b/src/pretix/control/templates/pretixcontrol/vouchers/bulk.html @@ -26,6 +26,7 @@ {% bootstrap_field form.codes layout="horizontal" %} + {% bootstrap_field form.max_usages layout="horizontal" %}
{% trans "Voucher details" %} diff --git a/src/pretix/control/templates/pretixcontrol/vouchers/detail.html b/src/pretix/control/templates/pretixcontrol/vouchers/detail.html index 5428ca8bdd..92ae2a5520 100644 --- a/src/pretix/control/templates/pretixcontrol/vouchers/detail.html +++ b/src/pretix/control/templates/pretixcontrol/vouchers/detail.html @@ -23,6 +23,7 @@
{% trans "Voucher details" %} {% bootstrap_field form.code layout="horizontal" %} + {% bootstrap_field form.max_usages layout="horizontal" %} {% bootstrap_field form.valid_until layout="horizontal" %} {% bootstrap_field form.block_quota layout="horizontal" %} {% bootstrap_field form.allow_ignore_quota layout="horizontal" %} diff --git a/src/pretix/control/templates/pretixcontrol/vouchers/index.html b/src/pretix/control/templates/pretixcontrol/vouchers/index.html index 54b19cf8c2..1f084dde1e 100644 --- a/src/pretix/control/templates/pretixcontrol/vouchers/index.html +++ b/src/pretix/control/templates/pretixcontrol/vouchers/index.html @@ -54,7 +54,7 @@ {% trans "Voucher code" %} - {% trans "Is redeemed" %} + {% trans "Redemptions" %} {% trans "Expiry" %} {% trans "Tag" %} {% trans "Product" %} @@ -68,7 +68,7 @@ {{ v.code }} - {% if v.redeemed %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %} + {{ v.redeemed }} / {{ v.max_usages }} {{ v.valid_until|date }} {{ v.tag }} diff --git a/src/pretix/control/views/vouchers.py b/src/pretix/control/views/vouchers.py index abaa067e09..29b7c2d2cc 100644 --- a/src/pretix/control/views/vouchers.py +++ b/src/pretix/control/views/vouchers.py @@ -5,7 +5,7 @@ from django.conf import settings from django.contrib import messages from django.core.urlresolvers import resolve, reverse from django.db import transaction -from django.db.models import Case, Count, IntegerField, Q, Sum, When +from django.db.models import Count, Q, Sum from django.http import ( Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect, JsonResponse, @@ -41,11 +41,11 @@ class VoucherList(EventPermissionRequiredMixin, ListView): if self.request.GET.get("status", "") != "": s = self.request.GET.get("status", "") if s == 'v': - qs = qs.filter(Q(valid_until__isnull=True) | Q(valid_until__gt=now())).filter(redeemed=False) + qs = qs.filter(Q(valid_until__isnull=True) | Q(valid_until__gt=now())).filter(redeemed=0) elif s == 'r': - qs = qs.filter(redeemed=True) + qs = qs.filter(redeemed__gt=0) elif s == 'e': - qs = qs.filter(Q(valid_until__isnull=False) & Q(valid_until__lt=now())).filter(redeemed=False) + qs = qs.filter(Q(valid_until__isnull=False) & Q(valid_until__lt=now())).filter(redeemed=0) return qs def get(self, request, *args, **kwargs): @@ -59,7 +59,7 @@ class VoucherList(EventPermissionRequiredMixin, ListView): headers = [ _('Voucher code'), _('Valid until'), _('Product'), _('Reserve quota'), _('Bypass quota'), - _('Price'), _('Tag'), _('Redeemed') + _('Price'), _('Tag'), _('Redeemed'), _('Maximum usages') ] writer.writerow(headers) @@ -79,7 +79,8 @@ class VoucherList(EventPermissionRequiredMixin, ListView): _("Yes") if v.allow_ignore_quota else _("No"), str(v.price) if v.price else "", v.tag, - _("Yes") if v.redeemed else _("No"), + str(v.redeemed), + str(v.max_usages) ] writer.writerow(row) @@ -97,14 +98,7 @@ class VoucherTags(EventPermissionRequiredMixin, TemplateView): tags = self.request.event.vouchers.order_by('tag').filter(tag__isnull=False).values('tag').annotate( total=Count('id'), - # This is a fix for this MySQL issue: https://code.djangoproject.com/ticket/24662 - redeemed=Sum( - Case( - When(redeemed=True, then=1), - When(redeemed=False, then=0), - output_field=IntegerField() - ) - ) + redeemed=Sum('redeemed') ) for t in tags: t['percentage'] = int((t['redeemed'] / t['total']) * 100) @@ -128,7 +122,7 @@ class VoucherDelete(EventPermissionRequiredMixin, DeleteView): raise Http404(_("The requested voucher does not exist.")) def get(self, request, *args, **kwargs): - if self.get_object().redeemed: + if self.get_object().redeemed > 0: messages.error(request, _('A voucher can not be deleted if it already has been redeemed.')) return HttpResponseRedirect(self.get_success_url()) return super().get(request, *args, **kwargs) @@ -138,7 +132,7 @@ class VoucherDelete(EventPermissionRequiredMixin, DeleteView): self.object = self.get_object() success_url = self.get_success_url() - if self.object.redeemed: + if self.object.redeemed > 0: messages.error(request, _('A voucher can not be deleted if it already has been redeemed.')) else: self.object.log_action('pretix.voucher.deleted', user=self.request.user) diff --git a/src/pretix/presale/templates/pretixpresale/event/voucher.html b/src/pretix/presale/templates/pretixpresale/event/voucher.html index 488c500e4c..e5a0c1d404 100644 --- a/src/pretix/presale/templates/pretixpresale/event/voucher.html +++ b/src/pretix/presale/templates/pretixpresale/event/voucher.html @@ -79,9 +79,14 @@ {% if var.cached_availability.0 == 100 %}
{% else %} @@ -128,9 +133,14 @@ {% if item.cached_availability.0 == 100 %}
{% else %} diff --git a/src/pretix/presale/views/cart.py b/src/pretix/presale/views/cart.py index 0b9ff8a06c..12fe1814b9 100644 --- a/src/pretix/presale/views/cart.py +++ b/src/pretix/presale/views/cart.py @@ -30,21 +30,14 @@ class CartActionMixin: def get_error_url(self): return self.get_next_url() - def _item_from_post_value(self, key, value): + def _item_from_post_value(self, key, value, voucher=None): if value.strip() == '' or '_' not in key: return - parts = key.split("_") - if parts[-1] == "voucher": - voucher = value - value = 1 - parts = parts[:-1] - else: - voucher = None - if not key.startswith('item_') and not key.startswith('variation_'): return + parts = key.split("_") try: amount = int(value) except ValueError: @@ -86,7 +79,7 @@ class CartActionMixin: req_items = list(self.request.POST.lists()) if '_voucher_item' in self.request.POST and '_voucher_code' in self.request.POST: req_items.append(( - '%s_voucher' % self.request.POST['_voucher_item'], (self.request.POST['_voucher_code'],) + '%s' % self.request.POST['_voucher_item'], ('1',) )) pass @@ -94,7 +87,7 @@ class CartActionMixin: for key, values in req_items: for value in values: try: - item = self._item_from_post_value(key, value) + item = self._item_from_post_value(key, value, self.request.POST.get('_voucher_code')) except CartError as e: messages.error(self.request, str(e)) return @@ -169,6 +162,7 @@ class RedeemView(EventViewMixin, TemplateView): context = super().get_context_data(**kwargs) context['voucher'] = self.voucher + context['max_times'] = self.voucher.max_usages - self.voucher.redeemed # Fetch all items items = self.request.event.items.all().filter( @@ -242,10 +236,18 @@ class RedeemView(EventViewMixin, TemplateView): v = v.strip() try: self.voucher = Voucher.objects.get(code=v, event=request.event) - if self.voucher.redeemed: + if self.voucher.redeemed >= self.voucher.max_usages: err = error_messages['voucher_redeemed'] if self.voucher.valid_until is not None and self.voucher.valid_until < now(): err = error_messages['voucher_expired'] + + redeemed_in_carts = CartPosition.objects.filter( + Q(voucher=self.voucher) & Q(event=request.event) & + (Q(expires__gte=now()) | Q(cart_id=request.session.session_key)) + ) + v_avail = self.voucher.max_usages - self.voucher.redeemed - redeemed_in_carts.count() + if v_avail < 1: + err = error_messages['voucher_redeemed'] except Voucher.DoesNotExist: err = error_messages['voucher_invalid'] else: diff --git a/src/tests/base/test_models.py b/src/tests/base/test_models.py index ad79d44f0a..640ef162f3 100644 --- a/src/tests/base/test_models.py +++ b/src/tests/base/test_models.py @@ -234,6 +234,37 @@ class QuotaTestCase(BaseQuotaTestCase): v.save() self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_ORDERED, 0)) + def test_voucher_quota_multiuse(self): + self.quota.size = 5 + self.quota.variations.add(self.var1) + self.quota.save() + Voucher.objects.create(quota=self.quota, event=self.event, block_quota=True, max_usages=5, redeemed=2) + self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_OK, 2)) + Voucher.objects.create(quota=self.quota, event=self.event, block_quota=True, max_usages=2) + self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_ORDERED, 0)) + + def test_voucher_multiuse_count_overredeemed(self): + Voucher.objects.create(quota=self.quota, event=self.event, block_quota=True, max_usages=2, redeemed=4) + self.assertEqual(self.quota.count_blocking_vouchers(), 0) + + def test_voucher_quota_multiuse_multiproduct(self): + q2 = Quota.objects.create(event=self.event, name="foo", size=10) + q2.items.add(self.item1) + self.quota.size = 5 + self.quota.items.add(self.item1) + self.quota.items.add(self.item2) + self.quota.items.add(self.item3) + self.quota.variations.add(self.var1) + self.quota.variations.add(self.var2) + self.quota.variations.add(self.var3) + self.quota.save() + Voucher.objects.create(item=self.item1, event=self.event, block_quota=True, max_usages=5, redeemed=2) + Voucher.objects.create(item=self.item2, variation=self.var2, event=self.event, block_quota=True, max_usages=5, + redeemed=2) + Voucher.objects.create(item=self.item2, variation=self.var2, event=self.event, block_quota=True, max_usages=5, + redeemed=2) + self.assertEqual(self.quota.count_blocking_vouchers(), 9) + def test_voucher_quota_expiring_soon(self): self.quota.variations.add(self.var1) self.quota.size = 1 diff --git a/src/tests/control/test_vouchers.py b/src/tests/control/test_vouchers.py index bfe345ef2a..6bd560f6da 100644 --- a/src/tests/control/test_vouchers.py +++ b/src/tests/control/test_vouchers.py @@ -81,23 +81,23 @@ class VoucherFormTest(SoupTest): self.event.vouchers.create(item=self.ticket, code='ABCDEFG') doc = self.client.get('/control/event/%s/%s/vouchers/?download=yes' % (self.orga.slug, self.event.slug)) assert doc.content.strip() == '"Voucher code","Valid until","Product","Reserve quota","Bypass quota","Price",' \ - '"Tag","Redeemed"\r\n"ABCDEFG","","Early-bird ticket","No","No","","",' \ - '"No"'.encode('utf-8') + '"Tag","Redeemed","Maximum usages"\r\n"ABCDEFG","","Early-bird ticket","No",' \ + '"No","","","0","1"'.encode('utf-8') def test_filter_status_valid(self): v = self.event.vouchers.create(item=self.ticket) doc = self.client.get('/control/event/%s/%s/vouchers/?status=v' % (self.orga.slug, self.event.slug)) assert v.code in doc.rendered_content - v.redeemed = True + v.redeemed = 1 v.save() doc = self.client.get('/control/event/%s/%s/vouchers/?status=v' % (self.orga.slug, self.event.slug)) assert v.code not in doc.rendered_content def test_filter_status_redeemed(self): - v = self.event.vouchers.create(item=self.ticket, redeemed=True) + v = self.event.vouchers.create(item=self.ticket, redeemed=1) doc = self.client.get('/control/event/%s/%s/vouchers/?status=r' % (self.orga.slug, self.event.slug)) assert v.code in doc.rendered_content - v.redeemed = False + v.redeemed = 0 v.save() doc = self.client.get('/control/event/%s/%s/vouchers/?status=r' % (self.orga.slug, self.event.slug)) assert v.code not in doc.rendered_content @@ -406,7 +406,7 @@ class VoucherFormTest(SoupTest): assert not self.event.vouchers.filter(pk=v.id).exists() def test_delete_voucher_redeemed(self): - v = self.event.vouchers.create(quota=self.quota_tickets, redeemed=True) + v = self.event.vouchers.create(quota=self.quota_tickets, redeemed=1) doc = self.get_doc('/control/event/%s/%s/vouchers/%s/delete' % (self.orga.slug, self.event.slug, v.pk), follow=True) assert doc.select(".alert-danger") diff --git a/src/tests/presale/test_cart.py b/src/tests/presale/test_cart.py index f3f91f466d..47e3bcf582 100644 --- a/src/tests/presale/test_cart.py +++ b/src/tests/presale/test_cart.py @@ -35,6 +35,12 @@ class CartTestMixin: category=self.category, default_price=23) self.quota_tickets.items.add(self.ticket) + self.quota_all = Quota.objects.create(event=self.event, name='All', size=None) + self.quota_all.items.add(self.ticket) + self.quota_all.items.add(self.shirt) + self.quota_all.variations.add(self.shirt_blue) + self.quota_all.variations.add(self.shirt_red) + self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug)) self.session_key = self.client.cookies.get(settings.SESSION_COOKIE_NAME).value @@ -516,7 +522,8 @@ class CartTest(CartTestMixin, TestCase): def test_voucher(self): v = Voucher.objects.create(item=self.ticket, event=self.event) self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { - 'item_%d_voucher' % self.ticket.id: v.code, + 'item_%d' % self.ticket.id: '1', + '_voucher_code': v.code, }, follow=True) objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event)) self.assertEqual(len(objs), 1) @@ -531,7 +538,8 @@ class CartTest(CartTestMixin, TestCase): price=23, expires=now() - timedelta(minutes=10), voucher=v ) self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { - 'variation_%d_%d' % (self.shirt.id, self.shirt_red.id): '1' + 'variation_%d_%d' % (self.shirt.id, self.shirt_red.id): '1', + '_voucher_code': v.code, }, follow=True) obj = CartPosition.objects.get(id=cp1.id) self.assertGreater(obj.expires, now()) @@ -539,7 +547,8 @@ class CartTest(CartTestMixin, TestCase): def test_voucher_variation(self): v = Voucher.objects.create(item=self.shirt, variation=self.shirt_red, event=self.event) self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { - 'variation_%d_%d_voucher' % (self.shirt.id, self.shirt_red.id): v.code, + 'variation_%d_%d' % (self.shirt.id, self.shirt_red.id): '1', + '_voucher_code': v.code, }, follow=True) objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event)) self.assertEqual(len(objs), 1) @@ -549,7 +558,8 @@ class CartTest(CartTestMixin, TestCase): def test_voucher_quota(self): v = Voucher.objects.create(quota=self.quota_shirts, event=self.event) self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { - 'variation_%d_%d_voucher' % (self.shirt.id, self.shirt_red.id): v.code, + 'variation_%d_%d' % (self.shirt.id, self.shirt_red.id): '1', + '_voucher_code': v.code, }, follow=True) objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event)) self.assertEqual(len(objs), 1) @@ -559,7 +569,8 @@ class CartTest(CartTestMixin, TestCase): def test_voucher_quota_invalid_item(self): v = Voucher.objects.create(quota=self.quota_tickets, event=self.event) self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { - 'variation_%d_%d_voucher' % (self.shirt.id, self.shirt_red.id): v.code, + 'variation_%d_%d' % (self.shirt.id, self.shirt_red.id): '1', + '_voucher_code': v.code, }, follow=True) objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event)) self.assertEqual(len(objs), 0) @@ -567,7 +578,8 @@ class CartTest(CartTestMixin, TestCase): def test_voucher_item_invalid_item(self): v = Voucher.objects.create(item=self.shirt, event=self.event) self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { - 'item_%d_voucher' % self.ticket.id: v.code, + 'itme_%d' % self.ticket.id: '1', + '_voucher_code': v.code, }, follow=True) objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event)) self.assertEqual(len(objs), 0) @@ -575,7 +587,8 @@ class CartTest(CartTestMixin, TestCase): def test_voucher_item_invalid_variation(self): v = Voucher.objects.create(item=self.shirt, variation=self.shirt_blue, event=self.event) self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { - 'variation_%d_%d_voucher' % (self.shirt.id, self.shirt_red.id): v.code, + 'variation_%d_%d' % (self.shirt.id, self.shirt_red.id): '1', + '_voucher_code': v.code, }, follow=True) objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event)) self.assertEqual(len(objs), 0) @@ -583,7 +596,8 @@ class CartTest(CartTestMixin, TestCase): def test_voucher_price(self): v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event) self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { - 'item_%d_voucher' % self.ticket.id: v.code, + 'item_%d' % self.ticket.id: '1', + '_voucher_code': v.code, }, follow=True) objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event)) self.assertEqual(len(objs), 1) @@ -592,9 +606,10 @@ class CartTest(CartTestMixin, TestCase): self.assertEqual(objs[0].price, Decimal('12.00')) def test_voucher_redemed(self): - v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event, redeemed=True) + v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event, redeemed=1) response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { - 'item_%d_voucher' % self.ticket.id: v.code, + 'item_%d' % self.ticket.id: '1', + '_voucher_code': v.code, }, follow=True) doc = BeautifulSoup(response.rendered_content, "lxml") self.assertIn('already been used', doc.select('.alert-danger')[0].text) @@ -604,7 +619,8 @@ class CartTest(CartTestMixin, TestCase): v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event, valid_until=now() - timedelta(days=2)) response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { - 'item_%d_voucher' % self.ticket.id: v.code, + 'item_%d' % self.ticket.id: '1', + '_voucher_code': v.code, }, follow=True) doc = BeautifulSoup(response.rendered_content, "lxml") self.assertIn('expired', doc.select('.alert-danger')[0].text) @@ -612,7 +628,8 @@ class CartTest(CartTestMixin, TestCase): def test_voucher_invalid(self): response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { - 'item_%d_voucher' % self.ticket.id: 'ABC', + 'item_%d' % self.ticket.id: '1', + '_voucher_code': 'ASDFGH', }, follow=True) doc = BeautifulSoup(response.rendered_content, "lxml") self.assertIn('not known', doc.select('.alert-danger')[0].text) @@ -623,7 +640,8 @@ class CartTest(CartTestMixin, TestCase): self.quota_tickets.save() v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event) response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { - 'item_%d_voucher' % self.ticket.id: v.code, + 'item_%d' % self.ticket.id: '1', + '_voucher_code': v.code, }, follow=True) doc = BeautifulSoup(response.rendered_content, "lxml") self.assertIn('no longer available', doc.select('.alert-danger')[0].text) @@ -635,7 +653,8 @@ class CartTest(CartTestMixin, TestCase): v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event, allow_ignore_quota=True) self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { - 'item_%d_voucher' % self.ticket.id: v.code, + 'item_%d' % self.ticket.id: '1', + '_voucher_code': v.code, }, follow=True) objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event)) self.assertEqual(len(objs), 1) @@ -655,7 +674,8 @@ class CartTest(CartTestMixin, TestCase): self.assertIn('no longer available', doc.select('.alert-danger')[0].text) self.assertFalse(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).exists()) response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { - 'item_%d_voucher' % self.ticket.id: v.code, + 'item_%d' % self.ticket.id: '1', + '_voucher_code': v.code, }, follow=True) objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event)) self.assertEqual(len(objs), 1) @@ -666,7 +686,8 @@ class CartTest(CartTestMixin, TestCase): def test_voucher_doubled(self): v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event) response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { - 'item_%d_voucher' % self.ticket.id: v.code, + 'item_%d' % self.ticket.id: '1', + '_voucher_code': v.code, }, follow=True) objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event)) self.assertEqual(len(objs), 1) @@ -675,10 +696,11 @@ class CartTest(CartTestMixin, TestCase): self.assertEqual(objs[0].price, Decimal('12.00')) response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { - 'item_%d_voucher' % self.ticket.id: v.code, + 'item_%d' % self.ticket.id: '1', + '_voucher_code': v.code, }, follow=True) doc = BeautifulSoup(response.rendered_content, "lxml") - self.assertIn('already used', doc.select('.alert-danger')[0].text) + self.assertIn('already been used', doc.select('.alert-danger')[0].text) self.assertEqual(1, CartPosition.objects.filter(cart_id=self.session_key, event=self.event).count()) def test_require_voucher(self): @@ -686,7 +708,8 @@ class CartTest(CartTestMixin, TestCase): self.shirt.require_voucher = True self.shirt.save() self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { - 'variation_%d_%d_voucher' % (self.shirt.id, self.shirt_red.id): v.code, + 'variation_%d_%d' % (self.shirt.id, self.shirt_red.id): '1', + '_voucher_code': v.code }, follow=True) objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event)) self.assertEqual(len(objs), 1) @@ -707,7 +730,8 @@ class CartTest(CartTestMixin, TestCase): quota2.variations.add(self.shirt_red) v = Voucher.objects.create(quota=self.quota_shirts, event=self.event) self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { - 'variation_%d_%d_voucher' % (self.shirt.id, self.shirt_red.id): v.code, + 'variation_%d_%d' % (self.shirt.id, self.shirt_red.id): '1', + '_voucher_code': v.code }, follow=True) self.assertEqual(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).count(), 0) @@ -716,7 +740,8 @@ class CartTest(CartTestMixin, TestCase): self.shirt.hide_without_voucher = True self.shirt.save() self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { - 'variation_%d_%d_voucher' % (self.shirt.id, self.shirt_red.id): v.code, + 'variation_%d_%d' % (self.shirt.id, self.shirt_red.id): '1', + '_voucher_code': v.code }, follow=True) objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event)) self.assertEqual(len(objs), 1) @@ -731,3 +756,123 @@ class CartTest(CartTestMixin, TestCase): }, follow=True) objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event)) self.assertEqual(len(objs), 0) + + def test_voucher_multiuse_ok(self): + v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event, + max_usages=2, redeemed=0) + self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { + 'item_%d' % self.ticket.id: '2', + '_voucher_code': v.code, + }, follow=True) + positions = CartPosition.objects.filter(cart_id=self.session_key, event=self.event) + assert positions.exists() + assert all(cp.voucher == v for cp in positions) + + def test_voucher_multiuse_multiprod_ok(self): + v = Voucher.objects.create(quota=self.quota_all, price=Decimal('12.00'), event=self.event, + max_usages=2, redeemed=0) + self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { + 'item_%d' % self.ticket.id: '1', + 'variation_%d_%d' % (self.shirt.id, self.shirt_red.id): '1', + '_voucher_code': v.code, + }, follow=True) + positions = CartPosition.objects.filter(cart_id=self.session_key, event=self.event) + assert positions.exists() + assert all(cp.voucher == v for cp in positions) + + def test_voucher_multiuse_partially(self): + v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event, + max_usages=2, redeemed=1) + response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { + 'item_%d' % self.ticket.id: '2', + '_voucher_code': v.code, + }, follow=True) + doc = BeautifulSoup(response.rendered_content, "lxml") + self.assertIn('only be redeemed 1 more time', doc.select('.alert-danger')[0].text) + positions = CartPosition.objects.filter(cart_id=self.session_key, event=self.event) + assert not positions.exists() + + def test_voucher_multiuse_multiprod_partially(self): + v = Voucher.objects.create(quota=self.quota_all, price=Decimal('12.00'), event=self.event, + max_usages=2, redeemed=1) + response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { + 'item_%d' % self.ticket.id: '1', + 'variation_%d_%d' % (self.shirt.id, self.shirt_red.id): '1', + '_voucher_code': v.code, + }, follow=True) + doc = BeautifulSoup(response.rendered_content, "lxml") + self.assertIn('already been used', doc.select('.alert-danger')[0].text) + positions = CartPosition.objects.filter(cart_id=self.session_key, event=self.event) + assert positions.count() == 1 + assert all(cp.voucher == v for cp in positions) + + def test_voucher_multiuse_redeemed(self): + v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event, + max_usages=2, redeemed=2) + response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { + 'item_%d' % self.ticket.id: '2', + '_voucher_code': v.code, + }, follow=True) + doc = BeautifulSoup(response.rendered_content, "lxml") + self.assertIn('already been used', doc.select('.alert-danger')[0].text) + positions = CartPosition.objects.filter(cart_id=self.session_key, event=self.event) + assert not positions.exists() + + def test_voucher_multiuse_multiprod_redeemed(self): + v = Voucher.objects.create(quota=self.quota_all, price=Decimal('12.00'), event=self.event, + max_usages=2, redeemed=2) + response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { + 'item_%d' % self.ticket.id: '1', + 'variation_%d_%d' % (self.shirt.id, self.shirt_red.id): '1', + '_voucher_code': v.code, + }, follow=True) + doc = BeautifulSoup(response.rendered_content, "lxml") + self.assertIn('already been used', doc.select('.alert-danger')[0].text) + positions = CartPosition.objects.filter(cart_id=self.session_key, event=self.event) + assert not positions.exists() + + def test_voucher_multiuse_redeemed_in_my_cart(self): + v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event, + max_usages=2, redeemed=1) + CartPosition.objects.create( + expires=now() - timedelta(minutes=10), item=self.ticket, voucher=v, price=Decimal('12.00'), + event=self.event, cart_id=self.session_key + ) + response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { + 'item_%d' % self.ticket.id: '1', + '_voucher_code': v.code, + }, follow=True) + doc = BeautifulSoup(response.rendered_content, "lxml") + self.assertIn('already been used', doc.select('.alert-danger')[0].text) + positions = CartPosition.objects.filter(cart_id=self.session_key, event=self.event) + assert positions.count() == 1 + + def test_voucher_multiuse_redeemed_in_other_cart(self): + v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event, + max_usages=2, redeemed=1) + CartPosition.objects.create( + expires=now() + timedelta(minutes=10), item=self.ticket, voucher=v, price=Decimal('12.00'), + event=self.event, cart_id='other' + ) + response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { + 'item_%d' % self.ticket.id: '1', + '_voucher_code': v.code, + }, follow=True) + doc = BeautifulSoup(response.rendered_content, "lxml") + self.assertIn('already been used', doc.select('.alert-danger')[0].text) + positions = CartPosition.objects.filter(cart_id=self.session_key, event=self.event) + assert not positions.exists() + + def test_voucher_multiuse_redeemed_in_other_expired_cart(self): + v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event, + max_usages=2, redeemed=1) + CartPosition.objects.create( + expires=now() - timedelta(minutes=10), item=self.ticket, voucher=v, price=Decimal('12.00'), + event=self.event, cart_id='other' + ) + self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { + 'item_%d' % self.ticket.id: '1', + '_voucher_code': v.code, + }, follow=True) + positions = CartPosition.objects.filter(cart_id=self.session_key, event=self.event) + assert positions.count() == 1 diff --git a/src/tests/presale/test_checkout.py b/src/tests/presale/test_checkout.py index 734108e47b..2201714974 100644 --- a/src/tests/presale/test_checkout.py +++ b/src/tests/presale/test_checkout.py @@ -309,7 +309,7 @@ class CheckoutTestCase(TestCase): self.assertEqual(Order.objects.count(), 1) self.assertEqual(OrderPosition.objects.count(), 1) self.assertEqual(OrderPosition.objects.first().voucher, v) - self.assertTrue(Voucher.objects.get(pk=v.pk).redeemed) + self.assertEqual(Voucher.objects.get(pk=v.pk).redeemed, 1) def test_voucher_required(self): v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event, @@ -325,7 +325,7 @@ class CheckoutTestCase(TestCase): response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True) doc = BeautifulSoup(response.rendered_content, "lxml") self.assertEqual(len(doc.select(".thank-you")), 1) - self.assertTrue(Voucher.objects.get(pk=v.pk).redeemed) + self.assertEqual(Voucher.objects.get(pk=v.pk).redeemed, 1) def test_voucher_required_but_missing(self): self.ticket.require_voucher = True @@ -369,7 +369,7 @@ class CheckoutTestCase(TestCase): def test_voucher_redeemed(self): v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event, - valid_until=now() + timedelta(days=2), redeemed=True) + valid_until=now() + timedelta(days=2), redeemed=1) CartPosition.objects.create( event=self.event, cart_id=self.session_key, item=self.ticket, price=12, expires=now() - timedelta(minutes=10), voucher=v @@ -379,6 +379,102 @@ class CheckoutTestCase(TestCase): doc = BeautifulSoup(response.rendered_content, "lxml") self.assertIn("has already been", doc.select(".alert-danger")[0].text) + def test_voucher_multiuse_redeemed(self): + v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event, + valid_until=now() + timedelta(days=2), max_usages=3, redeemed=3) + CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, + price=12, expires=now() - timedelta(minutes=10), voucher=v + ) + self._set_session('payment', 'banktransfer') + response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True) + doc = BeautifulSoup(response.rendered_content, "lxml") + self.assertIn("has already been", doc.select(".alert-danger")[0].text) + + def test_voucher_multiuse_partially(self): + v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event, + valid_until=now() + timedelta(days=2), max_usages=3, redeemed=2) + CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, + price=12, expires=now() - timedelta(minutes=10), voucher=v + ) + CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, + price=12, expires=now() - timedelta(minutes=10), voucher=v + ) + self._set_session('payment', 'banktransfer') + response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True) + doc = BeautifulSoup(response.rendered_content, "lxml") + self.assertIn("has already been", doc.select(".alert-danger")[0].text) + assert CartPosition.objects.filter(cart_id=self.session_key).count() == 1 + + def test_voucher_multiuse_ok(self): + v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event, + valid_until=now() + timedelta(days=2), max_usages=3, redeemed=1) + CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, + price=12, expires=now() - timedelta(minutes=10), voucher=v + ) + CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, + price=12, expires=now() - timedelta(minutes=10), voucher=v + ) + self._set_session('payment', 'banktransfer') + response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True) + doc = BeautifulSoup(response.rendered_content, "lxml") + self.assertEqual(len(doc.select(".thank-you")), 1) + self.assertFalse(CartPosition.objects.filter(cart_id=self.session_key).exists()) + self.assertEqual(Order.objects.count(), 1) + self.assertEqual(OrderPosition.objects.count(), 2) + v.refresh_from_db() + assert v.redeemed == 3 + + def test_voucher_multiuse_in_other_cart_expired(self): + v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event, + valid_until=now() + timedelta(days=2), max_usages=3, redeemed=1) + CartPosition.objects.create( + event=self.event, cart_id='other', item=self.ticket, + price=12, expires=now() - timedelta(minutes=10), voucher=v + ) + CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, + price=12, expires=now() - timedelta(minutes=10), voucher=v + ) + CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, + price=12, expires=now() - timedelta(minutes=10), voucher=v + ) + self._set_session('payment', 'banktransfer') + response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True) + doc = BeautifulSoup(response.rendered_content, "lxml") + self.assertEqual(len(doc.select(".thank-you")), 1) + self.assertFalse(CartPosition.objects.filter(cart_id=self.session_key).exists()) + self.assertEqual(Order.objects.count(), 1) + self.assertEqual(OrderPosition.objects.count(), 2) + v.refresh_from_db() + assert v.redeemed == 3 + + def test_voucher_multiuse_in_other_cart(self): + v = Voucher.objects.create(item=self.ticket, price=Decimal('12.00'), event=self.event, + valid_until=now() + timedelta(days=2), max_usages=3, redeemed=1) + CartPosition.objects.create( + event=self.event, cart_id='other', item=self.ticket, + price=12, expires=now() + timedelta(minutes=10), voucher=v + ) + CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, + price=12, expires=now() - timedelta(minutes=10), voucher=v + ) + CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, + price=12, expires=now() - timedelta(minutes=10), voucher=v + ) + self._set_session('payment', 'banktransfer') + response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True) + doc = BeautifulSoup(response.rendered_content, "lxml") + self.assertIn("has already been", doc.select(".alert-danger")[0].text) + assert CartPosition.objects.filter(cart_id=self.session_key).count() == 1 + def test_voucher_ignore_quota(self): self.quota_tickets.size = 0 self.quota_tickets.save() @@ -428,16 +524,16 @@ class CheckoutTestCase(TestCase): q2 = self.event.quotas.create(name='Testquota', size=0) q2.items.add(self.ticket) v = Voucher.objects.create(quota=self.quota_tickets, price=Decimal('12.00'), event=self.event, - valid_until=now() - timedelta(days=2), block_quota=True) + valid_until=now() + timedelta(days=2), block_quota=True) CartPosition.objects.create( event=self.event, cart_id=self.session_key, item=self.ticket, - price=12, expires=now() + timedelta(minutes=10), voucher=v + price=12, expires=now() - timedelta(minutes=10), voucher=v ) self._set_session('payment', 'banktransfer') response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True) doc = BeautifulSoup(response.rendered_content, "lxml") - self.assertEqual(len(doc.select(".alert-danger")), 1) + self.assertTrue(doc.select(".alert-danger")) self.assertFalse(Order.objects.exists()) def test_voucher_double(self): diff --git a/src/tests/presale/test_event.py b/src/tests/presale/test_event.py index d7d1e309ba..a20933b722 100644 --- a/src/tests/presale/test_event.py +++ b/src/tests/presale/test_event.py @@ -305,7 +305,7 @@ class VoucherRedeemItemDisplayTest(EventTestMixin, SoupTest): assert "14.00" not in html.rendered_content def test_fail_redeemed(self): - self.v.redeemed = True + self.v.redeemed = 1 self.v.save() html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, self.v.code), follow=True) assert "alert-danger" in html.rendered_content