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 %}
  • + {% if url_name == "event.index" %}class="active"{% endif %}> {% trans "Dashboard" %} @@ -52,7 +52,7 @@ {% if request.eventperm.can_change_permissions %}
  • + {% if "event.settings.permissions" == url_name %}class="active"{% endif %}> {% trans "Permissions" %}
  • @@ -110,19 +110,28 @@
  • + {% if url_name == "event.orders.overview" %}class="active"{% endif %}> {% trans "Overview" %}
  • + {% if url_name == "event.orders.export" %}class="active"{% endif %}> {% trans "Export" %}
  • {% endif %} + {% if request.eventperm.can_change_vouchers %} +
  • + + + {% trans "Vouchers" %} + +
  • + {% endif %} {% for nav in nav_event %}
  • diff --git a/src/pretix/control/templates/pretixcontrol/vouchers/delete.html b/src/pretix/control/templates/pretixcontrol/vouchers/delete.html new file mode 100644 index 000000000..9bc41a299 --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/vouchers/delete.html @@ -0,0 +1,26 @@ +{% extends "pretixcontrol/items/base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% block title %}{% trans "Delete voucher" %}{% endblock %} +{% block inside %} +

    {% trans "Delete voucher" %}

    +
    + {% csrf_token %} + {% if not allowed %} +

    {% trans "You can not delete this voucher after it has been redeemed" %}

    + {% else %} +

    {% blocktrans %}Are you sure you want to delete the voucher + {{ voucher }}?{% endblocktrans %}

    + {% endif %} +
    + + {% trans "Cancel" %} + + {% if allowed %} + + {% endif %} +
    +
    +{% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/vouchers/detail.html b/src/pretix/control/templates/pretixcontrol/vouchers/detail.html new file mode 100644 index 000000000..28ad8fa71 --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/vouchers/detail.html @@ -0,0 +1,25 @@ +{% extends "pretixcontrol/items/base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% block title %}{% trans "Voucher" %}{% endblock %} +{% block inside %} +

    {% trans "Voucher" %}

    +
    + {% csrf_token %} + {% bootstrap_form_errors form %} +
    + {% trans "Voucher details" %} + {% bootstrap_field form.code layout="horizontal" %} + {% bootstrap_field form.valid_until layout="horizontal" %} + {% bootstrap_field form.block_quota layout="horizontal" %} + {% bootstrap_field form.allow_ignore_quota layout="horizontal" %} + {% bootstrap_field form.price layout="horizontal" %} + {% bootstrap_field form.item layout="horizontal" %} +
    +
    + +
    +
    +{% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/vouchers/index.html b/src/pretix/control/templates/pretixcontrol/vouchers/index.html new file mode 100644 index 000000000..b6f05c359 --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/vouchers/index.html @@ -0,0 +1,40 @@ +{% extends "pretixcontrol/items/base.html" %} +{% load i18n %} +{% block title %}{% trans "Vouchers" %}{% endblock %} +{% block inside %} +

    {% trans "Vouchers" %}

    +

    + {% trans "Create a new voucher" %} +

    +
    + + + + + + + + + + + + {% for v in vouchers %} + + + + + + + + {% endfor %} + +
    {% 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 }} + +
    +
    + {% include "pretixcontrol/pagination.html" %} +{% endblock %} diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index 8f7f2da2c..34b562734 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -1,7 +1,7 @@ from django.conf.urls import include, url from pretix.control.views import ( - auth, event, item, main, orders, organizer, user, + auth, event, item, main, orders, organizer, user, vouchers, ) urlpatterns = [ @@ -54,6 +54,11 @@ urlpatterns = [ url(r'^quotas/(?P\d+)/delete$', item.QuotaDelete.as_view(), name='event.items.quotas.delete'), url(r'^quotas/add$', item.QuotaCreate.as_view(), name='event.items.quotas.add'), + url(r'^vouchers/$', vouchers.VoucherList.as_view(), name='event.vouchers'), + url(r'^vouchers/(?P\d+)/$', vouchers.VoucherUpdate.as_view(), name='event.voucher'), + url(r'^vouchers/(?P\d+)/delete$', vouchers.VoucherDelete.as_view(), + name='event.voucher.delete'), + url(r'^vouchers/add$', vouchers.VoucherCreate.as_view(), name='event.vouchers.add'), url(r'^orders/(?P[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