API: Writeable methods for vouchers (#639)

This commit is contained in:
Raphael Michel
2017-10-12 14:09:44 +02:00
committed by GitHub
parent de086a2b07
commit be6496e569
11 changed files with 1029 additions and 109 deletions

View File

@@ -8,3 +8,36 @@ class VoucherSerializer(I18nAwareModelSerializer):
fields = ('id', 'code', 'max_usages', 'redeemed', 'valid_until', 'block_quota',
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
'tag', 'comment', 'subevent')
read_only_fields = ('id', 'redeemed')
def validate(self, data):
data = super().validate(data)
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
full_data.update(data)
Voucher.clean_item_properties(
full_data, self.context.get('event'),
full_data.get('quota'), full_data.get('item'), full_data.get('variation')
)
Voucher.clean_subevent(
full_data, self.context.get('event')
)
Voucher.clean_max_usages(full_data, self.instance.redeemed if self.instance else 0)
check_quota = Voucher.clean_quota_needs_checking(
full_data, self.instance,
item_changed=self.instance and (
full_data.get('item') != self.instance.item or
full_data.get('variation') != self.instance.variation or
full_data.get('quota') != self.instance.quota
),
creating=not self.instance
)
if check_quota:
Voucher.clean_quota_check(
full_data, 1, self.instance, self.context.get('event'),
full_data.get('quota'), full_data.get('item'), full_data.get('variation')
)
Voucher.clean_voucher_code(full_data, self.context.get('event'), self.instance.pk if self.instance else None)
return data

View File

@@ -4,10 +4,12 @@ from django_filters.rest_framework import (
BooleanFilter, DjangoFilterBackend, FilterSet,
)
from rest_framework import viewsets
from rest_framework.exceptions import PermissionDenied
from rest_framework.filters import OrderingFilter
from pretix.api.serializers.voucher import VoucherSerializer
from pretix.base.models import Voucher
from pretix.base.models.organizer import TeamAPIToken
class VoucherFilter(FilterSet):
@@ -27,7 +29,7 @@ class VoucherFilter(FilterSet):
(Q(valid_until__isnull=False) & Q(valid_until__lte=now())))
class VoucherViewSet(viewsets.ReadOnlyModelViewSet):
class VoucherViewSet(viewsets.ModelViewSet):
serializer_class = VoucherSerializer
queryset = Voucher.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
@@ -35,6 +37,49 @@ class VoucherViewSet(viewsets.ReadOnlyModelViewSet):
ordering_fields = ('id', 'code', 'max_usages', 'valid_until', 'value')
filter_class = VoucherFilter
permission = 'can_view_vouchers'
write_permission = 'can_change_vouchers'
def get_queryset(self):
return self.request.event.vouchers.all()
def create(self, request, *args, **kwargs):
with request.event.lock():
return super().create(request, *args, **kwargs)
def perform_create(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
'pretix.voucher.added',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
return ctx
def update(self, request, *args, **kwargs):
with request.event.lock():
return super().update(request, *args, **kwargs)
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.log_action(
'pretix.voucher.changed',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
data=self.request.data
)
def perform_destroy(self, instance):
if not instance.allow_delete():
raise PermissionDenied('This voucher can not be deleted as it has already been used.')
instance.log_action(
'pretix.voucher.deleted',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
)
super().perform_destroy(instance)

View File

@@ -4,6 +4,7 @@ from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import MinLengthValidator
from django.db import models
from django.db.models import Q
from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
@@ -180,24 +181,136 @@ class Voucher(LoggedModel):
def __str__(self):
return self.code
def allow_delete(self):
return self.redeemed == 0
def clean(self):
super().clean()
if self.quota:
if self.item:
Voucher.clean_item_properties(
{
'block_quota': self.block_quota,
},
self.event,
self.quota,
self.item,
self.variation
)
@staticmethod
def clean_item_properties(data, event, quota, item, variation):
if quota:
if item:
raise ValidationError(_('You cannot select a quota and a specific product at the same time.'))
elif self.item:
if self.variation and (not self.item or not self.item.has_variations):
elif item:
if variation and (not item or not item.has_variations):
raise ValidationError(_('You cannot select a variation without having selected a product that provides '
'variations.'))
if self.variation and not self.item.variations.filter(pk=self.variation.pk).exists():
if variation and not item.variations.filter(pk=variation.pk).exists():
raise ValidationError(_('This variation does not belong to this product.'))
if self.item.has_variations and not self.variation and self.block_quota:
if item.has_variations and not variation and data.get('block_quota'):
raise ValidationError(_('You can only block quota if you specify a specific product variation. '
'Otherwise it might be unclear which quotas to block.'))
if item.category and item.category.is_addon:
raise ValidationError(_('It is currently not possible to create vouchers for add-on products.'))
else:
raise ValidationError(_('You need to specify either a quota or a product.'))
if self.event.has_subevents and self.block_quota and not self.subevent:
@staticmethod
def clean_max_usages(data, redeemed):
if data.get('max_usages', 1) < 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': redeemed
}
)
@staticmethod
def clean_subevent(data, event):
if event.has_subevents and data.get('block_quota') and not data.get('subevent'):
raise ValidationError(_('If you want this voucher to block quota, you need to select a specific date.'))
elif data.get('subevent') and not event.has_subevents:
raise ValidationError(_('You can not select a subevent if your event is not an event series.'))
@staticmethod
def clean_quota_needs_checking(data, old_instance, item_changed, creating):
# We only need to check for quota on vouchers that are now blocking quota and haven't
# before (or have blocked a different quota before)
if data.get('block_quota', False):
is_valid = data.get('valid_until') is None or data.get('valid_until') >= now()
if not is_valid:
# If the voucher is not valid, it won't block any quota
return False
if creating:
# This is a new voucher
return True
if not old_instance.block_quota:
# Change from nonblocking to blocking
return True
if old_instance.valid_until is not None and old_instance.valid_until < now():
# This voucher has been expired and is now valid again and therefore blocks quota again
return True
if item_changed:
# The voucher has been reassigned to a different item, variation or quota
return True
if data.get('subevent') != old_instance.subevent:
# The voucher has been reassigned to a different subevent
return True
return False
@staticmethod
def clean_quota_get_ignored(old_instance):
quotas = set()
was_valid = old_instance and (
old_instance.valid_until is None or old_instance.valid_until >= now()
)
if old_instance and old_instance.block_quota and was_valid:
if old_instance.quota:
quotas.add(old_instance.quota)
elif old_instance.variation:
quotas |= set(old_instance.variation.quotas.filter(
subevent=old_instance.subevent))
elif old_instance.item:
quotas |= set(old_instance.item.quotas.filter(
subevent=old_instance.subevent))
return quotas
@staticmethod
def clean_quota_check(data, cnt, old_instance, event, quota, item, variation):
old_quotas = Voucher.clean_quota_get_ignored(old_instance)
if event.has_subevents and data.get('block_quota') and not data.get('subevent'):
raise ValidationError(_('If you want this voucher to block quota, you need to select a specific date.'))
if quota:
if quota in old_quotas:
return
else:
avail = quota.availability(count_waitinglist=False)
elif item and item.has_variations and not variation:
raise ValidationError(_('You can only block quota if you specify a specific product variation. '
'Otherwise it might be unclear which quotas to block.'))
elif item and variation:
avail = variation.check_quotas(ignored_quotas=old_quotas, subevent=data.get('subevent'))
elif item and not item.has_variations:
avail = item.check_quotas(ignored_quotas=old_quotas, subevent=data.get('subevent'))
else:
raise ValidationError(_('You need to specify either a quota or a product.'))
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < cnt):
raise ValidationError(_('You cannot create a voucher that blocks quota as the selected product or '
'quota is currently sold out or completely reserved.'))
@staticmethod
def clean_voucher_code(data, event, pk):
if 'code' in data and Voucher.objects.filter(Q(code=data['code']) & Q(event=event) & ~Q(pk=pk)).exists():
raise ValidationError(_('A voucher with this code already exists.'))
def save(self, *args, **kwargs):
self.code = self.code.upper()

View File

@@ -2,9 +2,7 @@ import copy
from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django.utils.translation import ugettext_lazy as _
from pretix.base.forms import I18nModelForm
from pretix.base.models import Item, ItemVariation, Quota, Voucher
@@ -92,113 +90,41 @@ class VoucherForm(I18nModelForm):
self.instance.variation = None
self.instance.quota = None
if self.instance.item.category and self.instance.item.category.is_addon:
raise ValidationError(_('It is currently not possible to create vouchers for add-on products.'))
else:
self.instance.quota = Quota.objects.get(pk=quotaid, event=self.instance.event)
self.instance.item = None
self.instance.variation = None
if data.get('max_usages', 0) < 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']) * data['max_usages']
else:
cnt = data['max_usages']
if self.instance.event.has_subevents and data['block_quota'] and not data.get('subevent'):
raise ValidationError(pgettext_lazy(
'subevent',
'If you want this voucher to block quota, you need to select a specific date.'
))
if self._clean_quota_needs_checking(data):
self._clean_quota_check(data, cnt)
if 'code' in data and Voucher.objects.filter(Q(code=data['code']) & Q(event=self.instance.event) & ~Q(pk=self.instance.pk)).exists():
raise ValidationError(_('A voucher with this code already exists.'))
Voucher.clean_item_properties(
data, self.instance.event,
self.instance.quota, self.instance.item, self.instance.variation
)
Voucher.clean_subevent(
data, self.instance.event
)
Voucher.clean_max_usages(data, self.instance.redeemed)
check_quota = Voucher.clean_quota_needs_checking(
data, self.initial_instance_data,
item_changed=data.get('itemvar') != self.initial.get('itemvar'),
creating=not self.instance.pk
)
if check_quota:
Voucher.clean_quota_check(
data, cnt, self.initial_instance_data, self.instance.event,
self.instance.quota, self.instance.item, self.instance.variation
)
Voucher.clean_voucher_code(data, self.instance.event, self.instance.pk)
voucher_form_validation.send(sender=self.instance.event, form=self, data=data)
return data
def _clean_quota_needs_checking(self, data):
# We only need to check for quota on vouchers that are now blocking quota and haven't
# before (or have blocked a different quota before)
if data.get('block_quota', False):
is_valid = data.get('valid_until') is None or data.get('valid_until') >= now()
if not is_valid:
# If the voucher is not valid, it won't block any quota
return False
if not self.instance.pk:
# This is a new voucher
return True
if not self.initial_instance_data.block_quota:
# Change from nonblocking to blocking
return True
if not self._clean_was_valid():
# This voucher has been expired and is now valid again and therefore blocks quota again
return True
if data.get('itemvar') != self.initial.get('itemvar'):
# The voucher has been reassigned to a different item, variation or quota
return True
if data.get('subevent') != self.initial.get('subevent'):
# The voucher has been reassigned to a different subevent
return True
return False
def _clean_was_valid(self):
return self.initial_instance_data.valid_until is None or self.initial_instance_data.valid_until >= now()
def _clean_quota_get_ignored(self):
quotas = set()
if self.initial_instance_data and self.initial_instance_data.block_quota and self._clean_was_valid():
if self.initial_instance_data.quota:
quotas.add(self.initial_instance_data.quota)
elif self.initial_instance_data.variation:
quotas |= set(self.initial_instance_data.variation.quotas.filter(
subevent=self.initial_instance_data.subevent))
elif self.initial_instance_data.item:
quotas |= set(self.initial_instance_data.item.quotas.filter(
subevent=self.initial_instance_data.subevent))
return quotas
def _clean_quota_check(self, data, cnt):
old_quotas = self._clean_quota_get_ignored()
if self.instance.quota:
if self.instance.quota in old_quotas:
return
else:
avail = self.instance.quota.availability(count_waitinglist=False)
elif self.instance.item and self.instance.item.has_variations and not self.instance.variation:
raise ValidationError(_('You can only block quota if you specify a specific product variation. '
'Otherwise it might be unclear which quotas to block.'))
elif self.instance.item and self.instance.variation:
avail = self.instance.variation.check_quotas(ignored_quotas=old_quotas, subevent=data.get('subevent'))
elif self.instance.item and not self.instance.item.has_variations:
avail = self.instance.item.check_quotas(ignored_quotas=old_quotas, subevent=data.get('subevent'))
else:
raise ValidationError(_('You need to specify either a quota or a product.'))
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < cnt):
raise ValidationError(_('You cannot create a voucher that blocks quota as the selected product or '
'quota is currently sold out or completely reserved.'))
def save(self, commit=True):
super().save(commit)

View File

@@ -126,7 +126,7 @@ class VoucherDelete(EventPermissionRequiredMixin, DeleteView):
raise Http404(_("The requested voucher does not exist."))
def get(self, request, *args, **kwargs):
if self.get_object().redeemed > 0:
if not self.get_object().allow_delete():
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)
@@ -136,7 +136,7 @@ class VoucherDelete(EventPermissionRequiredMixin, DeleteView):
self.object = self.get_object()
success_url = self.get_success_url()
if self.object.redeemed > 0:
if not self.object.allow_delete():
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)