mirror of
https://github.com/pretix/pretix.git
synced 2026-05-08 15:44:02 +00:00
Quotas: Add bulk-edit, bulk-delete and filter form (#6080)
* Quotas: Add bulk-edit, bulk-delete and filter form * Fix GroupConcat * Apply suggestions from code review Co-authored-by: Richard Schreiber <wiffbi@gmail.com> * Review notes * Fix handling of required fields --------- Co-authored-by: Richard Schreiber <wiffbi@gmail.com>
This commit is contained in:
@@ -1528,6 +1528,133 @@ class SubEventFilterForm(FilterForm):
|
|||||||
return self.event.organizer.meta_properties.filter(filter_allowed=True)
|
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):
|
class OrganizerFilterForm(FilterForm):
|
||||||
orders = {
|
orders = {
|
||||||
'slug': 'slug',
|
'slug': 'slug',
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ from django.core.exceptions import ValidationError
|
|||||||
from django.db.models import Max, Q
|
from django.db.models import Max, Q
|
||||||
from django.forms import ChoiceField, RadioSelect
|
from django.forms import ChoiceField, RadioSelect
|
||||||
from django.forms.formsets import DELETION_FIELD_NAME
|
from django.forms.formsets import DELETION_FIELD_NAME
|
||||||
|
from django.forms.utils import ErrorDict
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.html import escape, format_html
|
from django.utils.html import escape, format_html
|
||||||
@@ -375,6 +376,60 @@ class QuotaForm(I18nModelForm):
|
|||||||
return inst
|
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):
|
class ItemCreateForm(I18nModelForm):
|
||||||
NONE = 'none'
|
NONE = 'none'
|
||||||
EXISTING = 'existing'
|
EXISTING = 'existing'
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{% extends "pretixcontrol/event/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>
|
||||||
|
{% trans "Change multiple quotas" %}
|
||||||
|
<small>
|
||||||
|
{% blocktrans trimmed with number=quotas.count %}
|
||||||
|
{{ number }} selected
|
||||||
|
{% endblocktrans %}
|
||||||
|
</small>
|
||||||
|
</h1>
|
||||||
|
<form class="form-horizontal" action="" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form_errors form %}
|
||||||
|
<div class="hidden">
|
||||||
|
{% for d in quotas %}
|
||||||
|
<input type="hidden" name="quota" value="{{ d.pk }}">
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<fieldset>
|
||||||
|
<legend>{% trans "General information" %}</legend>
|
||||||
|
{% bootstrap_field form.name layout="bulkedit" %}
|
||||||
|
{% bootstrap_field form.size layout="bulkedit" %}
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>{% trans "Items" %}</legend>
|
||||||
|
<p>
|
||||||
|
{% 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 <strong>both</strong> quotas have capacity
|
||||||
|
left.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
{% bootstrap_field form.itemvars layout="bulkedit" %}
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>{% trans "Advanced options" %}</legend>
|
||||||
|
{% 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" %}
|
||||||
|
</fieldset>
|
||||||
|
<div class="form-group submit-group">
|
||||||
|
<button type="submit" class="btn btn-primary btn-save">
|
||||||
|
{% trans "Save" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{% extends "pretixcontrol/event/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% block title %}{% trans "Delete quotas" %}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>{% trans "Delete quotas" %}</h1>
|
||||||
|
<form action="" method="post" class="form-horizontal">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% if allowed %}
|
||||||
|
<p>{% 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 %}</p>
|
||||||
|
<ul>
|
||||||
|
{% for q in allowed %}
|
||||||
|
<li>
|
||||||
|
{{ q }} {% if q.subevent %}({{ q.subevent }}){% endif %}
|
||||||
|
<input type="hidden" name="quota" value="{{ q.pk }}">
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
<div class="form-group submit-group">
|
||||||
|
<a href="{% url "control:event.items.quotas" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||||
|
class="btn btn-default btn-cancel">
|
||||||
|
{% trans "Cancel" %}
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-danger btn-save" value="delete_confirm" name="action">
|
||||||
|
{% trans "Delete" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{% extends "pretixcontrol/items/base.html" %}
|
{% extends "pretixcontrol/items/base.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load urlreplace %}
|
{% load urlreplace %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
{% block title %}{% trans "Quotas" %}{% endblock %}
|
{% block title %}{% trans "Quotas" %}{% endblock %}
|
||||||
{% block inside %}
|
{% block inside %}
|
||||||
<h1>{% trans "Quotas" %}</h1>
|
<h1>{% trans "Quotas" %}</h1>
|
||||||
@@ -13,21 +14,12 @@
|
|||||||
number of a specific ticket type at the same time.
|
number of a specific ticket type at the same time.
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
{% if request.event.has_subevents %}
|
{% if quotas|length == 0 and not filter_form.filtered %}
|
||||||
<form class="form-inline helper-display-inline" action="" method="get">
|
|
||||||
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %}
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
{% if quotas|length == 0 %}
|
|
||||||
<div class="empty-collection">
|
<div class="empty-collection">
|
||||||
<p>
|
<p>
|
||||||
{% if request.GET.subevent %}
|
{% blocktrans trimmed %}
|
||||||
{% trans "Your search did not match any quotas." %}
|
You haven't created any quotas yet.
|
||||||
{% else %}
|
{% endblocktrans %}
|
||||||
{% blocktrans trimmed %}
|
|
||||||
You haven't created any quotas yet.
|
|
||||||
{% endblocktrans %}
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{% if 'event.items:write' in request.eventpermset %}
|
{% if 'event.items:write' in request.eventpermset %}
|
||||||
@@ -36,79 +28,160 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">
|
||||||
|
{% trans "Filter" %}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<form class="panel-body filter-form" action="" method="get">
|
||||||
|
<div class="row">
|
||||||
|
<div class="{% if not filter_form.subevent %}col-lg-6{% else %}col-lg-2{% endif %} col-md-6 col-sm-6 col-xs-12">
|
||||||
|
{% bootstrap_field filter_form.query %}
|
||||||
|
</div>
|
||||||
|
{% if filter_form.subevent %}
|
||||||
|
<div class="col-lg-2 col-md-6 col-md-2 col-sm-6 col-xs-12">
|
||||||
|
{% bootstrap_field filter_form.subevent %}
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-2 col-md-3 col-sm-6 col-xs-12">
|
||||||
|
{% bootstrap_field filter_form.date_from %}
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-2 col-md-3 col-sm-6 col-xs-12">
|
||||||
|
{% bootstrap_field filter_form.date_until %}
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-2 col-md-3 col-sm-6 col-xs-12">
|
||||||
|
{% bootstrap_field filter_form.time_from %}
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-2 col-md-3 col-sm-6 col-xs-12">
|
||||||
|
{% bootstrap_field filter_form.time_until %}
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 one-line-checkboxes">
|
||||||
|
{% bootstrap_field filter_form.weekday %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-right flip">
|
||||||
|
<button class="btn btn-primary btn-lg" type="submit">
|
||||||
|
<span class="fa fa-filter"></span>
|
||||||
|
{% trans "Filter" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
{% if 'event.items:write' in request.eventpermset %}
|
{% if 'event.items:write' in request.eventpermset %}
|
||||||
<p>
|
<p>
|
||||||
<a href="{% url "control:event.items.quotas.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new quota" %}
|
<a href="{% url "control:event.items.quotas.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new quota" %}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="table-responsive">
|
<form action="{% url "control:event.items.quotas.bulkaction" organizer=request.event.organizer.slug event=request.event.slug %}" method="post">
|
||||||
<table class="table table-hover table-quotas">
|
{% csrf_token %}
|
||||||
<thead>
|
{% for field in filter_form %}
|
||||||
<tr>
|
{{ field.as_hidden }}
|
||||||
<th>{% trans "Quota name" %}
|
{% endfor %}
|
||||||
<a href="?{% url_replace request 'ordering' '-name' %}"><i class="fa fa-caret-down"></i></a>
|
<div class="table-responsive">
|
||||||
<a href="?{% url_replace request 'ordering' 'name' %}"><i class="fa fa-caret-up"></i></a>
|
<table class="table table-hover table-quotas">
|
||||||
</th>
|
<thead>
|
||||||
<th>{% trans "Products" %}</th>
|
|
||||||
{% if request.event.has_subevents %}
|
|
||||||
<th>{% trans "Date" context "subevent" %}
|
|
||||||
<a href="?{% url_replace request 'ordering' '-date' %}"><i class="fa fa-caret-down"></i></a>
|
|
||||||
<a href="?{% url_replace request 'ordering' 'date' %}"><i class="fa fa-caret-up"></i></a>
|
|
||||||
</th>
|
|
||||||
{% endif %}
|
|
||||||
<th>{% trans "Total capacity" %}
|
|
||||||
<a href="?{% url_replace request 'ordering' '-size' %}"><i class="fa fa-caret-down"></i></a>
|
|
||||||
<a href="?{% url_replace request 'ordering' 'size' %}"><i class="fa fa-caret-up"></i></a>
|
|
||||||
</th>
|
|
||||||
<th>{% trans "Capacity left" %}</th>
|
|
||||||
<th class="action-col-2"></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for q in quotas %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
{% if "event.items:write" in request.eventpermset %}
|
||||||
<strong><a href="{% url "control:event.items.quotas.show" organizer=request.event.organizer.slug event=request.event.slug quota=q.id %}">{{ q.name }}</a></strong>
|
<th>
|
||||||
{% if q.ignore_for_event_availability %}
|
<label aria-label="{% trans "select all rows for batch-operation" %}" class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
|
||||||
<span class="fa fa-eye-slash text-muted" data-toggle="tooltip" title="{% trans "Ignore this quota when determining event availability" %}"></span>
|
</th>
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<ul>
|
|
||||||
{% for item in q.cached_items %}
|
|
||||||
{% if not item.has_variations %}
|
|
||||||
<li><a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a></li>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% for v in q.variations.all %}
|
|
||||||
<li><a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=v.item.id %}#tab-0-3-open">
|
|
||||||
{{ v.item }} – {{ v }}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
{% if request.event.has_subevents %}
|
|
||||||
<td>
|
|
||||||
{{ q.subevent.name }} – {{ q.subevent.get_date_range_display_with_times }}
|
|
||||||
</td>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>{% if q.size == None %}Unlimited{% else %}{{ q.size }}{% endif %}</td>
|
<th>{% trans "Quota name" %}
|
||||||
<td>{% include "pretixcontrol/items/fragment_quota_availability.html" with availability=q.cached_avail closed=q.closed %}</td>
|
<a href="?{% url_replace request 'filter-ordering' '-name' %}"><i class="fa fa-caret-down"></i></a>
|
||||||
<td class="text-right flip">
|
<a href="?{% url_replace request 'filter-ordering' 'name' %}"><i class="fa fa-caret-up"></i></a>
|
||||||
{% if 'event.items:write' in request.eventpermset %}
|
</th>
|
||||||
<a href="{% url "control:event.items.quotas.edit" organizer=request.event.organizer.slug event=request.event.slug quota=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
<th>{% trans "Products" %}</th>
|
||||||
<a href="{% url "control:event.items.quotas.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ q.id }}"
|
{% if request.event.has_subevents %}
|
||||||
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
|
<th>{% trans "Date" context "subevent" %}
|
||||||
<span class="fa fa-copy"></span>
|
<a href="?{% url_replace request 'filter-ordering' '-date' %}"><i class="fa fa-caret-down"></i></a>
|
||||||
</a>
|
<a href="?{% url_replace request 'filter-ordering' 'date' %}"><i class="fa fa-caret-up"></i></a>
|
||||||
<a href="{% url "control:event.items.quotas.delete" organizer=request.event.organizer.slug event=request.event.slug quota=q.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
<th>{% trans "Total capacity" %}
|
||||||
|
<a href="?{% url_replace request 'filter-ordering' '-size' %}"><i class="fa fa-caret-down"></i></a>
|
||||||
|
<a href="?{% url_replace request 'filter-ordering' 'size' %}"><i class="fa fa-caret-up"></i></a>
|
||||||
|
</th>
|
||||||
|
<th>{% trans "Capacity left" %}</th>
|
||||||
|
<th class="action-col-2"></th>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% if "event.items:write" in request.eventpermset and page_obj.paginator.num_pages > 1 %}
|
||||||
</tbody>
|
<tr class="table-select-all warning hidden">
|
||||||
</table>
|
<td>
|
||||||
</div>
|
<input type="checkbox" name="__ALL" id="__all" data-results-total="{{ page_obj.paginator.count }}">
|
||||||
|
</td>
|
||||||
|
<td colspan="6">
|
||||||
|
<label for="__all">
|
||||||
|
{% trans "Select all results on other pages as well" %}
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for q in quotas %}
|
||||||
|
<tr>
|
||||||
|
{% if "event.items:write" in request.eventpermset %}
|
||||||
|
<td>
|
||||||
|
<label aria-label="{% trans "select row for batch-operation" %}" class="batch-select-label"><input type="checkbox" name="quota" class="batch-select-checkbox" value="{{ q.pk }}"/></label>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
<td>
|
||||||
|
<strong><a href="{% url "control:event.items.quotas.show" organizer=request.event.organizer.slug event=request.event.slug quota=q.id %}">{{ q.name }}</a></strong>
|
||||||
|
{% if q.ignore_for_event_availability %}
|
||||||
|
<span class="fa fa-eye-slash text-muted" data-toggle="tooltip" title="{% trans "Ignore this quota when determining event availability" %}"></span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<ul>
|
||||||
|
{% for item in q.cached_items %}
|
||||||
|
{% if not item.has_variations %}
|
||||||
|
<li><a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% for v in q.variations.all %}
|
||||||
|
<li><a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=v.item.id %}#tab-0-3-open">
|
||||||
|
{{ v.item }} – {{ v }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
{% if request.event.has_subevents %}
|
||||||
|
<td>
|
||||||
|
{{ q.subevent.name }} – {{ q.subevent.get_date_range_display_with_times }}
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
<td>{% if q.size == None %}Unlimited{% else %}{{ q.size }}{% endif %}</td>
|
||||||
|
<td>{% include "pretixcontrol/items/fragment_quota_availability.html" with availability=q.cached_avail closed=q.closed %}</td>
|
||||||
|
<td class="text-right flip">
|
||||||
|
{% if 'event.items:write' in request.eventpermset %}
|
||||||
|
<a href="{% url "control:event.items.quotas.edit" organizer=request.event.organizer.slug event=request.event.slug quota=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||||
|
<a href="{% url "control:event.items.quotas.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ q.id }}"
|
||||||
|
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
|
||||||
|
<span class="fa fa-copy"></span>
|
||||||
|
</a>
|
||||||
|
<a href="{% url "control:event.items.quotas.delete" organizer=request.event.organizer.slug event=request.event.slug quota=q.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% if "event.items:write" in request.eventpermset %}
|
||||||
|
<div class="batch-select-actions">
|
||||||
|
<button type="submit" class="btn btn-danger btn-save" name="action" value="delete">
|
||||||
|
<i class="fa fa-trash"></i>{% trans "Delete selected" %}
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-primary btn-save" name="action" value="edit"
|
||||||
|
formaction="{% url "control:event.items.quotas.bulkedit" organizer=request.event.organizer.slug event=request.event.slug %}">
|
||||||
|
<i class="fa fa-edit"></i>{% trans "Edit selected" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include "pretixcontrol/pagination.html" %}
|
{% include "pretixcontrol/pagination.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -349,6 +349,8 @@ urlpatterns = [
|
|||||||
name='event.items.questions.edit'),
|
name='event.items.questions.edit'),
|
||||||
re_path(r'^questions/add$', item.QuestionCreate.as_view(), name='event.items.questions.add'),
|
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/$', 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<quota>\d+)/$', item.QuotaView.as_view(), name='event.items.quotas.show'),
|
re_path(r'^quotas/(?P<quota>\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/select$', typeahead.quotas_select2, name='event.items.quotas.select2'),
|
||||||
re_path(r'^quotas/(?P<quota>\d+)/change$', item.QuotaUpdate.as_view(), name='event.items.quotas.edit'),
|
re_path(r'^quotas/(?P<quota>\d+)/change$', item.QuotaUpdate.as_view(), name='event.items.quotas.edit'),
|
||||||
|
|||||||
@@ -41,21 +41,22 @@ from json.decoder import JSONDecodeError
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from django.db import transaction
|
from django.db import models, transaction
|
||||||
from django.db.models import (
|
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.forms.models import inlineformset_factory
|
||||||
from django.http import (
|
from django.http import (
|
||||||
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect,
|
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect,
|
||||||
)
|
)
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect, render
|
||||||
from django.urls import resolve, reverse
|
from django.urls import resolve, reverse
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext, gettext_lazy as _
|
from django.utils.translation import gettext, gettext_lazy as _
|
||||||
from django.views.decorators.http import require_http_methods
|
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.views.generic.detail import DetailView, SingleObjectMixin
|
||||||
from django_countries.fields import Country
|
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.forms import I18nFormSet
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CartPosition, Item, ItemCategory, ItemProgramTime, ItemVariation,
|
CartPosition, Item, ItemCategory, ItemProgramTime, ItemVariation, LogEntry,
|
||||||
OrderPosition, Question, QuestionAnswer, QuestionOption, Quota,
|
OrderPosition, Question, QuestionAnswer, QuestionOption, Quota,
|
||||||
SeatCategoryMapping, Voucher,
|
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.quotas import QuotaAvailability
|
||||||
from pretix.base.services.tickets import invalidate_cache
|
from pretix.base.services.tickets import invalidate_cache
|
||||||
from pretix.base.signals import quota_availability
|
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 (
|
from pretix.control.forms.item import (
|
||||||
CategoryForm, ItemAddOnForm, ItemAddOnsFormSet, ItemBundleForm,
|
CategoryForm, ItemAddOnForm, ItemAddOnsFormSet, ItemBundleForm,
|
||||||
ItemBundleFormSet, ItemCreateForm, ItemMetaValueForm, ItemProgramTimeForm,
|
ItemBundleFormSet, ItemCreateForm, ItemMetaValueForm, ItemProgramTimeForm,
|
||||||
ItemProgramTimeFormSet, ItemUpdateForm, ItemVariationForm,
|
ItemProgramTimeFormSet, ItemUpdateForm, ItemVariationForm,
|
||||||
ItemVariationsFormSet, QuestionForm, QuestionOptionForm, QuotaForm,
|
ItemVariationsFormSet, QuestionForm, QuestionOptionForm, QuotaBulkEditForm,
|
||||||
|
QuotaForm,
|
||||||
)
|
)
|
||||||
from pretix.control.permissions import (
|
from pretix.control.permissions import (
|
||||||
EventPermissionRequiredMixin, event_permission_required,
|
EventPermissionRequiredMixin, event_permission_required,
|
||||||
@@ -87,6 +91,7 @@ from pretix.control.permissions import (
|
|||||||
from pretix.control.signals import item_forms, item_formsets
|
from pretix.control.signals import item_forms, item_formsets
|
||||||
from pretix.helpers.models import modelcopy
|
from pretix.helpers.models import modelcopy
|
||||||
|
|
||||||
|
from ...helpers import GroupConcat
|
||||||
from ...helpers.compat import CompatDeleteView
|
from ...helpers.compat import CompatDeleteView
|
||||||
from . import ChartContainingView, CreateView, PaginationMixin, UpdateView
|
from . import ChartContainingView, CreateView, PaginationMixin, UpdateView
|
||||||
|
|
||||||
@@ -831,13 +836,38 @@ class QuestionCreate(EventPermissionRequiredMixin, QuestionMixin, CreateView):
|
|||||||
return ret
|
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
|
model = Quota
|
||||||
context_object_name = 'quotas'
|
context_object_name = 'quotas'
|
||||||
template_name = 'pretixcontrol/items/quotas.html'
|
template_name = 'pretixcontrol/items/quotas.html'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = self.request.event.quotas.prefetch_related(
|
return super().get_queryset().prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
"items",
|
"items",
|
||||||
queryset=Item.objects.annotate(
|
queryset=Item.objects.annotate(
|
||||||
@@ -852,28 +882,10 @@ class QuotaList(PaginationMixin, ListView):
|
|||||||
queryset=self.request.event.subevents.all()
|
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):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data()
|
ctx = super().get_context_data()
|
||||||
|
ctx['filter_form'] = self.filter_form
|
||||||
|
|
||||||
qa = QuotaAvailability()
|
qa = QuotaAvailability()
|
||||||
qa.queue(*ctx['quotas'])
|
qa.queue(*ctx['quotas'])
|
||||||
@@ -884,6 +896,165 @@ class QuotaList(PaginationMixin, ListView):
|
|||||||
return ctx
|
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):
|
class QuotaCreate(EventPermissionRequiredMixin, CreateView):
|
||||||
model = Quota
|
model = Quota
|
||||||
form_class = QuotaForm
|
form_class = QuotaForm
|
||||||
|
|||||||
@@ -117,12 +117,17 @@ class GroupConcat(Aggregate):
|
|||||||
template = "%(function)s(%(distinct)s%(field)s::text, '%(separator)s' ORDER BY %(field)s::text ASC)"
|
template = "%(function)s(%(distinct)s%(field)s::text, '%(separator)s' ORDER BY %(field)s::text ASC)"
|
||||||
else:
|
else:
|
||||||
template = "%(function)s(%(distinct)s%(field)s::text, '%(separator)s')"
|
template = "%(function)s(%(distinct)s%(field)s::text, '%(separator)s')"
|
||||||
return super().as_sql(
|
|
||||||
|
template, params = super().as_sql(
|
||||||
compiler, connection,
|
compiler, connection,
|
||||||
function='string_agg',
|
function='string_agg',
|
||||||
template=template,
|
template=template,
|
||||||
**extra_context,
|
**extra_context,
|
||||||
)
|
)
|
||||||
|
if self.ordered:
|
||||||
|
# ordered statement requires field parameters twice
|
||||||
|
params = params + params
|
||||||
|
return template, params
|
||||||
|
|
||||||
|
|
||||||
class ReplicaRouter:
|
class ReplicaRouter:
|
||||||
|
|||||||
Reference in New Issue
Block a user