diff --git a/src/pretix/control/forms/vouchers.py b/src/pretix/control/forms/vouchers.py
index 63f8d3069e..3bff507591 100644
--- a/src/pretix/control/forms/vouchers.py
+++ b/src/pretix/control/forms/vouchers.py
@@ -40,6 +40,7 @@ from django import forms
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.validators import EmailValidator
from django.db.models.functions import Upper
+from django.forms.utils import ErrorDict
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django_scopes.forms import SafeModelChoiceField
@@ -265,6 +266,49 @@ class VoucherForm(I18nModelForm):
return super().save(commit)
+class VoucherBulkEditForm(VoucherForm):
+ # TODO: clean quota changes!
+
+ def __init__(self, *args, **kwargs):
+ self.mixed_values = kwargs.pop('mixed_values')
+ self.queryset = kwargs.pop('queryset')
+ super().__init__(**kwargs)
+
+ def save(self, commit=True):
+ objs = list(self.queryset)
+ fields = set()
+
+ for k in self.fields:
+ cb_val = self.prefix + k
+ if cb_val not in self.data.getlist('_bulk'):
+ continue
+
+ fields.add(k)
+ for obj in objs:
+ if k == 'itemvar':
+ selected_items = set(list(self.event.items.filter(id__in=[
+ i.split('-')[0] for i in self.cleaned_data['itemvars']
+ ])))
+ selected_variations = list(ItemVariation.objects.filter(item__event=self.event, id__in=[
+ i.split('-')[1] for i in self.cleaned_data['itemvars'] if '-' in i
+ ]))
+ obj.items.set(selected_items)
+ obj.variations.set(selected_variations)
+ else:
+ setattr(obj, k, self.cleaned_data[k])
+
+ fields = [f for f in fields if f != 'itemvars']
+ if fields:
+ Voucher.objects.bulk_update(objs, fields, 200)
+
+ def full_clean(self):
+ if len(self.data) == 0:
+ # form wasn't submitted
+ self._errors = ErrorDict()
+ return
+ super().full_clean()
+
+
class VoucherBulkForm(VoucherForm):
codes = forms.CharField(
widget=forms.Textarea,
diff --git a/src/pretix/control/templates/pretixcontrol/vouchers/index.html b/src/pretix/control/templates/pretixcontrol/vouchers/index.html
index 22b6828863..8894b5d9a5 100644
--- a/src/pretix/control/templates/pretixcontrol/vouchers/index.html
+++ b/src/pretix/control/templates/pretixcontrol/vouchers/index.html
@@ -223,6 +223,10 @@
{% trans "Delete selected" %}
+
{% endif %}
diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py
index 35c659fd37..48cb9d0834 100644
--- a/src/pretix/control/urls.py
+++ b/src/pretix/control/urls.py
@@ -379,6 +379,7 @@ urlpatterns = [
re_path(r'^vouchers/bulk_add$', vouchers.VoucherBulkCreate.as_view(), name='event.vouchers.bulk'),
re_path(r'^vouchers/bulk_add/mail_preview$', vouchers.VoucherBulkMailPreview.as_view(), name='event.vouchers.bulk.mail_preview'),
re_path(r'^vouchers/bulk_action$', vouchers.VoucherBulkAction.as_view(), name='event.vouchers.bulkaction'),
+ #re_path(r'^vouchers/bulk_edit$', vouchers.VoucherBulkUpdateView.as_view(), name='event.vouchers.bulkedit'),
re_path(r'^vouchers/import/$', modelimport.VoucherImportView.as_view(), name='event.vouchers.import'),
re_path(r'^vouchers/import/(?P[^/]+)/$', modelimport.VoucherProcessView.as_view(), name='event.vouchers.import.process'),
re_path(r'^orders/(?P[0-9A-Z]+)/transition$', orders.OrderTransition.as_view(),
diff --git a/src/pretix/control/views/vouchers.py b/src/pretix/control/views/vouchers.py
index 607a08114b..0081dff710 100644
--- a/src/pretix/control/views/vouchers.py
+++ b/src/pretix/control/views/vouchers.py
@@ -42,7 +42,7 @@ from django.conf import settings
from django.contrib import messages
from django.core.exceptions import PermissionDenied, ValidationError
from django.db import connection, transaction
-from django.db.models import Exists, OuterRef, Sum
+from django.db.models import Exists, OuterRef, Sum, Subquery
from django.http import (
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect,
JsonResponse,
@@ -55,7 +55,7 @@ from django.utils.safestring import mark_safe
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django.views.generic import (
- CreateView, ListView, TemplateView, UpdateView, View,
+ CreateView, ListView, TemplateView, UpdateView, View, FormView,
)
from django_scopes import scopes_disabled
@@ -663,3 +663,110 @@ class VoucherBulkAction(VoucherQueryMixin, EventPermissionRequiredMixin, View):
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
})
+
+
+
+class VoucherBulkUpdateView(VoucherQueryMixin, EventPermissionRequiredMixin, FormView):
+ template_name = 'pretixcontrol/vouchers/bulk_edit.html'
+ permission = 'event.vouchers:write'
+ context_object_name = 'voucher'
+ form_class = VoucherBulkEditForm
+
+ def get_queryset(self):
+ return super().get_queryset().prefetch_related(None).order_by()
+
+ def get(self, request, *args, **kwargs):
+ return HttpResponse(status=405)
+
+ @cached_property
+ def is_submitted(self):
+ # Usually, django considers a form "bound" / "submitted" on every POST request. However, this view is always
+ # called with POST method, even if just to pass the selection of objects to work on, so we want to modify
+ # that behaviour
+ return '_bulk' in self.request.POST
+
+ def get_form_kwargs(self):
+ initial = {}
+ mixed_values = set()
+ qs = self.get_queryset().annotate()
+
+ fields = {
+ 'name': 'name',
+ 'size': 'size',
+ 'subevent': 'subevent',
+ 'close_when_sold_out': 'close_when_sold_out',
+ 'release_after_exit': 'release_after_exit',
+ 'ignore_for_event_availability': 'ignore_for_event_availability',
+ }
+ for k, f in fields.items():
+ existing_values = list(qs.order_by(f).values(f).annotate(c=Count('*')))
+ if len(existing_values) == 1:
+ initial[k] = existing_values[0][f]
+ elif len(existing_values) > 1:
+ mixed_values.add(k)
+ initial[k] = None
+
+ item_values = list(qs.order_by("items_list").values("items_list").annotate(c=Count('*')))
+ var_values = list(qs.order_by("vars_list").values("vars_list").annotate(c=Count('*')))
+ if len(item_values) > 1 or len(var_values) > 1:
+ mixed_values.add("itemvars")
+ else:
+ initial["itemvars"] = [iv for iv in (item_values[0]["items_list"] or "").split(",") + (var_values[0]["vars_list"] or "").split(",") if iv]
+
+ kwargs = super().get_form_kwargs()
+ kwargs['event'] = self.request.event
+ kwargs['prefix'] = 'bulkedit'
+ kwargs['initial'] = initial
+ kwargs['queryset'] = self.get_queryset()
+ kwargs['mixed_values'] = mixed_values
+ if not self.is_submitted:
+ kwargs['data'] = None
+ kwargs['files'] = None
+ return kwargs
+
+ def get_success_url(self):
+ return reverse('control:event.items.quotas', kwargs={
+ 'organizer': self.request.event.organizer.slug,
+ 'event': self.request.event.slug,
+ })
+
+ @transaction.atomic()
+ def form_valid(self, form):
+ log_entries = []
+
+ # Main form
+ form.save()
+ data = {
+ k: v
+ for k, v in form.cleaned_data.items()
+ if k in form.changed_data
+ }
+ data['_raw_bulk_data'] = self.request.POST.dict()
+ for obj in self.get_queryset():
+ log_entries.append(
+ obj.log_action('pretix.event.quota.changed', data=data, user=self.request.user, save=False)
+ )
+
+ LogEntry.bulk_create_and_postprocess(log_entries)
+
+ messages.success(self.request, _('Your changes have been saved.'))
+ return super().form_valid(form)
+
+ def get_context_data(self, **kwargs):
+ ctx = super().get_context_data(**kwargs)
+ ctx['quotas'] = self.get_queryset()
+ ctx['bulk_selected'] = self.request.POST.getlist("_bulk")
+ return ctx
+
+ def post(self, request, *args, **kwargs):
+ form = self.get_form()
+ is_valid = (
+ self.is_submitted and
+ form.is_valid()
+ )
+ if is_valid:
+ return self.form_valid(form)
+ else:
+ if self.is_submitted:
+ messages.error(self.request, _('We could not save your changes. See below for details.'))
+ return self.form_invalid(form)