mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
Organizer-level plugins (#5305)
* Add version notes to the docs * Adapt signal handling * Add UI * Add API * API and tests * Fix registry * Update doc/development/api/plugins.rst Co-authored-by: Felix Rindt <felix@rindt.me> * Fix failing tests * Apply suggestions from code review Co-authored-by: Richard Schreiber <schreiber@rami.io> * Update src/pretix/control/templates/pretixcontrol/organizers/plugin_events.html Co-authored-by: luelista <weller@rami.io> * Update src/pretix/control/templates/pretixcontrol/organizers/plugins.html Co-authored-by: luelista <weller@rami.io> * Update src/pretix/control/templates/pretixcontrol/organizers/plugins.html Co-authored-by: luelista <weller@rami.io> * Update src/pretix/control/navigation.py Co-authored-by: luelista <weller@rami.io> * Update src/pretix/control/urls.py Co-authored-by: luelista <weller@rami.io> * Apply suggestion from @wiffbi * REbase migration * Fix review note * Fix test cases * Remove plugin from all events if disabled on org level * Update doc/development/api/plugins.rst * Unify registries * Rebase migration --------- Co-authored-by: Felix Rindt <felix@rindt.me> Co-authored-by: Richard Schreiber <schreiber@rami.io> Co-authored-by: luelista <weller@rami.io>
This commit is contained in:
@@ -38,13 +38,13 @@ from pretix.base.datasync.sourcefields import (
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.logentrytype_registry import make_link
|
||||
from pretix.base.models.datasync import OrderSyncQueue, OrderSyncResult
|
||||
from pretix.base.signals import EventPluginRegistry
|
||||
from pretix.base.signals import PluginAwareRegistry
|
||||
from pretix.helpers import OF_SELF
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
datasync_providers = EventPluginRegistry({"identifier": lambda o: o.identifier})
|
||||
datasync_providers = PluginAwareRegistry({"identifier": lambda o: o.identifier})
|
||||
|
||||
|
||||
class BaseSyncError(Exception):
|
||||
|
||||
@@ -26,7 +26,7 @@ from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.signals import EventPluginRegistry
|
||||
from pretix.base.signals import PluginAwareRegistry
|
||||
|
||||
|
||||
def make_link(a_map, wrapper, is_active=True, event=None, plugin_name=None):
|
||||
@@ -55,7 +55,7 @@ def make_link(a_map, wrapper, is_active=True, event=None, plugin_name=None):
|
||||
return format_html(wrapper, **a_map)
|
||||
|
||||
|
||||
class LogEntryTypeRegistry(EventPluginRegistry):
|
||||
class LogEntryTypeRegistry(PluginAwareRegistry):
|
||||
def __init__(self):
|
||||
super().__init__({'action_type': lambda o: getattr(o, 'action_type')})
|
||||
|
||||
|
||||
18
src/pretix/base/migrations/0287_organizer_plugins.py
Normal file
18
src/pretix/base/migrations/0287_organizer_plugins.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.17 on 2025-07-12 09:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0286_settingsstore_unique"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="organizer",
|
||||
name="plugins",
|
||||
field=models.TextField(default=""),
|
||||
),
|
||||
]
|
||||
@@ -551,8 +551,7 @@ class Event(EventMixin, LoggedModel):
|
||||
:type presale_end: datetime
|
||||
:param location: venue
|
||||
:type location: str
|
||||
:param plugins: A comma-separated list of plugin names that are active for this
|
||||
event.
|
||||
:param plugins: A comma-separated list of plugin names that are active for this event.
|
||||
:type plugins: str
|
||||
:param has_subevents: Enable event series functionality
|
||||
:type has_subevents: bool
|
||||
@@ -1393,7 +1392,7 @@ class Event(EventMixin, LoggedModel):
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
|
||||
return {
|
||||
p.module: p for p in get_all_plugins(self)
|
||||
p.module: p for p in get_all_plugins(event=self)
|
||||
if not p.name.startswith('.') and getattr(p, 'visible', True)
|
||||
}
|
||||
|
||||
@@ -1412,12 +1411,20 @@ class Event(EventMixin, LoggedModel):
|
||||
self.plugins = ",".join(modules)
|
||||
|
||||
def enable_plugin(self, module, allow_restricted=frozenset()):
|
||||
"""
|
||||
Adds a plugin to the list of plugins, calling its ``installed`` hook (if available).
|
||||
It is the caller's responsibility to save the event object.
|
||||
"""
|
||||
plugins_active = self.get_plugins()
|
||||
if module not in plugins_active:
|
||||
plugins_active.append(module)
|
||||
self.set_active_plugins(plugins_active, allow_restricted=allow_restricted)
|
||||
|
||||
def disable_plugin(self, module):
|
||||
"""
|
||||
Adds a plugin to the list of plugins, calling its ``uninstalled`` hook (if available).
|
||||
It is the caller's responsibility to save the event object.
|
||||
"""
|
||||
plugins_active = self.get_plugins()
|
||||
if module in plugins_active:
|
||||
plugins_active.remove(module)
|
||||
|
||||
@@ -40,9 +40,6 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import connections, models
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
from pretix.base.logentrytype_registry import log_entry_types, make_link
|
||||
from pretix.base.signals import is_app_active, logentry_object_link
|
||||
|
||||
|
||||
class VisibleOnlyManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
@@ -91,6 +88,8 @@ class LogEntry(models.Model):
|
||||
indexes = [models.Index(fields=["datetime", "id"])]
|
||||
|
||||
def display(self):
|
||||
from pretix.base.logentrytype_registry import log_entry_types
|
||||
|
||||
log_entry_type, meta = log_entry_types.get(action_type=self.action_type)
|
||||
if log_entry_type:
|
||||
return log_entry_type.display(self, self.parsed_data)
|
||||
@@ -128,6 +127,11 @@ class LogEntry(models.Model):
|
||||
|
||||
@cached_property
|
||||
def display_object(self):
|
||||
from pretix.base.logentrytype_registry import (
|
||||
log_entry_types, make_link,
|
||||
)
|
||||
from pretix.base.signals import is_app_active, logentry_object_link
|
||||
|
||||
from . import (
|
||||
Discount, Event, Item, Order, Question, Quota, SubEvent, Voucher,
|
||||
)
|
||||
|
||||
@@ -68,6 +68,8 @@ class Organizer(LoggedModel):
|
||||
:param slug: A globally unique, short name for this organizer, to be used
|
||||
in URLs and similar places.
|
||||
:type slug: str
|
||||
:param plugins: A comma-separated list of plugin names that are active for this organizer.
|
||||
:type plugins: str
|
||||
"""
|
||||
|
||||
settings_namespace = 'organizer'
|
||||
@@ -91,6 +93,10 @@ class Organizer(LoggedModel):
|
||||
verbose_name=_("Short form"),
|
||||
unique=True
|
||||
)
|
||||
plugins = models.TextField(
|
||||
verbose_name=_("Plugins"),
|
||||
null=False, blank=True, default="",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Organizer")
|
||||
@@ -119,6 +125,11 @@ class Organizer(LoggedModel):
|
||||
"""
|
||||
self.settings.cookie_consent = True
|
||||
|
||||
plugins = [p for p in settings.PRETIX_PLUGINS_ORGANIZER_DEFAULT.split(",") if p]
|
||||
if plugins:
|
||||
self.set_active_plugins(plugins, allow_restricted=plugins)
|
||||
self.save()
|
||||
|
||||
def get_cache(self):
|
||||
"""
|
||||
Returns an :py:class:`ObjectRelatedCache` object. This behaves equivalent to
|
||||
@@ -143,6 +154,61 @@ class Organizer(LoggedModel):
|
||||
|
||||
return ObjectRelatedCache(self)
|
||||
|
||||
def get_plugins(self):
|
||||
"""
|
||||
Returns the names of the plugins activated for this organizer as a list.
|
||||
"""
|
||||
if not self.plugins:
|
||||
return []
|
||||
return self.plugins.split(",")
|
||||
|
||||
def get_available_plugins(self):
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
|
||||
return {
|
||||
p.module: p for p in get_all_plugins(organizer=self)
|
||||
if not p.name.startswith('.') and getattr(p, 'visible', True)
|
||||
}
|
||||
|
||||
def set_active_plugins(self, modules, allow_restricted=frozenset()):
|
||||
plugins_active = self.get_plugins()
|
||||
plugins_available = self.get_available_plugins()
|
||||
|
||||
enable = [m for m in modules if m not in plugins_active and m in plugins_available]
|
||||
|
||||
for module in enable:
|
||||
if getattr(plugins_available[module].app, 'restricted', False) and module not in allow_restricted:
|
||||
modules.remove(module)
|
||||
elif hasattr(plugins_available[module].app, 'installed'):
|
||||
getattr(plugins_available[module].app, 'installed')(self)
|
||||
|
||||
self.plugins = ",".join(modules)
|
||||
|
||||
def enable_plugin(self, module, allow_restricted=frozenset()):
|
||||
"""
|
||||
Adds a plugin to the list of plugins, calling its ``installed`` hook (if available).
|
||||
It is the caller's responsibility to save the organizer object.
|
||||
"""
|
||||
plugins_active = self.get_plugins()
|
||||
if module not in plugins_active:
|
||||
plugins_active.append(module)
|
||||
self.set_active_plugins(plugins_active, allow_restricted=allow_restricted)
|
||||
|
||||
def disable_plugin(self, module):
|
||||
"""
|
||||
Removes a plugin from the list of plugins, calling its ``uninstalled`` hook (if available).
|
||||
It is the caller's responsibility to save the organizer object and, in case of a hybrid organizer-event plugin,
|
||||
to remove it from all events.
|
||||
"""
|
||||
plugins_active = self.get_plugins()
|
||||
if module in plugins_active:
|
||||
plugins_active.remove(module)
|
||||
self.set_active_plugins(plugins_active)
|
||||
|
||||
plugins_available = self.get_available_plugins()
|
||||
if module in plugins_available and hasattr(plugins_available[module].app, 'uninstalled'):
|
||||
getattr(plugins_available[module].app, 'uninstalled')(self)
|
||||
|
||||
@property
|
||||
def timezone(self):
|
||||
return pytz_deprecation_shim.timezone(self.settings.timezone)
|
||||
|
||||
@@ -28,8 +28,13 @@ import importlib_metadata as metadata
|
||||
from django.apps import AppConfig, apps
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from packaging.requirements import Requirement
|
||||
|
||||
PLUGIN_LEVEL_EVENT = 'event'
|
||||
PLUGIN_LEVEL_ORGANIZER = 'organizer'
|
||||
PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID = 'event_organizer'
|
||||
|
||||
|
||||
class PluginType(Enum):
|
||||
"""
|
||||
@@ -43,11 +48,14 @@ class PluginType(Enum):
|
||||
EXPORT = 4
|
||||
|
||||
|
||||
def get_all_plugins(event=None) -> List[type]:
|
||||
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
|
||||
@@ -56,8 +64,26 @@ def get_all_plugins(event=None) -> List[type]:
|
||||
if app.name in settings.PRETIX_PLUGINS_EXCLUDE:
|
||||
continue
|
||||
|
||||
if hasattr(app, 'is_available') and event:
|
||||
if not app.is_available(event):
|
||||
level = getattr(app, "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:
|
||||
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
|
||||
|
||||
plugins.append(meta)
|
||||
@@ -91,3 +117,26 @@ class PluginConfig(AppConfig, metaclass=PluginConfigMeta):
|
||||
self.name, req, requirement_version
|
||||
))
|
||||
sys.exit(1)
|
||||
|
||||
if not hasattr(self.PretixPluginMeta, 'level'):
|
||||
self.PretixPluginMeta.level = PLUGIN_LEVEL_EVENT
|
||||
if self.PretixPluginMeta.level not in (PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_ORGANIZER, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID):
|
||||
raise ImproperlyConfigured(f"Unknown plugin level '{self.PretixPluginMeta.level}'")
|
||||
|
||||
|
||||
CATEGORY_ORDER = [
|
||||
'FEATURE',
|
||||
'PAYMENT',
|
||||
'INTEGRATION',
|
||||
'CUSTOMIZATION',
|
||||
'FORMAT',
|
||||
'API',
|
||||
]
|
||||
CATEGORY_LABELS = {
|
||||
'FEATURE': _('Features'),
|
||||
'PAYMENT': _('Payment providers'),
|
||||
'INTEGRATION': _('Integrations'),
|
||||
'CUSTOMIZATION': _('Customizations'),
|
||||
'FORMAT': _('Output and export formats'),
|
||||
'API': _('API features'),
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ def restricted_plugin_kwargs():
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
|
||||
plugins_available = [
|
||||
(p.module, p.name) for p in get_all_plugins(None)
|
||||
(p.module, p.name) for p in get_all_plugins()
|
||||
if (
|
||||
not p.name.startswith('.') and
|
||||
getattr(p, 'restricted', False) and
|
||||
|
||||
@@ -33,16 +33,23 @@
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import warnings
|
||||
from typing import Any, Callable, List, Tuple
|
||||
from typing import Any, Callable, Generic, List, Tuple, TypeVar
|
||||
|
||||
import django.dispatch
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.dispatch.dispatcher import NO_RECEIVERS
|
||||
|
||||
from .models import Event
|
||||
from .models.event import Event
|
||||
from .models.organizer import Organizer
|
||||
from .plugins import (
|
||||
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
|
||||
PLUGIN_LEVEL_ORGANIZER,
|
||||
)
|
||||
|
||||
app_cache = {}
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
def _populate_app_cache():
|
||||
@@ -56,6 +63,9 @@ def get_defining_app(o):
|
||||
if "sentry" in o.__module__:
|
||||
o = o.__wrapped__
|
||||
|
||||
if hasattr(o, "__mocked_app"):
|
||||
return o.__mocked_app
|
||||
|
||||
# Find the Django application this belongs to
|
||||
searchpath = o.__module__
|
||||
|
||||
@@ -74,43 +84,71 @@ def get_defining_app(o):
|
||||
return app
|
||||
|
||||
|
||||
def is_app_active(sender, app):
|
||||
def is_app_active(sender, app, allow_legacy_plugins=False):
|
||||
if app == 'CORE':
|
||||
return True
|
||||
|
||||
excluded = settings.PRETIX_PLUGINS_EXCLUDE
|
||||
if sender and app and app.name in sender.get_plugins() and app.name not in excluded:
|
||||
if not sender or not app or app.name in excluded:
|
||||
return False
|
||||
|
||||
level = getattr(app.PretixPluginMeta, "level", PLUGIN_LEVEL_EVENT)
|
||||
if level == PLUGIN_LEVEL_EVENT:
|
||||
if isinstance(sender, Event):
|
||||
enabled = app.name in sender.get_plugins()
|
||||
elif isinstance(sender, Organizer) and allow_legacy_plugins:
|
||||
# Deprecated behaviour: Event plugins that are registered on organizer level are considered active for
|
||||
# all organizers in the context of signals that used to be global signals before the introduction of
|
||||
# organizer-level plugin. A deprecation warning is emitted at .connect() time.
|
||||
enabled = True
|
||||
else:
|
||||
raise ImproperlyConfigured(f"Cannot check if event-level plugin is active on {type(sender)}")
|
||||
elif level == PLUGIN_LEVEL_ORGANIZER:
|
||||
if isinstance(sender, Organizer):
|
||||
enabled = app.name in sender.get_plugins()
|
||||
elif isinstance(sender, Event):
|
||||
enabled = app.name in sender.organizer.get_plugins()
|
||||
else:
|
||||
raise ImproperlyConfigured(f"Cannot check if organizer-level plugin is active on {type(sender)}")
|
||||
elif level == PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID:
|
||||
if isinstance(sender, Organizer):
|
||||
enabled = app.name in sender.get_plugins()
|
||||
elif isinstance(sender, Event):
|
||||
enabled = app.name in sender.get_plugins() and app.name in sender.organizer.get_plugins()
|
||||
else:
|
||||
raise ImproperlyConfigured(f"Cannot check if hybrid event/organizer-level plugin is active on {type(sender)}")
|
||||
else:
|
||||
raise ImproperlyConfigured("Unknown plugin level")
|
||||
|
||||
if enabled:
|
||||
if not hasattr(app, 'compatibility_errors') or not app.compatibility_errors:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_receiver_active(sender, receiver):
|
||||
def is_receiver_active(sender, receiver, allow_legacy_plugins=False):
|
||||
if sender is None:
|
||||
# Send to all events!
|
||||
return True
|
||||
|
||||
app = get_defining_app(receiver)
|
||||
|
||||
return is_app_active(sender, app)
|
||||
return is_app_active(sender, app, allow_legacy_plugins)
|
||||
|
||||
|
||||
class EventPluginSignal(django.dispatch.Signal):
|
||||
"""
|
||||
This is an extension to Django's built-in signals which differs in a way that it sends
|
||||
out it's events only to receivers which belong to plugins that are enabled for the given
|
||||
Event.
|
||||
"""
|
||||
class PluginSignal(Generic[T], django.dispatch.Signal):
|
||||
type = None
|
||||
|
||||
def send(self, sender: Event, **named) -> List[Tuple[Callable, Any]]:
|
||||
def _is_receiver_active(self, sender, receiver):
|
||||
return is_receiver_active(sender, receiver)
|
||||
|
||||
def send(self, sender: T, **named) -> List[Tuple[Callable, Any]]:
|
||||
"""
|
||||
Send signal from sender to all connected receivers that belong to
|
||||
plugins enabled for the given Event.
|
||||
|
||||
sender is required to be an instance of ``pretix.base.models.Event``.
|
||||
plugins enabled for the given event / organizer.
|
||||
"""
|
||||
if sender and not isinstance(sender, Event):
|
||||
raise ValueError("Sender needs to be an event.")
|
||||
if sender and not isinstance(sender, self.type):
|
||||
raise ValueError(f"Sender needs to be of type {self.type}.")
|
||||
|
||||
responses = []
|
||||
if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS:
|
||||
@@ -120,20 +158,18 @@ class EventPluginSignal(django.dispatch.Signal):
|
||||
_populate_app_cache()
|
||||
|
||||
for receiver in self._sorted_receivers(sender):
|
||||
if is_receiver_active(sender, receiver):
|
||||
if self._is_receiver_active(sender, receiver):
|
||||
response = receiver(signal=self, sender=sender, **named)
|
||||
responses.append((receiver, response))
|
||||
return responses
|
||||
|
||||
def send_chained(self, sender: Event, chain_kwarg_name, **named) -> List[Tuple[Callable, Any]]:
|
||||
def send_chained(self, sender: T, chain_kwarg_name, **named) -> List[Tuple[Callable, Any]]:
|
||||
"""
|
||||
Send signal from sender to all connected receivers. The return value of the first receiver
|
||||
will be used as the keyword argument specified by ``chain_kwarg_name`` in the input to the
|
||||
second receiver and so on. The return value of the last receiver is returned by this method.
|
||||
|
||||
sender is required to be an instance of ``pretix.base.models.Event``.
|
||||
"""
|
||||
if sender and not isinstance(sender, Event):
|
||||
if sender and not isinstance(sender, self.type):
|
||||
raise ValueError("Sender needs to be an event.")
|
||||
|
||||
response = named.get(chain_kwarg_name)
|
||||
@@ -144,20 +180,18 @@ class EventPluginSignal(django.dispatch.Signal):
|
||||
_populate_app_cache()
|
||||
|
||||
for receiver in self._sorted_receivers(sender):
|
||||
if is_receiver_active(sender, receiver):
|
||||
if self._is_receiver_active(sender, receiver):
|
||||
named[chain_kwarg_name] = response
|
||||
response = receiver(signal=self, sender=sender, **named)
|
||||
return response
|
||||
|
||||
def send_robust(self, sender: Event, **named) -> List[Tuple[Callable, Any]]:
|
||||
def send_robust(self, sender: T, **named) -> List[Tuple[Callable, Any]]:
|
||||
"""
|
||||
Send signal from sender to all connected receivers. If a receiver raises an exception
|
||||
instead of returning a value, the exception is included as the result instead of
|
||||
stopping the response chain at the offending receiver.
|
||||
|
||||
sender is required to be an instance of ``pretix.base.models.Event``.
|
||||
"""
|
||||
if sender and not isinstance(sender, Event):
|
||||
if sender and not isinstance(sender, self.type):
|
||||
raise ValueError("Sender needs to be an event.")
|
||||
|
||||
responses = []
|
||||
@@ -171,7 +205,7 @@ class EventPluginSignal(django.dispatch.Signal):
|
||||
_populate_app_cache()
|
||||
|
||||
for receiver in self._sorted_receivers(sender):
|
||||
if is_receiver_active(sender, receiver):
|
||||
if self._is_receiver_active(sender, receiver):
|
||||
try:
|
||||
response = receiver(signal=self, sender=sender, **named)
|
||||
except Exception as err:
|
||||
@@ -193,6 +227,67 @@ class EventPluginSignal(django.dispatch.Signal):
|
||||
return sorted_list
|
||||
|
||||
|
||||
class EventPluginSignal(PluginSignal[Event]):
|
||||
"""
|
||||
This is an extension to Django's built-in signals which differs in a way that it sends
|
||||
out it's events only to receivers which belong to plugins that are enabled for the given
|
||||
Event.
|
||||
"""
|
||||
type = Event
|
||||
|
||||
def connect(self, receiver, sender=None, weak=True, dispatch_uid=None):
|
||||
app = get_defining_app(receiver)
|
||||
if app != "CORE":
|
||||
if not hasattr(app, "PretixPluginMeta"):
|
||||
raise ImproperlyConfigured(
|
||||
f"{app} uses an EventPluginSignal but is not a pretix plugin"
|
||||
)
|
||||
allowed_levels = (PLUGIN_LEVEL_ORGANIZER, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID, PLUGIN_LEVEL_EVENT)
|
||||
if getattr(app.PretixPluginMeta, "level", PLUGIN_LEVEL_EVENT) not in allowed_levels:
|
||||
# This check is redundant for now, but will be useful if we ever add other levels
|
||||
raise ImproperlyConfigured(
|
||||
f"{app} uses an EventPluginSignal but is not a plugin that can be active on event or organizer level"
|
||||
)
|
||||
return super().connect(receiver, sender, weak, dispatch_uid)
|
||||
|
||||
|
||||
class OrganizerPluginSignal(PluginSignal[Organizer]):
|
||||
"""
|
||||
This is an extension to Django's built-in signals which differs in a way that it sends
|
||||
out it's events only to receivers which belong to plugins that are enabled for the given
|
||||
Organizer.
|
||||
"""
|
||||
type = Organizer
|
||||
|
||||
def __init__(self, allow_legacy_plugins=False):
|
||||
self.allow_legacy_plugins = allow_legacy_plugins
|
||||
super().__init__()
|
||||
|
||||
def _is_receiver_active(self, sender, receiver):
|
||||
return is_receiver_active(sender, receiver, allow_legacy_plugins=self.allow_legacy_plugins)
|
||||
|
||||
def connect(self, receiver, sender=None, weak=True, dispatch_uid=None):
|
||||
app = get_defining_app(receiver)
|
||||
if app != "CORE":
|
||||
if not hasattr(app, "PretixPluginMeta"):
|
||||
raise ImproperlyConfigured(
|
||||
f"{app} uses an OrganizerPluginSignal but is not a pretix plugin"
|
||||
)
|
||||
allowed_levels = (PLUGIN_LEVEL_ORGANIZER, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID)
|
||||
if getattr(app.PretixPluginMeta, "level", PLUGIN_LEVEL_EVENT) not in allowed_levels:
|
||||
if getattr(app.PretixPluginMeta, "level", PLUGIN_LEVEL_EVENT) == PLUGIN_LEVEL_EVENT and self.allow_legacy_plugins:
|
||||
warnings.warn(
|
||||
'This signal will soon be only available for plugins that declare to be organizer-level',
|
||||
stacklevel=3,
|
||||
category=DeprecationWarning,
|
||||
)
|
||||
else:
|
||||
raise ImproperlyConfigured(
|
||||
f"{app} uses an OrganizerPluginSignal but is not a plugin that can be active on organizer level"
|
||||
)
|
||||
return super().connect(receiver, sender, weak, dispatch_uid)
|
||||
|
||||
|
||||
class GlobalSignal(django.dispatch.Signal):
|
||||
def send_chained(self, sender: Event, chain_kwarg_name, **named) -> List[Tuple[Callable, Any]]:
|
||||
"""
|
||||
@@ -211,10 +306,14 @@ class GlobalSignal(django.dispatch.Signal):
|
||||
return response
|
||||
|
||||
|
||||
class DeprecatedSignal(django.dispatch.Signal):
|
||||
class DeprecatedSignal(GlobalSignal):
|
||||
|
||||
def connect(self, receiver, sender=None, weak=True, dispatch_uid=None):
|
||||
warnings.warn('This signal is deprecated and will soon be removed', stacklevel=3)
|
||||
warnings.warn(
|
||||
'This signal is deprecated and will soon be removed',
|
||||
stacklevel=3,
|
||||
category=DeprecationWarning,
|
||||
)
|
||||
super().connect(receiver, sender=None, weak=True, dispatch_uid=None)
|
||||
|
||||
|
||||
@@ -324,20 +423,39 @@ class Registry:
|
||||
)
|
||||
|
||||
|
||||
class EventPluginRegistry(Registry):
|
||||
class PluginAwareRegistry(Registry):
|
||||
"""
|
||||
A Registry which automatically annotates entries with a "plugin" key, specifying which plugin
|
||||
the entry is defined in. This allows the consumer of entries to determine whether an entry is
|
||||
enabled for a given event, or filter only for entries defined by enabled plugins.
|
||||
enabled for a given event or organizer, or filter only for entries defined by enabled plugins.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
logtype, meta = my_registry.find(action_type="foo.bar.baz")
|
||||
# meta["plugin"] contains the django app name of the defining plugin
|
||||
"""
|
||||
allowed_levels = [
|
||||
PLUGIN_LEVEL_EVENT,
|
||||
PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
|
||||
PLUGIN_LEVEL_ORGANIZER,
|
||||
]
|
||||
|
||||
def __init__(self, keys):
|
||||
super().__init__({"plugin": lambda o: get_defining_app(o), **keys})
|
||||
def get_plugin(o):
|
||||
app = get_defining_app(o)
|
||||
if app != "CORE":
|
||||
if not hasattr(app, "PretixPluginMeta"):
|
||||
raise ImproperlyConfigured(
|
||||
f"{app} uses an PluginAwareRegistry but is not a pretix plugin"
|
||||
)
|
||||
level = getattr(app.PretixPluginMeta, "level", PLUGIN_LEVEL_EVENT)
|
||||
if level not in self.allowed_levels:
|
||||
raise ImproperlyConfigured(
|
||||
f"{app} has level {level} but should have one of {self.allowed_levels} to use this registry"
|
||||
)
|
||||
return app
|
||||
|
||||
super().__init__({"plugin": get_plugin, **keys})
|
||||
|
||||
def filter(self, active_in=None, **kwargs):
|
||||
result = super().filter(**kwargs)
|
||||
@@ -357,6 +475,9 @@ class EventPluginRegistry(Registry):
|
||||
return item, meta
|
||||
|
||||
|
||||
EventPluginRegistry = PluginAwareRegistry # for backwards compatibility
|
||||
|
||||
|
||||
event_live_issues = EventPluginSignal()
|
||||
"""
|
||||
This signal is sent out to determine whether an event can be taken live. If you want to
|
||||
@@ -449,7 +570,7 @@ This signal is sent out when a notification is sent.
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
register_sales_channel_types = django.dispatch.Signal()
|
||||
register_sales_channel_types = GlobalSignal()
|
||||
"""
|
||||
This signal is sent out to get all known sales channels types. Receivers should return an
|
||||
instance of a subclass of ``pretix.base.channels.SalesChannelType`` or a list of such
|
||||
@@ -467,10 +588,8 @@ subclass of pretix.base.exporter.BaseExporter
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
register_multievent_data_exporters = django.dispatch.Signal()
|
||||
register_multievent_data_exporters = OrganizerPluginSignal(allow_legacy_plugins=True)
|
||||
"""
|
||||
Arguments: ``event``
|
||||
|
||||
This signal is sent out to get all known data exporters, which support exporting data for
|
||||
multiple events. Receivers should return a subclass of pretix.base.exporter.BaseExporter
|
||||
|
||||
@@ -742,7 +861,7 @@ The ``sender`` keyword argument will contain the event. The ``target`` will cont
|
||||
copy to, the ``source`` keyword argument will contain the product to **copy from**.
|
||||
"""
|
||||
|
||||
periodic_task = django.dispatch.Signal()
|
||||
periodic_task = GlobalSignal()
|
||||
"""
|
||||
This is a regular django signal (no pretix event signal) that we send out every
|
||||
time the periodic task cronjob runs. This interval is not sharply defined, it can
|
||||
@@ -751,13 +870,13 @@ idempotent, i.e. it should not make a difference if this is sent out more often
|
||||
than expected.
|
||||
"""
|
||||
|
||||
register_global_settings = django.dispatch.Signal()
|
||||
register_global_settings = GlobalSignal()
|
||||
"""
|
||||
All plugins that are installed may send fields for the global settings form, as
|
||||
an OrderedDict of (setting name, form field).
|
||||
"""
|
||||
|
||||
gift_card_transaction_display = django.dispatch.Signal()
|
||||
gift_card_transaction_display = GlobalSignal() # todo: replace with OrganizerPluginSignal?
|
||||
"""
|
||||
Arguments: ``transaction``, ``customer_facing``
|
||||
|
||||
@@ -969,7 +1088,7 @@ return a dictionary mapping names of attributes in the settings store to DRF ser
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
customer_created = GlobalSignal()
|
||||
customer_created = OrganizerPluginSignal(allow_legacy_plugins=True)
|
||||
"""
|
||||
Arguments: ``customer``
|
||||
|
||||
@@ -979,7 +1098,7 @@ object is given as the first argument.
|
||||
The ``sender`` keyword argument will contain the organizer.
|
||||
"""
|
||||
|
||||
customer_signed_in = GlobalSignal()
|
||||
customer_signed_in = OrganizerPluginSignal(allow_legacy_plugins=True)
|
||||
"""
|
||||
Arguments: ``customer``
|
||||
|
||||
@@ -989,7 +1108,7 @@ is given as the first argument.
|
||||
The ``sender`` keyword argument will contain the organizer.
|
||||
"""
|
||||
|
||||
device_info_updated = django.dispatch.Signal()
|
||||
device_info_updated = GlobalSignal() # todo: replace with OrganizerPluginSignal?
|
||||
"""
|
||||
Arguments: ``old_device``, ``new_device``
|
||||
|
||||
|
||||
Reference in New Issue
Block a user