From 27183a26eee470d0c1dccd4c0ca8552882e30c42 Mon Sep 17 00:00:00 2001 From: luelista Date: Mon, 4 May 2026 11:34:05 +0200 Subject: [PATCH] Respect per-event plugin availability in OrganizerPluginEvents view (#5983) * Allow plugins to declare their availability per event * Fix message type * small optimization of PluginsField serializer --- src/pretix/api/serializers/fields.py | 4 +- src/pretix/base/plugins.py | 53 +++++++++++++++------------ src/pretix/control/views/organizer.py | 36 ++++++++---------- 3 files changed, 47 insertions(+), 46 deletions(-) diff --git a/src/pretix/api/serializers/fields.py b/src/pretix/api/serializers/fields.py index d3cc5f1b8a..d877028ab7 100644 --- a/src/pretix/api/serializers/fields.py +++ b/src/pretix/api/serializers/fields.py @@ -115,10 +115,10 @@ class PluginsField(serializers.Field): def to_representation(self, obj): from pretix.base.plugins import get_all_plugins - + active_plugins = set(obj.get_plugins()) return sorted([ p.module for p in get_all_plugins() - if not p.name.startswith('.') and getattr(p, 'visible', True) and p.module in obj.get_plugins() + if not p.name.startswith('.') and getattr(p, 'visible', True) and p.module in active_plugins ]) def to_internal_value(self, data): diff --git a/src/pretix/base/plugins.py b/src/pretix/base/plugins.py index 35647a1ba6..a1e1eec71d 100644 --- a/src/pretix/base/plugins.py +++ b/src/pretix/base/plugins.py @@ -49,14 +49,39 @@ class PluginType(Enum): EXPORT = 4 +def plugin_is_available(meta, event=None, organizer=None): + if not hasattr(meta.app, 'is_available'): + return True + + level = getattr(meta, "level", PLUGIN_LEVEL_EVENT) + if level == PLUGIN_LEVEL_EVENT: + if event: + return meta.app.is_available(event) + elif organizer: + if not hasattr(organizer, '_plugin_availability_fallback_event'): + with scope(organizer=organizer): + setattr(organizer, '_plugin_availability_fallback_event', organizer.events.first()) + return ( + organizer._plugin_availability_fallback_event + and meta.app.is_available(organizer._plugin_availability_fallback_event) + ) + elif level == PLUGIN_LEVEL_ORGANIZER: + if organizer: + return meta.app.is_available(organizer) + elif event: + return meta.app.is_available(event.organizer) + elif level == PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID and (event or organizer): + return meta.app.is_available(event or organizer) + + return True + + def get_all_plugins(*, event=None, organizer=None) -> List[type]: """ Returns the PretixPluginMeta classes of all plugins found in the installed Django apps. """ assert not event or not organizer plugins = [] - event_fallback = None - event_fallback_used = False for app in apps.get_app_configs(): if hasattr(app, 'PretixPluginMeta'): meta = app.PretixPluginMeta @@ -65,28 +90,8 @@ def get_all_plugins(*, event=None, organizer=None) -> List[type]: if app.name in settings.PRETIX_PLUGINS_EXCLUDE: continue - level = getattr(meta, "level", PLUGIN_LEVEL_EVENT) - if level == PLUGIN_LEVEL_EVENT: - if event and hasattr(app, 'is_available'): - if not app.is_available(event): - continue - elif organizer and hasattr(app, 'is_available'): - if not event_fallback_used: - with scope(organizer=organizer): - event_fallback = organizer.events.first() - event_fallback_used = True - if not event_fallback or not app.is_available(event_fallback): - continue - elif level == PLUGIN_LEVEL_ORGANIZER: - if organizer and hasattr(app, 'is_available'): - if not app.is_available(organizer): - continue - elif event and hasattr(app, 'is_available'): - if not app.is_available(event.organizer): - continue - elif level == PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID and (event or organizer) and hasattr(app, 'is_available'): - if not app.is_available(event or organizer): - continue + if not plugin_is_available(meta, event, organizer): + continue plugins.append(meta) return sorted( diff --git a/src/pretix/control/views/organizer.py b/src/pretix/control/views/organizer.py index 03404813f9..e4854b24cb 100644 --- a/src/pretix/control/views/organizer.py +++ b/src/pretix/control/views/organizer.py @@ -102,7 +102,7 @@ from pretix.base.models.organizer import ( from pretix.base.payment import PaymentException from pretix.base.plugins import ( PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID, - PLUGIN_LEVEL_ORGANIZER, + PLUGIN_LEVEL_ORGANIZER, plugin_is_available, ) from pretix.base.services.export import ( init_organizer_exporters, multiexport, scheduled_organizer_export, @@ -597,6 +597,13 @@ class OrganizerCreate(CreateView): }) +def available_plugins(organizer): + from pretix.base.plugins import get_all_plugins + + return (p for p in get_all_plugins(organizer=organizer) if not p.name.startswith('.') + and getattr(p, 'visible', True)) + + class OrganizerPlugins(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, TemplateView, SingleObjectMixin): model = Organizer context_object_name = 'organizer' @@ -606,12 +613,6 @@ class OrganizerPlugins(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi def get_object(self, queryset=None) -> Organizer: return self.request.organizer - def available_plugins(self, organizer): - from pretix.base.plugins import get_all_plugins - - return (p for p in get_all_plugins(organizer=organizer) if not p.name.startswith('.') - and getattr(p, 'visible', True)) - def prepare_links(self, pluginmeta, key): links = getattr(pluginmeta, key, []) try: @@ -637,7 +638,7 @@ class OrganizerPlugins(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi from pretix.base.plugins import CATEGORY_LABELS, CATEGORY_ORDER context = super().get_context_data(*args, **kwargs) - plugins = list(self.available_plugins(self.object)) + plugins = list(available_plugins(self.object)) active_counter = Counter() events_total = 0 @@ -685,7 +686,7 @@ class OrganizerPlugins(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi self.object = self.get_object() plugins_available = { - p.module: p for p in self.available_plugins(self.object) + p.module: p for p in available_plugins(self.object) } choose_events_next = False with transaction.atomic(): @@ -786,12 +787,6 @@ class OrganizerPluginEvents(OrganizerDetailViewMixin, OrganizerPermissionRequire } return kwargs - def available_plugins(self, organizer): - from pretix.base.plugins import get_all_plugins - - return (p for p in get_all_plugins(organizer=organizer) if not p.name.startswith('.') - and getattr(p, 'visible', True)) - def get_context_data(self, **kwargs): return super().get_context_data( plugin=self.plugin, @@ -799,12 +794,10 @@ class OrganizerPluginEvents(OrganizerDetailViewMixin, OrganizerPermissionRequire ) def dispatch(self, request, *args, **kwargs): - plugins_available = { - p.module: p for p in self.available_plugins(self.request.organizer) - } - if kwargs["plugin"] not in plugins_available: + try: + self.plugin = next(p for p in available_plugins(self.request.organizer) if p.module == kwargs["plugin"]) + except StopIteration: raise Http404(_("Unknown plugin.")) - self.plugin = plugins_available[kwargs["plugin"]] level = getattr(self.plugin, "level", PLUGIN_LEVEL_EVENT) if level == PLUGIN_LEVEL_ORGANIZER: raise Http404(_("This plugin can only be enabled for the entire organizer account.")) @@ -835,6 +828,9 @@ class OrganizerPluginEvents(OrganizerDetailViewMixin, OrganizerPermissionRequire logentries_to_save = [] for e in self.request.organizer.events.filter(pk__in=events_to_enable): + if not plugin_is_available(self.plugin, organizer=self.request.organizer, event=e): + messages.warning(self.request, _("This plugin cannot be activated for event {}.").format(e.name)) + continue logentries_to_save.append( e.log_action('pretix.event.plugins.enabled', user=self.request.user, data={'plugin': self.plugin.module}, save=False) )