# # This file is part of pretix (Community Edition). # # Copyright (C) 2014-2020 Raphael Michel and contributors # Copyright (C) 2020-today pretix GmbH and contributors # # This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General # Public License as published by the Free Software Foundation in version 3 of the License. # # ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are # applicable granting you additional permissions and placing additional restrictions on your usage of this software. # Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive # this file, see . # # This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more # details. # # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # # This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of # the Apache License 2.0 can be obtained at . # # This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A # full history of changes and contributors is available at . # # This file contains Apache-licensed contributions copyrighted by: Jakob Schnell, Tobias Kunze, Tobias Kunze, pajowu # # Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations under the License. import warnings 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.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(): apps.check_apps_ready() for ac in apps.app_configs.values(): app_cache[ac.name] = ac def get_defining_app(o): # If sentry packed this in a wrapper, unpack that 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__ # Core modules are always active if any(searchpath.startswith(cm) for cm in settings.CORE_MODULES): return 'CORE' if not app_cache: _populate_app_cache() while True: app = app_cache.get(searchpath) if "." not in searchpath or app: break searchpath, _ = searchpath.rsplit(".", 1) return app def is_app_active(sender, app, allow_legacy_plugins=False): if app == 'CORE': return True excluded = settings.PRETIX_PLUGINS_EXCLUDE 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 plugins. A deprecation warning is emitted at .connect() time. enabled = True else: raise ImproperlyConfigured(f"Cannot check if event 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 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 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, 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, allow_legacy_plugins) class PluginSignal(Generic[T], django.dispatch.Signal): type = None 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 / organizer. """ 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: return responses if not app_cache: _populate_app_cache() for receiver in self._sorted_receivers(sender): 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: 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. """ if sender and not isinstance(sender, self.type): raise ValueError(f"Sender needs to be of type {self.type}.") response = named.get(chain_kwarg_name) if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS: return response if not app_cache: _populate_app_cache() for receiver in self._sorted_receivers(sender): 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: 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. """ 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 ): return [] if not app_cache: _populate_app_cache() for receiver in self._sorted_receivers(sender): if self._is_receiver_active(sender, receiver): try: response = receiver(signal=self, sender=sender, **named) except Exception as err: responses.append((receiver, err)) else: responses.append((receiver, response)) return responses def _sorted_receivers(self, sender): orig_list = self._live_receivers(sender) sorted_list = sorted( orig_list, key=lambda receiver: ( 0 if any(receiver.__module__.startswith(m) for m in settings.CORE_MODULES) else 1, receiver.__module__, receiver.__name__, ) ) 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 its 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 its 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]]: """ 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. """ response = named.get(chain_kwarg_name) if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS: return response for receiver in self._live_receivers(sender): named[chain_kwarg_name] = response response = receiver(signal=self, sender=sender, **named) return response 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, category=DeprecationWarning, ) super().connect(receiver, sender=None, weak=True, dispatch_uid=None) class Registry: """ A Registry is a collection of objects (entries), annotated with metadata. Entries can be searched and filtered by metadata keys, and metadata is returned as part of the result. Entry metadata is generated during registration using to the accessor functions given to the Registry constructor. Example: .. code-block:: python animal_sound_registry = Registry({"animal": lambda s: s.animal}) @animal_sound_registry.new("dog", "woof") @animal_sound_registry.new("cricket", "chirp") class AnimalSound: def __init__(self, animal, sound): self.animal = animal self.sound = sound def make_sound(self): return self.sound @animal_sound_registry.new() class CatSound(AnimalSound): def __init__(self): super().__init__(animal="cat", sound=["meow", "meww", "miaou"]) def make_sound(self): return random.choice(self.sound) """ def __init__(self, keys): """ :param keys: Dictionary with `{key: accessor_function}` When a new entry is registered, all accessor functions are called with the new entry as parameter. Their return value is stored as the metadata value for that key. """ self.keys = keys self.clear() def clear(self): """ Removes all entries from the registry. """ self.registered_entries = dict() self.by_key = {key: {} for key in self.keys.keys()} def register(self, *objs): """ Register one or more entries in this registry. Usable as a regular method or as decorator on a class or function. If used on a class, the class type object itself is registered, not an instance of the class. To register an instance, use the ``new`` method. .. code-block:: python @some_registry.register def my_new_entry(foo): # ... """ for obj in objs: if obj in self.registered_entries: raise RuntimeError('Object already registered: {}'.format(obj)) meta = {k: accessor(obj) for k, accessor in self.keys.items()} tup = (obj, meta) for key, value in meta.items(): self.by_key[key][value] = tup self.registered_entries[obj] = meta if len(objs) == 1: return objs[0] def new(self, *args, **kwargs): """ Instantiate the decorated class with the given `*args` and `**kwargs`, and register the instance in this registry. May be used multiple times. .. code-block:: python @animal_sound_registry.new("meow") @animal_sound_registry.new("woof") class AnimalSound: def __init__(self, sound): # ... """ def reg(clz): obj = clz(*args, **kwargs) self.register(obj) return clz return reg def get(self, **kwargs): (key, value), = kwargs.items() return self.by_key.get(key).get(value, (None, None)) def filter(self, **kwargs): return ( (entry, meta) for entry, meta in self.registered_entries.items() if all(value == meta[key] for key, value in kwargs.items()) ) 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 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): 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) if active_in is not None: result = ( (entry, meta) for entry, meta in result if is_app_active(active_in, meta['plugin']) ) return result def get(self, active_in=None, **kwargs): item, meta = super().get(**kwargs) if meta and active_in is not None: if not is_app_active(active_in, meta['plugin']): return None, None 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 prevent the event from going live, return a string that will be displayed to the user as the error message. If you don't, your receiver should return ``None``. As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ register_payment_providers = EventPluginSignal() """ This signal is sent out to get all known payment providers. Receivers should return a subclass of pretix.base.payment.BasePaymentProvider or a list of these As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ register_text_placeholders = EventPluginSignal() """ This signal is sent out to get all known text placeholders. Receivers should return an instance of a subclass of pretix.base.services.placeholders.BaseTextPlaceholder or a list of these. As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ register_mail_placeholders = EventPluginSignal() """ **DEPRECATED**: This signal has a new name, please use ``register_text_placeholders`` instead. """ register_html_mail_renderers = EventPluginSignal() """ This signal is sent out to get all known HTML email renderers. Receivers should return a subclass of pretix.base.email.BaseHTMLMailRenderer or a list of these As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ register_invoice_renderers = EventPluginSignal() """ This signal is sent out to get all known invoice renderers. Receivers should return a subclass of pretix.base.invoice.BaseInvoiceRenderer or a list of these As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ register_ticket_secret_generators = EventPluginSignal() """ This signal is sent out to get all known ticket secret generators. Receivers should return a subclass of ``pretix.base.secrets.BaseTicketSecretGenerator`` or a list of these As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ register_data_shredders = EventPluginSignal() """ This signal is sent out to get all known data shredders. Receivers should return a subclass of pretix.base.shredder.BaseDataShredder or a list of these As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ register_ticket_outputs = EventPluginSignal() """ This signal is sent out to get all known ticket outputs. Receivers should return a subclass of pretix.base.ticketoutput.BaseTicketOutput As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ register_notification_types = EventPluginSignal() """ This signal is sent out to get all known notification types. Receivers should return an instance of a subclass of pretix.base.notifications.NotificationType or a list of such instances. As with all event-plugin signals, the ``sender`` keyword argument will contain the event, however for this signal, the ``sender`` **may also be None** to allow creating the general notification settings! """ notification = EventPluginSignal() """ Arguments: ``logentry_id``, ``notification_type`` 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 = 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 instances. """ register_sales_channels = DeprecatedSignal() # TODO: remove me register_data_exporters = EventPluginSignal() """ This signal is sent out to get all known data exporters. Receivers should return a subclass of pretix.base.exporter.BaseExporter As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ register_multievent_data_exporters = OrganizerPluginSignal(allow_legacy_plugins=True) """ 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 The ``sender`` keyword argument will contain an organizer. """ build_invoice_data = EventPluginSignal() """ Arguments: ``invoice`` This signal is sent out every time an invoice is built, after the invoice model was created and filled and before the PDF generation task is started. You can use this to make changes to the invoice, but we recommend to mostly use it to add content to ``Invoice.plugin_data``. You are responsible for saving any changes to the database. As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ validate_order = EventPluginSignal() """ Arguments: ``payments``, ``positions``, ``email``, ``locale``, ``invoice_address``, ``meta_info``, ``customer`` This signal is sent out when the user tries to confirm the order, before we actually create the order. It allows you to inspect the cart positions. Your return value will be ignored, but you can raise an OrderError with an appropriate exception message if you like to block the order. We strongly discourage making changes to the order here. As with all event-plugin signals, the ``sender`` keyword argument will contain the event. **DEPRECTATION:** Stop listening to the ``payment_provider`` attribute, it will be removed in the future, as the ``payments`` attribute gives more information. """ order_valid_if_pending = EventPluginSignal() """ Arguments: ``payments``, ``positions``, ``email``, ``locale``, ``invoice_address``, ``meta_info``, ``customer`` This signal is sent out when the user tries to confirm the order, before we actually create the order. It allows you to set the ``valid_if_pending`` of the order even before it is created. Whenever any plugin returns ``True``, the order will be valid if pending. As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ validate_cart = EventPluginSignal() """ Arguments: ``positions`` This signal is sent out before the user starts checkout. It includes an iterable with the current CartPosition objects. The response of receivers will be ignored, but you can raise a CartError with an appropriate exception message. As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ validate_cart_addons = EventPluginSignal() """ Arguments: ``addons``, ``base_position``, ``iao`` This signal is sent when a user tries to select a combination of addons. In contrast to ``validate_cart``, this is executed before the cart is actually modified. You are passed an argument ``addons`` containing a dict of ``(item, variation or None) → count`` tuples as well as the ``ItemAddOn`` object as the argument ``iao`` and the base cart position as ``base_position``. The response of receivers will be ignored, but you can raise a CartError with an appropriate exception message. As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ order_placed = EventPluginSignal() """ Arguments: ``order``, ``bulk`` This signal is sent out every time an order is placed. The order object is given as the first argument. The ``bulk`` argument specifies whether the order was placed as part of a bulk action, e.g. an import from a file. This signal is *not* sent out if an order is created through splitting an existing order, so you can not expect to see all orders by listening to this signal. As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ order_paid = EventPluginSignal() """ Arguments: ``order`` This signal is sent out every time an order is paid. The order object is given as the first argument. This signal is *not* sent out if an order is marked as paid because an already-paid order has been split. As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ order_canceled = EventPluginSignal() """ Arguments: ``order`` This signal is sent out every time an order is canceled. The order object is given as the first argument. As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ order_reactivated = EventPluginSignal() """ Arguments: ``order`` This signal is sent out every time a canceled order is reactivated. The order object is given as the first argument. As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ order_expired = EventPluginSignal() """ Arguments: ``order`` This signal is sent out every time an order is marked as expired. The order object is given as the first argument. As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ order_expiry_changed = EventPluginSignal() """ Arguments: ``order`` This signal is sent out every time an order expiry date is changed as an explicit operation (i.e. not if this is the result of an approval or order change). The order object is given as the first argument. As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ order_modified = EventPluginSignal() """ Arguments: ``order`` This signal is sent out every time an order's information is modified. The order object is given as the first argument. In contrast to ``order_changed``, this signal is sent out if information of an order or any of it's position is changed that concerns user input, such as attendee names, invoice addresses or question answers. If the order changes in a material way, such as changed products, prices, or tax rates, ``order_changed`` is used instead. As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ order_changed = EventPluginSignal() """ Arguments: ``order`` This signal is sent out every time an order's content is changed. The order object is given as the first argument. In contrast to ``modified``, this signal is sent out if the order or any of its positions changes in a material way, such as changed products, prices, or tax rates, ``order_changed`` is used instead. If "only" user input is changed, such as attendee names, invoice addresses or question answers, ``order_modified`` is used instead. As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ order_approved = EventPluginSignal() """ Arguments: ``order`` This signal is sent out every time an order is being approved. The order object is given as the first argument. As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ order_denied = EventPluginSignal() """ Arguments: ``order`` This signal is sent out every time an order is being denied. The order object is given as the first argument. As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ order_gracefully_delete = EventPluginSignal() """ Arguments: ``order`` This signal is sent out every time a test-mode order is being deleted. The order object is given as the first argument. Any plugin receiving this signals is supposed to perform any cleanup necessary at this point, so that the underlying order has no more external constraints that would inhibit the deletion of the order. As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ checkin_created = EventPluginSignal() """ Arguments: ``checkin`` This signal is sent out every time a check-in is created (i.e. an order position is marked as checked in). It is not send if the position was already checked in and is force-checked-in a second time. The check-in object is given as the first argument. For backwards compatibility reasons, this signal is only sent when a **successful** scan is saved. As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ checkin_annulled = EventPluginSignal() """ Arguments: ``checkin`` This signal is sent out every time a check-in is annulled (i.e. changed to unsuccessful after it already was successful). As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ logentry_display = EventPluginSignal() """ Arguments: ``logentry`` **DEPRECTATION:** Please do not use this signal for new LogEntry types. Use the log_entry_types registry instead, as described in https://docs.pretix.eu/en/latest/development/implementation/logging.html """ logentry_object_link = EventPluginSignal() """ Arguments: ``logentry`` **DEPRECTATION:** Please do not use this signal for new LogEntry types. Use the log_entry_types registry instead, as described in https://docs.pretix.eu/en/latest/development/implementation/logging.html """ requiredaction_display = EventPluginSignal() """ **DEPRECATED**, will no longer be called. """ event_copy_data = EventPluginSignal() """ Arguments: "other", ``tax_map``, ``category_map``, ``item_map``, ``question_map``, ``variation_map``, ``checkin_list_map``, ``quota_map`` This signal is sent out when a new event is created as a clone of an existing event, i.e. the settings from the older event are copied to the newer one. You can listen to this signal to copy data or configuration stored within your plugin's models as well. You don't need to copy data inside the general settings storage which is cloned automatically, but you might need to modify that data. The ``sender`` keyword argument will contain the event of the **new** event. The ``other`` keyword argument will contain the event to **copy from**. The keyword arguments ``tax_map``, ``category_map``, ``item_map``, ``question_map``, ``quota_map``, ``variation_map`` and ``checkin_list_map`` contain mappings from object IDs in the original event to objects in the new event of the respective types. """ orderposition_blocked_display = EventPluginSignal() """ Arguments: ``orderposition``, ``block_name`` To display the reason for a blocked ticket to a backend user, ``pretix.base.signals.orderposition_block_display`` will be sent out. The first received response that is not ``None`` will be used to display the block to the user. The receivers are expected to return plain text. As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ item_copy_data = EventPluginSignal() """ Arguments: ``source``, ``target`` This signal is sent out when a new product is created as a clone of an existing product, i.e. the settings from the older product are copied to the newer one. You can listen to this signal to copy data or configuration stored within your plugin's models as well. The ``sender`` keyword argument will contain the event. The ``target`` will contain the item to copy to, the ``source`` keyword argument will contain the product to **copy from**. """ 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 be everything between a minute and a day. The actions you perform should be idempotent, i.e. it should not make a difference if this is sent out more often than expected. """ 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 = GlobalSignal() # todo: replace with OrganizerPluginSignal? """ Arguments: ``transaction``, ``customer_facing`` To display an instance of the ``GiftCardTransaction`` model to a human user, ``pretix.base.signals.gift_card_transaction_display`` will be sent out with a ``transaction`` argument. The ``customer_facing`` argument specifies whether the HTML will be shown to an end-user or if it is being used in the backend. The first received response that is not ``None`` will be used to display the log entry to the user. The receivers are expected to return a string (that might be marked with ``mark_safe`` from Django if it contains HTML). """ order_fee_calculation = EventPluginSignal() """ Arguments: ``positions``, ``invoice_address``, ``meta_info``, ``total``, ``gift_cards``, ``payment_requests`` This signals allows you to add fees to an order while it is being created. You are expected to return a list of ``OrderFee`` objects that are not yet saved to the database (because there is no order yet). As with all event plugin signals, the ``sender`` keyword argument will contain the event. A ``positions`` argument will contain the cart positions and ``invoice_address`` the invoice address (useful for tax calculation). The argument ``meta_info`` contains the order's meta dictionary. The ``total`` keyword argument will contain the total cart sum without any fees. You should not rely on this ``total`` value for fee calculations as other fees might interfere. The ``gift_cards`` argument lists the gift cards in use. **DEPRECTATION:** Stop listening to the ``gift_cards`` attribute, it will be removed in the future. """ order_fee_type_name = EventPluginSignal() """ Arguments: ``request``, ``fee`` This signals allows you to return a human-readable description for a fee type based on the ``fee_type`` and ``internal_type`` attributes of the ``OrderFee`` model that you get as keyword arguments. You are expected to return a string or None, if you don't know about this fee. As with all event plugin signals, the ``sender`` keyword argument will contain the event. """ allow_ticket_download = EventPluginSignal() """ Arguments: ``order`` This signal is sent out to check if tickets for an order can be downloaded. If any receiver returns false, a download will not be offered. If a receiver returns a list of OrderPositions, only those will be downloadable. As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ email_filter = EventPluginSignal() """ Arguments: ``message``, ``order``, ``user`` This signal allows you to implement a middleware-style filter on all outgoing emails. You are expected to return a (possibly modified) copy of the message object passed to you. As with all event-plugin signals, the ``sender`` keyword argument will contain the event. The ``message`` argument will contain an ``EmailMultiAlternatives`` object. If the email is associated with a specific order, the ``order`` argument will be passed as well, otherwise it will be ``None``. If the email is associated with a specific user, e.g. a notification email, the ``user`` argument will be passed as well, otherwise it will be ``None``. """ global_email_filter = GlobalSignal() """ Arguments: ``message``, ``order``, ``user``, ``customer``, ``organizer`` This signal allows you to implement a middleware-style filter on all outgoing emails. You are expected to return a (possibly modified) copy of the message object passed to you. This signal is called on all events and even if there is no known event. ``sender`` is an event or None. The ``message`` argument will contain an ``EmailMultiAlternatives`` object. If the email is associated with a specific order, the ``order`` argument will be passed as well, otherwise it will be ``None``. If the email is associated with a specific user, e.g. a notification email, the ``user`` argument will be passed as well, otherwise it will be ``None``. """ layout_text_variables = EventPluginSignal() """ This signal is sent out to collect variables that can be used to display text in ticket-related PDF layouts. Receivers are expected to return a dictionary with globally unique identifiers as keys and more dictionaries as values that contain keys like in the following example:: return { "product": { "label": _("Product name"), "editor_sample": _("Sample product"), "evaluate": lambda orderposition, order, event: str(orderposition.item), "evaluate_bulk": lambda orderpositions: [str(op.item) for op in orderpositions], } } The ``evaluate`` member will be called with the order position, order and event as arguments. The event might also be a subevent, if applicable. The ``evaluate_bulk`` member is optional but can significantly improve performance in some situations because you can perform database fetches in bulk instead of single queries for every position. """ layout_image_variables = EventPluginSignal() """ This signal is sent out to collect variables that can be used to display dynamic images in ticket-related PDF layouts. Receivers are expected to return a dictionary with globally unique identifiers as keys and more dictionaries as values that contain keys like in the following example:: return { "profile": { "label": _("Profile picture"), "evaluate": lambda orderposition, order, event: ContentFile(b"some-image-data"), "etag": lambda orderposition, order, event: hash(b"some-image-data") } } The ``evaluate`` member will be called with the order position, order and event as arguments. The event might also be a subevent, if applicable. The return value of ``evaluate`` should be an instance of Django's ``File`` class and point to a valid JPEG or PNG file. If no image is available, ``evaluate`` should return ``None``. The ``etag`` member will be called with the same arguments as ``evaluate`` but should return a ``str`` value uniquely identifying the version of the file. This can be a hash of the file, but can also be something else. If no image is available, ``etag`` should return ``None``. In some cases, this can speed up the implementation. """ timeline_events = EventPluginSignal() """ This signal is sent out to collect events for the time line shown on event dashboards. You are passed a ``subevent`` argument which might be none and you are expected to return a list of instances of ``pretix.base.timeline.TimelineEvent``, which is a ``namedtuple`` with the fields ``event``, ``subevent``, ``datetime``, ``description`` and ``edit_url``. """ quota_availability = EventPluginSignal() """ Arguments: ``quota``, ``result``, ``count_waitinglist`` This signal allows you to modify the availability of a quota. You are passed the ``quota`` and an ``availability`` result calculated by pretix code or other plugins. ``availability`` is a tuple with the first entry being one of the ``Quota.AVAILABILITY_*`` constants and the second entry being the number of available tickets (or ``None`` for unlimited). You are expected to return a value of the same type. The parameter ``count_waitinglists`` specifies whether waiting lists should be taken into account. **Warning: Use this signal with great caution, it allows you to screw up the performance of the system really bad.** Also, keep in mind that your response is subject to caching and out-of-date quotas might be used for display (not for actual order processing). """ order_split = EventPluginSignal() """ Arguments: ``original``, ``split_order`` This signal is sent out when an order is split into two orders and allows you to copy related models to the new order. You will be passed the old order as ``original`` and the new order as ``split_order``. """ invoice_line_text = EventPluginSignal() """ Arguments: ``position`` This signal is sent out when an invoice is built for an order. You can return additional text that should be shown on the invoice for the given ``position``. """ order_import_columns = EventPluginSignal() """ This signal is sent out if the user performs an import of orders from an external source. You can use this to define additional columns that can be read during import. You are expected to return a list of instances of ``ImportColumn`` subclasses. As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ voucher_import_columns = EventPluginSignal() """ This signal is sent out if the user performs an import of vouchers from an external source. You can use this to define additional columns that can be read during import. You are expected to return a list of instances of ``ImportColumn`` subclasses. As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ validate_event_settings = EventPluginSignal() """ Arguments: ``settings_dict`` This signal is sent out if the user performs an update of event settings through the API or web interface. You are passed a ``settings_dict`` dictionary with the new state of the event settings object and are expected to raise a ``django.core.exceptions.ValidationError`` if the new state is not valid. You can not modify the dictionary. This is only recommended to use if you have multiple settings that can only be validated together. To validate individual settings, pass a validator to the serializer field instead. As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ api_event_settings_fields = EventPluginSignal() """ This signal is sent out to collect serializable settings fields for the API. You are expected to return a dictionary mapping names of attributes in the settings store to DRF serializer field instances. As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ customer_created = OrganizerPluginSignal(allow_legacy_plugins=True) """ Arguments: ``customer`` This signal is sent out every time a customer account is created. The ``customer`` object is given as the first argument. The ``sender`` keyword argument will contain the organizer. """ customer_signed_in = OrganizerPluginSignal(allow_legacy_plugins=True) """ Arguments: ``customer`` This signal is sent out every time a customer signs in. The ``customer`` object is given as the first argument. The ``sender`` keyword argument will contain the organizer. """ device_info_updated = GlobalSignal() # todo: replace with OrganizerPluginSignal? """ Arguments: ``old_device``, ``new_device`` This signal is sent out each time the information for a Device is modified. Both the original and updated versions of the Device are included to allow receivers to see what has been updated. """