Refs #145 -- Multi-use vouchers

This commit is contained in:
Raphael Michel
2016-11-27 00:02:28 +01:00
parent 6c2ecd153c
commit db6fb51fc6
18 changed files with 470 additions and 104 deletions

View File

@@ -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'),
),
]

View File

@@ -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)
)
)

View File

@@ -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

View File

@@ -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

View File

@@ -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']

View File

@@ -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

View File

@@ -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()

View File

@@ -26,6 +26,7 @@
</div>
</div>
{% bootstrap_field form.codes layout="horizontal" %}
{% bootstrap_field form.max_usages layout="horizontal" %}
</fieldset>
<fieldset>
<legend>{% trans "Voucher details" %}</legend>

View File

@@ -23,6 +23,7 @@
<fieldset>
<legend>{% trans "Voucher details" %}</legend>
{% 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" %}

View File

@@ -54,7 +54,7 @@
<thead>
<tr>
<th>{% trans "Voucher code" %}</th>
<th>{% trans "Is redeemed" %}</th>
<th>{% trans "Redemptions" %}</th>
<th>{% trans "Expiry" %}</th>
<th>{% trans "Tag" %}</th>
<th>{% trans "Product" %}</th>
@@ -68,7 +68,7 @@
<strong><a href="
{% url "control:event.voucher" organizer=request.event.organizer.slug event=request.event.slug voucher=v.id %}">{{ v.code }}</a></strong>
</td>
<td>{% if v.redeemed %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %}</td>
<td>{{ v.redeemed }} / {{ v.max_usages }}</td>
<td>{{ v.valid_until|date }}</td>
<td>
{{ v.tag }}

View File

@@ -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)

View File

@@ -79,9 +79,14 @@
{% if var.cached_availability.0 == 100 %}
<div class="col-md-2 col-xs-6 availability-box available radio-box">
<label>
<input type="radio" name="_voucher_item"
{% if options == 1 %}checked="checked"{% endif %}
value="variation_{{ item.id }}_{{ var.id }}">
{% if max_times > 1 %}
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
max="{{ item.order_max }}" name="variation_{{ item.id }}_{{ var.id }}">
{% else %}
<input type="radio" name="_voucher_item"
{% if options == 1 %}checked="checked"{% endif %}
value="variation_{{ item.id }}_{{ var.id }}">
{% endif %}
</label>
</div>
{% else %}
@@ -128,9 +133,14 @@
{% if item.cached_availability.0 == 100 %}
<div class="col-md-2 col-xs-6 availability-box available radio-box">
<label>
<input type="radio" name="_voucher_item"
{% if options == 1 %}checked="checked"{% endif %}
value="item_{{ item.id }}">
{% if max_times > 1 %}
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
max="{{ item.order_max }}" name="item_{{ item.id }}">
{% else %}
<input type="radio" name="_voucher_item"
{% if options == 1 %}checked="checked"{% endif %}
value="item_{{ item.id }}">
{% endif %}
</label>
</div>
{% else %}

View File

@@ -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:

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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):

View File

@@ -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