Compare commits

..

13 Commits

Author SHA1 Message Date
Mira Weller
e5d464192f Update docs 2025-01-16 11:54:07 +01:00
Mira Weller
74d802d2ec Default implementation for object_link_args 2025-01-16 11:50:03 +01:00
Mira Weller
27139e00c6 Add missing license header 2025-01-16 11:45:04 +01:00
Mira
48296b1f96 Apply suggestions from code review
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2025-01-16 11:44:51 +01:00
Mira Weller
eceed8df68 fix code formatting 2025-01-13 15:15:21 +01:00
Mira Weller
0c50d88fec add unit tests for Registry and LogEntryTypeRegistry 2025-01-13 14:39:42 +01:00
Mira Weller
d1e287b6bd move LogEntryType validation into LogEntryTypeRegistry 2025-01-13 14:39:30 +01:00
Mira Weller
4f7dfdf98a prevent duplicates in Registry 2025-01-13 14:22:24 +01:00
Mira Weller
0c41dcb2ed improve formatting of documentation 2025-01-13 13:12:52 +01:00
Mira
e2ae0b6780 Apply suggestions from code review 2024-12-19 17:39:28 +01:00
Mira
2ea25cfd5a Apply suggestions from code review 2024-12-19 17:38:05 +01:00
Mira
3b5630a66c Apply suggestions from code review
Co-authored-by: Raphael Michel <michel@rami.io>
2024-12-19 17:26:38 +01:00
Mira Weller
7b8783e089 deprecation notice for logentry_display and logentry_object_link 2024-12-18 11:28:40 +01:00
7 changed files with 282 additions and 151 deletions

View File

@@ -121,7 +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`:
Signals
-------
@@ -154,7 +154,7 @@ 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`:
Registries
----------
@@ -163,18 +163,15 @@ 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.
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:
To register a class, you can use one of several decorators 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
.. autoclass:: pretix.base.logentrytypes.LogEntryTypeRegistry
:members: register, new, new_from_dict
All files in which classes are registered need to be imported in the ``AppConfig.ready`` as explained
in `Signals`_ above.
in `Signals <signals>`_ above.
Views
-----

View File

@@ -73,7 +73,7 @@ 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.logentrytypes.log_entry_types` :ref:`Registry <Registries>` 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
@@ -90,43 +90,58 @@ implementation could look like:
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.
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 cannot be automatically detected 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.
The base ``LogEntryType`` classes allow 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 want to add another log message for an existing core object (e.g. an :class:`Order <pretix.base.models.Order>`,
:class:`Item <pretix.base.models.Item>`, or :class:`Voucher <pretix.base.models.Voucher>`), you can inherit
from its predefined :class:`LogEntryType <pretix.base.logentrytypes.LogEntryType>`, e.g.
:class:`OrderLogEntryType <pretix.base.logentrytypes.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 define 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 :class:`Event <pretix.base.models.Event>`, you can inherit from :class:`EventLogEntryType <pretix.base.logentrytypes.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 additional arguments provided by
``object_link_args``. The default implementation of ``object_link_args`` will return an argument named by
````object_link_argname``, with a value of ``content_object.pk`` (the primary key of the model object).
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 ItemLogEntryType(EventLogEntryType):
object_link_wrapper = _('Product {val}')
# link will be generated as reverse('control:event.item', {'organizer': ..., 'event': ..., 'item': item.pk})
object_link_viewname = 'control:event.item'
object_link_argname = 'item'
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
# link will be generated as reverse('control:event.order', {'organizer': ..., 'event': ..., 'code': order.code})
object_link_viewname = 'control:event.order'
def object_link_args(self, order):
return {'code': 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:
To show more sophisticated message strings, e.g. varying the message depending on information from the :class:`LogEntry <pretix.base.models.log.LogEntry>`'s
`data` object, override the `display` method:
.. code-block:: python
@@ -145,15 +160,14 @@ To show more sophisticated message strings, e.g. varying the message depending o
.. automethod:: pretix.base.logentrytypes.LogEntryType.display
If your new model object does not belong to an `Event`, you need to implement
If your new model object does not belong to an :class:`Event <pretix.base.models.Event>`, you need to inherit directly from ``LogEntryType`` instead
of ``EventLogEntryType``, providing your own implementation of ``get_object_link_info`` if object links should be
displayed.
meow
.. autoclass:: pretix.base.logentrytypes.LogEntryType
:members: get_object_link_info
.. autoclass:: pretix.base.logentrytypes.Registry
:members: new
.. autoclass:: pretix.base.logentrytypes.LogEntryTypeRegistry
:members: new, new_from_dict
Sending notifications
---------------------

View File

@@ -1,4 +1,3 @@
import json
#
# This file is part of pretix (Community Edition).
#
@@ -21,9 +20,7 @@ import json
# <https://www.gnu.org/licenses/>.
#
from collections import defaultdict
from functools import cached_property
import jsonschema
from django.urls import reverse
from django.utils.html import escape
from django.utils.translation import gettext_lazy as _, pgettext_lazy
@@ -56,9 +53,22 @@ def make_link(a_map, wrapper, is_active=True, event=None, plugin_name=None):
class LogEntryTypeRegistry(EventPluginRegistry):
def __init__(self):
super().__init__({'action_type': lambda o: getattr(o, 'action_type')})
def register(self, *objs):
for obj in objs:
if not isinstance(obj, LogEntryType):
raise TypeError('Entries must be derived from LogEntryType')
if obj.__module__ == LogEntryType.__module__:
raise TypeError('Must not register base classes, only derived ones')
return super().register(*objs)
def new_from_dict(self, data):
"""
Register multiple instance of a LogEntryType class with different action_type
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:
@@ -89,29 +99,7 @@ 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')})
def prepare_schema(schema):
def handle_properties(t):
return {"shred_properties": [k for k, v in t["properties"].items() if v["shred"]]}
def walk_tree(schema):
if type(schema) is dict:
new_keys = {}
for k, v in schema.items():
if k == "properties":
new_keys = handle_properties(schema)
walk_tree(v)
if schema.get("type") == "object" and "additionalProperties" not in new_keys:
new_keys["additionalProperties"] = False
schema.update(new_keys)
elif type(schema) is list:
for v in schema:
walk_tree(v)
walk_tree(schema)
return schema
log_entry_types = LogEntryTypeRegistry()
class LogEntryType:
@@ -119,12 +107,7 @@ class LogEntryType:
Base class for a type of LogEntry, identified by its action_type.
"""
data_schema = None # {"type": "object", "properties": []}
def __init__(self, action_type=None, plain=None):
assert self.__module__ != LogEntryType.__module__ # must not instantiate base classes, only derived ones
if self.data_schema:
print(self.__class__.__name__, "has schema", self._prepared_schema)
if action_type:
self.action_type = action_type
if plain:
@@ -146,12 +129,12 @@ class LogEntryType:
def get_object_link_info(self, logentry) -> dict:
"""
Return information to generate a link to the content_object of a given logentry.
Return information to generate a link to the `content_object` of a given log entry.
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)
:return: Dictionary with the keys ``href`` (containing a URL to view/edit the object) and ``val`` (containing the
escaped text for the anchor element)
"""
pass
@@ -161,54 +144,35 @@ class LogEntryType:
object_link_wrapper = '{val}'
def validate_data(self, parsed_data):
if not self._prepared_schema:
return
jsonschema.validate(parsed_data, self._prepared_schema)
@cached_property
def _prepared_schema(self):
if self.data_schema:
return prepare_schema(self.data_schema)
def shred_pii(self, logentry):
"""
To be used for shredding personally identified information contained in the data field of a LogEntry of this
type.
"""
if self._prepared_schema:
def shred_fun(validator, value, instance, schema):
for key in value:
instance[key] = "##########"
v = jsonschema.validators.extend(jsonschema.validators.Draft202012Validator,
validators={"shred_properties": shred_fun})
data = logentry.parsed_data
jsonschema.validate(data, self._prepared_schema, v)
logentry.data = json.dumps(data)
else:
raise NotImplementedError
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`.
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:
if hasattr(self, 'object_link_viewname') and logentry.content_object:
return {
'href': reverse(self.object_link_viewname, kwargs={
'event': logentry.event.slug,
'organizer': logentry.event.organizer.slug,
self.object_link_argname: self.object_link_argvalue(logentry.content_object),
**self.object_link_args(logentry.content_object),
}),
'val': escape(self.object_link_display_name(logentry.content_object)),
}
def object_link_argvalue(self, content_object):
"""Return the identifier used in a link to content_object."""
return content_object.id
def object_link_args(self, content_object):
"""Return the kwargs for the url used in a link to content_object."""
if hasattr(self, 'object_link_argname'):
return {self.object_link_argname: content_object.pk}
return {}
def object_link_display_name(self, content_object):
"""Return the display name to refer to content_object in the user interface."""
@@ -218,10 +182,9 @@ class EventLogEntryType(LogEntryType):
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_args(self, order):
return {'code': order.code}
def object_link_display_name(self, order):
return order.code
@@ -233,7 +196,9 @@ class VoucherLogEntryType(EventLogEntryType):
object_link_argname = 'voucher'
def object_link_display_name(self, voucher):
return voucher.code[:6]
if len(voucher.code) > 6:
return voucher.code[:6] + ""
return voucher.code
class ItemLogEntryType(EventLogEntryType):

View File

@@ -31,7 +31,6 @@ from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from pretix.base.logentrytypes import log_entry_types
from pretix.helpers.json import CustomJSONEncoder
@@ -125,13 +124,7 @@ class LoggingMixin:
if (sensitivekey in k) and v:
data[k] = "********"
type, meta = log_entry_types.get(action_type=action)
if not type:
raise TypeError("Undefined log entry type '%s'" % action)
logentry.data = json.dumps(data, cls=CustomJSONEncoder, sort_keys=True)
type.validate_data(json.loads(logentry.data))
elif data:
raise TypeError("You should only supply dictionaries as log data.")
if save:

View File

@@ -33,15 +33,14 @@
# License for the specific language governing permissions and limitations under the License.
import warnings
from typing import TYPE_CHECKING, Any, Callable, List, Tuple
from typing import Any, Callable, List, Tuple
import django.dispatch
from django.apps import apps
from django.conf import settings
from django.dispatch.dispatcher import NO_RECEIVERS
if TYPE_CHECKING:
from .models import Event
from .models import Event
app_cache = {}
@@ -104,14 +103,13 @@ class EventPluginSignal(django.dispatch.Signal):
Event.
"""
def send(self, sender: "Event", **named) -> List[Tuple[Callable, Any]]:
def send(self, sender: Event, **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``.
"""
from .models import Event
if sender and not isinstance(sender, Event):
raise ValueError("Sender needs to be an event.")
@@ -128,7 +126,7 @@ class EventPluginSignal(django.dispatch.Signal):
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: 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
@@ -136,7 +134,6 @@ class EventPluginSignal(django.dispatch.Signal):
sender is required to be an instance of ``pretix.base.models.Event``.
"""
from .models import Event
if sender and not isinstance(sender, Event):
raise ValueError("Sender needs to be an event.")
@@ -153,7 +150,7 @@ class EventPluginSignal(django.dispatch.Signal):
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: Event, **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
@@ -161,7 +158,6 @@ class EventPluginSignal(django.dispatch.Signal):
sender is required to be an instance of ``pretix.base.models.Event``.
"""
from .models import Event
if sender and not isinstance(sender, Event):
raise ValueError("Sender needs to be an event.")
@@ -199,7 +195,7 @@ class EventPluginSignal(django.dispatch.Signal):
class GlobalSignal(django.dispatch.Signal):
def send_chained(self, sender: "Event", chain_kwarg_name, **named) -> List[Tuple[Callable, Any]]:
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
@@ -258,11 +254,11 @@ class Registry:
def __init__(self, keys):
"""
:param keys: dictionary {key: accessor_function}
: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.registered_entries = list()
self.registered_entries = dict()
self.keys = keys
self.by_key = {key: {} for key in self.keys.keys()}
@@ -280,15 +276,21 @@ class Registry:
# ...
"""
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.append(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.
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
@@ -310,10 +312,11 @@ class Registry:
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())
)
return (
(entry, meta)
for entry, meta in self.registered_entries.items()
if all(value == meta[key] for key, value in kwargs.items())
)
class EventPluginRegistry(Registry):

View File

@@ -403,6 +403,7 @@ def pretixcontrol_orderposition_blocked_display(sender: Event, orderposition, bl
'pretix.event.order.valid_if_pending.set': _('The order has been set to be usable before it is paid.'),
'pretix.event.order.valid_if_pending.unset': _('The order has been set to require payment before use.'),
'pretix.event.order.expired': _('The order has been marked as expired.'),
'pretix.event.order.paid': _('The order has been marked as paid.'),
'pretix.event.order.cancellationrequest.deleted': _('The cancellation request has been deleted.'),
'pretix.event.order.refunded': _('The order has been refunded.'),
'pretix.event.order.reactivated': _('The order has been reactivated.'),
@@ -468,21 +469,6 @@ class CoreOrderLogEntryType(OrderLogEntryType):
pass
@log_entry_types.new()
class OrderPaidLogEntryType(CoreOrderLogEntryType):
action_type = 'pretix.event.order.paid'
plain = _('The order has been marked as paid.')
data_schema = {
"type": "object",
"properties": {
"provider": {"type": "string", "shred": False, },
"info": {"type": ["null", "string", "object"], "shred": True, },
"date": {"type": "string", "shred": False, },
"force": {"type": "boolean", "shred": False, },
},
}
@log_entry_types.new_from_dict({
'pretix.voucher.added': _('The voucher has been created.'),
'pretix.voucher.sent': _('The voucher has been sent to {recipient}.'),
@@ -500,12 +486,6 @@ class CoreVoucherLogEntryType(VoucherLogEntryType):
class VoucherRedeemedLogEntryType(VoucherLogEntryType):
action_type = 'pretix.voucher.redeemed'
plain = _('The voucher has been redeemed in order {order_code}.')
data_schema = {
"type": "object",
"properties": {
"order_code": {"type": "string", "shred": False, },
},
}
def display(self, logentry):
data = json.loads(logentry.data)

View File

@@ -0,0 +1,179 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io 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 <https://pretix.eu/about/en/license>.
#
# 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
# <https://www.gnu.org/licenses/>.
#
from unittest import mock
import pytest
from pretix.base.logentrytypes import (
ItemLogEntryType, LogEntryType, LogEntryTypeRegistry,
)
from pretix.base.signals import Registry
def test_registry_classes():
animal_type_registry = Registry({"type": lambda s: s.__name__, "classis": lambda s: s.classis})
@animal_type_registry.register
class Cat:
classis = 'mammalia'
def make_sound(self):
return "meow"
@animal_type_registry.register
class Dog:
classis = 'mammalia'
def make_sound(self):
return "woof"
@animal_type_registry.register
class Cricket:
classis = 'insecta'
def make_sound(self):
return "chirp"
# test retrieving and instantiating a class based on metadata value
clz, meta = animal_type_registry.get(type="Cat")
assert clz().make_sound() == "meow"
assert meta.get('type') == "Cat"
clz, meta = animal_type_registry.get(type="Dog")
assert clz().make_sound() == "woof"
assert meta.get('type') == "Dog"
# check that None is returned when no class exists with the specified metadata value
clz, meta = animal_type_registry.get(type="Unicorn")
assert clz is None
assert meta is None
# check that an error is raised when trying to retrieve by an undefined metadata key
with pytest.raises(Exception):
_, _ = animal_type_registry.get(whatever="hello")
# test finding all entries with a given metadata value
mammals = animal_type_registry.filter(classis='mammalia')
assert set(cls for cls, meta in mammals) == {Cat, Dog}
assert all(meta['classis'] == 'mammalia' for cls, meta in mammals)
insects = animal_type_registry.filter(classis='insecta')
assert set(cls for cls, meta in insects) == {Cricket}
fantasy = animal_type_registry.filter(classis='fantasia')
assert set(cls for cls, meta in fantasy) == set()
# check normal object instantiation still works with our decorator
assert Cat().make_sound() == "meow"
def test_registry_instances():
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"])
self.i = 0
def make_sound(self):
self.i += 1
return self.sound[self.i % len(self.sound)]
# test registry
assert animal_sound_registry.get(animal='dog')[0].make_sound() == "woof"
assert animal_sound_registry.get(animal='dog')[0].make_sound() == "woof"
assert animal_sound_registry.get(animal='cricket')[0].make_sound() == "chirp"
assert animal_sound_registry.get(animal='cat')[0].make_sound() == "meww"
assert animal_sound_registry.get(animal='cat')[0].make_sound() == "miaou"
assert animal_sound_registry.get(animal='cat')[0].make_sound() == "meow"
# check normal object instantiation still works with our decorator
assert AnimalSound("test", "test").make_sound() == "test"
def test_registry_prevent_duplicates():
my_registry = Registry({"animal": lambda s: s.animal})
class AnimalSound:
def __init__(self, animal, sound):
self.animal = animal
self.sound = sound
cat = AnimalSound("cat", "meow")
my_registry.register(cat)
with pytest.raises(RuntimeError):
my_registry.register(cat)
def test_logentrytype_registry():
reg = LogEntryTypeRegistry()
with mock.patch('pretix.base.signals.get_defining_app') as mock_get_defining_app:
mock_get_defining_app.return_value = 'my_plugin'
@reg.new("foo.mytype")
class MyType(LogEntryType):
pass
@reg.new("foo.myothertype")
class MyOtherType(LogEntryType):
pass
typ, meta = reg.get(action_type="foo.mytype")
assert isinstance(typ, MyType)
assert meta['action_type'] == "foo.mytype"
assert meta['plugin'] == 'my_plugin'
typ, meta = reg.get(action_type="foo.myothertype")
assert isinstance(typ, MyOtherType)
assert meta['action_type'] == "foo.myothertype"
assert meta['plugin'] is None
by_my_plugin = reg.filter(plugin='my_plugin')
assert set(type(typ) for typ, meta in by_my_plugin) == {MyType}
def test_logentrytype_registry_validation():
reg = LogEntryTypeRegistry()
with pytest.raises(TypeError, match='Must not register base classes, only derived ones'):
reg.register(LogEntryType("foo.mytype"))
with pytest.raises(TypeError, match='Must not register base classes, only derived ones'):
reg.new_from_dict({"foo.mytype": "My Log Entry"})(ItemLogEntryType)
with pytest.raises(TypeError, match='Entries must be derived from LogEntryType'):
@reg.new()
class MyType:
pass