diff --git a/src/pretix/api/serializers/event.py b/src/pretix/api/serializers/event.py index 5ffa7cfbfb..dd20324e62 100644 --- a/src/pretix/api/serializers/event.py +++ b/src/pretix/api/serializers/event.py @@ -268,14 +268,9 @@ class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer): return {'seat_category_mapping': result} def validate_plugins(self, value): - from pretix.base.plugins import get_all_plugins - - plugins_available = { - p.module: p for p in get_all_plugins(event=self.instance) - if not p.name.startswith('.') and getattr(p, 'visible', True) - } current_plugins = self.instance.get_plugins() if self.instance and self.instance.pk else [] settings_holder = self.instance if self.instance and self.instance.pk else self.context['organizer'] + plugins_available = settings_holder.get_available_plugins() allowed_levels = (PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID) for plugin in value.get('plugins'): diff --git a/src/pretix/api/serializers/fields.py b/src/pretix/api/serializers/fields.py index d877028ab7..e1a9e51b0c 100644 --- a/src/pretix/api/serializers/fields.py +++ b/src/pretix/api/serializers/fields.py @@ -114,11 +114,11 @@ class UploadedFileField(serializers.Field): class PluginsField(serializers.Field): def to_representation(self, obj): - from pretix.base.plugins import get_all_plugins + from pretix.base.plugins import iter_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 active_plugins + p.module for p in iter_all_plugins(only_visible=True) + if p.module in active_plugins ]) def to_internal_value(self, data): diff --git a/src/pretix/api/serializers/organizer.py b/src/pretix/api/serializers/organizer.py index fc77cc5d24..7047b01c15 100644 --- a/src/pretix/api/serializers/organizer.py +++ b/src/pretix/api/serializers/organizer.py @@ -80,12 +80,7 @@ class OrganizerSerializer(I18nAwareModelSerializer): fields = ('name', 'slug', 'public_url', 'plugins') def validate_plugins(self, value): - from pretix.base.plugins import get_all_plugins - - plugins_available = { - p.module: p for p in get_all_plugins(organizer=self.instance) - if not p.name.startswith('.') and getattr(p, 'visible', True) - } + plugins_available = self.instance.get_available_plugins() settings_holder = self.instance allowed_levels = (PLUGIN_LEVEL_ORGANIZER, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID) diff --git a/src/pretix/api/views/organizer.py b/src/pretix/api/views/organizer.py index a712d37dae..d8bff6f2ab 100644 --- a/src/pretix/api/views/organizer.py +++ b/src/pretix/api/views/organizer.py @@ -92,8 +92,6 @@ class OrganizerViewSet(mixins.UpdateModelMixin, viewsets.ReadOnlyModelViewSet): @transaction.atomic() def perform_update(self, serializer): - from pretix.base.plugins import get_all_plugins - original_data = self.get_serializer(instance=serializer.instance).data current_plugins_value = serializer.instance.get_plugins() @@ -111,11 +109,7 @@ class OrganizerViewSet(mixins.UpdateModelMixin, viewsets.ReadOnlyModelViewSet): disabled = {m: 'disabled' for m in current_plugins_value if m not in updated_plugins_value} changed = merge_dicts(enabled, disabled) - plugins_available = { - p.module: p - for p in get_all_plugins(organizer=serializer.instance) - if not p.name.startswith('.') and getattr(p, 'visible', True) - } + plugins_available = serializer.instance.get_available_plugins() qs = [] for module in disabled: pluginmeta = plugins_available[module] diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index 235ab89f9c..f8ca0369e8 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -1475,12 +1475,9 @@ class Event(EventMixin, LoggedModel): self.subevents.all().delete() def get_available_plugins(self): - from pretix.base.plugins import get_all_plugins + from pretix.base.plugins import get_all_plugins_map - return { - p.module: p for p in get_all_plugins(event=self) - if not p.name.startswith('.') and getattr(p, 'visible', True) - } + return get_all_plugins_map(event=self, only_visible=True) def set_active_plugins(self, modules, allow_restricted=frozenset()): plugins_active = self.get_plugins() diff --git a/src/pretix/base/models/organizer.py b/src/pretix/base/models/organizer.py index 37608d3dad..8704d69c2c 100644 --- a/src/pretix/base/models/organizer.py +++ b/src/pretix/base/models/organizer.py @@ -175,12 +175,9 @@ class Organizer(LoggedModel): return self.plugins.split(",") def get_available_plugins(self): - from pretix.base.plugins import get_all_plugins + from pretix.base.plugins import get_all_plugins_map - return { - p.module: p for p in get_all_plugins(organizer=self) - if not p.name.startswith('.') and getattr(p, 'visible', True) - } + return get_all_plugins_map(organizer=self, only_visible=True) def set_active_plugins(self, modules, allow_restricted=frozenset()): plugins_active = self.get_plugins() diff --git a/src/pretix/base/plugins.py b/src/pretix/base/plugins.py index a1e1eec71d..789ed8af25 100644 --- a/src/pretix/base/plugins.py +++ b/src/pretix/base/plugins.py @@ -22,7 +22,7 @@ import os import sys from enum import Enum -from typing import List +from typing import Iterable, List import importlib_metadata as metadata from django.apps import AppConfig, apps @@ -49,7 +49,10 @@ class PluginType(Enum): EXPORT = 4 -def plugin_is_available(meta, event=None, organizer=None): +def plugin_is_available(meta, event=None, organizer=None, only_visible=False): + if only_visible and (meta.name.startswith('.') or not getattr(meta, 'visible', True)): + return False + if not hasattr(meta.app, 'is_available'): return True @@ -76,30 +79,44 @@ def plugin_is_available(meta, event=None, organizer=None): return True -def get_all_plugins(*, event=None, organizer=None) -> List[type]: +def get_plugin_meta_from_app_config(app): + if not hasattr(app, 'PretixPluginMeta'): + return None + meta = app.PretixPluginMeta + meta.module = app.name + meta.app = app + if app.name in settings.PRETIX_PLUGINS_EXCLUDE: + return None + return meta + + +def iter_all_plugins(*, event=None, organizer=None, only_visible=False) -> Iterable[type]: + assert not event or not organizer + for app in apps.get_app_configs(): + if meta := get_plugin_meta_from_app_config(app): + if plugin_is_available(meta, event, organizer, only_visible): + yield meta + + +def get_all_plugins(*, event=None, organizer=None, only_visible=False) -> List[type]: """ Returns the PretixPluginMeta classes of all plugins found in the installed Django apps. + + Of the `event` and `organizer` params, at most one may be filled, and they are only used for + calling `is_available`, not for filtering by plugin level. """ - assert not event or not organizer - plugins = [] - for app in apps.get_app_configs(): - if hasattr(app, 'PretixPluginMeta'): - meta = app.PretixPluginMeta - meta.module = app.name - meta.app = app - if app.name in settings.PRETIX_PLUGINS_EXCLUDE: - continue - - if not plugin_is_available(meta, event, organizer): - continue - - plugins.append(meta) return sorted( - plugins, + iter_all_plugins(event=event, organizer=organizer, only_visible=only_visible), key=lambda m: (0 if m.module.startswith('pretix.') else 1, str(m.name).lower().replace('pretix ', '')) ) +def get_all_plugins_map(*, event=None, organizer=None, only_visible=False) -> dict[str, type]: + return { + p.module: p for p in iter_all_plugins(event=event, organizer=organizer, only_visible=only_visible) + } + + class PluginConfigMeta(type): def __getattribute__(cls, item): if item == "default" and cls is PluginConfig: diff --git a/src/pretix/control/views/organizer.py b/src/pretix/control/views/organizer.py index 0eea804b53..b37c43ab68 100644 --- a/src/pretix/control/views/organizer.py +++ b/src/pretix/control/views/organizer.py @@ -793,9 +793,8 @@ class OrganizerPluginEvents(OrganizerDetailViewMixin, OrganizerPermissionRequire ) def dispatch(self, request, *args, **kwargs): - try: - self.plugin = next(p for p in available_plugins(self.request.organizer) if p.module == kwargs["plugin"]) - except StopIteration: + self.plugin = self.request.organizer.get_available_plugins().get(kwargs["plugin"]) + if not self.plugin: raise Http404(_("Unknown plugin.")) level = getattr(self.plugin, "level", PLUGIN_LEVEL_EVENT) if level == PLUGIN_LEVEL_ORGANIZER: diff --git a/src/pretix/plugins/stripe/payment.py b/src/pretix/plugins/stripe/payment.py index 46ce909dcc..3db3179820 100644 --- a/src/pretix/plugins/stripe/payment.py +++ b/src/pretix/plugins/stripe/payment.py @@ -69,7 +69,7 @@ from pretix.base.models import ( from pretix.base.payment import ( BasePaymentProvider, PaymentException, WalletQueries, ) -from pretix.base.plugins import get_all_plugins +from pretix.base.plugins import get_all_plugins_map from pretix.base.settings import SettingsSandbox from pretix.base.views.redirect import safelink from pretix.helpers import OF_SELF @@ -234,7 +234,7 @@ class StripeSettingsHolder(BasePaymentProvider): @property def settings_form_fields(self): - if 'pretix_resellers' in [p.module for p in get_all_plugins()]: + if 'pretix_resellers' in get_all_plugins_map(): moto_settings = [ ('reseller_moto', forms.BooleanField(