UI for creating and chacnging vouchers

This commit is contained in:
Raphael Michel
2016-02-09 11:47:16 +01:00
parent 61d3954a13
commit 6e22149a21
11 changed files with 323 additions and 13 deletions

View File

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

View File

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

View File

@@ -237,6 +237,10 @@ class EventPermission(models.Model):
default=True, default=True,
verbose_name=_("Can change orders") verbose_name=_("Can change orders")
) )
can_change_vouchers = models.BooleanField(
default=True,
verbose_name=_("Can change vouchers")
)
class Meta: class Meta:
verbose_name = _("Event permission") verbose_name = _("Event permission")

View File

@@ -1,12 +1,23 @@
import random
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from .base import LoggedModel
from .event import Event from .event import Event
from .items import Item from .items import Item
from .orders import CartPosition, OrderPosition 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 = models.ForeignKey(
Event, Event,
on_delete=models.CASCADE, on_delete=models.CASCADE,
@@ -15,7 +26,7 @@ class Voucher(models.Model):
) )
code = models.CharField( code = models.CharField(
verbose_name=_("Voucher code"), verbose_name=_("Voucher code"),
max_length=255 max_length=255, default=generate_code
) )
valid_until = models.DateTimeField( valid_until = models.DateTimeField(
blank=True, null=True, blank=True, null=True,
@@ -38,9 +49,10 @@ class Voucher(models.Model):
) )
price = models.DecimalField( price = models.DecimalField(
verbose_name=_("Set product price to"), 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', Item, related_name='vouchers',
verbose_name=_("Product"), verbose_name=_("Product"),
help_text=_( help_text=_(
@@ -53,16 +65,19 @@ class Voucher(models.Model):
verbose_name_plural = _("Vouchers") verbose_name_plural = _("Vouchers")
unique_together = (("event", "code"),) unique_together = (("event", "code"),)
def __str__(self):
return self.code
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.code = self.code.upper() self.code = self.code.upper()
super().save(*args, **kwargs) super().save(*args, **kwargs)
def is_ordered(self) -> int: def is_ordered(self) -> int:
return OrderPosition.objects.current.filter( return OrderPosition.objects.filter(
voucher=self.voucher voucher=self
).exists() ).exists()
def is_in_cart(self) -> int: def is_in_cart(self) -> int:
return CartPosition.objects.current.filter( return CartPosition.objects.filter(
voucher=self.voucher voucher=self
).count() ).count()

View File

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

View File

@@ -123,6 +123,15 @@
</ul> </ul>
</li> </li>
{% endif %} {% endif %}
{% if request.eventperm.can_change_vouchers %}
<li>
<a href="{% url 'control:event.vouchers' organizer=request.event.organizer.slug event=request.event.slug %}"
{% if url_name == "event.vouchers" %}class="active"{% endif %}>
<i class="fa fa-tags fa-fw"></i>
{% trans "Vouchers" %}
</a>
</li>
{% endif %}
{% for nav in nav_event %} {% for nav in nav_event %}
<li> <li>
<a href="{{ nav.url }}" {% if nav.active %}class="active"{% endif %}> <a href="{{ nav.url }}" {% if nav.active %}class="active"{% endif %}>

View File

@@ -0,0 +1,26 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Delete voucher" %}{% endblock %}
{% block inside %}
<h1>{% trans "Delete voucher" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% if not allowed %}
<p>{% trans "You can not delete this voucher after it has been redeemed" %}</p>
{% else %}
<p>{% blocktrans %}Are you sure you want to delete the voucher
<strong>{{ voucher }}</strong>?{% endblocktrans %}</p>
{% endif %}
<div class="form-group submit-group">
<a href="{% url "control:event.vouchers" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
{% if allowed %}
<button type="submit" class="btn btn-danger btn-save">
{% trans "Delete" %}
</button>
{% endif %}
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,25 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Voucher" %}{% endblock %}
{% block inside %}
<h1>{% trans "Voucher" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_form_errors form %}
<fieldset>
<legend>{% trans "Voucher details" %}</legend>
{% 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" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,40 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% block title %}{% trans "Vouchers" %}{% endblock %}
{% block inside %}
<h1>{% trans "Vouchers" %}</h1>
<p>
<a href="{% url "control:event.vouchers.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new voucher" %}</a>
</p>
<div class="table-responsive">
<table class="table table-hover table-quotas">
<thead>
<tr>
<th>{% trans "Voucher code" %}</th>
<th>{% trans "Is redeemed" %}</th>
<th>{% trans "Expiry" %}</th>
<th>{% trans "Product" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for v in vouchers %}
<tr>
<td>
<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.is_ordered %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %}</td>
<td>{{ v.valid_until|date }}</td>
<td>{{ v.item }}</td>
<td class="text-right">
<a href="{% url "control:event.voucher.delete" organizer=request.event.organizer.slug event=request.event.slug voucher=v.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% include "pretixcontrol/pagination.html" %}
{% endblock %}

View File

@@ -1,7 +1,7 @@
from django.conf.urls import include, url from django.conf.urls import include, url
from pretix.control.views import ( from pretix.control.views import (
auth, event, item, main, orders, organizer, user, auth, event, item, main, orders, organizer, user, vouchers,
) )
urlpatterns = [ urlpatterns = [
@@ -54,6 +54,11 @@ urlpatterns = [
url(r'^quotas/(?P<quota>\d+)/delete$', item.QuotaDelete.as_view(), url(r'^quotas/(?P<quota>\d+)/delete$', item.QuotaDelete.as_view(),
name='event.items.quotas.delete'), name='event.items.quotas.delete'),
url(r'^quotas/add$', item.QuotaCreate.as_view(), name='event.items.quotas.add'), 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<voucher>\d+)/$', vouchers.VoucherUpdate.as_view(), name='event.voucher'),
url(r'^vouchers/(?P<voucher>\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<code>[0-9A-Z]+)/transition$', orders.OrderTransition.as_view(), url(r'^orders/(?P<code>[0-9A-Z]+)/transition$', orders.OrderTransition.as_view(),
name='event.order.transition'), name='event.order.transition'),
url(r'^orders/(?P<code>[0-9A-Z]+)/extend$', orders.OrderExtend.as_view(), url(r'^orders/(?P<code>[0-9A-Z]+)/extend$', orders.OrderExtend.as_view(),

View File

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