diff --git a/src/pretix/control/forms/item.py b/src/pretix/control/forms/item.py index 071a5c347..6b41cc4b8 100644 --- a/src/pretix/control/forms/item.py +++ b/src/pretix/control/forms/item.py @@ -67,7 +67,7 @@ from pretix.control.forms import ( ButtonGroupRadioSelect, ItemMultipleChoiceField, SizeValidationMixin, SplitDateTimeField, SplitDateTimePickerWidget, ) -from pretix.control.forms.widgets import Select2 +from pretix.control.forms.widgets import Select2, Select2ItemVarMulti from pretix.helpers.models import modelcopy from pretix.helpers.money import change_decimal_field @@ -207,14 +207,20 @@ class QuestionOptionForm(I18nModelForm): class QuotaForm(I18nModelForm): + itemvars = forms.MultipleChoiceField( + label=_("Products"), + required=True, + ) + def __init__(self, **kwargs): self.instance = kwargs.get('instance', None) self.event = kwargs.get('event') items = kwargs.pop('items', None) or self.event.items.prefetch_related('variations') + searchable_selection = kwargs.pop('searchable_selection', None) self.original_instance = modelcopy(self.instance) if self.instance else None initial = kwargs.get('initial', {}) if self.instance and self.instance.pk and 'itemvars' not in initial: - initial['itemvars'] = [str(i.pk) for i in self.instance.items.all()] + [ + initial['itemvars'] = [str(i.pk) for i in self.instance.items.all() if (len(i.variations.all()) == 0)] + [ '{}-{}'.format(v.item_id, v.pk) for v in self.instance.variations.all() ] kwargs['initial'] = initial @@ -231,12 +237,22 @@ class QuotaForm(I18nModelForm): else: choices.append(('{}'.format(item.pk), str(item) if item.active else mark_safe(f'{escape(item)}'))) - self.fields['itemvars'] = forms.MultipleChoiceField( - label=_('Products'), - required=True, - choices=choices, - widget=forms.CheckboxSelectMultiple - ) + if searchable_selection: + self.fields['itemvars'].widget = Select2ItemVarMulti( + attrs={ + 'data-model-select2': 'generic', + 'data-select2-url': reverse('control:event.items.itemvars.select2', kwargs={ + 'event': self.event.slug, + 'organizer': self.event.organizer.slug, + }), + 'data-placeholder': _('All products') + }, + choices=choices, + ) + else: + self.fields['itemvars'].widget = forms.CheckboxSelectMultiple() + + self.fields['itemvars'].choices = choices if self.event.has_subevents: self.fields['subevent'].queryset = self.event.subevents.all() diff --git a/src/pretix/control/forms/subevents.py b/src/pretix/control/forms/subevents.py index f2fe4dbaa..4565c00c0 100644 --- a/src/pretix/control/forms/subevents.py +++ b/src/pretix/control/forms/subevents.py @@ -360,6 +360,7 @@ class BulkSubEventItemVariationForm(SubEventItemVariationForm): class QuotaFormSet(I18nInlineFormSet): def __init__(self, *args, **kwargs): + self.searchable_selection = kwargs.pop('searchable_selection', None) self.event = kwargs.pop('event', None) self.locales = self.event.settings.get('locales') super().__init__(*args, **kwargs) @@ -372,7 +373,7 @@ class QuotaFormSet(I18nInlineFormSet): kwargs['locales'] = self.locales kwargs['event'] = self.event kwargs['items'] = self.items - kwargs['items'] = self.items + kwargs['searchable_selection'] = self.searchable_selection return super()._construct_form(i, **kwargs) @property diff --git a/src/pretix/control/forms/widgets.py b/src/pretix/control/forms/widgets.py index 875ecdb3d..b65cc22bc 100644 --- a/src/pretix/control/forms/widgets.py +++ b/src/pretix/control/forms/widgets.py @@ -77,3 +77,19 @@ class Select2ItemVarQuotaMixin(Select2Mixin): class Select2ItemVarQuota(Select2ItemVarQuotaMixin, forms.Select): pass + + +class Select2ItemVarMulti(Select2Mixin, forms.SelectMultiple): + def options(self, name, value, attrs=None): + # we need this for multi-selection without a queryset for the selection of items and variations + for i, v in enumerate(value): + yield self.create_option( + None, + v, + dict(self.choices)[v], + True, + i, + subindex=None, + attrs=attrs + ) + return diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index 2e4b760ff..c9a761b22 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -293,6 +293,7 @@ urlpatterns = [ re_path(r'^items/typeahead/meta/$', typeahead.item_meta_values, name='event.items.meta.typeahead'), re_path(r'^items/select2$', typeahead.items_select2, name='event.items.select2'), re_path(r'^items/select2/itemvar$', typeahead.itemvar_select2, name='event.items.itemvar.select2'), + re_path(r'^items/select2/itemvars$', typeahead.itemvars_select2, name='event.items.itemvars.select2'), re_path(r'^items/select2/variation$', typeahead.variations_select2, name='event.items.variations.select2'), re_path(r'^categories/$', item.CategoryList.as_view(), name='event.items.categories'), re_path(r'^categories/select2$', typeahead.category_select2, name='event.items.categories.select2'), diff --git a/src/pretix/control/views/subevents.py b/src/pretix/control/views/subevents.py index 707e94f25..0348bbf30 100644 --- a/src/pretix/control/views/subevents.py +++ b/src/pretix/control/views/subevents.py @@ -295,6 +295,7 @@ class SubEventEditorMixin(MetaDataEditorMixin): ] extra = 0 + kwargs['searchable_selection'] = True formsetclass = inlineformset_factory( SubEvent, Quota, form=QuotaForm, formset=QuotaFormSet, min_num=1, validate_min=True, diff --git a/src/pretix/control/views/typeahead.py b/src/pretix/control/views/typeahead.py index 9090cf252..6c89c457c 100644 --- a/src/pretix/control/views/typeahead.py +++ b/src/pretix/control/views/typeahead.py @@ -684,6 +684,47 @@ def itemvar_select2(request, **kwargs): return JsonResponse(doc) +@event_permission_required(None) +def itemvars_select2(request, **kwargs): + query = request.GET.get('query', '') + try: + page = int(request.GET.get('page', '1')) + except ValueError: + page = 1 + + pagesize = 20 + offset = (page - 1) * pagesize + + choices = [] + + # We are very unlikely to need pagination + itemqs = request.event.items.prefetch_related('variations').filter( + Q(name__icontains=i18ncomp(query)) | Q(internal_name__icontains=query)) + total = itemqs.count() + + for i in itemqs[offset:offset + pagesize]: + variations = list(i.variations.all()) + if variations: + for v in variations: + choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (i, v.value), not v.active)) + else: + choices.append((str(i.pk), str(i), not i.active)) + + doc = { + 'results': [ + { + 'id': k, + 'text': str(v), + } + for k, v, d in choices + ], + 'pagination': { + "more": total >= (offset + pagesize) + } + } + return JsonResponse(doc) + + @event_permission_required(None) def itemvarquota_select2(request, **kwargs): query = request.GET.get('query', '') diff --git a/src/pretix/static/pretixcontrol/js/ui/main.js b/src/pretix/static/pretixcontrol/js/ui/main.js index 53965b3be..61dfefefc 100644 --- a/src/pretix/static/pretixcontrol/js/ui/main.js +++ b/src/pretix/static/pretixcontrol/js/ui/main.js @@ -573,6 +573,7 @@ var form_handlers = function (el) { el.find('[data-model-select2=generic]').each(function () { var $s = $(this); $s.select2({ + closeOnSelect: !this.hasAttribute('multiple'), theme: "bootstrap", delay: 100, allowClear: !$s.prop("required"),