forked from CGM_Public/pretix_original
Refs #145 -- Multi-use vouchers
This commit is contained in:
38
src/pretix/base/migrations/0047_auto_20161126_1300.py
Normal file
38
src/pretix/base/migrations/0047_auto_20161126_1300.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" %}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user