diff --git a/src/pretix/base/migrations/0003_eventpermission_can_change_vouchers.py b/src/pretix/base/migrations/0003_eventpermission_can_change_vouchers.py new file mode 100644 index 000000000..ceb3c14cc --- /dev/null +++ b/src/pretix/base/migrations/0003_eventpermission_can_change_vouchers.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2016-02-09 09:59 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0002_auto_20160209_0940'), + ] + + operations = [ + migrations.AddField( + model_name='eventpermission', + name='can_change_vouchers', + field=models.BooleanField(default=True, verbose_name='Can change vouchers'), + ), + ] diff --git a/src/pretix/base/migrations/0004_auto_20160209_1023.py b/src/pretix/base/migrations/0004_auto_20160209_1023.py new file mode 100644 index 000000000..5bda71ae8 --- /dev/null +++ b/src/pretix/base/migrations/0004_auto_20160209_1023.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2016-02-09 10:23 +from __future__ import unicode_literals + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0003_eventpermission_can_change_vouchers'), + ] + + operations = [ + migrations.RemoveField( + model_name='voucher', + name='item', + ), + migrations.AddField( + model_name='voucher', + name='item', + field=models.ForeignKey(default=None, help_text="This product is added to the user's cart if the voucher is redeemed.", on_delete=django.db.models.deletion.CASCADE, related_name='vouchers', to='pretixbase.Item', verbose_name='Product'), + preserve_default=False, + ), + migrations.AlterField( + model_name='voucher', + name='price', + field=models.DecimalField(blank=True, decimal_places=2, help_text='If empty, the product will cost its normal price.', max_digits=10, null=True, verbose_name='Set product price to'), + ), + ] diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index ca04bfd60..01120e88c 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -237,6 +237,10 @@ class EventPermission(models.Model): default=True, verbose_name=_("Can change orders") ) + can_change_vouchers = models.BooleanField( + default=True, + verbose_name=_("Can change vouchers") + ) class Meta: verbose_name = _("Event permission") diff --git a/src/pretix/base/models/vouchers.py b/src/pretix/base/models/vouchers.py index 55f56b4cc..e9909eb9f 100644 --- a/src/pretix/base/models/vouchers.py +++ b/src/pretix/base/models/vouchers.py @@ -1,12 +1,23 @@ +import random + from django.db import models from django.utils.translation import ugettext_lazy as _ +from .base import LoggedModel from .event import Event from .items import Item from .orders import CartPosition, OrderPosition -class Voucher(models.Model): +def generate_code(): + charset = list('ABCDEFGHKLMNPQRSTUVWXYZ23456789') + while True: + code = "".join([random.choice(charset) for i in range(16)]) + if not Voucher.objects.filter(code=code).exists(): + return code + + +class Voucher(LoggedModel): event = models.ForeignKey( Event, on_delete=models.CASCADE, @@ -15,7 +26,7 @@ class Voucher(models.Model): ) code = models.CharField( verbose_name=_("Voucher code"), - max_length=255 + max_length=255, default=generate_code ) valid_until = models.DateTimeField( blank=True, null=True, @@ -38,9 +49,10 @@ class Voucher(models.Model): ) price = models.DecimalField( verbose_name=_("Set product price to"), - decimal_places=2, max_digits=10, null=True, blank=True + decimal_places=2, max_digits=10, null=True, blank=True, + help_text=_('If empty, the product will cost its normal price.') ) - item = models.ManyToManyField( + item = models.ForeignKey( Item, related_name='vouchers', verbose_name=_("Product"), help_text=_( @@ -53,16 +65,19 @@ class Voucher(models.Model): verbose_name_plural = _("Vouchers") unique_together = (("event", "code"),) + def __str__(self): + return self.code + def save(self, *args, **kwargs): self.code = self.code.upper() super().save(*args, **kwargs) def is_ordered(self) -> int: - return OrderPosition.objects.current.filter( - voucher=self.voucher + return OrderPosition.objects.filter( + voucher=self ).exists() def is_in_cart(self) -> int: - return CartPosition.objects.current.filter( - voucher=self.voucher + return CartPosition.objects.filter( + voucher=self ).count() diff --git a/src/pretix/control/forms/vouchers.py b/src/pretix/control/forms/vouchers.py new file mode 100644 index 000000000..9e11e6f3b --- /dev/null +++ b/src/pretix/control/forms/vouchers.py @@ -0,0 +1,14 @@ +from pretix.base.forms import I18nModelForm +from pretix.base.models import Voucher + + +class VoucherForm(I18nModelForm): + class Meta: + model = Voucher + localized_fields = '__all__' + fields = [ + 'code', 'valid_until', 'block_quota', 'allow_ignore_quota', 'price', 'item' + ] + + def _get_validation_exclusions(self): + return [] diff --git a/src/pretix/control/templates/pretixcontrol/event/base.html b/src/pretix/control/templates/pretixcontrol/event/base.html index bfc8278f1..2b5cbb71f 100644 --- a/src/pretix/control/templates/pretixcontrol/event/base.html +++ b/src/pretix/control/templates/pretixcontrol/event/base.html @@ -4,7 +4,7 @@ {% block nav %}
+ {% trans "Create a new voucher" %} +
+| {% trans "Voucher code" %} | +{% trans "Is redeemed" %} | +{% trans "Expiry" %} | +{% trans "Product" %} | ++ |
|---|---|---|---|---|
| + {{ v.code }} + | +{% if v.is_ordered %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %} | +{{ v.valid_until|date }} | +{{ v.item }} | ++ + | +
[0-9A-Z]+)/transition$', orders.OrderTransition.as_view(),
name='event.order.transition'),
url(r'^orders/(?P[0-9A-Z]+)/extend$', orders.OrderExtend.as_view(),
diff --git a/src/pretix/control/views/vouchers.py b/src/pretix/control/views/vouchers.py
new file mode 100644
index 000000000..073389ebe
--- /dev/null
+++ b/src/pretix/control/views/vouchers.py
@@ -0,0 +1,121 @@
+from django.contrib import messages
+from django.core.urlresolvers import resolve, reverse
+from django.db import transaction
+from django.http import Http404, HttpResponseRedirect
+from django.utils.translation import ugettext_lazy as _
+from django.views.generic import CreateView, DeleteView, ListView, UpdateView
+
+from pretix.base.models import Voucher
+from pretix.control.forms.vouchers import VoucherForm
+from pretix.control.permissions import EventPermissionRequiredMixin
+
+
+class VoucherList(EventPermissionRequiredMixin, ListView):
+ model = Voucher
+ context_object_name = 'vouchers'
+ paginate_by = 30
+ template_name = 'pretixcontrol/vouchers/index.html'
+ permission = 'can_change_vouchers'
+
+ def get_queryset(self):
+ return self.request.event.vouchers.all().select_related('item')
+
+
+class VoucherDelete(EventPermissionRequiredMixin, DeleteView):
+ model = Voucher
+ template_name = 'pretixcontrol/vouchers/delete.html'
+ permission = 'can_change_vouchers'
+ context_object_name = 'voucher'
+
+ def get_object(self, queryset=None) -> Voucher:
+ try:
+ return self.request.event.vouchers.get(
+ id=self.kwargs['voucher']
+ )
+ except Voucher.DoesNotExist:
+ raise Http404(_("The requested voucher does not exist."))
+
+ def get_context_data(self, *args, **kwargs) -> dict:
+ context = super().get_context_data(*args, **kwargs)
+ context['allowed'] = not self.get_object().is_ordered()
+ return context
+
+ @transaction.atomic()
+ def delete(self, request, *args, **kwargs):
+ self.object = self.get_object()
+ success_url = self.get_success_url()
+
+ if self.object.is_ordered():
+ 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)
+ self.object.delete()
+ messages.success(request, _('The selected voucher has been deleted.'))
+ return HttpResponseRedirect(success_url)
+
+ def get_success_url(self) -> str:
+ return reverse('control:event.vouchers', kwargs={
+ 'organizer': self.request.event.organizer.slug,
+ 'event': self.request.event.slug,
+ })
+
+
+class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
+ model = Voucher
+ form_class = VoucherForm
+ template_name = 'pretixcontrol/vouchers/detail.html'
+ permission = 'can_change_vouchers'
+ context_object_name = 'voucher'
+
+ def get_object(self, queryset=None) -> VoucherForm:
+ url = resolve(self.request.path_info)
+ try:
+ return self.request.event.vouchers.get(
+ id=url.kwargs['voucher']
+ )
+ except Voucher.DoesNotExist:
+ raise Http404(_("The requested voucher does not exist."))
+
+ @transaction.atomic()
+ def form_valid(self, form):
+ messages.success(self.request, _('Your changes have been saved.'))
+ if form.has_changed():
+ self.object.log_action(
+ 'pretix.voucher.changed', user=self.request.user, data={
+ k: form.cleaned_data.get(k) for k in form.changed_data
+ }
+ )
+ return super().form_valid(form)
+
+ def get_success_url(self) -> str:
+ return reverse('control:event.vouchers', kwargs={
+ 'organizer': self.request.event.organizer.slug,
+ 'event': self.request.event.slug,
+ })
+
+
+class VoucherCreate(EventPermissionRequiredMixin, CreateView):
+ model = Voucher
+ form_class = VoucherForm
+ template_name = 'pretixcontrol/vouchers/detail.html'
+ permission = 'can_change_vouchers'
+ context_object_name = 'voucher'
+
+ def get_success_url(self) -> str:
+ return reverse('control:event.vouchers', kwargs={
+ 'organizer': self.request.event.organizer.slug,
+ 'event': self.request.event.slug,
+ })
+
+ def get_form_kwargs(self):
+ kwargs = super().get_form_kwargs()
+ kwargs['instance'] = Voucher(event=self.request.event)
+ return kwargs
+
+ @transaction.atomic()
+ def form_valid(self, form):
+ form.instance.event = self.request.event
+ messages.success(self.request, _('The new voucher has been created.'))
+ ret = super().form_valid(form)
+ form.instance.log_action('pretix.voucher.added', data=dict(form.cleaned_data), user=self.request.user)
+ return ret