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,
verbose_name=_("Can change orders")
)
can_change_vouchers = models.BooleanField(
default=True,
verbose_name=_("Can change vouchers")
)
class Meta:
verbose_name = _("Event permission")

View File

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

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

@@ -4,7 +4,7 @@
{% block nav %}
<li>
<a href="{% url 'control:event.index' organizer=request.event.organizer.slug event=request.event.slug %}"
{% if url_name == "event.index" %}class="active"{% endif %}>
{% if url_name == "event.index" %}class="active"{% endif %}>
<i class="fa fa-dashboard fa-fw"></i>
{% trans "Dashboard" %}
</a>
@@ -52,7 +52,7 @@
{% if request.eventperm.can_change_permissions %}
<li>
<a href="{% url 'control:event.settings.permissions' organizer=request.event.organizer.slug event=request.event.slug %}"
{% if "event.settings.permissions" == url_name %}class="active"{% endif %}>
{% if "event.settings.permissions" == url_name %}class="active"{% endif %}>
{% trans "Permissions" %}
</a>
</li>
@@ -110,19 +110,28 @@
</li>
<li>
<a href="{% url 'control:event.orders.overview' organizer=request.event.organizer.slug event=request.event.slug %}"
{% if url_name == "event.orders.overview" %}class="active"{% endif %}>
{% if url_name == "event.orders.overview" %}class="active"{% endif %}>
{% trans "Overview" %}
</a>
</li>
<li>
<a href="{% url 'control:event.orders.export' organizer=request.event.organizer.slug event=request.event.slug %}"
{% if url_name == "event.orders.export" %}class="active"{% endif %}>
{% if url_name == "event.orders.export" %}class="active"{% endif %}>
{% trans "Export" %}
</a>
</li>
</ul>
</li>
{% 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 %}
<li>
<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 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<quota>\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<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(),
name='event.order.transition'),
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