From d555b232755f2f1e0d57e6ae48527d16fa282181 Mon Sep 17 00:00:00 2001 From: Richard Schreiber Date: Tue, 2 Jun 2026 12:23:25 +0200 Subject: [PATCH] Add _none-option to ModelChoiceField and filters for organizer and event-permission in event-typeahead (#6224) * Add optional filters for organizer and event-permission on event-typeahead * include _none option only if no search query given Co-authored-by: luelista * allow _none in Select2, add ModelChoiceFieldWithNone * fix flake8 --------- Co-authored-by: luelista --- src/pretix/control/forms/__init__.py | 28 +++++++++++++++ src/pretix/control/forms/widgets.py | 35 +++++++++++++------ src/pretix/control/views/typeahead.py | 31 ++++++++++++---- src/pretix/static/pretixcontrol/js/ui/main.js | 12 ++++--- 4 files changed, 84 insertions(+), 22 deletions(-) diff --git a/src/pretix/control/forms/__init__.py b/src/pretix/control/forms/__init__.py index 6f5fb6c089..a4adbe3d91 100644 --- a/src/pretix/control/forms/__init__.py +++ b/src/pretix/control/forms/__init__.py @@ -461,3 +461,31 @@ class SalesChannelCheckboxSelectMultiple(forms.CheckboxSelectMultiple): **super().create_option(name, value, label, selected, index, subindex, attrs), "plugin_missing": plugin and plugin not in self.event.get_plugins(), } + + +class ModelChoiceIteratorWithNone(forms.models.ModelChoiceIterator): + # see django.forms.models.ModelChoiceIterator for original implementation + def __iter__(self): + if self.field.empty_label is not None: + yield ("", self.field.empty_label) + if self.field.none_label is not None: + yield ("_none", self.field.none_label) + queryset = self.queryset + # Can't use iterator() when queryset uses prefetch_related() + if not queryset._prefetch_related_lookups: + queryset = queryset.iterator() + for obj in queryset: + yield self.choice(obj) + + +class ModelChoiceFieldWithNone(forms.ModelChoiceField): + iterator = ModelChoiceIteratorWithNone + + def __init__(self, *args, **kwargs): + self.none_label = kwargs.pop("none_label", None) + super().__init__(*args, **kwargs) + + def to_python(self, value): + if value == "_none": + return value + return super().to_python(value) diff --git a/src/pretix/control/forms/widgets.py b/src/pretix/control/forms/widgets.py index d054be63a5..5773a8069b 100644 --- a/src/pretix/control/forms/widgets.py +++ b/src/pretix/control/forms/widgets.py @@ -29,17 +29,30 @@ class Select2Mixin: super().__init__(*args, **kwargs) def options(self, name, value, attrs=None): - if value and value[0]: - for i, selected in enumerate(self.choices.queryset.filter(pk__in=value)): - yield self.create_option( - None, - self.choices.field.prepare_value(selected), - self.choices.field.label_from_instance(selected), - True, - i, - subindex=None, - attrs=attrs - ) + if not value or not value[0]: + return + has_none = "_none" in value + if has_none: + value = [v for v in value if v != "_none"] + yield self.create_option( + None, + "_none", + self.choices.field.none_label, + True, + 0, + subindex=None, + attrs=attrs + ) + for i, selected in enumerate(self.choices.queryset.filter(pk__in=value)): + yield self.create_option( + None, + self.choices.field.prepare_value(selected), + self.choices.field.label_from_instance(selected), + True, + i + (1 if has_none else 0), + subindex=None, + attrs=attrs + ) return def optgroups(self, name, value, attrs=None): diff --git a/src/pretix/control/views/typeahead.py b/src/pretix/control/views/typeahead.py index 30fa683156..e15c743796 100644 --- a/src/pretix/control/views/typeahead.py +++ b/src/pretix/control/views/typeahead.py @@ -145,11 +145,21 @@ def event_list(request): if 'can_copy' in request.GET: qs = EventWizardCopyForm.copy_from_queryset(request.user, request.session) else: - qs = request.user.get_events_with_any_permission(request) + permission = request.GET.get('permission') + if permission: + qs = request.user.get_events_with_permission(permission, request) + else: + qs = request.user.get_events_with_any_permission(request) + + name_slug_q = Q(name__icontains=i18ncomp(query)) | Q(slug__icontains=query) + organizer = request.GET.get('organizer') + if organizer: + qs = qs.filter(organizer__slug=organizer) + else: + name_slug_q |= Q(organizer__name__icontains=i18ncomp(query)) | Q(organizer__slug__icontains=query) qs = qs.filter( - Q(name__icontains=i18ncomp(query)) | Q(slug__icontains=query) | - Q(organizer__name__icontains=i18ncomp(query)) | Q(organizer__slug__icontains=query) + name_slug_q ).annotate( min_from=Min('subevents__date_from'), max_from=Max('subevents__date_from'), @@ -162,10 +172,19 @@ def event_list(request): total = qs.count() pagesize = 20 offset = (page - 1) * pagesize + results = [] + if page == 1 and 'include_none' in request.GET and not query: + results.append({ + 'id': "_none", + 'text': _("No event"), + 'name': _("No event"), + 'type': "event", + }) + results += [ + serialize_event(e) for e in qs.select_related('organizer')[offset:offset + pagesize] + ] doc = { - 'results': [ - serialize_event(e) for e in qs.select_related('organizer')[offset:offset + pagesize] - ], + 'results': results, 'pagination': { "more": total >= (offset + pagesize) } diff --git a/src/pretix/static/pretixcontrol/js/ui/main.js b/src/pretix/static/pretixcontrol/js/ui/main.js index 06c436192b..9b48016b53 100644 --- a/src/pretix/static/pretixcontrol/js/ui/main.js +++ b/src/pretix/static/pretixcontrol/js/ui/main.js @@ -639,11 +639,13 @@ var form_handlers = function (el) { ).append(" ").append($("
").text(res.organizer).html()) ); } - $ret.append( - $("").addClass("event-daterange").append( - $("").addClass("fa fa-calendar fa-fw") - ).append(" ").append(res.date_range) - ); + if (res.date_range) { + $ret.append( + $("").addClass("event-daterange").append( + $("").addClass("fa fa-calendar fa-fw") + ).append(" ").append(res.date_range) + ); + } return $ret; }, }).on("select2:select", function () {