Add documentation

This commit is contained in:
Mira Weller
2024-12-17 12:22:28 +01:00
parent 074c632260
commit f60dde3c3a
5 changed files with 239 additions and 22 deletions
+23
View File
@@ -121,6 +121,7 @@ This will automatically make pretix discover this plugin as soon as it is instal
through ``pip``. During development, you can just run ``python setup.py develop`` inside
your plugin source directory to make it discoverable.
.. _`Signals`:
Signals
-------
@@ -153,6 +154,28 @@ in the ``installed`` method:
Note that ``installed`` will *not* be called if the plugin is indirectly activated for an event
because the event is created with settings copied from another event.
.. _`Registries`:
Registries
----------
Many signals in pretix are used so that plugins can "register" a class, e.g. a payment provider or a
ticket renderer.
However, for some of them (types of :ref:`Log Entries <logging>`) we use a different method to keep track of them:
In a ``Registry``, classes are collected at application startup, along with a unique key (in case
of LogEntryType, the action_type) as well as which plugin registered them.
To register a class, you can use one of several decorator provided by the Registry object:
.. code-block:: python
@log_entry_types.new('my_pretix_plugin.some.action', _('Some action in My Pretix Plugin occured.'))
class MyPretixPluginLogEntryType(EventLogEntryType):
pass
All files in which classes are registered need to be imported in the ``AppConfig.ready`` as explained
in `Signals`_ above.
Views
-----
+77 -13
View File
@@ -20,7 +20,8 @@ To actually log an action, you can just call the ``log_action`` method on your o
.. code-block:: python
order.log_action('pretix.event.order.canceled', user=user, data={})
order.log_action('pretix.event.order.comment', user=user,
data={"new_comment": "Hello, world."})
The positional ``action`` argument should represent the type of action and should be globally unique, we
recommend to prefix it with your package name, e.g. ``paypal.payment.rejected``. The ``user`` argument is
@@ -72,24 +73,87 @@ following ready-to-include template::
{% include "pretixcontrol/includes/logs.html" with obj=order %}
We now need a way to translate the action codes like ``pretix.event.changed`` into human-readable
strings. The :py:attr:`pretix.base.signals.logentry_display` signals allows you to do so. A simple
strings. The :py:attr:`pretix.base.logentrytypes.log_entry_types` :ref:`Registry <Registries>` allows you to do so. A simple
implementation could look like:
.. code-block:: python
from django.utils.translation import gettext as _
from pretix.base.signals import logentry_display
from pretix.base.logentrytypes import log_entry_types
@receiver(signal=logentry_display)
def pretixcontrol_logentry_display(sender, logentry, **kwargs):
plains = {
'pretix.event.order.paid': _('The order has been marked as paid.'),
'pretix.event.order.refunded': _('The order has been refunded.'),
'pretix.event.order.canceled': _('The order has been canceled.'),
...
}
if logentry.action_type in plains:
return plains[logentry.action_type]
@log_entry_types.new_from_dict({
'pretix.event.order.comment': _('The order\'s internal comment has been updated to: {new_comment}'),
'pretix.event.order.paid': _('The order has been marked as paid.'),
# ...
})
class CoreOrderLogEntryType(OrderLogEntryType):
pass
Please note that you always need to define your own inherited LogEntryType class in your plugin. If you would just
register an instance of a LogEntryType class defined in pretix core, it is not correctly registered as belonging to
your plugin, leading to confusing user interface situations.
Customizing log entry display
""""""""""""""""""""""""""""""""""
The base LogEntryType classes allows for varying degree of customization in their descendants.
If you want to add another log message for an existing core object (e.g. an Order, Item or Voucher), you can inherit
from its predefined LogEntryType, e.g. `OrderLogEntryType` and just specify a new plaintext string. You can use format
strings to insert information from the LogEntry's `data` object as shown in the section above.
If you defined a new model object in your plugin, you should make sure proper object links in the user interface are
displayed for it. If your model object belongs logically to a pretix `Event`, you can inherit from `EventLogEntryType`,
and set the `object_link_*` fields accordingly. `object_link_viewname` refers to a django url name, which needs to
accept the arguments `organizer` and `event`, containing the respective slugs, and an argument named by `object_link_argname`.
The latter will contain the ID of the model object, if not customized by overriding `object_link_argvalue`.
If you want to customize the name displayed for the object (instead of the result of calling `str()` on it),
overwrite `object_link_display_name`.
.. code-block:: python
class OrderLogEntryType(EventLogEntryType):
object_link_wrapper = _('Order {val}')
object_link_viewname = 'control:event.order'
object_link_argname = 'code'
def object_link_argvalue(self, order):
return order.code
def object_link_display_name(self, order):
return order.code
To show more sophisticated message strings, e.g. varying the message depending on information from the LogEntry's
`data` object, overwrite the `display` method:
.. code-block:: python
@log_entry_types.new()
class PaypalEventLogEntryType(EventLogEntryType):
action_type = 'pretix.plugins.paypal.event'
def display(self, logentry):
event_type = logentry.parsed_data.get('event_type')
text = {
'PAYMENT.SALE.COMPLETED': _('Payment completed.'),
'PAYMENT.SALE.DENIED': _('Payment denied.'),
# ...
}.get(event_type, f"({event_type})")
return _('PayPal reported an event: {}').format(text)
.. automethod:: pretix.base.logentrytypes.LogEntryType.display
If your new model object does not belong to an `Event`, you need to implement
meow
.. autoclass:: pretix.base.logentrytypes.Registry
:members: new
.. autoclass:: pretix.base.logentrytypes.LogEntryTypeRegistry
:members: new, new_from_dict
Sending notifications
---------------------
+54 -2
View File
@@ -33,16 +33,45 @@ def make_link(a_map, wrapper, is_active=True, event=None, plugin_name=None):
class LogEntryTypeRegistry(EventPluginRegistry):
def new_from_dict(self, data):
"""
Register multiple instance of a LogEntryType class with different action_type
and plain text strings, as given by the items of the specified data dictionary.
This method is designed to be used as a decorator as follows:
.. code-block:: python
@log_entry_types.new_from_dict({
'pretix.event.item.added': _('The product has been created.'),
'pretix.event.item.changed': _('The product has been changed.'),
# ...
})
class CoreItemLogEntryType(ItemLogEntryType):
# ...
:param data: action types and descriptions
``{"some_action_type": "Plain text description", ...}``
"""
def reg(clz):
for action_type, plain in data.items():
self.register(clz(action_type=action_type, plain=plain))
return reg
"""
Registry for LogEntry types.
Each entry in this registry should be an instance of a subclass of ``LogEntryType``.
They are annotated with their ``action_type`` and the defining ``plugin``.
"""
log_entry_types = LogEntryTypeRegistry({'action_type': lambda o: getattr(o, 'action_type')})
class LogEntryType:
"""
Base class for a type of LogEntry, identified by its action_type.
"""
def __init__(self, action_type=None, plain=None):
assert self.__module__ != LogEntryType.__module__ # must not instantiate base classes, only derived ones
if action_type:
@@ -51,6 +80,11 @@ class LogEntryType:
self.plain = plain
def display(self, logentry):
"""
Returns the message to be displayed for a given logentry of this type.
:return: `str` or `LazyI18nString`
"""
if hasattr(self, 'plain'):
plain = str(self.plain)
if '{' in plain:
@@ -60,6 +94,14 @@ class LogEntryType:
return plain
def get_object_link_info(self, logentry) -> dict:
"""
Return information to generate a link to the content_object of a given logentry.
Not implemented in the base class, causing the object link to be omitted.
:return: `dict` with the keys `href` (containing a URL to view/edit the object) and `val` (containing the
escaped text for the anchor element)
"""
pass
def get_object_link(self, logentry):
@@ -69,10 +111,18 @@ class LogEntryType:
object_link_wrapper = '{val}'
def shred_pii(self, logentry):
"""
To be used for shredding personally identified information contained in the data field of a LogEntry of this
type.
"""
raise NotImplementedError
class EventLogEntryType(LogEntryType):
"""
Base class for any LogEntry type whose content_object is either an `Event` itself or belongs to a specific `Event`.
"""
def get_object_link_info(self, logentry) -> dict:
if hasattr(self, 'object_link_viewname') and hasattr(self, 'object_link_argname') and logentry.content_object:
return {
@@ -85,9 +135,11 @@ class EventLogEntryType(LogEntryType):
}
def object_link_argvalue(self, content_object):
"""Return the identifier used in a link to content_object."""
return content_object.id
def object_link_display_name(self, content_object):
"""Return the display name to refer to content_object in the user interface."""
return str(content_object)
@@ -108,8 +160,8 @@ class VoucherLogEntryType(EventLogEntryType):
object_link_viewname = 'control:event.voucher'
object_link_argname = 'voucher'
def object_link_display_name(self, order):
return order.code[:6]
def object_link_display_name(self, voucher):
return voucher.code[:6]
class ItemLogEntryType(EventLogEntryType):
+2 -2
View File
@@ -93,7 +93,7 @@ class LogEntry(models.Model):
indexes = [models.Index(fields=["datetime", "id"])]
def display(self):
log_entry_type, meta = log_entry_types.find(action_type=self.action_type)
log_entry_type, meta = log_entry_types.get(action_type=self.action_type)
if log_entry_type:
return log_entry_type.display(self)
@@ -134,7 +134,7 @@ class LogEntry(models.Model):
Discount, Event, Item, Order, Question, Quota, SubEvent, Voucher,
)
log_entry_type, meta = log_entry_types.find(action_type=self.action_type)
log_entry_type, meta = log_entry_types.get(action_type=self.action_type)
if log_entry_type:
link_info = log_entry_type.get_object_link_info(self)
if is_app_active(self.event, meta['plugin']):
+83 -5
View File
@@ -220,32 +220,110 @@ class DeprecatedSignal(django.dispatch.Signal):
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):
self.registered_items = list()
"""
:param keys: dictionary {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.registered_entries = list()
self.keys = keys
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:
meta = {k: accessor(obj) for k, accessor in self.keys.items()}
tup = (obj, meta)
for key, accessor in self.keys.items():
self.by_key[key][accessor(obj)] = tup
self.registered_items.append(tup)
for key, value in meta.items():
self.by_key[key][value] = tup
self.registered_entries.append(tup)
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 find(self, **kwargs):
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
if all(value == meta[key] for key, value in kwargs.items())
)
class EventPluginRegistry(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.
.. code-block:: python
logtype, meta = my_registry.find(action_type="foo.bar.baz")
# meta["plugin"] contains the django app name of the defining plugin
"""
def __init__(self, keys):
super().__init__({"plugin": lambda o: get_defining_app(o), **keys})