cleanup
This commit is contained in:
parent
0499bb7616
commit
b09e8a1808
|
|
@ -122,6 +122,7 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
|
|||
| `menu` | The dotted path to a top-level navigation menu provided by the plugin (default: `navigation.menu`) |
|
||||
| `menu_items` | The dotted path to the list of menu items provided by the plugin (default: `navigation.menu_items`) |
|
||||
| `graphql_schema` | The dotted path to the plugin's GraphQL schema class, if any (default: `graphql.schema`) |
|
||||
| `serializer_resolver` | The dotted path to a callable that resolves REST API serializers for the plugin's models (default: `api.serializers.serializer_resolver`) |
|
||||
| `user_preferences` | The dotted path to the dictionary mapping of user preferences defined by the plugin (default: `preferences.preferences`) |
|
||||
|
||||
All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored.
|
||||
|
|
|
|||
|
|
@ -95,5 +95,13 @@ urlpatterns = router.urls
|
|||
|
||||
This will make the plugin's view accessible at `/api/plugins/my-plugin/my-model/`.
|
||||
|
||||
## Serializer Resolvers
|
||||
|
||||
NetBox resolves the REST API serializer for a model by importing `{app_label}.api.serializers.{ModelName}Serializer`. Plugins whose models are generated dynamically (and therefore have no importable serializer at that path), or that need to override resolution for specific models, may register a **serializer resolver**.
|
||||
|
||||
A resolver is a callable with the signature `resolver(model, prefix='') -> Serializer subclass or None`. Resolvers are consulted in registration order before the default import-path lookup; the first non-`None` return wins. If no resolver matches, the default lookup runs unchanged.
|
||||
|
||||
Register a resolver either by setting the `serializer_resolver` attribute on the plugin's `PluginConfig` (loaded from `api.serializers.serializer_resolver` by default) or by calling `netbox.plugins.register_serializer_resolver()` directly. A resolver that raises an exception, or returns a value that is not a `Serializer` subclass, is skipped and logged.
|
||||
|
||||
!!! warning
|
||||
The examples provided here are intended to serve as a minimal reference implementation only. This documentation does not address authentication, performance, or myriad other concerns that plugin authors may need to address.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import logging
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.core.exceptions import (
|
||||
FieldDoesNotExist,
|
||||
|
|
@ -22,6 +24,8 @@ from netbox.registry import registry
|
|||
from .query import count_related, dict_to_filter_params
|
||||
from .string import title
|
||||
|
||||
logger = logging.getLogger('netbox.utilities.api')
|
||||
|
||||
__all__ = (
|
||||
'IsSuperuser',
|
||||
'get_annotations_for_serializer',
|
||||
|
|
@ -54,9 +58,21 @@ def get_serializer_for_model(model, prefix=''):
|
|||
wins. If no resolver matches, the default import-path lookup runs.
|
||||
"""
|
||||
for resolver in registry['serializer_resolvers']:
|
||||
serializer = resolver(model, prefix=prefix)
|
||||
if serializer is not None:
|
||||
return serializer
|
||||
try:
|
||||
serializer = resolver(model, prefix=prefix)
|
||||
except Exception:
|
||||
# A buggy resolver must not break serializer lookup for the rest of NetBox.
|
||||
logger.exception("Serializer resolver %r raised an exception; skipping.", resolver)
|
||||
continue
|
||||
if serializer is None:
|
||||
continue
|
||||
if not (isinstance(serializer, type) and issubclass(serializer, Serializer)):
|
||||
logger.warning(
|
||||
"Serializer resolver %r returned %r, which is not a Serializer subclass; skipping.",
|
||||
resolver, serializer,
|
||||
)
|
||||
continue
|
||||
return serializer
|
||||
|
||||
app_label, model_name = model._meta.label.split('.')
|
||||
serializer_name = f'{app_label}.api.serializers.{prefix}{model_name}Serializer'
|
||||
|
|
|
|||
|
|
@ -2,15 +2,19 @@ from django.test import Client, TestCase, override_settings, tag
|
|||
from django.urls import reverse
|
||||
from drf_spectacular.drainage import GENERATOR_STATS
|
||||
from rest_framework import status
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from core.models import ObjectType
|
||||
from dcim.api.serializers import SiteSerializer
|
||||
from dcim.models import Region, Site
|
||||
from extras.choices import CustomFieldTypeChoices
|
||||
from extras.models import CustomField
|
||||
from ipam.models import VLAN
|
||||
from netbox.api.serializers import BaseModelSerializer
|
||||
from netbox.config import get_config
|
||||
from utilities.api import get_prefetches_for_serializer, get_view_name
|
||||
from netbox.plugins import register_serializer_resolver
|
||||
from netbox.registry import registry
|
||||
from utilities.api import get_prefetches_for_serializer, get_serializer_for_model, get_view_name
|
||||
from utilities.testing import APITestCase, disable_warnings
|
||||
|
||||
|
||||
|
|
@ -476,6 +480,14 @@ class GetPrefetchesForSerializerTestCase(TestCase):
|
|||
)
|
||||
|
||||
|
||||
class _ResolvedSerializerA(Serializer):
|
||||
pass
|
||||
|
||||
|
||||
class _ResolvedSerializerB(Serializer):
|
||||
pass
|
||||
|
||||
|
||||
class SerializerResolverRegistryTestCase(TestCase):
|
||||
"""
|
||||
Verify that registered serializer resolvers are consulted before the
|
||||
|
|
@ -483,67 +495,44 @@ class SerializerResolverRegistryTestCase(TestCase):
|
|||
"""
|
||||
|
||||
def setUp(self):
|
||||
from netbox.registry import registry
|
||||
|
||||
# Snapshot and clear the resolver list so each test starts from a
|
||||
# known state and can't leak resolvers into the rest of the suite.
|
||||
self._saved_resolvers = list(registry['serializer_resolvers'])
|
||||
registry['serializer_resolvers'].clear()
|
||||
|
||||
def tearDown(self):
|
||||
from netbox.registry import registry
|
||||
|
||||
registry['serializer_resolvers'].clear()
|
||||
registry['serializer_resolvers'].extend(self._saved_resolvers)
|
||||
|
||||
def test_default_lookup_when_no_resolvers_registered(self):
|
||||
from dcim.api.serializers import SiteSerializer
|
||||
from utilities.api import get_serializer_for_model
|
||||
|
||||
self.assertIs(get_serializer_for_model(Site), SiteSerializer)
|
||||
|
||||
def test_registered_resolver_overrides_default(self):
|
||||
from netbox.plugins import register_serializer_resolver
|
||||
from utilities.api import get_serializer_for_model
|
||||
register_serializer_resolver(lambda model, prefix='': _ResolvedSerializerA)
|
||||
|
||||
sentinel = object()
|
||||
register_serializer_resolver(lambda model, prefix='': sentinel)
|
||||
|
||||
self.assertIs(get_serializer_for_model(Site), sentinel)
|
||||
self.assertIs(get_serializer_for_model(Site), _ResolvedSerializerA)
|
||||
|
||||
def test_resolver_returning_none_falls_through_to_default(self):
|
||||
from dcim.api.serializers import SiteSerializer
|
||||
from netbox.plugins import register_serializer_resolver
|
||||
from utilities.api import get_serializer_for_model
|
||||
|
||||
register_serializer_resolver(lambda model, prefix='': None)
|
||||
|
||||
self.assertIs(get_serializer_for_model(Site), SiteSerializer)
|
||||
|
||||
def test_resolvers_tried_in_registration_order(self):
|
||||
from netbox.plugins import register_serializer_resolver
|
||||
from utilities.api import get_serializer_for_model
|
||||
|
||||
first = object()
|
||||
second = object()
|
||||
# First resolver only handles VLAN; second handles everything else.
|
||||
register_serializer_resolver(
|
||||
lambda model, prefix='': first if model is VLAN else None
|
||||
lambda model, prefix='': _ResolvedSerializerA if model is VLAN else None
|
||||
)
|
||||
register_serializer_resolver(lambda model, prefix='': second)
|
||||
register_serializer_resolver(lambda model, prefix='': _ResolvedSerializerB)
|
||||
|
||||
self.assertIs(get_serializer_for_model(VLAN), first)
|
||||
self.assertIs(get_serializer_for_model(Site), second)
|
||||
self.assertIs(get_serializer_for_model(VLAN), _ResolvedSerializerA)
|
||||
self.assertIs(get_serializer_for_model(Site), _ResolvedSerializerB)
|
||||
|
||||
def test_resolver_receives_prefix(self):
|
||||
from netbox.plugins import register_serializer_resolver
|
||||
from utilities.api import get_serializer_for_model
|
||||
|
||||
seen = {}
|
||||
|
||||
def resolver(model, prefix=''):
|
||||
seen['prefix'] = prefix
|
||||
return object()
|
||||
return _ResolvedSerializerA
|
||||
|
||||
register_serializer_resolver(resolver)
|
||||
get_serializer_for_model(Site, prefix='Nested')
|
||||
|
|
@ -551,11 +540,25 @@ class SerializerResolverRegistryTestCase(TestCase):
|
|||
self.assertEqual(seen['prefix'], 'Nested')
|
||||
|
||||
def test_register_rejects_non_callable(self):
|
||||
from netbox.plugins import register_serializer_resolver
|
||||
|
||||
with self.assertRaises(TypeError):
|
||||
register_serializer_resolver('not a callable')
|
||||
|
||||
def test_raising_resolver_is_skipped(self):
|
||||
def broken_resolver(model, prefix=''):
|
||||
raise RuntimeError("intentional failure")
|
||||
|
||||
register_serializer_resolver(broken_resolver)
|
||||
register_serializer_resolver(lambda model, prefix='': _ResolvedSerializerA)
|
||||
|
||||
with self.assertLogs('netbox.utilities.api', level='ERROR'):
|
||||
self.assertIs(get_serializer_for_model(Site), _ResolvedSerializerA)
|
||||
|
||||
def test_resolver_returning_non_serializer_is_skipped(self):
|
||||
register_serializer_resolver(lambda model, prefix='': object())
|
||||
|
||||
with self.assertLogs('netbox.utilities.api', level='WARNING'):
|
||||
self.assertIs(get_serializer_for_model(Site), SiteSerializer)
|
||||
|
||||
|
||||
class APITrailingSlashTestCase(APITestCase):
|
||||
"""
|
||||
|
|
|
|||
Loading…
Reference in New Issue