diff --git a/src/pretix/control/forms/filter.py b/src/pretix/control/forms/filter.py index 806c5e2ea2..fda7545a03 100644 --- a/src/pretix/control/forms/filter.py +++ b/src/pretix/control/forms/filter.py @@ -1528,6 +1528,133 @@ class SubEventFilterForm(FilterForm): return self.event.organizer.meta_properties.filter(filter_allowed=True) +class QuotaFilterForm(FilterForm): + orders = { + '-date': ('-subevent__date_from', 'name', 'pk'), + 'date': ('subevent__date_from', '-name', '-pk'), + 'size': ('size', 'name', 'pk'), + '-size': ('-size', '-name', '-pk'), + 'name': ('name', 'pk'), + '-name': ('-name', '-pk'), + } + subevent = forms.ModelChoiceField( + label=pgettext_lazy('subevent', 'Date'), + queryset=SubEvent.objects.none(), + required=False, + empty_label=pgettext_lazy('subevent', 'All dates') + ) + date_from = forms.DateField( + label=_('Date from'), + required=False, + widget=DatePickerWidget({ + 'placeholder': _('Date from'), + }), + ) + date_until = forms.DateField( + label=_('Date until'), + required=False, + widget=DatePickerWidget({ + 'placeholder': _('Date until'), + }), + ) + time_from = forms.TimeField( + label=_('Start time from'), + required=False, + widget=TimePickerWidget({}), + ) + time_until = forms.TimeField( + label=_('Start time until'), + required=False, + widget=TimePickerWidget({}), + ) + weekday = forms.MultipleChoiceField( + label=_('Weekday'), + choices=( + ('2', _('Monday')), + ('3', _('Tuesday')), + ('4', _('Wednesday')), + ('5', _('Thursday')), + ('6', _('Friday')), + ('7', _('Saturday')), + ('1', _('Sunday')), + ), + widget=forms.CheckboxSelectMultiple, + required=False + ) + query = forms.CharField( + label=_('Quota name'), + widget=forms.TextInput(), + required=False + ) + + def __init__(self, *args, **kwargs): + self.event = kwargs.pop('event') + super().__init__(*args, **kwargs) + if self.event.has_subevents: + self.fields['date_from'].widget = DatePickerWidget() + self.fields['date_until'].widget = DatePickerWidget() + self.fields['subevent'].queryset = self.event.subevents.all() + self.fields['subevent'].widget = Select2( + attrs={ + 'data-model-select2': 'event', + 'data-select2-url': reverse('control:event.subevents.select2', kwargs={ + 'event': self.event.slug, + 'organizer': self.event.organizer.slug, + }), + 'data-placeholder': pgettext_lazy('subevent', 'All dates') + } + ) + self.fields['subevent'].widget.choices = self.fields['subevent'].choices + else: + del self.fields['subevent'] + del self.fields['date_from'] + del self.fields['date_until'] + del self.fields['time_from'] + del self.fields['time_until'] + del self.fields['weekday'] + + def filter_qs(self, qs): + fdata = self.cleaned_data + + if fdata.get('weekday'): + qs = qs.annotate(wday=ExtractWeekDay('subevent__date_from')).filter(wday__in=fdata.get('weekday')) + + if fdata.get('subevent'): + qs = qs.filter(subevent=fdata["subevent"]) + + if fdata.get('query'): + query = fdata.get('query') + qs = qs.filter(name__icontains=query) + + if fdata.get('date_until'): + date_end = make_aware(datetime.combine( + fdata.get('date_until') + timedelta(days=1), + time(hour=0, minute=0, second=0, microsecond=0) + ), get_current_timezone()) + qs = qs.filter( + Q(subevent__date_to__isnull=True, subevent__date_from__lt=date_end) | + Q(subevent__date_to__isnull=False, subevent__date_to__lt=date_end) + ) + if fdata.get('date_from'): + date_start = make_aware(datetime.combine( + fdata.get('date_from'), + time(hour=0, minute=0, second=0, microsecond=0) + ), get_current_timezone()) + qs = qs.filter(subevent__date_from__gte=date_start) + + if fdata.get('time_until'): + qs = qs.filter(subevent__date_from__time__lte=fdata.get('time_until')) + if fdata.get('time_from'): + qs = qs.filter(subevent__date_from__time__gte=fdata.get('time_from')) + + if fdata.get('ordering'): + qs = qs.order_by(*get_deterministic_ordering(Quota, self.get_order_by())) + else: + qs = qs.order_by('-subevent__date_from', 'name', 'pk') + + return qs + + class OrganizerFilterForm(FilterForm): orders = { 'slug': 'slug', diff --git a/src/pretix/control/forms/item.py b/src/pretix/control/forms/item.py index 6829907cd4..6fd90da244 100644 --- a/src/pretix/control/forms/item.py +++ b/src/pretix/control/forms/item.py @@ -43,6 +43,7 @@ from django.core.exceptions import ValidationError from django.db.models import Max, Q from django.forms import ChoiceField, RadioSelect from django.forms.formsets import DELETION_FIELD_NAME +from django.forms.utils import ErrorDict from django.urls import reverse from django.utils.functional import cached_property from django.utils.html import escape, format_html @@ -375,6 +376,60 @@ class QuotaForm(I18nModelForm): return inst +class QuotaBulkEditForm(QuotaForm): + + def __init__(self, *args, **kwargs): + self.mixed_values = kwargs.pop('mixed_values') + self.queryset = kwargs.pop('queryset') + super().__init__(**kwargs) + self.fields.pop("subevent", None) # Would add extra complexity and it's hard to imagine a use case for that + self.fields["name"].required = False + self.fields["itemvars"].required = False + + def clean(self): + d = super().clean() + if self.prefix + "name" in self.data.getlist('_bulk') and not d.get("name"): + raise ValidationError({"name": _("This field is required.")}) + if self.prefix + "itemvars" in self.data.getlist('_bulk') and not d.get("itemvars"): + raise ValidationError({"itemvars": _("This field is required.")}) + return d + + 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) + if k == 'itemvars': + 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 + ])) + for obj in objs: + obj.items.set(selected_items) + obj.variations.set(selected_variations) + else: + for obj in objs: + setattr(obj, k, self.cleaned_data[k]) + + fields = [f for f in fields if f != 'itemvars'] + if fields: + Quota.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 ItemCreateForm(I18nModelForm): NONE = 'none' EXISTING = 'existing' diff --git a/src/pretix/control/templates/pretixcontrol/items/quota_bulk_edit.html b/src/pretix/control/templates/pretixcontrol/items/quota_bulk_edit.html new file mode 100644 index 0000000000..080f312cde --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/items/quota_bulk_edit.html @@ -0,0 +1,49 @@ +{% extends "pretixcontrol/event/base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% block content %} +

+ {% trans "Change multiple quotas" %} + + {% blocktrans trimmed with number=quotas.count %} + {{ number }} selected + {% endblocktrans %} + +

+
+ {% csrf_token %} + {% bootstrap_form_errors form %} + +
+ {% trans "General information" %} + {% bootstrap_field form.name layout="bulkedit" %} + {% bootstrap_field form.size layout="bulkedit" %} +
+
+ {% trans "Items" %} +

+ {% blocktrans trimmed %} + Please select the products or product variations this quota should be applied to. If you apply two + quotas to the same product, it will only be available if both quotas have capacity + left. + {% endblocktrans %} +

+ {% bootstrap_field form.itemvars layout="bulkedit" %} +
+
+ {% trans "Advanced options" %} + {% bootstrap_field form.close_when_sold_out layout="bulkedit" %} + {% bootstrap_field form.release_after_exit layout="bulkedit" %} + {% bootstrap_field form.ignore_for_event_availability layout="bulkedit" %} +
+
+ +
+
+{% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/items/quota_delete_bulk.html b/src/pretix/control/templates/pretixcontrol/items/quota_delete_bulk.html new file mode 100644 index 0000000000..2420d8bf12 --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/items/quota_delete_bulk.html @@ -0,0 +1,34 @@ +{% extends "pretixcontrol/event/base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% block title %}{% trans "Delete quotas" %}{% endblock %} +{% block content %} +

{% trans "Delete quotas" %}

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

{% blocktrans trimmed count num=allowed|length %} + Are you sure you want to delete the following quota? + {% plural %} + Are you sure you want to delete the following {{ num }} quotas? + {% endblocktrans %}

+ + {% endif %} +
+ + {% trans "Cancel" %} + + +
+
+{% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/items/quotas.html b/src/pretix/control/templates/pretixcontrol/items/quotas.html index 5e2729bfca..0da36c9be5 100644 --- a/src/pretix/control/templates/pretixcontrol/items/quotas.html +++ b/src/pretix/control/templates/pretixcontrol/items/quotas.html @@ -1,6 +1,7 @@ {% extends "pretixcontrol/items/base.html" %} {% load i18n %} {% load urlreplace %} +{% load bootstrap3 %} {% block title %}{% trans "Quotas" %}{% endblock %} {% block inside %}

{% trans "Quotas" %}

@@ -13,21 +14,12 @@ number of a specific ticket type at the same time. {% endblocktrans %}

- {% if request.event.has_subevents %} -
- {% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %} -
- {% endif %} - {% if quotas|length == 0 %} + {% if quotas|length == 0 and not filter_form.filtered %}

- {% if request.GET.subevent %} - {% trans "Your search did not match any quotas." %} - {% else %} - {% blocktrans trimmed %} - You haven't created any quotas yet. - {% endblocktrans %} - {% endif %} + {% blocktrans trimmed %} + You haven't created any quotas yet. + {% endblocktrans %}

{% if 'event.items:write' in request.eventpermset %} @@ -36,79 +28,160 @@ {% endif %}
{% else %} + +
+
+

+ {% trans "Filter" %} +

+
+
+
+
+ {% bootstrap_field filter_form.query %} +
+ {% if filter_form.subevent %} +
+ {% bootstrap_field filter_form.subevent %} +
+
+ {% bootstrap_field filter_form.date_from %} +
+
+ {% bootstrap_field filter_form.date_until %} +
+
+ {% bootstrap_field filter_form.time_from %} +
+
+ {% bootstrap_field filter_form.time_until %} +
+
+ {% bootstrap_field filter_form.weekday %} +
+ {% endif %} +
+ +
+ +
+
+
{% if 'event.items:write' in request.eventpermset %}

{% trans "Create a new quota" %}

{% endif %} -
- - - - - - {% if request.event.has_subevents %} - - {% endif %} - - - - - - - {% for q in quotas %} + + {% csrf_token %} + {% for field in filter_form %} + {{ field.as_hidden }} + {% endfor %} +
+
{% trans "Quota name" %} - - - {% trans "Products" %}{% trans "Date" context "subevent" %} - - - {% trans "Total capacity" %} - - - {% trans "Capacity left" %}
+ - - - {% if request.event.has_subevents %} - + {% if "event.items:write" in request.eventpermset %} + {% endif %} - - - + + + {% if request.event.has_subevents %} + + {% endif %} + + + - {% endfor %} - -
- {{ q.name }} - {% if q.ignore_for_event_availability %} - - {% endif %} - -
    - {% for item in q.cached_items %} - {% if not item.has_variations %} -
  • {{ item }}
  • - {% endif %} - {% endfor %} - {% for v in q.variations.all %} -
  • - {{ v.item }} – {{ v }}
  • - {% endfor %} -
-
- {{ q.subevent.name }} – {{ q.subevent.get_date_range_display_with_times }} - + + {% if q.size == None %}Unlimited{% else %}{{ q.size }}{% endif %}{% include "pretixcontrol/items/fragment_quota_availability.html" with availability=q.cached_avail closed=q.closed %} - {% if 'event.items:write' in request.eventpermset %} - - - - - - {% endif %} - {% trans "Quota name" %} + + + {% trans "Products" %}{% trans "Date" context "subevent" %} + + + {% trans "Total capacity" %} + + + {% trans "Capacity left" %}
-
+ {% if "event.items:write" in request.eventpermset and page_obj.paginator.num_pages > 1 %} + + + + + + + + + {% endif %} + + + {% for q in quotas %} + + {% if "event.items:write" in request.eventpermset %} + + + + {% endif %} + + {{ q.name }} + {% if q.ignore_for_event_availability %} + + {% endif %} + + + + + {% if request.event.has_subevents %} + + {{ q.subevent.name }} – {{ q.subevent.get_date_range_display_with_times }} + + {% endif %} + {% if q.size == None %}Unlimited{% else %}{{ q.size }}{% endif %} + {% include "pretixcontrol/items/fragment_quota_availability.html" with availability=q.cached_avail closed=q.closed %} + + {% if 'event.items:write' in request.eventpermset %} + + + + + + {% endif %} + + + {% endfor %} + + + + {% if "event.items:write" in request.eventpermset %} +
+ + +
+ {% endif %} + {% endif %} {% include "pretixcontrol/pagination.html" %} {% endblock %} diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index 35c659fd37..1d95761265 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -349,6 +349,8 @@ urlpatterns = [ name='event.items.questions.edit'), re_path(r'^questions/add$', item.QuestionCreate.as_view(), name='event.items.questions.add'), re_path(r'^quotas/$', item.QuotaList.as_view(), name='event.items.quotas'), + re_path(r'^quotas/bulk_action$', item.QuotaBulkAction.as_view(), name='event.items.quotas.bulkaction'), + re_path(r'^quotas/bulk_edit$', item.QuotaBulkUpdateView.as_view(), name='event.items.quotas.bulkedit'), re_path(r'^quotas/(?P\d+)/$', item.QuotaView.as_view(), name='event.items.quotas.show'), re_path(r'^quotas/select$', typeahead.quotas_select2, name='event.items.quotas.select2'), re_path(r'^quotas/(?P\d+)/change$', item.QuotaUpdate.as_view(), name='event.items.quotas.edit'), diff --git a/src/pretix/control/views/item.py b/src/pretix/control/views/item.py index f55f678c0e..624999f4f1 100644 --- a/src/pretix/control/views/item.py +++ b/src/pretix/control/views/item.py @@ -41,21 +41,22 @@ from json.decoder import JSONDecodeError from django.contrib import messages from django.core.exceptions import PermissionDenied from django.core.files import File -from django.db import transaction +from django.db import models, transaction from django.db.models import ( - Count, Exists, F, OuterRef, Prefetch, ProtectedError, Q, + Count, Exists, F, OuterRef, Prefetch, ProtectedError, Q, Subquery, Value, ) +from django.db.models.functions import Cast, Concat from django.forms.models import inlineformset_factory from django.http import ( Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect, ) -from django.shortcuts import redirect +from django.shortcuts import redirect, render from django.urls import resolve, reverse from django.utils.functional import cached_property from django.utils.timezone import now from django.utils.translation import gettext, gettext_lazy as _ from django.views.decorators.http import require_http_methods -from django.views.generic import ListView +from django.views.generic import FormView, ListView, View from django.views.generic.detail import DetailView, SingleObjectMixin from django_countries.fields import Country @@ -65,7 +66,7 @@ from pretix.api.serializers.item import ( ) from pretix.base.forms import I18nFormSet from pretix.base.models import ( - CartPosition, Item, ItemCategory, ItemProgramTime, ItemVariation, + CartPosition, Item, ItemCategory, ItemProgramTime, ItemVariation, LogEntry, OrderPosition, Question, QuestionAnswer, QuestionOption, Quota, SeatCategoryMapping, Voucher, ) @@ -74,12 +75,15 @@ from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue from pretix.base.services.quotas import QuotaAvailability from pretix.base.services.tickets import invalidate_cache from pretix.base.signals import quota_availability -from pretix.control.forms.filter import QuestionAnswerFilterForm +from pretix.control.forms.filter import ( + QuestionAnswerFilterForm, QuotaFilterForm, +) from pretix.control.forms.item import ( CategoryForm, ItemAddOnForm, ItemAddOnsFormSet, ItemBundleForm, ItemBundleFormSet, ItemCreateForm, ItemMetaValueForm, ItemProgramTimeForm, ItemProgramTimeFormSet, ItemUpdateForm, ItemVariationForm, - ItemVariationsFormSet, QuestionForm, QuestionOptionForm, QuotaForm, + ItemVariationsFormSet, QuestionForm, QuestionOptionForm, QuotaBulkEditForm, + QuotaForm, ) from pretix.control.permissions import ( EventPermissionRequiredMixin, event_permission_required, @@ -87,6 +91,7 @@ from pretix.control.permissions import ( from pretix.control.signals import item_forms, item_formsets from pretix.helpers.models import modelcopy +from ...helpers import GroupConcat from ...helpers.compat import CompatDeleteView from . import ChartContainingView, CreateView, PaginationMixin, UpdateView @@ -831,13 +836,38 @@ class QuestionCreate(EventPermissionRequiredMixin, QuestionMixin, CreateView): return ret -class QuotaList(PaginationMixin, ListView): +class QuotaQueryMixin: + + @cached_property + def request_data(self): + if self.request.method == "POST": + return self.request.POST + return self.request.GET + + def get_queryset(self): + qs = self.request.event.quotas + if self.filter_form.is_valid(): + qs = self.filter_form.filter_qs(qs) + + if 'quota' in self.request_data and '__ALL' not in self.request_data: + qs = qs.filter( + id__in=self.request_data.getlist('quota') + ) + + return qs + + @cached_property + def filter_form(self): + return QuotaFilterForm(data=self.request_data, prefix='filter', event=self.request.event) + + +class QuotaList(PaginationMixin, QuotaQueryMixin, ListView): model = Quota context_object_name = 'quotas' template_name = 'pretixcontrol/items/quotas.html' def get_queryset(self): - qs = self.request.event.quotas.prefetch_related( + return super().get_queryset().prefetch_related( Prefetch( "items", queryset=Item.objects.annotate( @@ -852,28 +882,10 @@ class QuotaList(PaginationMixin, ListView): queryset=self.request.event.subevents.all() ) ) - if self.request.GET.get("subevent", "") != "": - s = self.request.GET.get("subevent", "") - qs = qs.filter(subevent_id=s) - - valid_orders = { - '-date': ('-subevent__date_from', 'name', 'pk'), - 'date': ('subevent__date_from', '-name', '-pk'), - 'size': ('size', 'name', 'pk'), - '-size': ('-size', '-name', '-pk'), - 'name': ('name', 'pk'), - '-name': ('-name', '-pk'), - } - - if self.request.GET.get("ordering", "-date") in valid_orders: - qs = qs.order_by(*valid_orders[self.request.GET.get("ordering", "-date")]) - else: - qs = qs.order_by('name', 'subevent__date_from', 'pk') - - return qs def get_context_data(self, **kwargs): ctx = super().get_context_data() + ctx['filter_form'] = self.filter_form qa = QuotaAvailability() qa.queue(*ctx['quotas']) @@ -884,6 +896,165 @@ class QuotaList(PaginationMixin, ListView): return ctx +class QuotaBulkAction(QuotaQueryMixin, EventPermissionRequiredMixin, View): + permission = 'event.items:write' + + @transaction.atomic + def post(self, request, *args, **kwargs): + if request.POST.get('action') == 'delete': + return render(request, 'pretixcontrol/items/quota_delete_bulk.html', { + 'allowed': self.get_queryset().select_related("subevent"), + }) + elif request.POST.get('action') == 'delete_confirm': + log_entries = [] + to_delete = [] + for obj in self.get_queryset(): + log_entries.append(obj.log_action('pretix.event.quota.deleted', user=self.request.user, save=False)) + to_delete.append(obj.pk) + + if to_delete: + LogEntry.bulk_create_and_postprocess(log_entries) + Quota.objects.filter(pk__in=to_delete).delete() + messages.success(request, _('The selected quotas have been deleted or disabled.')) + return redirect(self.get_success_url()) + + def get_success_url(self) -> str: + return reverse('control:event.items.quotas', kwargs={ + 'organizer': self.request.event.organizer.slug, + 'event': self.request.event.slug, + }) + + +class QuotaBulkUpdateView(QuotaQueryMixin, EventPermissionRequiredMixin, FormView): + template_name = 'pretixcontrol/items/quota_bulk_edit.html' + permission = 'event.items:write' + context_object_name = 'quota' + form_class = QuotaBulkEditForm + + 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( + items_list=Subquery( + Quota.items.through.objects.filter( + quota_id=OuterRef('pk'), + item__variations__isnull=True, + ).order_by().values('quota_id').annotate( + g=GroupConcat('item_id', separator=',', ordered=True) + ).values('g') + ), + vars_list=Subquery( + Quota.variations.through.objects.filter( + quota_id=OuterRef('pk') + ).order_by().values('quota_id').annotate( + g=GroupConcat( + Concat( + Cast(F('itemvariation__item_id'), output_field=models.TextField()), + Value('-', output_field=models.TextField()), + Cast(F('itemvariation_id'), output_field=models.TextField()), + ), + separator=',', + ordered=True + ) + ).values('g') + ), + ) + + 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) + + class QuotaCreate(EventPermissionRequiredMixin, CreateView): model = Quota form_class = QuotaForm diff --git a/src/pretix/helpers/database.py b/src/pretix/helpers/database.py index 1289f4f1af..d9d8ffb16d 100644 --- a/src/pretix/helpers/database.py +++ b/src/pretix/helpers/database.py @@ -117,12 +117,17 @@ class GroupConcat(Aggregate): template = "%(function)s(%(distinct)s%(field)s::text, '%(separator)s' ORDER BY %(field)s::text ASC)" else: template = "%(function)s(%(distinct)s%(field)s::text, '%(separator)s')" - return super().as_sql( + + template, params = super().as_sql( compiler, connection, function='string_agg', template=template, **extra_context, ) + if self.ordered: + # ordered statement requires field parameters twice + params = params + params + return template, params class ReplicaRouter: