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:
Raphael Michel
2025-08-19 11:33:34 +02:00
committed by GitHub
parent 56964b6764
commit a51a6123f5
50 changed files with 1623 additions and 192 deletions

View File

@@ -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):

View File

@@ -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')})

View 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=""),
),
]

View File

@@ -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)

View File

@@ -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,
)

View File

@@ -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)

View File

@@ -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'),
}

View File

@@ -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

View File

@@ -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``