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)